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