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

296 lines
10 KiB
JavaScript

/**
* Survey Preview Module
* Provides real-time preview of survey questions as they are being built
*/
class SurveyPreview {
constructor() {
this.previewContainer = null;
this.init();
}
init() {
document.addEventListener('DOMContentLoaded', () => {
this.createPreviewPanel();
this.setupEventListeners();
});
}
createPreviewPanel() {
// Find the main container
const mainContainer = document.querySelector('.container-fluid');
if (!mainContainer) return;
// Create preview card
const previewCard = document.createElement('div');
previewCard.className = 'card';
previewCard.id = 'survey-preview-card';
previewCard.style.marginTop = '20px';
previewCard.style.display = 'none'; // Hidden by default
previewCard.innerHTML = `
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-eye me-2"></i>Survey Preview</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-primary" id="toggle-preview">
<i class="bi bi-arrows-expand"></i> Expand
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="close-preview">
<i class="bi bi-x-lg"></i> Close
</button>
</div>
</div>
<div class="card-body" id="preview-content">
<div class="text-center text-muted py-5">
<i class="bi bi-eye-slash" style="font-size: 3rem;"></i>
<p class="mt-3">Add questions to see preview</p>
</div>
</div>
`;
// Add preview toggle button to header
const headerTitle = document.querySelector('h2');
if (headerTitle) {
const toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'btn btn-outline-primary ms-3';
toggleBtn.innerHTML = '<i class="bi bi-eye"></i> Preview';
toggleBtn.addEventListener('click', () => this.togglePreview());
headerTitle.parentNode.appendChild(toggleBtn);
}
// Insert preview card after main form
const mainRow = document.querySelector('.row');
if (mainRow) {
mainRow.parentNode.insertBefore(previewCard, mainRow.nextSibling);
}
this.previewContainer = previewCard;
}
setupEventListeners() {
// Toggle preview button
const toggleBtn = document.getElementById('toggle-preview');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.togglePreviewSize());
}
// Close preview button
const closeBtn = document.getElementById('close-preview');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.togglePreview());
}
// Watch for form changes
const form = document.getElementById('survey-template-form');
if (form) {
const observer = new MutationObserver(() => {
this.updatePreview();
});
observer.observe(form, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['value', 'checked']
});
}
// Listen for input changes
document.addEventListener('input', (e) => {
if (e.target.closest('#questions-container')) {
this.updatePreview();
}
});
document.addEventListener('change', (e) => {
if (e.target.closest('#questions-container')) {
this.updatePreview();
}
});
}
togglePreview() {
if (!this.previewContainer) return;
const isHidden = this.previewContainer.style.display === 'none';
this.previewContainer.style.display = isHidden ? 'block' : 'none';
if (isHidden) {
this.updatePreview();
}
}
togglePreviewSize() {
if (!this.previewContainer) return;
const cardBody = this.previewContainer.querySelector('.card-body');
const isExpanded = cardBody.classList.contains('expanded');
if (isExpanded) {
cardBody.classList.remove('expanded');
cardBody.style.maxHeight = '500px';
cardBody.style.overflowY = 'auto';
} else {
cardBody.classList.add('expanded');
cardBody.style.maxHeight = 'none';
cardBody.style.overflow = 'visible';
}
}
updatePreview() {
if (!this.previewContainer || this.previewContainer.style.display === 'none') {
return;
}
const previewContent = document.getElementById('preview-content');
if (!previewContent) return;
// Get survey name
const nameInput = document.querySelector('input[name="name"]');
const name = nameInput ? nameInput.value : 'Untitled Survey';
// Get all questions
const questionForms = document.querySelectorAll('.question-form:not([style*="display: none"])');
const questions = Array.from(questionForms).map(form => this.extractQuestionData(form)).filter(q => q);
if (questions.length === 0) {
previewContent.innerHTML = `
<div class="text-center text-muted py-5">
<i class="bi bi-eye-slash" style="font-size: 3rem;"></i>
<p class="mt-3">Add questions to see preview</p>
</div>
`;
return;
}
// Generate preview HTML
let previewHTML = `
<div class="survey-preview">
<div class="survey-title mb-4">
<h4>${this.escapeHtml(name)}</h4>
<p class="text-muted">Preview - ${questions.length} question(s)</p>
</div>
`;
questions.forEach((q, index) => {
previewHTML += this.renderQuestionPreview(q, index + 1);
});
previewHTML += '</div>';
previewContent.innerHTML = previewHTML;
}
extractQuestionData(form) {
const textInput = form.querySelector('input[name$="-text"]');
const textArInput = form.querySelector('input[name$="-text_ar"]');
const typeSelect = form.querySelector('select[name$="-question_type"]');
const requiredCheckbox = form.querySelector('input[name$="-is_required"]');
const choicesTextarea = form.querySelector('textarea[name$="-choices_json"]');
if (!textInput || !textInput.value.trim()) {
return null;
}
const data = {
text: textInput.value,
text_ar: textArInput ? textArInput.value : '',
type: typeSelect ? typeSelect.value : 'text',
required: requiredCheckbox ? requiredCheckbox.checked : false,
choices: []
};
if (choicesTextarea) {
try {
const choices = JSON.parse(choicesTextarea.value);
if (Array.isArray(choices)) {
data.choices = choices;
}
} catch (e) {
// Invalid JSON, ignore
}
}
return data;
}
renderQuestionPreview(question, number) {
let questionHTML = `
<div class="survey-question-preview mb-4 p-3 border rounded">
<div class="mb-2">
<strong>Q${number}:</strong> ${this.escapeHtml(question.text)}
${question.required ? '<span class="text-danger">*</span>' : ''}
</div>
`;
switch (question.type) {
case 'text':
questionHTML += `
<input type="text" class="form-control" placeholder="Enter your answer" disabled>
`;
break;
case 'rating':
questionHTML += `
<div class="rating-preview">
${[1, 2, 3, 4, 5].map(n => `
<label class="rating-option me-2">
<input type="radio" name="preview-rating-${number}" value="${n}" disabled>
<span class="badge ${n <= 3 ? 'bg-warning' : 'bg-success'}">${n}</span>
</label>
`).join('')}
</div>
`;
break;
case 'single_choice':
if (question.choices.length > 0) {
questionHTML += '<div class="choices-preview">';
question.choices.forEach(choice => {
questionHTML += `
<label class="d-block mb-1">
<input type="radio" name="preview-choice-${number}" disabled>
${this.escapeHtml(choice.label)}
</label>
`;
});
questionHTML += '</div>';
} else {
questionHTML += '<p class="text-muted small">No choices defined</p>';
}
break;
case 'multiple_choice':
if (question.choices.length > 0) {
questionHTML += '<div class="choices-preview">';
question.choices.forEach(choice => {
questionHTML += `
<label class="d-block mb-1">
<input type="checkbox" disabled>
${this.escapeHtml(choice.label)}
</label>
`;
});
questionHTML += '</div>';
} else {
questionHTML += '<p class="text-muted small">No choices defined</p>';
}
break;
}
questionHTML += '</div>';
return questionHTML;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
window.surveyPreview = new SurveyPreview();
});