Add Quill rich text editor, multi-select questions, per-field rich text toggle
- Replace textarea+toolbar with Quill.js (free, MIT) for body editor - Full WYSIWYG: headings, bold/italic, lists, blockquote, code, link (inline, no popup) - Rich Text toggle button on each question text field and option text field - Plain textarea by default; toggle activates mini Quill editor per field - New 'Multiple Select' question type: checkboxes, all correct must be chosen - Backend scoring: allCorrectChosen && noWrongChosen - Results show all correct options highlighted, wrong selections flagged - quiz display uses sanitizeHtml for question/option text (supports formatted HTML) - sanitizeHtml allows Quill class attrs (ql-indent, etc.)
This commit is contained in:
parent
6c0c911dbd
commit
d4e57177e4
5 changed files with 248 additions and 83 deletions
|
|
@ -112,27 +112,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body editor with toolbar -->
|
||||
<!-- Body editor (Quill rich text) -->
|
||||
<div class="cms-body-section">
|
||||
<label class="cms-label">Body</label>
|
||||
<div class="cms-editor-toolbar">
|
||||
<button type="button" class="cms-tb-btn" data-insert="b" title="Bold"><i class="fas fa-bold"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="i" title="Italic"><i class="fas fa-italic"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="u" title="Underline"><i class="fas fa-underline"></i></button>
|
||||
<span class="cms-tb-sep"></span>
|
||||
<button type="button" class="cms-tb-btn" data-insert="h2" title="Heading 2">H2</button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="h3" title="Heading 3">H3</button>
|
||||
<span class="cms-tb-sep"></span>
|
||||
<button type="button" class="cms-tb-btn" data-insert="ul" title="Bullet List"><i class="fas fa-list-ul"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="ol" title="Numbered List"><i class="fas fa-list-ol"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="blockquote" title="Quote"><i class="fas fa-quote-left"></i></button>
|
||||
<span class="cms-tb-sep"></span>
|
||||
<button type="button" class="cms-tb-btn" data-insert="a" title="Link"><i class="fas fa-link"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="code" title="Code"><i class="fas fa-code"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="table" title="Table"><i class="fas fa-table"></i></button>
|
||||
<button type="button" class="cms-tb-btn" data-insert="hr" title="Horizontal Rule"><i class="fas fa-minus"></i></button>
|
||||
</div>
|
||||
<textarea id="lh-cms-edit-body" class="cms-body-editor" rows="16" placeholder="Write your content here..."></textarea>
|
||||
<div id="lh-body-editor" class="cms-quill-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Quiz builder -->
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
|
|||
.cms-stat{padding:8px 10px;}
|
||||
.cms-stat-value{font-size:18px;}
|
||||
}
|
||||
.lh-option-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;padding:6px 8px;border-radius:6px;background:var(--g50);}
|
||||
.lh-option-row{display:flex;align-items:flex-start;gap:8px;margin-bottom:6px;padding:6px 8px;border-radius:6px;background:var(--g50);}
|
||||
|
||||
@media(max-width:640px){
|
||||
.lh-feed-item-header{flex-direction:column;gap:6px;}
|
||||
|
|
@ -659,3 +659,32 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
|
|||
.lh-option-row{flex-wrap:wrap;}
|
||||
.lh-option-row .lh-opt-expl{width:100%;}
|
||||
}
|
||||
|
||||
/* ── Quill editor overrides ───────────────────────────────── */
|
||||
.cms-quill-body{border:1px solid var(--g300);border-radius:8px;overflow:hidden;background:white;}
|
||||
.cms-quill-body .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);border-radius:0;padding:8px;}
|
||||
.cms-quill-body .ql-container{border:none;font-family:'Inter',system-ui,sans-serif;font-size:14px;}
|
||||
.cms-quill-body .ql-editor{min-height:280px;padding:16px;}
|
||||
.cms-quill-body .ql-editor p{margin-bottom:8px;}
|
||||
.cms-quill-body .ql-editor h2{font-size:20px;font-weight:700;margin:16px 0 8px;}
|
||||
.cms-quill-body .ql-editor h3{font-size:16px;font-weight:600;margin:12px 0 6px;}
|
||||
.cms-quill-body .ql-editor blockquote{border-left:4px solid var(--blue);padding-left:12px;color:var(--g600);margin:10px 0;}
|
||||
.cms-quill-body .ql-editor pre{background:var(--g900);color:#e5e7eb;padding:12px;border-radius:6px;font-size:13px;overflow-x:auto;}
|
||||
|
||||
/* Mini quill on questions/options */
|
||||
.lh-q-richtext-wrap{border:1.5px solid var(--g300);border-radius:8px;overflow:hidden;margin-bottom:8px;}
|
||||
.lh-q-richtext-wrap .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);padding:4px 6px;}
|
||||
.lh-q-richtext-wrap .ql-container{border:none;font-size:14px;font-family:'Inter',system-ui,sans-serif;}
|
||||
.lh-q-richtext-wrap .ql-editor{min-height:60px;padding:10px;}
|
||||
.lh-opt-richtext-wrap{border:1.5px solid var(--g300);border-radius:6px;overflow:hidden;flex:2;}
|
||||
.lh-opt-richtext-wrap .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);padding:2px 4px;}
|
||||
.lh-opt-richtext-wrap .ql-container{border:none;font-size:13px;font-family:'Inter',system-ui,sans-serif;}
|
||||
.lh-opt-richtext-wrap .ql-editor{min-height:40px;padding:6px 8px;}
|
||||
|
||||
/* Rich text toggle button */
|
||||
.lh-richtext-btn{font-size:11px;padding:3px 8px;border:1px solid var(--g300);border-radius:4px;background:white;cursor:pointer;color:var(--g500);white-space:nowrap;}
|
||||
.lh-richtext-btn.active{background:var(--blue-light);border-color:var(--blue);color:var(--blue);font-weight:600;}
|
||||
|
||||
/* Multi-select quiz options */
|
||||
.lh-quiz-option input[type="checkbox"]{accent-color:var(--blue);width:18px;height:18px;flex-shrink:0;}
|
||||
.lh-quiz-multi-hint{font-size:12px;color:var(--g500);margin-bottom:10px;font-style:italic;}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.7/quill.snow.css">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
|
@ -265,6 +266,7 @@
|
|||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- SCRIPTS (defer for faster initial paint) -->
|
||||
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
|
||||
<script defer src="/js/milestonesData.js"></script>
|
||||
<script defer src="/js/pediatricScheduleData.js"></script>
|
||||
<script defer src="/js/app.js"></script>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
var cmsLoaded = false;
|
||||
var currentContent = null;
|
||||
var currentView = 'feed'; // 'feed' | 'category' | 'viewer'
|
||||
var _bodyQuill = null; // Quill instance for body editor
|
||||
|
||||
// ── Load when tab activated ────────────────────────────────
|
||||
document.addEventListener('tabChanged', function(e) {
|
||||
|
|
@ -64,9 +65,9 @@
|
|||
if (e.target.closest('#btn-lh-delete-content')) { deleteContent(); return; }
|
||||
if (e.target.closest('#btn-lh-add-question')) { addQuestionBlock(); return; }
|
||||
|
||||
// Editor toolbar buttons
|
||||
var tbBtn = e.target.closest('.cms-tb-btn');
|
||||
if (tbBtn && tbBtn.dataset.insert) { insertTag(tbBtn.dataset.insert); return; }
|
||||
// Rich text toggle for question or option
|
||||
var rtBtn = e.target.closest('.lh-richtext-btn');
|
||||
if (rtBtn) { toggleRichText(rtBtn); return; }
|
||||
|
||||
// CMS content item edit
|
||||
var cmsItem = e.target.closest('.lh-cms-content-item');
|
||||
|
|
@ -286,20 +287,28 @@
|
|||
if (countEl) countEl.textContent = questions.length + ' question' + (questions.length !== 1 ? 's' : '');
|
||||
|
||||
container.innerHTML = questions.map(function(q, idx) {
|
||||
var typeLabel = q.question_type === 'true_false' ? 'True / False' : 'Multiple Choice';
|
||||
var isMulti = q.question_type === 'multi';
|
||||
var typeLabel = q.question_type === 'true_false' ? 'True / False'
|
||||
: isMulti ? 'Multiple Select' : 'Single Choice';
|
||||
var inputType = isMulti ? 'checkbox' : 'radio';
|
||||
var nameAttr = isMulti ? 'lh-qm-' + q.id : 'lh-q-' + q.id;
|
||||
|
||||
var optionsHtml = (q.options || []).map(function(opt) {
|
||||
return '<label class="lh-quiz-option">' +
|
||||
'<input type="radio" name="lh-q-' + q.id + '" value="' + opt.id + '">' +
|
||||
'<span>' + esc(opt.option_text) + '</span>' +
|
||||
'<input type="' + inputType + '" name="' + nameAttr + '" value="' + opt.id + '">' +
|
||||
'<span>' + sanitizeHtml(opt.option_text) + '</span>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
|
||||
return '<div class="lh-quiz-q" data-qid="' + q.id + '">' +
|
||||
var hint = isMulti ? '<div class="lh-quiz-multi-hint">Select all that apply</div>' : '';
|
||||
|
||||
return '<div class="lh-quiz-q" data-qid="' + q.id + '" data-qtype="' + q.question_type + '">' +
|
||||
'<div class="lh-quiz-q-header">' +
|
||||
'<span class="lh-quiz-q-num">Q' + (idx + 1) + '</span>' +
|
||||
'<span class="lh-quiz-q-type">' + typeLabel + '</span>' +
|
||||
'</div>' +
|
||||
'<p class="lh-quiz-q-text">' + sanitizeHtml(q.question_text) + '</p>' +
|
||||
hint +
|
||||
'<div class="lh-quiz-options">' + optionsHtml + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
|
@ -310,11 +319,15 @@
|
|||
|
||||
var answers = [];
|
||||
currentContent.questions.forEach(function(q) {
|
||||
var selected = document.querySelector('input[name="lh-q-' + q.id + '"]:checked');
|
||||
answers.push({
|
||||
questionId: q.id,
|
||||
optionId: selected ? parseInt(selected.value) : null
|
||||
});
|
||||
if (q.question_type === 'multi') {
|
||||
var checkedEls = document.querySelectorAll('input[name="lh-qm-' + q.id + '"]:checked');
|
||||
var optionIds = [];
|
||||
checkedEls.forEach(function(el) { optionIds.push(parseInt(el.value)); });
|
||||
answers.push({ questionId: q.id, optionIds: optionIds });
|
||||
} else {
|
||||
var selected = document.querySelector('input[name="lh-q-' + q.id + '"]:checked');
|
||||
answers.push({ questionId: q.id, optionId: selected ? parseInt(selected.value) : null });
|
||||
}
|
||||
});
|
||||
|
||||
showLoading('Submitting quiz...');
|
||||
|
|
@ -373,12 +386,20 @@
|
|||
data.results.forEach(function(r) {
|
||||
var qDiv = document.querySelector('.lh-quiz-q[data-qid="' + r.questionId + '"]');
|
||||
if (!qDiv) return;
|
||||
var isMulti = r.questionType === 'multi';
|
||||
qDiv.querySelectorAll('.lh-quiz-option').forEach(function(label) {
|
||||
var input = label.querySelector('input');
|
||||
if (!input) return;
|
||||
var optId = parseInt(input.value);
|
||||
if (optId === r.correctOptionId) label.classList.add('lh-opt-correct');
|
||||
if (optId === r.selectedOptionId && !r.isCorrect) label.classList.add('lh-opt-wrong');
|
||||
if (isMulti) {
|
||||
var correctIds = r.correctOptionIds || [];
|
||||
var selectedIds = r.selectedOptionIds || [];
|
||||
if (correctIds.indexOf(optId) !== -1) label.classList.add('lh-opt-correct');
|
||||
if (selectedIds.indexOf(optId) !== -1 && correctIds.indexOf(optId) === -1) label.classList.add('lh-opt-wrong');
|
||||
} else {
|
||||
if (optId === r.correctOptionId) label.classList.add('lh-opt-correct');
|
||||
if (optId === r.selectedOptionId && !r.isCorrect) label.classList.add('lh-opt-wrong');
|
||||
}
|
||||
input.disabled = true;
|
||||
});
|
||||
});
|
||||
|
|
@ -560,30 +581,131 @@
|
|||
renderCmsContentList(filtered);
|
||||
}
|
||||
|
||||
// Editor toolbar — insert HTML tags at cursor in textarea
|
||||
function insertTag(tag) {
|
||||
var ta = document.getElementById('lh-cms-edit-body');
|
||||
if (!ta) return;
|
||||
var start = ta.selectionStart, end = ta.selectionEnd;
|
||||
var sel = ta.value.substring(start, end);
|
||||
var insert = '';
|
||||
switch(tag) {
|
||||
case 'b': case 'i': case 'u': case 'code': case 'h2': case 'h3':
|
||||
insert = '<' + tag + '>' + (sel || 'text') + '</' + tag + '>'; break;
|
||||
case 'a':
|
||||
var url = prompt('Enter URL:');
|
||||
if (!url) return;
|
||||
insert = '<a href="' + url + '">' + (sel || 'link text') + '</a>'; break;
|
||||
case 'ul': insert = '<ul>\n <li>' + (sel || 'item') + '</li>\n <li></li>\n</ul>'; break;
|
||||
case 'ol': insert = '<ol>\n <li>' + (sel || 'item') + '</li>\n <li></li>\n</ol>'; break;
|
||||
case 'blockquote': insert = '<blockquote>' + (sel || 'quote') + '</blockquote>'; break;
|
||||
case 'table': insert = '<table>\n<tr><th>Header 1</th><th>Header 2</th></tr>\n<tr><td>' + (sel || 'data') + '</td><td></td></tr>\n</table>'; break;
|
||||
case 'hr': insert = '<hr>'; break;
|
||||
default: return;
|
||||
// ── Quill body editor ─────────────────────────────────────
|
||||
function initBodyEditor() {
|
||||
var container = document.getElementById('lh-body-editor');
|
||||
if (!container || _bodyQuill) return;
|
||||
_bodyQuill = new Quill(container, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
|
||||
['blockquote', 'code-block'],
|
||||
['link'],
|
||||
[{ color: [] }],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mini Quill for question text or option text
|
||||
function makeMinQuill(container, existingHtml, isOption) {
|
||||
var toolbar = isOption
|
||||
? [['bold', 'italic'], ['link']]
|
||||
: [['bold', 'italic'], [{ list: 'ordered' }, { list: 'bullet' }], ['link'], ['clean']];
|
||||
var q = new Quill(container, {
|
||||
theme: 'snow',
|
||||
modules: { toolbar: toolbar }
|
||||
});
|
||||
if (existingHtml) q.clipboard.dangerouslyPasteHTML(existingHtml);
|
||||
return q;
|
||||
}
|
||||
|
||||
// Toggle rich text on a question text area or option text area
|
||||
function toggleRichText(btn) {
|
||||
var isActive = btn.classList.contains('active');
|
||||
var target = btn.dataset.target; // 'question' or 'option'
|
||||
var block = btn.closest(target === 'question' ? '.lh-question-block' : '.lh-option-row');
|
||||
if (!block) return;
|
||||
|
||||
if (target === 'question') {
|
||||
var ta = block.querySelector('.lh-q-text');
|
||||
var wrap = block.querySelector('.lh-q-richtext-wrap');
|
||||
|
||||
if (!isActive) {
|
||||
// Enable: hide textarea, show quill
|
||||
var currentVal = ta ? ta.value : '';
|
||||
btn.classList.add('active');
|
||||
btn.textContent = 'Plain Text';
|
||||
if (ta) ta.style.display = 'none';
|
||||
if (!wrap) {
|
||||
wrap = document.createElement('div');
|
||||
wrap.className = 'lh-q-richtext-wrap';
|
||||
ta.parentNode.insertBefore(wrap, ta.nextSibling);
|
||||
}
|
||||
wrap.style.display = '';
|
||||
if (!block._questionQuill) {
|
||||
block._questionQuill = makeMinQuill(wrap, currentVal, false);
|
||||
} else {
|
||||
wrap.style.display = '';
|
||||
}
|
||||
} else {
|
||||
// Disable: get HTML from quill, restore textarea
|
||||
btn.classList.remove('active');
|
||||
btn.textContent = 'Rich Text';
|
||||
if (block._questionQuill) {
|
||||
var html = block._questionQuill.root.innerHTML;
|
||||
if (html === '<p><br></p>') html = '';
|
||||
if (ta) { ta.value = html; ta.style.display = ''; }
|
||||
}
|
||||
if (wrap) wrap.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// option
|
||||
var row = block;
|
||||
var optInput = row.querySelector('.lh-opt-text');
|
||||
var optWrap = row.querySelector('.lh-opt-richtext-wrap');
|
||||
|
||||
if (!isActive) {
|
||||
var curVal = optInput ? optInput.value : '';
|
||||
btn.classList.add('active');
|
||||
btn.textContent = 'Plain';
|
||||
if (optInput) optInput.style.display = 'none';
|
||||
if (!optWrap) {
|
||||
optWrap = document.createElement('div');
|
||||
optWrap.className = 'lh-opt-richtext-wrap';
|
||||
optInput.parentNode.insertBefore(optWrap, optInput.nextSibling);
|
||||
}
|
||||
optWrap.style.display = '';
|
||||
if (!row._optQuill) {
|
||||
row._optQuill = makeMinQuill(optWrap, curVal, true);
|
||||
}
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
btn.textContent = 'Rich';
|
||||
if (row._optQuill) {
|
||||
var optHtml = row._optQuill.root.innerHTML;
|
||||
if (optHtml === '<p><br></p>') optHtml = '';
|
||||
if (optInput) { optInput.value = optHtml; optInput.style.display = ''; }
|
||||
}
|
||||
if (optWrap) optWrap.style.display = 'none';
|
||||
}
|
||||
}
|
||||
ta.value = ta.value.substring(0, start) + insert + ta.value.substring(end);
|
||||
ta.focus();
|
||||
ta.selectionStart = ta.selectionEnd = start + insert.length;
|
||||
}
|
||||
|
||||
// Read question text whether in rich text mode or plain
|
||||
function getQText(block) {
|
||||
if (block._questionQuill && block.querySelector('.lh-q-richtext-wrap') &&
|
||||
block.querySelector('.lh-q-richtext-wrap').style.display !== 'none') {
|
||||
var html = block._questionQuill.root.innerHTML;
|
||||
return html === '<p><br></p>' ? '' : html;
|
||||
}
|
||||
var ta = block.querySelector('.lh-q-text');
|
||||
return ta ? ta.value.trim() : '';
|
||||
}
|
||||
|
||||
// Read option text whether in rich text mode or plain
|
||||
function getOptText(row) {
|
||||
if (row._optQuill && row.querySelector('.lh-opt-richtext-wrap') &&
|
||||
row.querySelector('.lh-opt-richtext-wrap').style.display !== 'none') {
|
||||
var html = row._optQuill.root.innerHTML;
|
||||
return html === '<p><br></p>' ? '' : html;
|
||||
}
|
||||
var inp = row.querySelector('.lh-opt-text');
|
||||
return inp ? inp.value.trim() : '';
|
||||
}
|
||||
|
||||
function openEditor(contentId, contentType) {
|
||||
|
|
@ -597,12 +719,13 @@
|
|||
document.getElementById('lh-cms-edit-id').value = '';
|
||||
document.getElementById('lh-cms-edit-title').value = '';
|
||||
document.getElementById('lh-cms-edit-subject').value = '';
|
||||
document.getElementById('lh-cms-edit-body').value = '';
|
||||
document.getElementById('lh-cms-edit-category').value = '';
|
||||
document.getElementById('lh-cms-edit-type').value = contentType || 'article';
|
||||
document.getElementById('lh-cms-edit-published').value = 'false';
|
||||
document.getElementById('lh-cms-questions').innerHTML = '';
|
||||
document.getElementById('btn-lh-delete-content').classList.add('hidden');
|
||||
initBodyEditor();
|
||||
if (_bodyQuill) _bodyQuill.setContents([{ insert: '\n' }]);
|
||||
|
||||
// For quiz type, add a starter question
|
||||
if (contentType === 'quiz' && !contentId) {
|
||||
|
|
@ -620,7 +743,7 @@
|
|||
document.getElementById('lh-cms-edit-id').value = c.id;
|
||||
document.getElementById('lh-cms-edit-title').value = c.title;
|
||||
document.getElementById('lh-cms-edit-subject').value = c.subject || '';
|
||||
document.getElementById('lh-cms-edit-body').value = c.body || '';
|
||||
if (_bodyQuill) _bodyQuill.clipboard.dangerouslyPasteHTML(c.body || '');
|
||||
document.getElementById('lh-cms-edit-category').value = c.category_id || '';
|
||||
document.getElementById('lh-cms-edit-type').value = c.content_type || 'article';
|
||||
document.getElementById('lh-cms-edit-published').value = c.published ? 'true' : 'false';
|
||||
|
|
@ -648,7 +771,7 @@
|
|||
var id = document.getElementById('lh-cms-edit-id').value;
|
||||
var title = document.getElementById('lh-cms-edit-title').value.trim();
|
||||
var subject = document.getElementById('lh-cms-edit-subject').value.trim();
|
||||
var body = document.getElementById('lh-cms-edit-body').value;
|
||||
var body = _bodyQuill ? (_bodyQuill.root.innerHTML === '<p><br></p>' ? '' : _bodyQuill.root.innerHTML) : '';
|
||||
var category_id = document.getElementById('lh-cms-edit-category').value || null;
|
||||
var content_type = document.getElementById('lh-cms-edit-type').value;
|
||||
var published = document.getElementById('lh-cms-edit-published').value === 'true';
|
||||
|
|
@ -661,7 +784,7 @@
|
|||
var qBlocks = document.querySelectorAll('#lh-cms-questions .lh-question-block');
|
||||
for (var qi = 0; qi < qBlocks.length; qi++) {
|
||||
var qBlock = qBlocks[qi];
|
||||
var qText = qBlock.querySelector('.lh-q-text').value.trim();
|
||||
var qText = getQText(qBlock);
|
||||
if (!qText) continue;
|
||||
var qType = qBlock.querySelector('.lh-q-type').value;
|
||||
var qExpl = qBlock.querySelector('.lh-q-explanation').value.trim();
|
||||
|
|
@ -669,7 +792,7 @@
|
|||
|
||||
var options = [];
|
||||
qBlock.querySelectorAll('.lh-option-row').forEach(function(row) {
|
||||
var optText = row.querySelector('.lh-opt-text').value.trim();
|
||||
var optText = getOptText(row);
|
||||
if (!optText) return;
|
||||
options.push({
|
||||
option_text: optText,
|
||||
|
|
@ -792,8 +915,10 @@
|
|||
'<span style="background:linear-gradient(135deg,var(--blue),var(--purple));color:white;font-size:12px;font-weight:700;padding:3px 10px;border-radius:6px;">Q' + qNum + '</span>' +
|
||||
'<select class="lh-q-type" style="font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">' +
|
||||
'<option value="mcq"' + (existingQ && existingQ.question_type === 'mcq' ? ' selected' : '') + '>Single Choice</option>' +
|
||||
'<option value="multi"' + (existingQ && existingQ.question_type === 'multi' ? ' selected' : '') + '>Multiple Select</option>' +
|
||||
'<option value="true_false"' + (existingQ && existingQ.question_type === 'true_false' ? ' selected' : '') + '>True / False</option>' +
|
||||
'</select>' +
|
||||
'<button type="button" class="lh-richtext-btn" data-target="question">Rich Text</button>' +
|
||||
'</div>' +
|
||||
'<button class="btn-sm btn-ghost lh-rm-question" style="padding:2px 8px;font-size:12px;color:var(--red);"><i class="fas fa-trash"></i></button>' +
|
||||
'</div>' +
|
||||
|
|
@ -831,13 +956,18 @@
|
|||
var row = document.createElement('div');
|
||||
row.className = 'lh-option-row';
|
||||
row.innerHTML =
|
||||
'<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:28px;" title="Mark as correct answer">' +
|
||||
'<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:28px;padding-top:6px;" title="Mark as correct answer">' +
|
||||
'<input type="checkbox" class="lh-opt-correct"' + (opt && opt.is_correct ? ' checked' : '') + ' style="accent-color:var(--green);width:16px;height:16px;">' +
|
||||
'</label>' +
|
||||
'<input type="text" class="lh-opt-text" placeholder="Option text" value="' + esc(opt ? opt.option_text : '') + '" style="flex:2;font-size:13px;padding:6px 10px;border:1.5px solid var(--g300);border-radius:6px;">' +
|
||||
'<div style="flex:2;display:flex;flex-direction:column;gap:3px;">' +
|
||||
'<input type="text" class="lh-opt-text" placeholder="Option text" style="width:100%;font-size:13px;padding:6px 10px;border:1.5px solid var(--g300);border-radius:6px;box-sizing:border-box;">' +
|
||||
'<button type="button" class="lh-richtext-btn" data-target="option" style="align-self:flex-start;">Rich</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" class="lh-opt-expl" placeholder="Explanation if wrong" value="' + esc(opt ? opt.explanation || '' : '') + '" style="flex:1;font-size:12px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;color:var(--g600);">' +
|
||||
'<button class="btn-sm btn-ghost lh-rm-option" style="padding:2px 6px;font-size:12px;color:var(--g400);"><i class="fas fa-times"></i></button>';
|
||||
'<button class="btn-sm btn-ghost lh-rm-option" style="padding:2px 6px;font-size:12px;color:var(--g400);margin-top:4px;"><i class="fas fa-times"></i></button>';
|
||||
container.appendChild(row);
|
||||
// Set option text value after insertion (can't use value="" on nested input via innerHTML)
|
||||
row.querySelector('.lh-opt-text').value = opt ? opt.option_text || '' : '';
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
|
@ -851,7 +981,7 @@
|
|||
var ALLOWED_TAGS = ['p','br','b','strong','i','em','u','s','h1','h2','h3','h4','h5','h6',
|
||||
'ul','ol','li','a','blockquote','code','pre','table','thead','tbody','tr','th','td',
|
||||
'hr','div','span','sub','sup','dl','dt','dd'];
|
||||
var ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'] };
|
||||
var ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'], 'li': ['class'], 'ol': ['class'], 'ul': ['class'], 'span': ['class'], 'p': ['class'] };
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
|
|
|
|||
|
|
@ -127,9 +127,9 @@ router.post('/submit-quiz', async function(req, res) {
|
|||
return res.status(400).json({ error: 'contentId and answers array required' });
|
||||
}
|
||||
|
||||
// Get all questions for this content
|
||||
// Get all questions for this content (include question_type)
|
||||
var questions = await db.all(
|
||||
'SELECT id, question_text, explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
|
||||
'SELECT id, question_text, question_type, explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
|
||||
[contentId]
|
||||
);
|
||||
|
||||
|
|
@ -139,7 +139,6 @@ router.post('/submit-quiz', async function(req, res) {
|
|||
for (var i = 0; i < questions.length; i++) {
|
||||
var q = questions[i];
|
||||
var userAnswer = answers.find(function(a) { return a.questionId === q.id; });
|
||||
var selectedOptionId = userAnswer ? userAnswer.optionId : null;
|
||||
|
||||
// Get all options with correct flag
|
||||
var options = await db.all(
|
||||
|
|
@ -147,21 +146,43 @@ router.post('/submit-quiz', async function(req, res) {
|
|||
[q.id]
|
||||
);
|
||||
|
||||
var correctOption = options.find(function(o) { return o.is_correct; });
|
||||
var selectedOption = selectedOptionId ? options.find(function(o) { return o.id === selectedOptionId; }) : null;
|
||||
var isCorrect = selectedOption ? selectedOption.is_correct : false;
|
||||
if (isCorrect) score++;
|
||||
var isCorrect, resultEntry;
|
||||
|
||||
results.push({
|
||||
questionId: q.id,
|
||||
questionText: q.question_text,
|
||||
selectedOptionId: selectedOptionId,
|
||||
isCorrect: isCorrect,
|
||||
correctOptionId: correctOption ? correctOption.id : null,
|
||||
correctOptionText: correctOption ? correctOption.option_text : '',
|
||||
selectedExplanation: selectedOption && !isCorrect ? selectedOption.explanation : '',
|
||||
generalExplanation: q.general_explanation || ''
|
||||
});
|
||||
if (q.question_type === 'multi') {
|
||||
var selectedIds = (userAnswer && Array.isArray(userAnswer.optionIds)) ? userAnswer.optionIds.map(Number) : [];
|
||||
var correctIds = options.filter(function(o) { return o.is_correct; }).map(function(o) { return o.id; });
|
||||
var allCorrectChosen = correctIds.length > 0 && correctIds.every(function(id) { return selectedIds.indexOf(id) !== -1; });
|
||||
var noWrongChosen = selectedIds.every(function(id) { return correctIds.indexOf(id) !== -1; });
|
||||
isCorrect = allCorrectChosen && noWrongChosen && selectedIds.length > 0;
|
||||
if (isCorrect) score++;
|
||||
resultEntry = {
|
||||
questionId: q.id,
|
||||
questionType: 'multi',
|
||||
questionText: q.question_text,
|
||||
selectedOptionIds: selectedIds,
|
||||
correctOptionIds: correctIds,
|
||||
isCorrect: isCorrect,
|
||||
generalExplanation: q.general_explanation || ''
|
||||
};
|
||||
} else {
|
||||
var selectedOptionId = userAnswer ? userAnswer.optionId : null;
|
||||
var correctOption = options.find(function(o) { return o.is_correct; });
|
||||
var selectedOption = selectedOptionId ? options.find(function(o) { return o.id === selectedOptionId; }) : null;
|
||||
isCorrect = selectedOption ? selectedOption.is_correct : false;
|
||||
if (isCorrect) score++;
|
||||
resultEntry = {
|
||||
questionId: q.id,
|
||||
questionType: q.question_type,
|
||||
questionText: q.question_text,
|
||||
selectedOptionId: selectedOptionId,
|
||||
isCorrect: isCorrect,
|
||||
correctOptionId: correctOption ? correctOption.id : null,
|
||||
correctOptionText: correctOption ? correctOption.option_text : '',
|
||||
selectedExplanation: selectedOption && !isCorrect ? selectedOption.explanation : '',
|
||||
generalExplanation: q.general_explanation || ''
|
||||
};
|
||||
}
|
||||
results.push(resultEntry);
|
||||
}
|
||||
|
||||
// Save progress
|
||||
|
|
|
|||
Loading…
Reference in a new issue