/* ============================================================ SURVAM — Main JavaScript (app.js) · v1.4 ============================================================ */ 'use strict'; document.addEventListener('DOMContentLoaded', () => { // ── Nav toggle (mobile) ────────────────────────────────── const navToggle = document.getElementById('navToggle'); const navLinks = document.querySelector('.nav-links'); if (navToggle && navLinks) { navToggle.addEventListener('click', () => { navLinks.classList.toggle('open'); }); } // ── User dropdown ──────────────────────────────────────── const userMenuBtn = document.getElementById('userMenuBtn'); const userDropdown = document.getElementById('userDropdown'); if (userMenuBtn && userDropdown) { userMenuBtn.addEventListener('click', (e) => { e.stopPropagation(); userDropdown.classList.toggle('open'); }); document.addEventListener('click', () => userDropdown.classList.remove('open')); } // ── Flash auto-dismiss ─────────────────────────────────── document.querySelectorAll('.flash').forEach(f => { setTimeout(() => f.style.transition = 'opacity 0.5s', 3500); setTimeout(() => { f.style.opacity = '0'; setTimeout(() => f.remove(), 500); }, 4000); }); // ── Tabs ───────────────────────────────────────────────── document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const group = btn.closest('[data-tabs]') || btn.closest('.tabs')?.parentElement; if (!group) return; const target = btn.dataset.tab; group.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); group.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); btn.classList.add('active'); const panel = group.querySelector(`[data-panel="${target}"]`); if (panel) panel.classList.add('active'); }); }); // ── Modals ─────────────────────────────────────────────── document.querySelectorAll('[data-modal-open]').forEach(btn => { btn.addEventListener('click', () => openModal(btn.dataset.modalOpen)); }); document.querySelectorAll('[data-modal-close]').forEach(btn => { btn.addEventListener('click', () => closeModal(btn.dataset.modalClose || btn.closest('.modal-overlay')?.id)); }); document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(overlay.id); }); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open')); }); }); function openModal(id) { const el = document.getElementById(id); if (el) { el.classList.add('open'); document.body.style.overflow = 'hidden'; } } function closeModal(id) { const el = document.getElementById(id); if (el) { el.classList.remove('open'); document.body.style.overflow = ''; } } // ── AJAX helper ───────────────────────────────────────────── async function apiPost(url, data) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(data) }); return res.json(); } // ── Form AJAX submit with loading state ────────────────────── document.querySelectorAll('[data-ajax-form]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const btn = form.querySelector('[type="submit"]'); const origText = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' Working…'; const fd = new FormData(form); const data = Object.fromEntries(fd.entries()); try { const res = await apiPost(form.action, data); if (res.redirect) { window.location.href = res.redirect; return; } if (res.reload) { window.location.reload(); return; } if (res.success) { showToast(res.message || 'Saved!', 'success'); } if (res.error) { showToast(res.error, 'error'); } } catch(ex) { showToast('Something went wrong. Please try again.', 'error'); } btn.disabled = false; btn.innerHTML = origText; }); }); // ── Toast ───────────────────────────────────────────────────── function showToast(msg, type = 'info') { const toast = document.createElement('div'); toast.className = `flash flash-${type}`; toast.style.cssText = 'position:fixed;top:1.25rem;right:1.25rem;z-index:9999;min-width:280px;max-width:420px;animation:slideInDown 0.25s ease'; toast.innerHTML = `${escHtml(msg)}`; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.4s'; setTimeout(() => toast.remove(), 400); }, 4000); } function escHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ── Copy to clipboard ───────────────────────────────────────── function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => showToast('Copied to clipboard!', 'success')); } // ── Survey status toggler (inline) ─────────────────────────── function toggleSurveyStatus(surveyId, currentStatus) { const newStatus = currentStatus === 'active' ? 'paused' : 'active'; apiPost('/api/surveys.php', { action: 'update_status', id: surveyId, status: newStatus }) .then(r => { if (r.success) window.location.reload(); else showToast(r.error || 'Failed', 'error'); }); } // ── Confirm delete ──────────────────────────────────────────── function confirmDelete(url, msg) { if (confirm(msg || 'Are you sure? This cannot be undone.')) { window.location.href = url; } } // ── Admin sidebar toggle ────────────────────────────────────── const sidebarToggle = document.getElementById('sidebarToggle'); const adminSidebar = document.getElementById('adminSidebar'); if (sidebarToggle && adminSidebar) { sidebarToggle.addEventListener('click', () => adminSidebar.classList.toggle('open')); } // ── Drag-and-drop ranking (survey renderer) ─────────────────── function initRankDrag(listId) { const list = document.getElementById(listId); if (!list) return; let dragging = null; list.querySelectorAll('.rank-item').forEach(item => { item.draggable = true; item.addEventListener('dragstart', () => { dragging = item; item.style.opacity = '0.4'; }); item.addEventListener('dragend', () => { dragging = null; item.style.opacity = '1'; updateRankValues(list); }); item.addEventListener('dragover', (e) => { e.preventDefault(); const after = getDragAfter(list, e.clientY); if (after) list.insertBefore(dragging, after); else list.appendChild(dragging); }); }); } function getDragAfter(list, y) { const items = [...list.querySelectorAll('.rank-item:not([style*="opacity: 0.4"])')]; return items.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > (closest.offset || -Infinity)) return { offset, element: child }; return closest; }, {}).element; } function updateRankValues(list) { list.querySelectorAll('.rank-item').forEach((item, i) => { const numEl = item.querySelector('.rank-num'); if (numEl) numEl.textContent = i + 1; const inp = item.querySelector('input[type="hidden"]'); if (inp) inp.value = i + 1; }); } // ── Slider live value display ───────────────────────────────── document.querySelectorAll('.slider-input').forEach(slider => { const valEl = slider.closest('.slider-wrap')?.querySelector('.slider-val'); if (valEl) { valEl.textContent = slider.value; slider.addEventListener('input', () => valEl.textContent = slider.value); } }); // ── Star rating ─────────────────────────────────────────────── document.querySelectorAll('.star-row').forEach(row => { const stars = [...row.querySelectorAll('.star-btn')]; stars.forEach((star, idx) => { star.addEventListener('click', () => { stars.forEach((s, i) => s.classList.toggle('lit', i <= idx)); const hiddenInp = row.querySelector('input[type="hidden"]'); if (hiddenInp) hiddenInp.value = idx + 1; }); star.addEventListener('mouseenter', () => stars.forEach((s, i) => s.style.color = i <= idx ? '#f59e0b' : '')); }); row.addEventListener('mouseleave', () => stars.forEach(s => s.style.color = '')); }); // ── NPS / Rating select ─────────────────────────────────────── document.querySelectorAll('.rating-btn, .nps-btn, .emoji-btn').forEach(btn => { btn.addEventListener('click', function() { const group = this.closest('.rating-row, .nps-row, .emoji-row'); if (group) group.querySelectorAll(this.tagName).forEach(b => b.classList.remove('selected')); this.classList.add('selected'); const hiddenInp = group?.querySelector('input[type="hidden"]'); if (hiddenInp) hiddenInp.value = this.dataset.value || this.textContent.trim(); }); }); // ── Choice item (radio/checkbox visual) ────────────────────── document.querySelectorAll('.choice-item').forEach(item => { item.addEventListener('click', () => { const inp = item.querySelector('input'); if (!inp) return; if (inp.type === 'radio') { item.closest('.choice-list')?.querySelectorAll('.choice-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); } else { item.classList.toggle('selected', inp.checked); } }); const inp = item.querySelector('input'); if (inp) inp.addEventListener('change', () => { if (inp.type === 'radio') { item.closest('.choice-list')?.querySelectorAll('.choice-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); } else { item.classList.toggle('selected', inp.checked); } }); }); // ── Survey page-by-page navigation ─────────────────────────── window.SurveyNav = { currentPage: 0, pages: [], init(pages) { this.pages = pages; this.show(0); }, show(idx) { this.pages.forEach((p, i) => p.style.display = i === idx ? '' : 'none'); this.currentPage = idx; this.updateProgress(); }, next() { if (this.currentPage < this.pages.length - 1) { // Skip validation entirely in preview mode if (!window.SURVAM_PREVIEW_MODE && !this.validatePage(this.currentPage)) return; this.show(this.currentPage + 1); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, prev() { if (this.currentPage > 0) { this.show(this.currentPage - 1); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, validatePage(idx) { let valid = true; const page = this.pages[idx]; // Find required blocks — skip display-only types entirely const requiredBlocks = [...page.querySelectorAll('[data-required="1"]')] .filter(b => !b.classList.contains('sq-display-only')); // If no required questions on this page, always allow navigation if (requiredBlocks.length === 0) return true; requiredBlocks.forEach(block => { let answered = false; // Radio / checkbox if (block.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked')) answered = true; // Text inputs if (!answered) { const inp = block.querySelector( 'textarea, select, input[type="text"], input[type="number"], ' + 'input[type="email"], input[type="date"], input[type="datetime-local"], ' + 'input[type="time"], input[type="tel"]' ); if (inp && inp.value && inp.value.trim()) answered = true; } // Hidden value fields (rating, NPS, slider, emoji — default value "50" counts) if (!answered) { const hidden = block.querySelector('input[type="hidden"][id$="_val"]'); if (hidden && hidden.value !== '' && hidden.value !== '0') answered = true; } // Visually selected if (!answered && block.querySelector('.selected')) answered = true; if (!answered) { block.classList.add('shake'); setTimeout(() => block.classList.remove('shake'), 600); valid = false; } }); if (!valid) showToast('Please answer all required questions before continuing.', 'warning'); return valid; }, updateProgress() { const pct = Math.round(((this.currentPage) / Math.max(this.pages.length - 1, 1)) * 100); const fill = document.getElementById('progressFill'); const label = document.getElementById('progressLabel'); if (fill) fill.style.width = pct + '%'; if (label) label.textContent = `Page ${this.currentPage + 1} of ${this.pages.length}`; } }; // ── Constant sum validation ─────────────────────────────────── document.querySelectorAll('.const-sum-inputs').forEach(wrap => { const total = parseInt(wrap.dataset.total || '100'); const inputs = wrap.querySelectorAll('input[type="number"]'); const remaining = wrap.querySelector('.const-sum-remaining'); const update = () => { let used = 0; inputs.forEach(inp => used += parseInt(inp.value || 0)); const left = total - used; if (remaining) { remaining.textContent = left; remaining.style.color = left === 0 ? 'var(--brand-teal)' : left < 0 ? 'var(--brand-accent)' : 'inherit'; } }; inputs.forEach(inp => inp.addEventListener('input', update)); update(); }); // ── Shake animation CSS (injected) ─────────────────────────── const shakeStyle = document.createElement('style'); shakeStyle.textContent = `@keyframes shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-6px)} 40%,80%{transform:translateX(6px)} } .shake{animation:shake 0.5s ease;border-color:var(--brand-accent)!important;}`; document.head.appendChild(shakeStyle); // ── Survey renderer: named select helpers (called inline from s/index.php) ── function selectRating(qid, val) { document.getElementById(qid + '_val').value = val; const row = document.getElementById('rating_' + qid); if (row) row.querySelectorAll('.rating-btn').forEach(b => { b.classList.toggle('selected', parseInt(b.dataset.value) <= val); }); } function selectStar(qid, val) { document.getElementById(qid + '_val').value = val; const row = document.getElementById('stars_' + qid); if (row) row.querySelectorAll('.star-btn').forEach((b, i) => { b.classList.toggle('lit', i < val); }); } function selectNPS(qid, val) { document.getElementById(qid + '_val').value = val; document.querySelectorAll('.nps-row .nps-btn').forEach(b => { b.classList.toggle('selected', parseInt(b.textContent.trim()) === val); }); } function selectEmoji(qid, label, el) { document.getElementById(qid + '_val').value = label; el.closest('.emoji-row').querySelectorAll('.emoji-btn').forEach(b => b.classList.remove('selected')); el.classList.add('selected'); } function clearSignature(qid) { const canvas = document.getElementById('sig_' + qid); if (canvas) { const ctx = canvas.getContext('2d'); ctx.clearRect(0,0,canvas.width,canvas.height); } document.getElementById(qid + '_data').value = ''; }