/** * 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 = '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 = ''; 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 = ''; 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 = ''; 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} `; // 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'); });