309 lines
11 KiB
JavaScript
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');
|
|
});
|