HH/static/surveys/js/builder.js
2026-01-24 15:27:30 +03:00

309 lines
11 KiB
JavaScript

/**
* Survey Builder JavaScript Module
* Handles dynamic question management for survey templates
*/
class SurveyBuilder {
constructor(formsetPrefix = 'questions') {
this.formsetPrefix = formsetPrefix;
this.formsetContainer = document.getElementById('questions-container');
this.managementForm = document.getElementById('id_' + formsetPrefix + '-TOTAL_FORMS');
this.addButton = this.createAddButton();
this.questionCounter = 0;
this.init();
}
init() {
if (!this.formsetContainer) {
console.error('Questions container not found');
return;
}
// Add the "Add Question" button
this.addAddButton();
// Add delete buttons to existing questions
this.addDeleteButtons();
// Add reorder buttons to existing questions
this.addReorderButtons();
// Update question numbers
this.updateQuestionNumbers();
// Setup question type change handlers
this.setupQuestionTypeHandlers();
console.log('Survey Builder initialized');
}
createAddButton() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-success mt-3';
button.innerHTML = '<i class="bi bi-plus-circle me-2"></i>Add Question';
button.addEventListener('click', () => this.addQuestion());
return button;
}
addAddButton() {
const container = document.getElementById('questions-container');
if (container) {
container.appendChild(this.addButton);
}
}
addQuestion() {
const totalForms = parseInt(this.managementForm.value);
const emptyForm = document.getElementById('empty-question-form');
if (!emptyForm) {
this.showError('Empty question form template not found');
return;
}
// Clone the empty form
const newForm = emptyForm.cloneNode(true);
newForm.id = '';
newForm.style.display = 'block';
newForm.className = 'question-form mb-4 p-3 border rounded new-question';
// Update form IDs and names
const formRegex = new RegExp('__prefix__', 'g');
newForm.innerHTML = newForm.innerHTML.replace(formRegex, totalForms);
// Add required attributes to cloned form
const textInput = newForm.querySelector('input[name$="-text"]');
const questionTypeSelect = newForm.querySelector('select[name$="-question_type"]');
const orderInput = newForm.querySelector('input[name$="-order"]');
if (textInput) textInput.required = true;
if (questionTypeSelect) questionTypeSelect.required = true;
if (orderInput) orderInput.required = true;
// Add delete button
const header = newForm.querySelector('.d-flex.justify-content-between');
if (header) {
const deleteBtn = this.createDeleteButton(totalForms);
header.appendChild(deleteBtn);
}
// Add reorder buttons
const controlsDiv = this.createReorderControls(totalForms);
const firstRow = newForm.querySelector('.row');
if (firstRow) {
const col = firstRow.querySelector('.col-md-6');
if (col) {
col.insertBefore(controlsDiv, col.firstChild);
}
}
// Add to formset
this.formsetContainer.insertBefore(newForm, this.addButton);
// Update total forms
this.managementForm.value = totalForms + 1;
this.questionCounter++;
// Update question numbers
this.updateQuestionNumbers();
// Setup handlers for new form
this.setupQuestionTypeHandlers();
// Scroll to new question
newForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Flash animation
newForm.classList.add('highlight-new');
setTimeout(() => newForm.classList.remove('highlight-new'), 2000);
}
createDeleteButton(formIndex) {
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'btn btn-sm btn-outline-danger';
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
deleteBtn.title = 'Delete Question';
deleteBtn.addEventListener('click', () => this.deleteQuestion(deleteBtn));
return deleteBtn;
}
addDeleteButtons() {
const questions = this.formsetContainer.querySelectorAll('.question-form');
questions.forEach((question, index) => {
const header = question.querySelector('.d-flex.justify-content-between');
const existingDelete = header?.querySelector('.delete-question-btn');
if (header && !existingDelete) {
const deleteBtn = this.createDeleteButton(index);
header.appendChild(deleteBtn);
}
});
}
deleteQuestion(button) {
const questionForm = button.closest('.question-form');
if (!questionForm) return;
// Find and check delete checkbox
const deleteCheckbox = questionForm.querySelector('input[name$="-DELETE"]');
if (deleteCheckbox) {
// Mark for deletion
deleteCheckbox.checked = true;
questionForm.style.opacity = '0.3';
questionForm.style.pointerEvents = 'none';
// Show confirm dialog
if (confirm('Are you sure you want to delete this question?')) {
questionForm.remove();
this.updateQuestionNumbers();
} else {
// Unmark and restore
deleteCheckbox.checked = false;
questionForm.style.opacity = '1';
questionForm.style.pointerEvents = 'auto';
}
} else {
// No delete checkbox, just remove
if (confirm('Are you sure you want to delete this question?')) {
questionForm.remove();
this.updateQuestionNumbers();
}
}
}
createReorderControls(formIndex) {
const div = document.createElement('div');
div.className = 'reorder-controls mb-2 d-flex gap-2';
const upBtn = document.createElement('button');
upBtn.type = 'button';
upBtn.className = 'btn btn-sm btn-outline-secondary';
upBtn.innerHTML = '<i class="bi bi-arrow-up"></i>';
upBtn.title = 'Move Up';
upBtn.addEventListener('click', () => this.moveQuestion(questionForm, 'up'));
const downBtn = document.createElement('button');
downBtn.type = 'button';
downBtn.className = 'btn btn-sm btn-outline-secondary';
downBtn.innerHTML = '<i class="bi bi-arrow-down"></i>';
downBtn.title = 'Move Down';
downBtn.addEventListener('click', () => this.moveQuestion(questionForm, 'down'));
div.appendChild(upBtn);
div.appendChild(downBtn);
return div;
}
addReorderButtons() {
const questions = this.formsetContainer.querySelectorAll('.question-form');
questions.forEach((question) => {
const controls = question.querySelector('.reorder-controls');
if (!controls) {
const firstRow = question.querySelector('.row');
if (firstRow) {
const col = firstRow.querySelector('.col-md-6');
if (col) {
const newControls = this.createReorderControls();
col.insertBefore(newControls, col.firstChild);
}
}
}
});
}
moveQuestion(questionForm, direction) {
const questions = Array.from(this.formsetContainer.querySelectorAll('.question-form'));
const currentIndex = questions.indexOf(questionForm);
if (direction === 'up' && currentIndex > 0) {
this.formsetContainer.insertBefore(questionForm, questions[currentIndex - 1]);
this.updateOrderNumbers();
} else if (direction === 'down' && currentIndex < questions.length - 1) {
this.formsetContainer.insertBefore(questions[currentIndex + 1], questionForm);
this.updateOrderNumbers();
}
}
updateOrderNumbers() {
const questions = this.formsetContainer.querySelectorAll('.question-form:not([style*="display: none"])');
questions.forEach((question, index) => {
const orderInput = question.querySelector('input[name$="-order"]');
if (orderInput) {
orderInput.value = index + 1;
}
});
}
updateQuestionNumbers() {
const questions = this.formsetContainer.querySelectorAll('.question-form:not([style*="display: none"])');
questions.forEach((question, index) => {
const questionNumber = question.querySelector('h6');
if (questionNumber) {
questionNumber.textContent = `Question #${index + 1}`;
}
});
}
setupQuestionTypeHandlers() {
const typeSelects = this.formsetContainer.querySelectorAll('select[name$="-question_type"]');
typeSelects.forEach(select => {
// Remove existing listener to avoid duplicates
select.removeEventListener('change', this.handleQuestionTypeChange);
// Add listener
select.addEventListener('change', (e) => this.handleQuestionTypeChange(e));
// Initial state
this.handleQuestionTypeChange({ target: select });
});
}
handleQuestionTypeChange(event) {
const select = event.target;
const questionForm = select.closest('.question-form');
const choicesField = questionForm?.querySelector('.choices-field');
if (!choicesField) return;
const questionType = select.value;
if (questionType === 'multiple_choice' || questionType === 'single_choice') {
choicesField.style.display = 'block';
choicesField.classList.add('fade-in');
} else {
choicesField.style.display = 'none';
}
}
showError(message) {
// Create alert element
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Add to page
const container = document.querySelector('.container-fluid');
if (container) {
container.insertBefore(alert, container.firstChild);
// Auto-remove after 5 seconds
setTimeout(() => {
alert.remove();
}, 5000);
}
}
}
// Initialize on DOM load
document.addEventListener('DOMContentLoaded', () => {
// Create survey builder instance
window.surveyBuilder = new SurveyBuilder('questions');
});