/** * quizbox.js - QuizBox interactive * Types: Choices (radio), Checkboxes, WordSort (SortableJS), Matching/Pairing (SVG lines) * Requires: jQuery, SortableJS * DOM is server-rendered by QuizBox.cshtml. */ ;(function ($) { 'use strict'; var isSubmitted = false; var pendingMatch = null; // Six-dot drag icon var ICON_DRAG = ''; // ── SHUFFLE LOGIC ─────────────────────────────────────────── function shuffleElements($container, selector) { if (!$container.length) return; var $items = $container.children(selector); if ($items.length > 1) { var arr = $items.get(); for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } $container.append(arr); } } function shuffleAll() { // 1. Choices / Checkboxes //$('ul.list-item-quiz').each(function () { // shuffleElements($(this), 'li.item-quiz'); //}); // 2. WordSort $('ul.list-item-sort.view-ques').each(function () { shuffleElements($(this), 'li.item-quiz'); // Update sort-num if they already exist $(this).find('li.item-quiz').each(function (i) { $(this).find('.sort-num').text(i + 1); }); }); // 3. Matching (shuffle right column) $('.list-mat.quesh').each(function () { shuffleElements($(this).find('.pairing-right'), '.item-pairing'); }); } // ── INIT ──────────────────────────────────────────────────── function init() { // Clean up empty paragraphs generated by the rich text editor (e.g.,

 

) $('.itemsec p').each(function () { var html = $.trim($(this).html()).replace(/ /g, ''); if (html === '' || html === '
') { $(this).remove(); } }); // Reset all form inputs on page load (prevent browser cache restoring checked state) $('input.quiz-value').prop('checked', false); // Shuffle options so they are random on load/F5 shuffleAll(); setupChoices(); setupWordSort(); setupMatching(); setupSubmit(); setupReset(); refreshSubmitBtn(); $(window).on('resize.quizbox', drawAllMatchingLines); } // ── 1. CHOICES / CHECKBOXES ───────────────────────────────── // DOM: ul.list-item-quiz > li.item-quiz > label.overlabel // > input.quiz-value + span.mark + span.boxcheck > span.answer-desc // Must call e.preventDefault() to stop browser auto-toggling. function setupChoices() { $(document).on('click.quizbox', 'ul.list-item-quiz li.item-quiz label.overlabel', function (e) { if (isSubmitted) return; e.preventDefault(); // prevent native toggle var $label = $(this); var $input = $label.find('input.quiz-value'); if (!$input.length) return; var type = $input.attr('type'); var $li = $label.closest('li.item-quiz'); var $ul = $li.closest('ul.list-item-quiz'); if (type === 'radio') { $ul.find('li.item-quiz').removeClass('opt-selected'); $ul.find('input.quiz-value').prop('checked', false); $input.prop('checked', true); $li.addClass('opt-selected'); } else { var checked = !$input.prop('checked'); $input.prop('checked', checked); $li.toggleClass('opt-selected', checked); } refreshSubmitBtn(); }); } // ── 2. WORD SORT (drag anywhere on item) ──────────────────── // DOM: ul.list-item-sort.view-ques > li.item-quiz > span.answer-text function setupWordSort() { $('ul.list-item-sort.view-ques').each(function () { var $ul = $(this); if ($ul.data('qb-sortable')) return; $ul.data('qb-sortable', true); // Inject order number and visual handle indicator (not functional handle) $ul.find('li.item-quiz').each(function (i) { var $li = $(this); if (!$li.find('.sort-num').length) { $li.prepend('' + (i + 1) + ''); } if (!$li.find('.sort-handle').length) { $li.append('' + ICON_DRAG + ''); } }); // SortableJS: drag from ANYWHERE on the item (no handle restriction) new Sortable(this, { animation: 200, ghostClass: 'sort-ghost', chosenClass: 'sort-chosen', dragClass: 'sort-dragging', onEnd: function () { $ul.addClass('user-interacted'); // Mark for "attempted" tracking $ul.find('li.item-quiz').each(function (i) { $(this).find('.sort-num').text(i + 1); }); refreshSubmitBtn(); } }); }); } // ── 3. MATCHING (SVG lines) ────────────────────────────────── // Container: div.list-mat.quesh // Left: div.box-mat.pairing-left > div.item-pairing[data-index] > span + i.pai-icon // Right: div.box-mat.pairing-right > div.item-pairing[data-index] > span + i.pai-icon function setupMatching() { $('.list-mat.quesh').each(function () { var $c = $(this); $c.css('position', 'relative'); // Inject SVG overlay if (!$c.find('svg.qb-svg-layer').length) { $c.prepend( '' ); } // Ensure column classes (server already adds these but belt+suspenders) $c.find('[id$="-left"], [class*="pairing-left"]').addClass('pairing-left'); $c.find('[id$="-right"], [class*="pairing-right"]').addClass('pairing-right'); }); // Click to pair $(document).on('click.quizbox', '.list-mat.quesh .item-pairing', function () { if (isSubmitted) return; var $el = $(this); var $c = $el.closest('.list-mat.quesh'); var side = $el.closest('.pairing-left').length ? 'left' : 'right'; var idx = parseInt($el.attr('data-index'), 10); if (!pendingMatch) { selectItem($el, idx, side, $c); return; } // Different container → restart if (!pendingMatch.$c.is($c)) { deselectItem(pendingMatch.$el); selectItem($el, idx, side, $c); return; } // Same column → switch selection if (pendingMatch.side === side) { deselectItem(pendingMatch.$el); if (pendingMatch.idx === idx) { pendingMatch = null; } else { selectItem($el, idx, side, $c); } return; } // Opposite column → connect var leftIdx = side === 'right' ? pendingMatch.idx : idx; var rightIdx = side === 'right' ? idx : pendingMatch.idx; deselectItem(pendingMatch.$el); $el.removeClass('mat-selected'); pendingMatch = null; connectPair($c, leftIdx, rightIdx); refreshSubmitBtn(); }); } function selectItem($el, idx, side, $c) { $el.addClass('mat-selected'); pendingMatch = { $el: $el, idx: idx, side: side, $c: $c }; } function deselectItem($el) { if ($el) $el.removeClass('mat-selected'); } function connectPair($c, leftIdx, rightIdx) { var key = 'data-conn-' + leftIdx; // Toggle off same pair if (parseInt($c.attr(key) || -1, 10) === rightIdx) { $c.removeAttr(key); updatePairingClass($c, leftIdx, rightIdx, false); drawLines($c); return; } // Remove old right connection (prevent duplicate lines on right) $c.find('.pairing-left .item-pairing').each(function () { var li = parseInt($(this).attr('data-index'), 10); var k = 'data-conn-' + li; if (parseInt($c.attr(k) || -1, 10) === rightIdx) { var oldRi = rightIdx; $c.removeAttr(k); updatePairingClass($c, li, oldRi, false); } }); // Remove old left connection if ($c.attr(key)) { var oldRi2 = parseInt($c.attr(key), 10); $c.removeAttr(key); updatePairingClass($c, leftIdx, oldRi2, false); } // Write new connection $c.attr(key, rightIdx); updatePairingClass($c, leftIdx, rightIdx, true); drawLines($c); } function updatePairingClass($c, li, ri, connected) { var $left = $c.find('.pairing-left .item-pairing[data-index="' + li + '"]'); var $right = $c.find('.pairing-right .item-pairing[data-index="' + ri + '"]'); if (connected) { $left.addClass('mat-connected'); $right.addClass('mat-connected'); } else { // Only remove connected if it has no other connection $left.removeClass('mat-connected'); $right.removeClass('mat-connected'); } } // Draw user's connection lines (and correct lines after submit) function drawLines($c) { var $svg = $c.find('svg.qb-svg-layer'); if (!$svg.length) return; // Keep SVG up to date with container size var cW = $c[0].offsetWidth; var cH = $c[0].offsetHeight; $svg.attr({ width: cW, height: cH }); $svg.empty(); var ns = 'http://www.w3.org/2000/svg'; var cRect = $c[0].getBoundingClientRect(); var scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0; var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0; function getCenter(el) { var r = el.getBoundingClientRect(); return { x: r.left + scrollX + r.width / 2 - (cRect.left + scrollX), y: r.top + scrollY + r.height / 2 - (cRect.top + scrollY) }; } function makeLine(x1, y1, x2, y2, color, dasharray) { var line = document.createElementNS(ns, 'line'); line.setAttribute('x1', x1); line.setAttribute('y1', y1); line.setAttribute('x2', x2); line.setAttribute('y2', y2); line.setAttribute('stroke', color); line.setAttribute('stroke-width', '2.5'); line.setAttribute('stroke-linecap', 'round'); if (dasharray) line.setAttribute('stroke-dasharray', dasharray); $svg[0].appendChild(line); } // Collect user connections $c.find('.pairing-left .item-pairing').each(function () { var li = parseInt($(this).attr('data-index'), 10); var ri = parseInt($c.attr('data-conn-' + li) || -1, 10); if (ri < 0) return; var $dotL = $(this).find('i.pai-icon'); var $dotR = $c.find('.pairing-right .item-pairing[data-index="' + ri + '"] i.pai-icon'); if (!$dotL.length || !$dotR.length) return; var pL = getCenter($dotL[0]); var pR = getCenter($dotR[0]); // Color based on correctness (only after submit) var color = '#64748b'; if (isSubmitted) { var exp = parseInt($c.attr('data-correct-' + li) || li, 10); color = (ri === exp) ? '#10b981' : '#ef4444'; } makeLine(pL.x, pL.y, pR.x, pR.y, color, ''); }); // After submit: also draw the CORRECT connections that user got wrong (dashed green) if (isSubmitted) { $c.find('.pairing-left .item-pairing').each(function () { var li = parseInt($(this).attr('data-index'), 10); var ri_user = parseInt($c.attr('data-conn-' + li) || -1, 10); var ri_exp = parseInt($c.attr('data-correct-' + li) || li, 10); // Only draw correct hint if user got it wrong if (ri_user === ri_exp) return; var $dotL = $(this).find('i.pai-icon'); var $dotR = $c.find('.pairing-right .item-pairing[data-index="' + ri_exp + '"] i.pai-icon'); if (!$dotL.length || !$dotR.length) return; var pL = getCenter($dotL[0]); var pR = getCenter($dotR[0]); makeLine(pL.x, pL.y, pR.x, pR.y, '#10b981', '6,4'); }); } } function drawAllMatchingLines() { $('.list-mat.quesh').each(function () { drawLines($(this)); }); } // ── 4. SUBMIT ──────────────────────────────────────────────── function setupSubmit() { $(document).on('click.quizbox', '#checkresult, a.checkresult', function (e) { e.preventDefault(); if (isSubmitted || $(this).hasClass('disabled')) return; isSubmitted = true; // -- Choices / Checkboxes -- $('ul.list-item-quiz').each(function () { $(this).find('li.item-quiz').each(function () { var $li = $(this); var $input = $li.find('input.quiz-value'); var selected = $input.prop('checked'); var correct = parseInt($input.attr('data-status'), 10) === 1; $li.removeClass('opt-selected opt-correct opt-wrong opt-disabled'); if (correct) $li.addClass('opt-correct'); else if (selected && !correct) $li.addClass('opt-wrong'); else $li.addClass('opt-disabled'); }); }); // -- WordSort -- $('ul.list-item-sort.view-ques').each(function () { var $ul = $(this); $ul.find('.sort-handle').hide(); $ul.find('li.item-quiz').css('cursor', 'default'); $ul.find('li.item-quiz').each(function (i) { var $li = $(this); var pos = parseInt($li.attr('data-position'), 10); var expected = i + 1; // Correct if it is at the matching 1-based index $li.addClass('r-disabled'); $li.addClass(pos === expected ? 'r-correct' : 'r-wrong'); }); }); // -- Matching -- $('.list-mat.quesh').each(function () { var $c = $(this); $c.find('.pairing-left .item-pairing').each(function () { var li = parseInt($(this).attr('data-index'), 10); var ri = parseInt($c.attr('data-conn-' + li) || -1, 10); var exp = parseInt($c.attr('data-correct-' + li) || li, 10); $(this).removeClass('mat-selected mat-connected'); var $right = $c.find('.pairing-right .item-pairing[data-index="' + ri + '"]'); $right.removeClass('mat-selected mat-connected'); if (ri >= 0) { var ok = (ri === exp); $(this).addClass(ok ? 'm-correct' : 'm-wrong'); $right.addClass(ok ? 'm-correct' : 'm-wrong'); } else { // No connection made → mark as wrong $(this).addClass('m-wrong'); } }); // Also mark right items that weren't connected $c.find('.pairing-right .item-pairing').each(function () { if (!$(this).hasClass('m-correct') && !$(this).hasClass('m-wrong')) { $(this).addClass('m-wrong'); } }); drawLines($c); // redraw with colors + correct dashed lines }); // Show notes / answer explanations $('li.liSection[data-type="Question"]').addClass('result-shown'); showResult(); $(this).addClass('disabled').prop('disabled', true).hide(); }); } // ── 5. RESET ──────────────────────────────────────────────── function setupReset() { $(document).on('click.quizbox', '.btn-reset-full', function () { isSubmitted = false; pendingMatch = null; // Reshuffle options on "Làm lại bài học" shuffleAll(); // Choices $('input.quiz-value').prop('checked', false); $('li.liSection').removeClass('result-shown'); $('li.item-quiz').removeClass('opt-selected opt-correct opt-wrong opt-disabled r-correct r-wrong r-disabled'); // Matching $('.list-mat.quesh').each(function () { var $c = $(this); var attrs = []; for (var a = 0; a < this.attributes.length; a++) { var n = this.attributes[a].name; if (n.indexOf('data-conn-') === 0) attrs.push(n); } $.each(attrs, function (_, n) { $c.removeAttr(n); }); $c.find('.item-pairing').removeClass('mat-selected mat-connected m-correct m-wrong'); drawLines($c); }); // WordSort $('ul.list-item-sort.view-ques .sort-handle').show(); $('ul.list-item-sort.view-ques li.item-quiz').css('cursor', 'grab').removeClass('r-correct r-wrong r-disabled'); // Result card $('#box-resultLast').addClass('hide'); $('#checkresult, a.checkresult').removeClass('disabled').prop('disabled', false).show(); refreshSubmitBtn(); var $container = $('.box-quiz-contentviewsucceed'); if ($container.length) { var offset = $container.offset().top - 40; // leaving a small 40px gap from top window.scrollTo({ top: offset, behavior: 'smooth' }); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); } }); } // ── 6. RESULT CARD ────────────────────────────────────────── function showResult() { var $box = $('#box-resultLast'); if (!$box.length) return; $box.removeClass('hide'); var totalQuestions = 0; var correctQuestions = 0; var attemptedQuestions = 0; var totalPoints = 0; $('li.liSection[data-type="Question"]').each(function () { totalQuestions++; var $q = $(this); var isAttempted = false; var qIsCorrect = true; var hasAnyAnswer = false; // 1. Choices / Checkboxes var $checkedInputs = $q.find('input.quiz-value:checked'); if ($checkedInputs.length > 0) { isAttempted = true; hasAnyAnswer = true; $checkedInputs.each(function() { var correct = parseInt($(this).attr('data-status'), 10) === 1; if (correct) { totalPoints += parseFloat($(this).attr('data-point') || 0); } else { qIsCorrect = false; } }); if ($q.find('.opt-wrong').length > 0) qIsCorrect = false; } // 2. WordSort var $sortLists = $q.find('ul.list-item-sort.view-ques'); if ($sortLists.length > 0) { hasAnyAnswer = true; if ($sortLists.hasClass('user-interacted')) { isAttempted = true; } $sortLists.each(function() { var $ul = $(this); var numItems = $ul.find('li.item-quiz').length; var numCorrect = $ul.find('li.r-correct').length; if (numCorrect < numItems) { qIsCorrect = false; } // Add proportional points depending on correct items if (numItems > 0 && numCorrect > 0) { var qPts = parseFloat($ul.attr('data-point') || 0); totalPoints += (qPts / numItems) * numCorrect; } }); } // 3. Matching var $matchLists = $q.find('.list-mat.quesh'); if ($matchLists.length > 0) { hasAnyAnswer = true; if ($q.find('.mat-connected').length > 0) { isAttempted = true; } var $matches = $matchLists.find('.pairing-left .item-pairing'); $matches.each(function() { if ($(this).hasClass('m-correct')) { totalPoints += parseFloat($(this).attr('data-point') || 0); } else { qIsCorrect = false; } }); } // Note: A question is only correct if 100% answers in it are correct var hasAnyCorrectMark = $q.find('.opt-correct, .m-correct, .r-correct').length > 0; if (hasAnyAnswer && qIsCorrect && hasAnyCorrectMark) { correctQuestions++; } if (isAttempted) { attemptedQuestions++; } }); // Format strictly to strip useless decimals (like 2.00 to 2) var rawScore = parseFloat(totalPoints.toFixed(2)); console.log("Tổng điểm:", rawScore); $box.find('.summary-attempted').text(attemptedQuestions); $box.find('.summary-score-badge').text(correctQuestions + ' / ' + totalQuestions); } // ── 7. SUBMIT BUTTON STATE ────────────────────────────────── function refreshSubmitBtn() { var hasInput = $('input.quiz-value:checked').length > 0; var hasMatching = $('.mat-connected').length > 0; var hasSortList = $('ul.list-item-sort.view-ques').length > 0; var enabled = hasInput || hasMatching || hasSortList; $('#checkresult, a.checkresult').toggleClass('disabled', !enabled).prop('disabled', !enabled); } // ── BOOT ──────────────────────────────────────────────────── $(function () { init(); // Draw matching lines after layout is fully stable setTimeout(drawAllMatchingLines, 300); $('img').on('load.quizbox', drawAllMatchingLines); }); })(jQuery);