munchboka-edutools 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of munchboka-edutools might be problematic. Click here for more details.
- munchboka_edutools/__init__.py +184 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +389 -0
- munchboka_edutools/directives/cas_popup.py +272 -0
- munchboka_edutools/directives/clear.py +103 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/factor_tree.py +549 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +165 -0
- munchboka_edutools/directives/horner.py +324 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/multi_plot.py +1126 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3105 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +516 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +274 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +112 -0
- munchboka_edutools/static/css/github-light.css +120 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +529 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/popup.css +115 -0
- munchboka_edutools/static/css/quiz.css +312 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/jeopardy.js +523 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/popup.js +85 -0
- munchboka_edutools/static/js/quiz.js +422 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
- munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
- munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
window.addEventListener("DOMContentLoaded", () => {
|
|
4
|
+
document.querySelectorAll(".popup-wrapper").forEach(wrapper => {
|
|
5
|
+
const trigger = wrapper.querySelector(".popup-trigger");
|
|
6
|
+
const bubble = wrapper.querySelector(".popup-bubble");
|
|
7
|
+
|
|
8
|
+
if (!trigger || !bubble) return;
|
|
9
|
+
|
|
10
|
+
let hideTimeout;
|
|
11
|
+
|
|
12
|
+
// Move bubble to body
|
|
13
|
+
const bodyBubble = bubble.cloneNode(true);
|
|
14
|
+
bodyBubble.style.display = "none";
|
|
15
|
+
document.body.appendChild(bodyBubble);
|
|
16
|
+
|
|
17
|
+
const positionBubble = () => {
|
|
18
|
+
const rect = trigger.getBoundingClientRect();
|
|
19
|
+
bodyBubble.style.position = "absolute";
|
|
20
|
+
bodyBubble.style.left = `${rect.left + window.scrollX}px`;
|
|
21
|
+
bodyBubble.style.top = `${rect.bottom + 6 + window.scrollY}px`;
|
|
22
|
+
bodyBubble.style.zIndex = "9999";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const showBubble = () => {
|
|
26
|
+
clearTimeout(hideTimeout);
|
|
27
|
+
document.querySelectorAll(".popup-bubble").forEach(b => b.style.display = "none");
|
|
28
|
+
positionBubble();
|
|
29
|
+
bodyBubble.style.display = "block";
|
|
30
|
+
|
|
31
|
+
if (window.renderMathInElement) {
|
|
32
|
+
renderMathInElement(bodyBubble, {
|
|
33
|
+
delimiters: [
|
|
34
|
+
{ left: "$$", right: "$$", display: true },
|
|
35
|
+
{ left: "$", right: "$", display: false },
|
|
36
|
+
{ left: "\\(", right: "\\)", display: false },
|
|
37
|
+
{ left: "\\[", right: "\\]", display: true }
|
|
38
|
+
],
|
|
39
|
+
throwOnError: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const hideBubble = () => {
|
|
45
|
+
hideTimeout = setTimeout(() => {
|
|
46
|
+
bodyBubble.style.display = "none";
|
|
47
|
+
}, 200);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Click toggle
|
|
51
|
+
trigger.addEventListener("click", e => {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
const visible = bodyBubble.style.display === "block";
|
|
54
|
+
document.querySelectorAll(".popup-bubble").forEach(b => b.style.display = "none");
|
|
55
|
+
if (!visible) showBubble();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Hover
|
|
59
|
+
trigger.addEventListener("mouseenter", showBubble);
|
|
60
|
+
trigger.addEventListener("mouseleave", hideBubble);
|
|
61
|
+
bodyBubble.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
|
62
|
+
bodyBubble.addEventListener("mouseleave", hideBubble);
|
|
63
|
+
|
|
64
|
+
// Global close
|
|
65
|
+
document.addEventListener("click", () => bodyBubble.style.display = "none");
|
|
66
|
+
document.addEventListener("keydown", e => {
|
|
67
|
+
if (e.key === "Escape") {
|
|
68
|
+
bodyBubble.style.display = "none";
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Render math in the trigger
|
|
73
|
+
if (window.renderMathInElement) {
|
|
74
|
+
renderMathInElement(trigger, {
|
|
75
|
+
delimiters: [
|
|
76
|
+
{ left: "$$", right: "$$", display: true },
|
|
77
|
+
{ left: "$", right: "$", display: false },
|
|
78
|
+
{ left: "\\(", right: "\\)", display: false },
|
|
79
|
+
{ left: "\\[", right: "\\]", display: true }
|
|
80
|
+
],
|
|
81
|
+
throwOnError: false
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// munchboka-edutools quiz runtime
|
|
2
|
+
// Combines SequentialMultipleChoiceQuiz and MultipleChoiceQuestion with minimal deps.
|
|
3
|
+
|
|
4
|
+
// Generate UUID function
|
|
5
|
+
function generateUUID() {
|
|
6
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
7
|
+
const r = (Math.random() * 16) | 0,
|
|
8
|
+
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
9
|
+
return v.toString(16);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shuffleArray(array) {
|
|
14
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
15
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
16
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class MultipleChoiceQuestion {
|
|
21
|
+
constructor({ id, content, answers }) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.content = content;
|
|
24
|
+
this.answers = answers.map((answer) => {
|
|
25
|
+
if (!Object.prototype.hasOwnProperty.call(answer, 'id')) {
|
|
26
|
+
answer.id = generateUUID();
|
|
27
|
+
}
|
|
28
|
+
return answer;
|
|
29
|
+
});
|
|
30
|
+
this.selectedAnswers = new Set();
|
|
31
|
+
this.elements = {}; // Store elements for easy access
|
|
32
|
+
this.correctlyAnswered = false; // Track if the question is correctly answered
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
shuffleAnswers() {
|
|
36
|
+
shuffleArray(this.answers);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(containerId) {
|
|
40
|
+
const container = document.getElementById(containerId);
|
|
41
|
+
|
|
42
|
+
// Create question element
|
|
43
|
+
const questionCard = document.createElement('div');
|
|
44
|
+
questionCard.classList.add('question-card');
|
|
45
|
+
questionCard.innerHTML = this.content;
|
|
46
|
+
|
|
47
|
+
// Append the question card to the container
|
|
48
|
+
container.appendChild(questionCard);
|
|
49
|
+
|
|
50
|
+
// Render LaTeX in the question
|
|
51
|
+
this.renderMathInElement(questionCard);
|
|
52
|
+
|
|
53
|
+
// Apply syntax highlighting to the question card
|
|
54
|
+
this.applySyntaxHighlighting(questionCard);
|
|
55
|
+
|
|
56
|
+
// Create answers container
|
|
57
|
+
const answersGrid = document.createElement('div');
|
|
58
|
+
answersGrid.classList.add('answers-grid');
|
|
59
|
+
|
|
60
|
+
// Append the answers grid to the container
|
|
61
|
+
container.appendChild(answersGrid);
|
|
62
|
+
|
|
63
|
+
// Create answer elements
|
|
64
|
+
this.answers.forEach((answer) => {
|
|
65
|
+
const answerCard = document.createElement('div');
|
|
66
|
+
answerCard.classList.add('answer-card');
|
|
67
|
+
answerCard.innerHTML = answer.content;
|
|
68
|
+
answerCard.dataset.answerId = answer.id;
|
|
69
|
+
|
|
70
|
+
// Append the answer card to the answers grid
|
|
71
|
+
answersGrid.appendChild(answerCard);
|
|
72
|
+
|
|
73
|
+
// Render LaTeX in the answer
|
|
74
|
+
this.renderMathInElement(answerCard);
|
|
75
|
+
|
|
76
|
+
// Apply syntax highlighting to the answer card
|
|
77
|
+
this.applySyntaxHighlighting(answerCard);
|
|
78
|
+
|
|
79
|
+
// Mark as selected if previously selected
|
|
80
|
+
if (this.selectedAnswers.has(answer.id)) {
|
|
81
|
+
answerCard.classList.add('selected');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Disable interaction if question is correctly answered
|
|
85
|
+
if (this.correctlyAnswered) {
|
|
86
|
+
answerCard.classList.add('disabled');
|
|
87
|
+
} else {
|
|
88
|
+
// Add click event to handle selection
|
|
89
|
+
answerCard.addEventListener('click', () => this.toggleSelection(answerCard));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Store elements for later access
|
|
94
|
+
this.elements.container = container;
|
|
95
|
+
this.elements.questionCard = questionCard;
|
|
96
|
+
this.elements.answersGrid = answersGrid;
|
|
97
|
+
|
|
98
|
+
// Apply correct class if previously answered correctly
|
|
99
|
+
if (this.correctlyAnswered) {
|
|
100
|
+
this.elements.questionCard.classList.add('correct');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
toggleSelection(answerCard) {
|
|
105
|
+
const answerId = answerCard.dataset.answerId;
|
|
106
|
+
if (this.selectedAnswers.has(answerId)) {
|
|
107
|
+
this.selectedAnswers.delete(answerId);
|
|
108
|
+
answerCard.classList.remove('selected');
|
|
109
|
+
} else {
|
|
110
|
+
// If single-choice, deselect other answers
|
|
111
|
+
if (this.isSingleChoice()) {
|
|
112
|
+
this.selectedAnswers.clear();
|
|
113
|
+
const allAnswerCards = this.elements.answersGrid.querySelectorAll('.answer-card');
|
|
114
|
+
allAnswerCards.forEach((card) => card.classList.remove('selected'));
|
|
115
|
+
}
|
|
116
|
+
this.selectedAnswers.add(answerId);
|
|
117
|
+
answerCard.classList.add('selected');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
checkAnswers(showAlert = true) {
|
|
122
|
+
// Get the correct answer IDs
|
|
123
|
+
const correctAnswerIds = this.answers.filter((answer) => answer.isCorrect).map((answer) => answer.id);
|
|
124
|
+
|
|
125
|
+
// Compare selectedAnswers with correctAnswerIds
|
|
126
|
+
const isCorrect = this.selectedAnswers.size === correctAnswerIds.length && [...this.selectedAnswers].every((id) => correctAnswerIds.includes(id));
|
|
127
|
+
|
|
128
|
+
// Provide visual feedback
|
|
129
|
+
if (isCorrect) {
|
|
130
|
+
this.elements.questionCard.classList.remove('incorrect'); // ✅ Remove incorrect class
|
|
131
|
+
this.elements.questionCard.classList.add('correct');
|
|
132
|
+
this.correctlyAnswered = true; // Mark question as correctly answered
|
|
133
|
+
// Disable further interaction
|
|
134
|
+
const allAnswerCards = this.elements.answersGrid.querySelectorAll('.answer-card');
|
|
135
|
+
allAnswerCards.forEach((card) => {
|
|
136
|
+
card.classList.add('disabled');
|
|
137
|
+
|
|
138
|
+
const newCard = card.cloneNode(true);
|
|
139
|
+
card.parentNode.replaceChild(newCard, card);
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
this.elements.questionCard.classList.add('incorrect');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Optionally display feedback if showAlert is true
|
|
146
|
+
if (showAlert) {
|
|
147
|
+
// eslint-disable-next-line no-alert
|
|
148
|
+
alert(isCorrect ? 'Riktig!' : 'Feil. Prøv igjen.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return isCorrect;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
markAsCorrectlyAnswered() {
|
|
155
|
+
this.correctlyAnswered = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isSingleChoice() {
|
|
159
|
+
// Determine if the question is single-choice
|
|
160
|
+
const correctAnswersCount = this.answers.filter((answer) => answer.isCorrect).length;
|
|
161
|
+
return correctAnswersCount === 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
renderMathInElement(element) {
|
|
165
|
+
// Ensure KaTeX renders LaTeX inside the element
|
|
166
|
+
if (typeof window !== 'undefined' && typeof window.renderMathInElement === 'function') {
|
|
167
|
+
window.renderMathInElement(element, {
|
|
168
|
+
delimiters: [
|
|
169
|
+
{ left: '$$', right: '$$', display: true },
|
|
170
|
+
{ left: '$', right: '$', display: false },
|
|
171
|
+
{ left: '\\[', right: '\\]', display: true },
|
|
172
|
+
{ left: '\\(', right: '\\)', display: false },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
applySyntaxHighlighting(element) {
|
|
179
|
+
// Apply syntax highlighting to code blocks if highlight.js is present
|
|
180
|
+
if (typeof window !== 'undefined' && window.hljs && typeof window.hljs.highlightElement === 'function') {
|
|
181
|
+
const codeBlocks = element.querySelectorAll('code');
|
|
182
|
+
codeBlocks.forEach((block) => {
|
|
183
|
+
window.hljs.highlightElement(block);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
class SequentialMultipleChoiceQuiz {
|
|
190
|
+
constructor(containerId, questionsData) {
|
|
191
|
+
this.containerId = containerId;
|
|
192
|
+
this.container = document.getElementById(containerId);
|
|
193
|
+
if (!this.container) {
|
|
194
|
+
throw new Error('Container not found');
|
|
195
|
+
}
|
|
196
|
+
this.questionsData = questionsData;
|
|
197
|
+
this.totalQuestions = questionsData.length;
|
|
198
|
+
this.currentQuestionIndex = 0;
|
|
199
|
+
this.uniqueId = generateUUID();
|
|
200
|
+
this.correctlyAnsweredQuestions = new Set(); // Track correctly answered questions
|
|
201
|
+
this.questionInstances = {}; // Store instances of MultipleChoiceQuestion
|
|
202
|
+
this.isFinished = false; // Track completion without destroying UI
|
|
203
|
+
this.init();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
init() {
|
|
207
|
+
this.generateHTML();
|
|
208
|
+
this.showQuestion();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
generateHTML() {
|
|
212
|
+
// Set up the main structure with Previous and Next buttons
|
|
213
|
+
this.container.innerHTML = `
|
|
214
|
+
<div id="question-counter-${this.uniqueId}" class="question-counter"></div>
|
|
215
|
+
<div id="question-container-${this.uniqueId}" class="mcq-container"></div>
|
|
216
|
+
<div class="button-container">
|
|
217
|
+
<button id="prev-question-${this.uniqueId}" class="button button-prev">← Forrige</button>
|
|
218
|
+
<button id="submit-answer-${this.uniqueId}" class="button button-run">Sjekk svar</button>
|
|
219
|
+
<button id="next-question-${this.uniqueId}" class="button button-next">Neste →</button>
|
|
220
|
+
</div>
|
|
221
|
+
<div id="quiz-completion-${this.uniqueId}" class="quiz-completion-message" style="display: none;">
|
|
222
|
+
<p>Da var quizen ferdig! 🎉</p>
|
|
223
|
+
</div>
|
|
224
|
+
<!-- Toast Notifications -->
|
|
225
|
+
<div id="toast-success-${this.uniqueId}" class="toast toast-success" style="display: none;">
|
|
226
|
+
<p>Riktig! 🎉</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div id="toast-error-${this.uniqueId}" class="toast toast-error" style="display: none;">
|
|
229
|
+
<p>Prøv igjen!</p>
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
// Add event listeners for the buttons
|
|
234
|
+
document.getElementById(`submit-answer-${this.uniqueId}`).addEventListener('click', () => this.submitAnswer());
|
|
235
|
+
document.getElementById(`prev-question-${this.uniqueId}`).addEventListener('click', () => this.goToPreviousQuestion());
|
|
236
|
+
document.getElementById(`next-question-${this.uniqueId}`).addEventListener('click', () => this.goToNextQuestion());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
showQuestion() {
|
|
240
|
+
// If we moved beyond the virtual completion card, clamp back
|
|
241
|
+
if (this.currentQuestionIndex > this.totalQuestions) {
|
|
242
|
+
this.currentQuestionIndex = this.totalQuestions;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update the question counter (hide count on completion card)
|
|
246
|
+
const counter = document.getElementById(`question-counter-${this.uniqueId}`);
|
|
247
|
+
if (this.currentQuestionIndex === this.totalQuestions) {
|
|
248
|
+
counter.textContent = '';
|
|
249
|
+
} else {
|
|
250
|
+
counter.textContent = `Spørsmål ${this.currentQuestionIndex + 1} / ${this.totalQuestions}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Clear the question container before rendering the new question
|
|
254
|
+
const questionContainer = document.getElementById(`question-container-${this.uniqueId}`);
|
|
255
|
+
questionContainer.innerHTML = ''; // Clear previous question
|
|
256
|
+
|
|
257
|
+
// If we are on the virtual completion card, show the completion banner
|
|
258
|
+
// and do NOT render a question (buttons remain for navigation)
|
|
259
|
+
const banner = document.getElementById(`quiz-completion-${this.uniqueId}`);
|
|
260
|
+
if (this.currentQuestionIndex === this.totalQuestions) {
|
|
261
|
+
if (banner) banner.style.display = 'block';
|
|
262
|
+
this.updateNavigationButtons();
|
|
263
|
+
return;
|
|
264
|
+
} else if (banner) {
|
|
265
|
+
banner.style.display = 'none';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const questionData = this.questionsData[this.currentQuestionIndex];
|
|
269
|
+
if (!questionData) {
|
|
270
|
+
return; // Nothing to show
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if we already have an instance of the question
|
|
274
|
+
if (Object.prototype.hasOwnProperty.call(this.questionInstances, this.currentQuestionIndex)) {
|
|
275
|
+
// Retrieve the existing instance
|
|
276
|
+
this.currentQuestion = this.questionInstances[this.currentQuestionIndex];
|
|
277
|
+
} else {
|
|
278
|
+
// Create a new instance and store it
|
|
279
|
+
this.currentQuestion = new MultipleChoiceQuestion(questionData);
|
|
280
|
+
// Shuffle the answers on first creation
|
|
281
|
+
this.currentQuestion.shuffleAnswers();
|
|
282
|
+
this.questionInstances[this.currentQuestionIndex] = this.currentQuestion;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Render the question
|
|
286
|
+
this.currentQuestion.render(`question-container-${this.uniqueId}`);
|
|
287
|
+
|
|
288
|
+
// Update navigation buttons and other UI elements
|
|
289
|
+
this.updateNavigationButtons();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
submitAnswer() {
|
|
293
|
+
// Disable the submit button to prevent multiple clicks
|
|
294
|
+
const submitButton = document.getElementById(`submit-answer-${this.uniqueId}`);
|
|
295
|
+
submitButton.disabled = true;
|
|
296
|
+
|
|
297
|
+
const isCorrect = this.currentQuestion.checkAnswers(false); // Pass 'false' to suppress alerts
|
|
298
|
+
|
|
299
|
+
if (isCorrect) {
|
|
300
|
+
this.correctlyAnsweredQuestions.add(this.currentQuestionIndex); // Track correct answer
|
|
301
|
+
this.showToast('success');
|
|
302
|
+
// Mark the question as correctly answered
|
|
303
|
+
this.currentQuestion.markAsCorrectlyAnswered();
|
|
304
|
+
// Update navigation buttons after a short delay
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
this.updateNavigationButtons();
|
|
307
|
+
}, 800); // Delay to allow the user to see the feedback
|
|
308
|
+
} else {
|
|
309
|
+
this.showToast('error');
|
|
310
|
+
// Re-enable the submit button so the user can try again
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
submitButton.disabled = false;
|
|
313
|
+
}, 1500); // Match the toast display time
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
showToast(type) {
|
|
318
|
+
const toastId = type === 'success' ? `toast-success-${this.uniqueId}` : `toast-error-${this.uniqueId}`;
|
|
319
|
+
const toast = document.getElementById(toastId);
|
|
320
|
+
|
|
321
|
+
if (!toast) {
|
|
322
|
+
console.error(`Toast element with ID ${toastId} not found.`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Ensure the container is positioned relatively
|
|
327
|
+
if (getComputedStyle(this.container).position === 'static') {
|
|
328
|
+
this.container.style.position = 'relative';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Display the toast in the center of the container
|
|
332
|
+
toast.style.position = 'absolute';
|
|
333
|
+
toast.style.top = '50%';
|
|
334
|
+
toast.style.left = '50%';
|
|
335
|
+
toast.style.transform = 'translate(-50%, -50%)';
|
|
336
|
+
toast.style.display = 'block';
|
|
337
|
+
|
|
338
|
+
// Hide the toast after a delay
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
toast.style.display = 'none';
|
|
341
|
+
}, 1500); // Display for 1.5 seconds
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
finishQuiz() {
|
|
345
|
+
// Mark finished and show completion banner, keep UI for navigation
|
|
346
|
+
this.isFinished = true;
|
|
347
|
+
const banner = document.getElementById(`quiz-completion-${this.uniqueId}`);
|
|
348
|
+
if (banner) {
|
|
349
|
+
banner.style.display = 'block';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
updateNavigationButtons() {
|
|
354
|
+
const prevButton = document.getElementById(`prev-question-${this.uniqueId}`);
|
|
355
|
+
const nextButton = document.getElementById(`next-question-${this.uniqueId}`);
|
|
356
|
+
const submitButton = document.getElementById(`submit-answer-${this.uniqueId}`);
|
|
357
|
+
|
|
358
|
+
const currentIndex = this.currentQuestionIndex;
|
|
359
|
+
|
|
360
|
+
// Show or hide the Previous button
|
|
361
|
+
if (currentIndex === 0 || !this.correctlyAnsweredQuestions.has(currentIndex - 1)) {
|
|
362
|
+
prevButton.style.display = 'none'; // Hide the button
|
|
363
|
+
} else {
|
|
364
|
+
prevButton.style.display = ''; // Show the button
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Show or hide the Next button
|
|
368
|
+
if (currentIndex === this.totalQuestions) {
|
|
369
|
+
nextButton.style.display = 'none'; // No next beyond completion card
|
|
370
|
+
} else if (this.correctlyAnsweredQuestions.has(currentIndex)) {
|
|
371
|
+
nextButton.style.display = ''; // Show the button (including on last real question)
|
|
372
|
+
} else {
|
|
373
|
+
nextButton.style.display = 'none'; // Hide the button
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Disable/hide the submit button on completion card; otherwise disable if already correct
|
|
377
|
+
if (currentIndex === this.totalQuestions) {
|
|
378
|
+
submitButton.style.display = 'none';
|
|
379
|
+
} else {
|
|
380
|
+
submitButton.style.display = '';
|
|
381
|
+
submitButton.disabled = this.correctlyAnsweredQuestions.has(currentIndex);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
goToPreviousQuestion() {
|
|
386
|
+
if (this.currentQuestionIndex > 0 && this.correctlyAnsweredQuestions.has(this.currentQuestionIndex - 1)) {
|
|
387
|
+
this.currentQuestionIndex--;
|
|
388
|
+
this.showQuestion();
|
|
389
|
+
// this.scrollToQuizContainer(); // Scroll to the quiz container
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
goToNextQuestion() {
|
|
394
|
+
if (this.currentQuestionIndex === this.totalQuestions) {
|
|
395
|
+
return; // Already at completion card
|
|
396
|
+
}
|
|
397
|
+
if (this.correctlyAnsweredQuestions.has(this.currentQuestionIndex)) {
|
|
398
|
+
if (this.currentQuestionIndex < this.totalQuestions - 1) {
|
|
399
|
+
this.currentQuestionIndex++;
|
|
400
|
+
this.showQuestion();
|
|
401
|
+
} else if (this.currentQuestionIndex === this.totalQuestions - 1) {
|
|
402
|
+
// Move to virtual completion card
|
|
403
|
+
this.currentQuestionIndex = this.totalQuestions;
|
|
404
|
+
this.finishQuiz();
|
|
405
|
+
this.showQuestion();
|
|
406
|
+
}
|
|
407
|
+
// this.scrollToQuizContainer(); // Scroll to the quiz container
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
scrollToQuizContainer() {
|
|
412
|
+
this.container.scrollIntoView({
|
|
413
|
+
behavior: 'smooth',
|
|
414
|
+
block: 'center',
|
|
415
|
+
inline: 'nearest',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Expose globally (for inline scripts)
|
|
421
|
+
window.SequentialMultipleChoiceQuiz = SequentialMultipleChoiceQuiz;
|
|
422
|
+
window.MultipleChoiceQuestion = MultipleChoiceQuestion;
|