MediaWiki:Gadget-wix-quiz.js: Difference between revisions
Appearance
Content deleted Content added
Created page with "/* ================================================================ WIX-QUIZ.JS — Wiki Interactive Experience: Quiz Engine ================================================================ Depends on: ext.gadget.wix-core (window.wix must exist) Finds every container with data-wix-module="quiz", reads the question blocks from the DOM, and manages the full lifecycle: sequencing → answer selection → feedback → next → results. Content-blind:..." |
No edit summary |
||
| Line 14: | Line 14: | ||
───────────────────────────────── |
───────────────────────────────── |
||
Container: [data-wix-module="quiz"] |
Container: [data-wix-module="quiz"] |
||
Question: [data-wix-question] |
Question: [data-wix-question="...question text..."] |
||
data-wix-correct letter of correct answer (a/b/c/d) |
data-wix-correct letter of correct answer (a/b/c/d) |
||
data-wix-topic topic label (optional) |
data-wix-topic topic label (optional) |
||
| Line 68: | Line 68: | ||
return { |
return { |
||
topic: wix.data( block, 'wix-topic', '' ), |
topic: wix.data( block, 'wix-topic', '' ), |
||
question: |
question: block.getAttribute( 'data-wix-question' ) || '', |
||
correct: ( wix.data( block, 'wix-correct', 'a' ) ).toLowerCase(), |
correct: ( wix.data( block, 'wix-correct', 'a' ) ).toLowerCase(), |
||
explanation: wix.data( block, 'wix-explanation', '' ), |
explanation: wix.data( block, 'wix-explanation', '' ), |
||
| Line 141: | Line 141: | ||
var feedback = wix.buildFeedback( card ); |
var feedback = wix.buildFeedback( card ); |
||
// Nav bar |
// Nav bar — Next button hidden until answer chosen |
||
var isLast = state.step === questions.length - 1; |
var isLast = state.step === questions.length - 1; |
||
var nav = wix.buildNav( container, { |
var nav = wix.buildNav( container, { |
||
| Line 151: | Line 151: | ||
} ); |
} ); |
||
// Hold answer data locally — committing to state triggers a full |
|||
// Answer handler |
|||
// re-render, so we only do that when the user clicks Next. |
|||
var answered = false; |
var answered = false; |
||
var pendingScore = state.score; |
|||
| ⚫ | |||
wix.on( optionsList, 'click', '.wix-option', function ( target ) { |
wix.on( optionsList, 'click', '.wix-option', function ( target ) { |
||
if ( answered ) { |
if ( answered ) { |
||
| Line 162: | Line 166: | ||
var isCorrect = chosen === q.correct; |
var isCorrect = chosen === q.correct; |
||
// |
// Record answer locally |
||
| ⚫ | |||
pendingAnswers = state.answers.concat( [ { |
|||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
// Disable all options and highlight correct / chosen-wrong |
|||
wix.qsa( '.wix-option', optionsList ).forEach( function ( btn ) { |
wix.qsa( '.wix-option', optionsList ).forEach( function ( btn ) { |
||
btn.disabled = true; |
btn.disabled = true; |
||
| Line 185: | Line 197: | ||
} |
} |
||
// |
// Reveal Next button |
||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
container._wixQuizState.set( { |
|||
| ⚫ | |||
| ⚫ | |||
| ⚫ | |||
nav.show( 'next' ); |
nav.show( 'next' ); |
||
} ); |
} ); |
||
// Next: commit accumulated state and advance step |
|||
nav.on( 'next', function () { |
nav.on( 'next', function () { |
||
container._wixQuizState.set( { |
container._wixQuizState.set( { |
||
step: state.step + 1, |
|||
score: pendingScore, |
|||
| ⚫ | |||
| ⚫ | |||
} ); |
} ); |
||
} |
} |
||
Latest revision as of 14:03, 31 March 2026
/* ================================================================
WIX-QUIZ.JS — Wiki Interactive Experience: Quiz Engine
================================================================
Depends on: ext.gadget.wix-core (window.wix must exist)
Finds every container with data-wix-module="quiz", reads the
question blocks from the DOM, and manages the full lifecycle:
sequencing → answer selection → feedback → next → results.
Content-blind: no questions are hardcoded here.
All content is read from data attributes set by Template:Quiz.
DOM contract (set by templates):
─────────────────────────────────
Container: [data-wix-module="quiz"]
Question: [data-wix-question="...question text..."]
data-wix-correct letter of correct answer (a/b/c/d)
data-wix-topic topic label (optional)
data-wix-explanation explanation text (optional)
Option: [data-wix-option] four per question
data-wix-letter a / b / c / d
data-wix-text display text
================================================================ */
( function () {
'use strict';
/* ── Guard ──────────────────────────────────────────────────── */
mw.hook( 'wikipage.content' ).add( function () {
var containers = wix.initModules( 'quiz' );
if ( !containers.length ) {
return;
}
containers.forEach( initQuiz );
} );
/* ── Main init ──────────────────────────────────────────────── */
function initQuiz( container ) {
var questions = readQuestions( container );
if ( !questions.length ) {
return;
}
// Clear the static template HTML and replace with dynamic UI
wix.empty( container );
var state = wix.createState(
{ step: 0, score: 0, answers: [] },
function ( s ) { render( container, questions, s ); }
);
render( container, questions, state.get() );
// Expose state setter on the container for navigation callbacks
container._wixQuizState = state;
}
/* ── Read questions from the template DOM ───────────────────── */
function readQuestions( container ) {
var blocks = wix.qsa( '[data-wix-question]', container );
return blocks.map( function ( block ) {
var options = wix.qsa( '[data-wix-option]', block );
return {
topic: wix.data( block, 'wix-topic', '' ),
question: block.getAttribute( 'data-wix-question' ) || '',
correct: ( wix.data( block, 'wix-correct', 'a' ) ).toLowerCase(),
explanation: wix.data( block, 'wix-explanation', '' ),
options: options.map( function ( opt ) {
return {
letter: ( wix.data( opt, 'wix-letter', '' ) ).toLowerCase(),
text: wix.data( opt, 'wix-text', '' )
};
} )
};
} );
}
/* ── Render ─────────────────────────────────────────────────── */
function render( container, questions, state ) {
wix.empty( container );
container.classList.add( 'wix-animate-in' );
if ( state.step >= questions.length ) {
renderResults( container, questions, state );
} else {
renderQuestion( container, questions, state );
}
}
/* ── Question view ──────────────────────────────────────────── */
function renderQuestion( container, questions, state ) {
var q = questions[ state.step ];
var total = questions.length;
var current = state.step + 1;
// Progress bar
var bar = wix.buildProgressBar( container );
bar.update( current, total );
// Card
var card = wix.el( 'div', { className: 'wix-card' } );
container.appendChild( card );
// Header: topic + question number
var headerParts = [];
if ( q.topic ) {
headerParts.push( wix.el( 'span', { className: 'wix-topic', textContent: q.topic } ) );
}
headerParts.push( wix.el( 'span', { className: 'wix-badge', textContent: String( current ) } ) );
card.appendChild( wix.el( 'div', { className: 'wix-quiz-header' }, headerParts ) );
// Question text
card.appendChild( wix.el( 'p', { className: 'wix-question', textContent: q.question } ) );
// Options
var optionsList = wix.el( 'div', { className: 'wix-options' } );
q.options.forEach( function ( opt ) {
var letter = wix.el( 'span', {
className: 'wix-option-letter',
textContent: opt.letter.toUpperCase()
} );
var text = wix.el( 'span', { className: 'wix-option-text', textContent: opt.text } );
var btn = wix.el( 'button', {
className: 'wix-option',
'data-wix-letter': opt.letter
}, [ letter, text ] );
optionsList.appendChild( btn );
} );
card.appendChild( optionsList );
// Feedback panel (hidden until answer chosen)
var feedback = wix.buildFeedback( card );
// Nav bar — Next button hidden until answer chosen
var isLast = state.step === questions.length - 1;
var nav = wix.buildNav( container, {
next: {
label: isLast ? 'See results' : 'Next',
primary: true,
hidden: true
}
} );
// Hold answer data locally — committing to state triggers a full
// re-render, so we only do that when the user clicks Next.
var answered = false;
var pendingScore = state.score;
var pendingAnswers = state.answers.slice();
wix.on( optionsList, 'click', '.wix-option', function ( target ) {
if ( answered ) {
return;
}
answered = true;
var chosen = wix.data( target, 'wix-letter' );
var isCorrect = chosen === q.correct;
// Record answer locally
pendingScore = state.score + ( isCorrect ? 1 : 0 );
pendingAnswers = state.answers.concat( [ {
step: state.step,
chosen: chosen,
correct: isCorrect
} ] );
// Disable all options and highlight correct / chosen-wrong
wix.qsa( '.wix-option', optionsList ).forEach( function ( btn ) {
btn.disabled = true;
var letter = wix.data( btn, 'wix-letter' );
if ( letter === q.correct ) {
btn.classList.add( 'wix-option--correct' );
} else if ( letter === chosen && !isCorrect ) {
btn.classList.add( 'wix-option--wrong' );
}
} );
// Show feedback
if ( isCorrect ) {
feedback.show( 'correct', 'Correct!', q.explanation );
} else {
var correctText = getOptionText( q, q.correct );
feedback.show(
'wrong',
'Not quite — the correct answer is ' + q.correct.toUpperCase() + ': ' + correctText,
q.explanation
);
}
// Reveal Next button
nav.show( 'next' );
} );
// Next: commit accumulated state and advance step
nav.on( 'next', function () {
container._wixQuizState.set( {
step: state.step + 1,
score: pendingScore,
answers: pendingAnswers
} );
} );
}
/* ── Results view ───────────────────────────────────────────── */
function renderResults( container, questions, state ) {
var total = questions.length;
var score = state.score;
var pct = total > 0 ? Math.round( ( score / total ) * 100 ) : 0;
var results = wix.el( 'div', { className: 'wix-card wix-results' } );
container.appendChild( results );
// Title
results.appendChild( wix.el( 'p', {
className: 'wix-results-title',
textContent: 'Quiz complete'
} ) );
// Score ring
var ring = wix.buildScoreRing( results );
ring.set( score, total );
// Colour the percentage
var pctEl = wix.qs( '.wix-score-pct', results );
if ( pctEl ) {
if ( pct >= 80 ) {
pctEl.classList.add( 'wix-score-pct--high' );
} else if ( pct >= 50 ) {
pctEl.classList.add( 'wix-score-pct--mid' );
} else {
pctEl.classList.add( 'wix-score-pct--low' );
}
}
// Message
results.appendChild( wix.el( 'p', {
className: 'wix-results-msg',
textContent: resultMessage( pct )
} ) );
// Review list
var reviewList = wix.el( 'div', { className: 'wix-review-list' } );
state.answers.forEach( function ( ans ) {
var q = questions[ ans.step ];
var icon = wix.el( 'span', {
className: 'wix-review-icon wix-review-icon--' + ( ans.correct ? 'correct' : 'wrong' ),
textContent: ans.correct ? '✓' : '✗'
} );
var text = wix.el( 'span', { textContent: q.question } );
reviewList.appendChild( wix.el( 'div', { className: 'wix-review-item' }, [ icon, text ] ) );
} );
results.appendChild( reviewList );
// Restart button
var nav = wix.buildNav( container, {
restart: { label: 'Try again', outline: true }
} );
nav.on( 'restart', function () {
container._wixQuizState.set( { step: 0, score: 0, answers: [] } );
} );
}
/* ── Helpers ────────────────────────────────────────────────── */
function getOptionText( q, letter ) {
for ( var i = 0; i < q.options.length; i++ ) {
if ( q.options[ i ].letter === letter ) {
return q.options[ i ].text;
}
}
return '';
}
function resultMessage( pct ) {
if ( pct === 100 ) {
return 'Perfect score — excellent work!';
}
if ( pct >= 80 ) {
return 'Great result. Review the questions you missed to consolidate your understanding.';
}
if ( pct >= 50 ) {
return 'Good effort. Re-read the relevant sections and try again to strengthen your knowledge.';
}
return 'Keep studying and give it another go — each attempt builds familiarity with the material.';
}
}() );