/* ============================================================
SURVAM — Survey Builder (builder.js)
============================================================ */
'use strict';
const Builder = (() => {
let state = {
surveyId: null,
pages: [], // [{id, page_number, title, questions:[...]}]
activePageIdx: 0,
activeQIdx: null,
dirty: false,
saving: false,
};
// ── Init ───────────────────────────────────────────────────
function init(surveyId, surveyData) {
state.surveyId = surveyId;
if (surveyData && surveyData.pages) {
state.pages = surveyData.pages;
} else {
state.pages = [newPage(1)];
}
renderAll();
bindEvents();
autoSave();
}
// ── Data helpers ──────────────────────────────────────────
function newPage(num) {
return { id: null, page_number: num, title: `Page ${num}`, description: '', questions: [] };
}
function newQuestion(type) {
const q = {
id: null, type, title: 'Untitled question', description: '',
required: false, options: defaultOptions(type), settings: defaultSettings(type),
logic: [], loop_config: null, pipe_config: null,
media_url: '', media_type: 'none',
_uid: 'q_' + Math.random().toString(36).substr(2,9),
};
return q;
}
function defaultOptions(type) {
const choiceTypes = ['single_choice','multi_choice','dropdown','image_choice'];
if (choiceTypes.includes(type)) return { choices: ['Option 1','Option 2','Option 3'] };
if (type === 'rating_scale') return { min:1, max:10, min_label:'Not at all', max_label:'Extremely' };
if (type === 'star_rating') return { stars:5 };
if (type === 'nps') return { min_label:'Not likely', max_label:'Extremely likely' };
if (type === 'slider') return { min:0, max:100, step:1, default:50 };
if (type === 'likert_scale') return { scale: ['Strongly Disagree','Disagree','Neutral','Agree','Strongly Agree'], statements: ['Statement 1'] };
if (type === 'matrix_single' || type === 'matrix_multi') return { rows:['Row 1','Row 2'], cols:['Col 1','Col 2','Col 3'] };
if (type === 'ranking') return { items: ['Item 1','Item 2','Item 3'] };
if (type === 'emoji_scale') return { emojis: [{icon:'😡',label:'Very Bad'},{icon:'😟',label:'Bad'},{icon:'😐',label:'Neutral'},{icon:'😊',label:'Good'},{icon:'😍',label:'Excellent'}] };
if (type === 'constant_sum') return { items:['Item 1','Item 2','Item 3'], total:100 };
if (type === 'max_diff') return { items:['Option A','Option B','Option C','Option D'], sets_count:3, per_set:3 };
if (type === 'demographic') return { fields: ['name','email','age','gender','location'] };
if (type === 'date_time') return { mode: 'date' }; // date|time|datetime
if (type === 'number') return { min:'', max:'', decimal:false };
if (type === 'file_upload') return { max_mb:5, allowed:'image/*,application/pdf' };
if (type === 'heatmap') return { image_url:'', max_clicks:3 };
if (type === 'card_sort') return { cards:['Card 1','Card 2','Card 3'], categories:['Category A','Category B'] };
return {};
}
function defaultSettings(type) {
return { randomize: false, other_option: false, none_option: false, other_text: 'Other (please specify)' };
}
// ── Render ────────────────────────────────────────────────
function renderAll() {
renderCanvas();
renderPageTabs();
if (state.activeQIdx !== null) renderProps();
else renderNoSelection();
}
function renderPageTabs() {
const tabs = document.getElementById('pageTabs');
if (!tabs) return;
tabs.innerHTML = state.pages.map((p,i) => `
`).join('') + ``;
}
function renderCanvas() {
const canvas = document.getElementById('builderCanvas');
if (!canvas) return;
const page = state.pages[state.activePageIdx];
if (!page) return;
canvas.innerHTML = `
${page.questions.length === 0 ? `
📋
Drag a question type from the left panel,
or click a type to add it here.
` : ''}
${page.questions.map((q,qi) => renderQuestionBlock(q, qi)).join('')}
`;
initQuestionDrag();
}
function renderQuestionBlock(q, qi) {
const isActive = qi === state.activeQIdx;
const mediaPreview = (q.media_url && q.media_type && q.media_type !== 'none') ? `
${q.media_type === 'image' ? `
})
` : ''}
${q.media_type === 'image_upload' ? `
})
` : ''}
${q.media_type === 'video' ? `
🎬 Video stimulus
${escH(q.media_url.substring(0,50))}${q.media_url.length>50?'...':''}
` : ''}
` : '';
return `
${qi + 1}
${questionTypeLabel(q.type)}
${mediaPreview}
${escH(q.title || 'Untitled question')}${q.required ? ' *' : ''}
`;
}
function renderProps() {
const panel = document.getElementById('propsPanel');
if (!panel) return;
const page = state.pages[state.activePageIdx];
const q = page?.questions[state.activeQIdx];
if (!q) { renderNoSelection(); return; }
panel.innerHTML = buildPropsHTML(q, state.activeQIdx);
}
function renderNoSelection() {
const panel = document.getElementById('propsPanel');
if (!panel) return;
panel.innerHTML = `
👈
Click a question to edit its properties
`;
}
function buildPropsHTML(q, qi) {
const s = q.settings || {};
const o = q.options || {};
let optionsHTML = buildOptionsHTML(q);
return `
${optionsHTML}
Branching / Skip Logic
📎 Stimulus Media
${(q.media_type && q.media_type !== 'none') ? `
${q.media_type === 'image_upload' ? `
${q.media_url ? `
})
` : '
🖼
'}
${q.media_url ? `
✓ Image uploaded
` : '
JPG, PNG, GIF, WebP · Max 5MB
'}
` : `
${q.media_url && q.media_type==='image' ? `
})
` : ''}
${q.media_url && q.media_type==='video' ? `
✓ Video URL saved — will embed in survey
` : ''}
`}
Show media above question
` : ''}
Piping
Pipe answer from earlier question
${q.pipe_config ? `
` : ''}
`;
}
function buildOptionsHTML(q) {
const o = q.options || {};
const qi = state.activeQIdx;
if (['single_choice','multi_choice','dropdown','image_choice'].includes(q.type)) {
const choices = o.choices || [];
const screenouts = o.screenout_urls || {};
return `
Answer Options
Tip: Add a 🚫 Screenout URL per option to redirect disqualified respondents.
${choices.map((c,ci) => `
`).join('')}
`;
}
if (q.type === 'ranking') {
const items = o.items || [];
return `
Items to Rank
${items.map((item,ii) => `
`).join('')}
`;
}
if (q.type === 'likert_scale') {
const stmts = o.statements || [];
return `
Statements
${stmts.map((s,si) => `
`).join('')}
Scale Labels
${(o.scale||[]).map((lbl,li) => `
`).join('')}
`;
}
if (q.type === 'matrix_single' || q.type === 'matrix_multi') {
return `
Rows
${(o.rows||[]).map((r,ri) => `
`).join('')}
Columns
${(o.cols||[]).map((c,ci) => `
`).join('')}
`;
}
if (q.type === 'rating_scale' || q.type === 'nps') {
return ``;
}
if (q.type === 'slider') {
return ``;
}
if (q.type === 'constant_sum') {
return ``;
}
return '';
}
// ── Logic Editor ──────────────────────────────────────────
function openLogicEditor(qi) {
const page = state.pages[state.activePageIdx];
const q = page.questions[qi];
const prevQs = page.questions.slice(0, qi);
const el = document.getElementById('logicEditorModal');
if (!el) return;
document.getElementById('logicEditorTitle').textContent = `Logic: ${q.title}`;
document.getElementById('logicEditorBody').innerHTML = buildLogicEditorHTML(q, prevQs, qi);
openModal('logicEditorModal');
}
function buildLogicEditorHTML(q, prevQs, qi) {
const rules = q.logic || [];
if (prevQs.length === 0) return `Add at least one question before this one to create logic rules.
`;
return `
Show/skip this question based on answers to previous questions.
${rules.map((rule,ri) => renderLogicRule(rule, ri, prevQs)).join('')}
`;
}
function renderLogicRule(rule, ri, prevQs) {
return `
`;
}
// ── Quota Editor ──────────────────────────────────────────
function openQuotaEditor() {
apiPost(window.SURVAM_API_URL, { action:'get_quotas', survey_id: state.surveyId })
.then(r => {
const el = document.getElementById('quotaEditorModal');
if (!el) return;
document.getElementById('quotaEditorBody').innerHTML = buildQuotaHTML(r.quotas || []);
openModal('quotaEditorModal');
});
}
function buildQuotaHTML(quotas) {
return `
${quotas.length === 0 ? `
No quotas set. Add a quota to cap responses by demographic or answer.
` : ''}
${quotas.map((q,i) => `
${escH(q.name)} — Limit: ${q.limit_count} (Used: ${q.current_count})
`).join('')}
Add New Quota
`;
}
// ── Save ──────────────────────────────────────────────────
async function save(showMsg = true) {
if (state.saving) return;
state.saving = true;
const saveBtn = document.getElementById('saveBtn');
if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving…'; }
try {
const res = await apiPost(window.SURVAM_API_URL, {
action: 'save_structure', survey_id: state.surveyId, pages: state.pages,
});
if (res.success) {
state.dirty = false;
// Update IDs from server response
if (res.page_ids) res.page_ids.forEach((pid,i) => { if (state.pages[i]) state.pages[i].id = pid; });
if (res.question_ids) {
let qi = 0;
state.pages.forEach(p => { p.questions.forEach(q => { if (res.question_ids[qi]) q.id = res.question_ids[qi]; qi++; }); });
}
if (showMsg) showToast('Survey saved!', 'success');
} else { showToast(res.error || 'Save failed', 'error'); }
} catch(e) { showToast('Save failed. Check connection.', 'error'); }
state.saving = false;
if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'Save'; }
}
function autoSave() {
setInterval(() => { if (state.dirty) save(false); }, 30000);
window.addEventListener('beforeunload', (e) => { if (state.dirty) { e.preventDefault(); e.returnValue = ''; } });
}
// ── Public API (prefixed with _ for internal use) ─────────
function _addQ(type) {
const page = state.pages[state.activePageIdx];
const q = newQuestion(type);
page.questions.push(q);
state.activeQIdx = page.questions.length - 1;
state.dirty = true;
renderAll();
document.querySelector(`[data-qi="${state.activeQIdx}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function _selectQ(qi) {
state.activeQIdx = qi;
renderCanvas();
renderProps();
}
function _deleteQ(qi) {
if (!confirm('Delete this question?')) return;
state.pages[state.activePageIdx].questions.splice(qi, 1);
state.activeQIdx = null; state.dirty = true;
renderAll();
}
function _duplicateQ(qi) {
const q = JSON.parse(JSON.stringify(state.pages[state.activePageIdx].questions[qi]));
q.id = null; q._uid = 'q_' + Math.random().toString(36).substr(2,9);
q.title = q.title + ' (copy)';
state.pages[state.activePageIdx].questions.splice(qi + 1, 0, q);
state.activeQIdx = qi + 1; state.dirty = true;
renderAll();
}
function _moveQ(qi, dir) {
const qs = state.pages[state.activePageIdx].questions;
const ni = qi + dir;
if (ni < 0 || ni >= qs.length) return;
[qs[qi], qs[ni]] = [qs[ni], qs[qi]];
state.activeQIdx = ni; state.dirty = true;
renderAll();
}
function _setField(qi, field, val) {
state.pages[state.activePageIdx].questions[qi][field] = val;
state.dirty = true;
// Re-render only the block title
const block = document.querySelector(`[data-qi="${qi}"] .question-title-display`);
if (block && field === 'title') block.innerHTML = escH(val) + (state.pages[state.activePageIdx].questions[qi].required ? ' *' : '');
if (field === 'required') { const star = document.querySelector(`[data-qi="${qi}"] .question-title-display`); if(star) star.innerHTML = escH(state.pages[state.activePageIdx].questions[qi].title) + (val ? ' *' : ''); }
}
function _setSetting(qi, key, val) {
if (!state.pages[state.activePageIdx].questions[qi].settings) state.pages[state.activePageIdx].questions[qi].settings = {};
state.pages[state.activePageIdx].questions[qi].settings[key] = val;
state.dirty = true;
}
function _setOpt(qi, key, val) {
if (!state.pages[state.activePageIdx].questions[qi].options) state.pages[state.activePageIdx].questions[qi].options = {};
state.pages[state.activePageIdx].questions[qi].options[key] = val;
state.dirty = true;
}
function _setChoice(qi, ci, val) {
state.pages[state.activePageIdx].questions[qi].options.choices[ci] = val;
state.dirty = true;
}
function _addChoice(qi) {
const choices = state.pages[state.activePageIdx].questions[qi].options.choices;
choices.push('Option ' + (choices.length + 1));
state.dirty = true; renderProps();
}
function _setScreenoutUrl(qi, ci, url) {
const q = state.pages[state.activePageIdx].questions[qi];
if (!q.options.screenout_urls) q.options.screenout_urls = {};
q.options.screenout_urls[ci] = url;
state.dirty = true;
}
function _removeChoice(qi, ci) {
state.pages[state.activePageIdx].questions[qi].options.choices.splice(ci, 1);
state.dirty = true; renderProps();
}
function _setOption(qi, key, idx, val) {
state.pages[state.activePageIdx].questions[qi].options[key][idx] = val;
state.dirty = true;
}
function _addOptionItem(qi, key, defaultVal) {
const arr = state.pages[state.activePageIdx].questions[qi].options[key] || [];
arr.push(defaultVal + ' ' + (arr.length + 1));
state.pages[state.activePageIdx].questions[qi].options[key] = arr;
state.dirty = true; renderProps();
}
function _removeOptionItem(qi, key, idx) {
state.pages[state.activePageIdx].questions[qi].options[key].splice(idx, 1);
state.dirty = true; renderProps();
}
function _changeType(qi, newType) {
const q = state.pages[state.activePageIdx].questions[qi];
q.type = newType;
q.options = defaultOptions(newType);
q.settings = defaultSettings(newType);
state.dirty = true; renderAll();
}
function _bulkEditChoices(qi) {
const choices = state.pages[state.activePageIdx].questions[qi].options.choices || [];
const val = prompt('Enter one option per line:', choices.join('\n'));
if (val !== null) {
state.pages[state.activePageIdx].questions[qi].options.choices = val.split('\n').map(s => s.trim()).filter(Boolean);
state.dirty = true; renderProps();
}
}
function _addPage() {
state.pages.push(newPage(state.pages.length + 1));
state.activePageIdx = state.pages.length - 1;
state.activeQIdx = null; state.dirty = true;
renderAll();
}
function _selectPage(idx) {
state.activePageIdx = idx;
state.activeQIdx = null;
renderAll();
}
function _deletePage(idx) {
if (!confirm('Delete this page and all its questions?')) return;
state.pages.splice(idx, 1);
state.activePageIdx = Math.max(0, idx - 1);
state.activeQIdx = null; state.dirty = true;
renderAll();
}
function _setPageTitle(idx, val) { state.pages[idx].title = val; state.dirty = true; renderPageTabs(); }
function _setPageDesc(idx, val) { state.pages[idx].description = val; state.dirty = true; }
function _addLogicRule(qi) {
const q = state.pages[state.activePageIdx].questions[qi];
if (!q.logic) q.logic = [];
q.logic.push({ q_uid: '', operator: 'equals', value: '' });
state.dirty = true; openLogicEditor(qi);
}
function _setLogicQ(ri, val) {
const q = state.pages[state.activePageIdx].questions[state.activeQIdx];
if (q.logic[ri]) q.logic[ri].q_uid = val; state.dirty = true;
}
function _setLogicOp(ri, val) {
const q = state.pages[state.activePageIdx].questions[state.activeQIdx];
if (q.logic[ri]) q.logic[ri].operator = val; state.dirty = true;
}
function _setLogicVal(ri, val) {
const q = state.pages[state.activePageIdx].questions[state.activeQIdx];
if (q.logic[ri]) q.logic[ri].value = val; state.dirty = true;
}
function _removeLogicRule(ri) {
const q = state.pages[state.activePageIdx].questions[state.activeQIdx];
q.logic.splice(ri, 1); state.dirty = true;
openLogicEditor(state.activeQIdx);
}
function _saveLogic(qi) {
const q = state.pages[state.activePageIdx].questions[qi];
q.logic_match = document.getElementById('logicMatchMode')?.value || 'all';
q.logic_action = document.getElementById('logicAction')?.value || 'show';
q.screenout_url = document.getElementById('screenoutUrl')?.value || '';
state.dirty = true;
closeModal('logicEditorModal');
renderProps();
showToast('Logic saved', 'success');
}
function _togglePipe(qi, enabled) {
const q = state.pages[state.activePageIdx].questions[qi];
q.pipe_config = enabled ? { source: '' } : null;
state.dirty = true; renderProps();
}
function _setPipeSource(qi, uid) {
state.pages[state.activePageIdx].questions[qi].pipe_config.source = uid;
state.dirty = true;
}
function _addQuota() {
const name = document.getElementById('newQuotaName')?.value?.trim();
const limit = parseInt(document.getElementById('newQuotaLimit')?.value || '0');
const action = document.getElementById('newQuotaAction')?.value;
if (!name || !limit) { showToast('Enter quota name and limit', 'warning'); return; }
apiPost(window.SURVAM_API_URL, { action:'add_quota', survey_id: state.surveyId, name, limit_count: limit, action, conditions: [] })
.then(r => { if (r.success) openQuotaEditor(); else showToast(r.error, 'error'); });
}
function _previewQ(qi) {
const page = state.pages[state.activePageIdx];
const q = page.questions[qi];
if (!q) return;
const o = q.options || {};
const s = q.settings || {};
// Build preview HTML for this question
const mediaUrl = q.media_url || s.media_url || '';
const mediaType = q.media_type || s.media_type || 'none';
const mediaPos = q.media_position || s.media_position || 'above';
let mediaHtml = '';
if (mediaUrl && mediaType !== 'none') {
if (mediaType === 'image' || mediaType === 'image_upload') {
mediaHtml = `
`;
} else if (mediaType === 'video') {
let embed = escH(mediaUrl);
const ytMatch = mediaUrl.match(/youtube\.com\/watch\?v=([^&]+)|youtu\.be\/([^?]+)/);
const vimMatch = mediaUrl.match(/vimeo\.com\/(\d+)/);
if (ytMatch) embed = `https://www.youtube.com/embed/${ytMatch[1]||ytMatch[2]}?rel=0`;
else if (vimMatch) embed = `https://player.vimeo.com/video/${vimMatch[1]}`;
mediaHtml = ``;
}
}
let answerHtml = '';
const type = q.type;
if (['single_choice','multi_choice','dropdown'].includes(type)) {
const choices = o.choices || [];
if (type === 'dropdown') {
answerHtml = ``;
} else {
const inputType = type === 'multi_choice' ? 'checkbox' : 'radio';
answerHtml = `${choices.map(c=>`
`).join('')}
`;
}
} else if (type === 'yes_no') {
answerHtml = ``;
} else if (type === 'text_short') {
answerHtml = ``;
} else if (type === 'text_long') {
answerHtml = ``;
} else if (type === 'number') {
answerHtml = ``;
} else if (type === 'rating_scale' || type === 'nps') {
const min = o.min ?? (type === 'nps' ? 0 : 1);
const max = o.max ?? (type === 'nps' ? 10 : 10);
const btns = [];
for (let i = min; i <= max; i++) btns.push(``);
answerHtml = `${btns.join('')}
`;
if (o.min_label || o.max_label) answerHtml += `${escH(o.min_label||'')}${escH(o.max_label||'')}
`;
} else if (type === 'star_rating') {
const stars = o.stars || 5;
answerHtml = `${Array.from({length:stars},(_,i)=>``).join('')}
`;
} else if (type === 'slider') {
answerHtml = ``;
} else if (type === 'emoji_scale') {
const emojis = o.emojis || [{icon:'😡',label:'Bad'},{icon:'😐',label:'Neutral'},{icon:'😊',label:'Good'}];
answerHtml = `${emojis.map(e=>``).join('')}
`;
} else if (type === 'date_time') {
const mode = o.mode || 'date';
answerHtml = ``;
} else if (type === 'statement') {
answerHtml = `${escH(q.description||q.title)}
`;
} else if (type === 'section_break') {
answerHtml = `
`;
} else {
answerHtml = `[${questionTypeLabel(type)} — preview not available for this type]
`;
}
const titleHtml = `${escH(q.title||'Untitled question')}${q.required?'*':''}
`;
const descHtml = q.description ? `${escH(q.description)}
` : '';
const aboveMedia = (mediaUrl && mediaPos === 'above') ? mediaHtml : '';
const belowMedia = (mediaUrl && mediaPos === 'below') ? mediaHtml : '';
const body = `${aboveMedia}${titleHtml}${descHtml}${belowMedia}${answerHtml}`;
// Create/show the modal
let overlay = document.getElementById('qPreviewOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'qPreviewOverlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(15,15,30,0.75);z-index:99999;display:flex;align-items:center;justify-content:center;padding:1.5rem;backdrop-filter:blur(4px);';
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.style.display='none'; });
document.body.appendChild(overlay);
}
overlay.innerHTML = `
Question Preview
${questionTypeLabel(q.type)}
${body}
Preview only — no data saved
`;
overlay.style.display = 'flex';
}
async function _uploadMediaFile(qi, input) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'media');
// Show uploading state
const btn = input.closest('[style*="dashed"]')?.querySelector('button');
const origText = btn ? btn.innerHTML : '';
if (btn) btn.innerHTML = ' Uploading…';
try {
const res = await fetch(window.SURVAM_API_URL.replace('surveys.php', 'upload.php'), {
method: 'POST', body: formData
});
const data = await res.json();
if (data.success) {
state.pages[state.activePageIdx].questions[qi].media_url = data.url;
state.dirty = true;
renderProps();
showToast('Image uploaded!', 'success');
} else {
showToast(data.error || 'Upload failed', 'error');
if (btn) btn.innerHTML = origText;
}
} catch(e) {
showToast('Upload failed. Check connection.', 'error');
if (btn) btn.innerHTML = origText;
}
}
function _deleteQuota(id) {
if (!confirm('Delete this quota?')) return;
apiPost(window.SURVAM_API_URL, { action:'delete_quota', id }).then(r => { if (r.success) openQuotaEditor(); });
}
function _openLogicEditor(qi) { openLogicEditor(qi); }
// ── Drag & drop questions ─────────────────────────────────
function initQuestionDrag() {
const list = document.getElementById('questionList');
if (!list) return;
let dragging = null;
list.querySelectorAll('.question-block').forEach(block => {
block.draggable = true;
block.addEventListener('dragstart', () => { dragging = block; block.classList.add('dragging'); });
block.addEventListener('dragend', () => {
dragging = null; block.classList.remove('dragging');
// Re-sync state order
const newOrder = [...list.querySelectorAll('.question-block')].map(b => parseInt(b.dataset.qi));
const page = state.pages[state.activePageIdx];
const reordered = newOrder.map(i => page.questions[i]);
page.questions = reordered;
state.dirty = true;
renderCanvas(); renderProps();
});
block.addEventListener('dragover', (e) => {
e.preventDefault();
const after = getDragAfterEl(list, e.clientY);
if (after) list.insertBefore(dragging, after);
else list.appendChild(dragging);
});
});
}
function getDragAfterEl(list, y) {
const els = [...list.querySelectorAll('.question-block:not(.dragging)')];
return els.reduce((closest, el) => {
const box = el.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > (closest.offset || -Infinity)) return { offset, element: el };
return closest;
}, {}).element;
}
// ── Events ────────────────────────────────────────────────
function bindEvents() {
document.getElementById('saveBtn')?.addEventListener('click', () => save(true));
document.getElementById('previewBtn')?.addEventListener('click', () => {
save(false).then(() => window.open(`/s/${state.surveyId}?preview=1`, '_blank'));
});
document.getElementById('openQuotaBtn')?.addEventListener('click', openQuotaEditor);
// Left panel: add question by clicking type
document.querySelectorAll('.q-type-btn').forEach(btn => {
btn.addEventListener('click', () => _addQ(btn.dataset.type));
});
}
// ── Helpers ───────────────────────────────────────────────
function getPipeableQuestions(qi) {
const page = state.pages[state.activePageIdx];
return page.questions.slice(0, qi).filter(q => !['statement','section_break'].includes(q.type));
}
function escH(str) {
return String(str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
return {
init, save,
_addQ, _selectQ, _deleteQ, _duplicateQ, _moveQ,
_setField, _setSetting, _setOpt,
_setChoice, _addChoice, _removeChoice,
_setScreenoutUrl,
_setOption, _addOptionItem, _removeOptionItem,
_changeType, _bulkEditChoices,
_addPage, _selectPage, _deletePage, _setPageTitle, _setPageDesc,
_addLogicRule, _setLogicQ, _setLogicOp, _setLogicVal, _removeLogicRule, _saveLogic,
_openLogicEditor,
_togglePipe, _setPipeSource,
_renderProps: renderProps,
_addQuota, _deleteQuota,
_previewQ,
_uploadMediaFile,
};
})();
// ── Question type registry ────────────────────────────────────
const ALL_QUESTION_TYPES = [
{ value:'single_choice', label:'Single Choice' },
{ value:'multi_choice', label:'Multiple Choice' },
{ value:'dropdown', label:'Dropdown' },
{ value:'text_short', label:'Short Text' },
{ value:'text_long', label:'Long Text / Essay' },
{ value:'rating_scale', label:'Rating Scale' },
{ value:'star_rating', label:'Star Rating' },
{ value:'likert_scale', label:'Likert Scale' },
{ value:'matrix_single', label:'Matrix – Single' },
{ value:'matrix_multi', label:'Matrix – Multiple' },
{ value:'ranking', label:'Ranking' },
{ value:'slider', label:'Slider' },
{ value:'nps', label:'NPS Score' },
{ value:'yes_no', label:'Yes / No' },
{ value:'date_time', label:'Date / Time' },
{ value:'number', label:'Number' },
{ value:'emoji_scale', label:'Emoji Scale' },
{ value:'image_choice', label:'Image Choice' },
{ value:'file_upload', label:'File Upload' },
{ value:'demographic', label:'Demographic' },
{ value:'video_response', label:'Video Response' },
{ value:'signature', label:'Signature' },
{ value:'heatmap', label:'Heat Map' },
{ value:'card_sort', label:'Card Sort' },
{ value:'constant_sum', label:'Constant Sum' },
{ value:'max_diff', label:'MaxDiff / Best-Worst' },
{ value:'statement', label:'Statement / Display Text' },
{ value:'section_break', label:'Section Break' },
];
function questionTypeLabel(type) { return ALL_QUESTION_TYPES.find(t => t.value === type)?.label || type; }