/**
* 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);