munchboka-edutools 0.2.3__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.

Files changed (157) hide show
  1. munchboka_edutools/__init__.py +184 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +389 -0
  6. munchboka_edutools/directives/cas_popup.py +428 -0
  7. munchboka_edutools/directives/clear.py +103 -0
  8. munchboka_edutools/directives/dialogue.py +137 -0
  9. munchboka_edutools/directives/escape_room.py +296 -0
  10. munchboka_edutools/directives/escape_room2.py +318 -0
  11. munchboka_edutools/directives/factor_tree.py +552 -0
  12. munchboka_edutools/directives/flashcards.py +233 -0
  13. munchboka_edutools/directives/ggb.py +209 -0
  14. munchboka_edutools/directives/ggb_icon.py +105 -0
  15. munchboka_edutools/directives/ggb_popup.py +308 -0
  16. munchboka_edutools/directives/horner.py +326 -0
  17. munchboka_edutools/directives/interactive_code.py +75 -0
  18. munchboka_edutools/directives/jeopardy.py +252 -0
  19. munchboka_edutools/directives/jeopardy2.py +636 -0
  20. munchboka_edutools/directives/multi_plot.py +2524 -0
  21. munchboka_edutools/directives/multi_plot2.py +252 -0
  22. munchboka_edutools/directives/pair_puzzle.py +191 -0
  23. munchboka_edutools/directives/parsons.py +109 -0
  24. munchboka_edutools/directives/plot.py +3758 -0
  25. munchboka_edutools/directives/poly_icon.py +111 -0
  26. munchboka_edutools/directives/polydiv.py +346 -0
  27. munchboka_edutools/directives/popup.py +245 -0
  28. munchboka_edutools/directives/quiz.py +291 -0
  29. munchboka_edutools/directives/quiz2.py +453 -0
  30. munchboka_edutools/directives/signchart.py +519 -0
  31. munchboka_edutools/directives/signchart2.py +1545 -0
  32. munchboka_edutools/directives/timed_quiz.py +436 -0
  33. munchboka_edutools/directives/turtle.py +157 -0
  34. munchboka_edutools/static/css/admonitions.css +2012 -0
  35. munchboka_edutools/static/css/cas_popup.css +242 -0
  36. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  37. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  38. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  39. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  40. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  41. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  42. munchboka_edutools/static/css/dialogue.css +92 -0
  43. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  44. munchboka_edutools/static/css/figures.css +321 -0
  45. munchboka_edutools/static/css/flashcards.css +219 -0
  46. munchboka_edutools/static/css/general_style.css +74 -0
  47. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  48. munchboka_edutools/static/css/github-dark.css +147 -0
  49. munchboka_edutools/static/css/github-light.css +155 -0
  50. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  51. munchboka_edutools/static/css/interactive_code.css +582 -0
  52. munchboka_edutools/static/css/jeopardy.css +553 -0
  53. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  54. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  55. munchboka_edutools/static/css/popup.css +115 -0
  56. munchboka_edutools/static/css/quiz.css +377 -0
  57. munchboka_edutools/static/css/timedQuiz.css +375 -0
  58. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  59. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  60. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  61. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  62. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  63. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  64. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  65. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  77. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  78. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  79. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  80. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  81. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  82. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  83. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  92. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  95. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  96. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  97. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  98. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  99. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  100. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  101. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  102. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  103. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  104. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  105. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  111. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  112. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  113. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  114. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  115. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  116. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  117. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  119. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  121. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  123. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  124. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  125. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  126. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  127. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  128. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  129. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  130. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  131. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  132. munchboka_edutools/static/js/casThemeManager.js +99 -0
  133. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  134. munchboka_edutools/static/js/flashcards.js +199 -0
  135. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  136. munchboka_edutools/static/js/highlight-init.js +6 -0
  137. munchboka_edutools/static/js/interactiveCode/codeEditor.js +648 -0
  138. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +441 -0
  139. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  140. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  141. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  142. munchboka_edutools/static/js/jeopardy.js +560 -0
  143. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  144. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  145. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  146. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  147. munchboka_edutools/static/js/popup.js +85 -0
  148. munchboka_edutools/static/js/quiz.js +566 -0
  149. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  150. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  151. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  152. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  153. munchboka_edutools/static/js/utils.js +3 -0
  154. munchboka_edutools-0.2.3.dist-info/METADATA +109 -0
  155. munchboka_edutools-0.2.3.dist-info/RECORD +157 -0
  156. munchboka_edutools-0.2.3.dist-info/WHEEL +4 -0
  157. munchboka_edutools-0.2.3.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,566 @@
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
+
204
+ // Storage key for localStorage
205
+ this.storageKey = 'quiz:' + (this.container.id || 'default');
206
+
207
+ this.init();
208
+ }
209
+
210
+ buildState() {
211
+ return {
212
+ currentQuestionIndex: this.currentQuestionIndex,
213
+ correctlyAnsweredQuestions: Array.from(this.correctlyAnsweredQuestions),
214
+ isFinished: this.isFinished,
215
+ questionStates: Object.keys(this.questionInstances).reduce((acc, key) => {
216
+ const q = this.questionInstances[key];
217
+ acc[key] = {
218
+ selectedAnswers: Array.from(q.selectedAnswers),
219
+ correctlyAnswered: q.correctlyAnswered
220
+ };
221
+ return acc;
222
+ }, {})
223
+ };
224
+ }
225
+
226
+ saveState() {
227
+ try {
228
+ localStorage.setItem(this.storageKey, JSON.stringify(this.buildState()));
229
+ } catch (e) {
230
+ console.warn('Failed to save quiz state:', e);
231
+ }
232
+ }
233
+
234
+ loadState() {
235
+ try {
236
+ const raw = localStorage.getItem(this.storageKey);
237
+ if (!raw) return null;
238
+ const obj = JSON.parse(raw);
239
+ if (!obj || typeof obj !== 'object') return null;
240
+ return obj;
241
+ } catch (e) {
242
+ console.warn('Failed to load quiz state:', e);
243
+ return null;
244
+ }
245
+ }
246
+
247
+ clearState() {
248
+ try {
249
+ localStorage.removeItem(this.storageKey);
250
+ } catch (e) {
251
+ console.warn('Failed to clear quiz state:', e);
252
+ }
253
+ }
254
+
255
+ applySavedState(saved) {
256
+ if (!saved) return;
257
+
258
+ // Restore correctly answered questions
259
+ if (Array.isArray(saved.correctlyAnsweredQuestions)) {
260
+ this.correctlyAnsweredQuestions = new Set(saved.correctlyAnsweredQuestions);
261
+ }
262
+
263
+ // Restore current question index
264
+ if (typeof saved.currentQuestionIndex === 'number') {
265
+ this.currentQuestionIndex = saved.currentQuestionIndex;
266
+ }
267
+
268
+ // Restore finished state
269
+ if (saved.isFinished) {
270
+ this.isFinished = true;
271
+ }
272
+
273
+ // Show the current question
274
+ this.showQuestion();
275
+ }
276
+
277
+ init() {
278
+ // Check for saved state before generating HTML
279
+ const saved = this.loadState();
280
+
281
+ this.generateHTML();
282
+
283
+ // Show resume prompt if there's saved progress
284
+ if (saved && (saved.correctlyAnsweredQuestions && saved.correctlyAnsweredQuestions.length > 0)) {
285
+ this.showResumePrompt(saved);
286
+ } else {
287
+ this.showQuestion();
288
+ }
289
+ }
290
+
291
+ showResumePrompt(saved) {
292
+ const resumePrompt = document.createElement('div');
293
+ resumePrompt.className = 'quiz-resume-prompt';
294
+
295
+ const txt = document.createElement('div');
296
+ txt.className = 'quiz-resume-text';
297
+ txt.textContent = 'Fortsett der du slapp?';
298
+
299
+ const actions = document.createElement('div');
300
+ actions.className = 'quiz-resume-actions';
301
+
302
+ const btnStart = document.createElement('button');
303
+ btnStart.className = 'button button-prev';
304
+ btnStart.textContent = 'Start fra begynnelsen';
305
+
306
+ const btnResume = document.createElement('button');
307
+ btnResume.className = 'button button-next';
308
+ btnResume.textContent = 'Fortsett';
309
+
310
+ actions.appendChild(btnStart);
311
+ actions.appendChild(btnResume);
312
+ resumePrompt.appendChild(txt);
313
+ resumePrompt.appendChild(actions);
314
+
315
+ // Hide quiz elements while showing resume prompt
316
+ const questionCounter = document.getElementById(`question-counter-${this.uniqueId}`);
317
+ const questionContainer = document.getElementById(`question-container-${this.uniqueId}`);
318
+ const buttonContainer = this.container.querySelector('.button-container');
319
+ const completionMessage = document.getElementById(`quiz-completion-${this.uniqueId}`);
320
+
321
+ if (questionCounter) questionCounter.style.display = 'none';
322
+ if (questionContainer) questionContainer.style.display = 'none';
323
+ if (buttonContainer) buttonContainer.style.display = 'none';
324
+ if (completionMessage) completionMessage.style.display = 'none';
325
+
326
+ btnStart.addEventListener('click', () => {
327
+ this.clearState();
328
+ resumePrompt.remove();
329
+ // Show quiz elements again
330
+ if (questionCounter) questionCounter.style.display = '';
331
+ if (questionContainer) questionContainer.style.display = '';
332
+ if (buttonContainer) buttonContainer.style.display = '';
333
+ this.showQuestion();
334
+ });
335
+
336
+ btnResume.addEventListener('click', () => {
337
+ resumePrompt.remove();
338
+ // Show quiz elements again
339
+ if (questionCounter) questionCounter.style.display = '';
340
+ if (questionContainer) questionContainer.style.display = '';
341
+ if (buttonContainer) buttonContainer.style.display = '';
342
+ this.applySavedState(saved);
343
+ });
344
+
345
+ // Insert at the beginning of the container
346
+ this.container.insertBefore(resumePrompt, this.container.firstChild);
347
+ }
348
+
349
+ generateHTML() {
350
+ // Set up the main structure with Previous and Next buttons
351
+ this.container.innerHTML += `
352
+ <div id="question-counter-${this.uniqueId}" class="question-counter"></div>
353
+ <div id="question-container-${this.uniqueId}" class="mcq-container"></div>
354
+ <div class="button-container">
355
+ <button id="prev-question-${this.uniqueId}" class="button button-prev">← Forrige</button>
356
+ <button id="submit-answer-${this.uniqueId}" class="button button-run">Sjekk svar</button>
357
+ <button id="next-question-${this.uniqueId}" class="button button-next">Neste →</button>
358
+ </div>
359
+ <div id="quiz-completion-${this.uniqueId}" class="quiz-completion-message" style="display: none;">
360
+ <p>Da var quizen ferdig! 🎉</p>
361
+ </div>
362
+ <!-- Toast Notifications -->
363
+ <div id="toast-success-${this.uniqueId}" class="toast toast-success" style="display: none;">
364
+ <p>Riktig! 🎉</p>
365
+ </div>
366
+ <div id="toast-error-${this.uniqueId}" class="toast toast-error" style="display: none;">
367
+ <p>Prøv igjen!</p>
368
+ </div>
369
+ `;
370
+
371
+ // Add event listeners for the buttons
372
+ document.getElementById(`submit-answer-${this.uniqueId}`).addEventListener('click', () => this.submitAnswer());
373
+ document.getElementById(`prev-question-${this.uniqueId}`).addEventListener('click', () => this.goToPreviousQuestion());
374
+ document.getElementById(`next-question-${this.uniqueId}`).addEventListener('click', () => this.goToNextQuestion());
375
+ }
376
+
377
+ showQuestion() {
378
+ // If we moved beyond the virtual completion card, clamp back
379
+ if (this.currentQuestionIndex > this.totalQuestions) {
380
+ this.currentQuestionIndex = this.totalQuestions;
381
+ }
382
+
383
+ // Update the question counter (hide count on completion card)
384
+ const counter = document.getElementById(`question-counter-${this.uniqueId}`);
385
+ if (this.currentQuestionIndex === this.totalQuestions) {
386
+ counter.textContent = '';
387
+ } else {
388
+ counter.textContent = `Spørsmål ${this.currentQuestionIndex + 1} / ${this.totalQuestions}`;
389
+ }
390
+
391
+ // Clear the question container before rendering the new question
392
+ const questionContainer = document.getElementById(`question-container-${this.uniqueId}`);
393
+ questionContainer.innerHTML = ''; // Clear previous question
394
+
395
+ // If we are on the virtual completion card, show the completion banner
396
+ // and do NOT render a question (buttons remain for navigation)
397
+ const banner = document.getElementById(`quiz-completion-${this.uniqueId}`);
398
+ if (this.currentQuestionIndex === this.totalQuestions) {
399
+ if (banner) banner.style.display = 'block';
400
+ this.updateNavigationButtons();
401
+ return;
402
+ } else if (banner) {
403
+ banner.style.display = 'none';
404
+ }
405
+
406
+ const questionData = this.questionsData[this.currentQuestionIndex];
407
+ if (!questionData) {
408
+ return; // Nothing to show
409
+ }
410
+
411
+ // Check if we already have an instance of the question
412
+ if (Object.prototype.hasOwnProperty.call(this.questionInstances, this.currentQuestionIndex)) {
413
+ // Retrieve the existing instance
414
+ this.currentQuestion = this.questionInstances[this.currentQuestionIndex];
415
+ } else {
416
+ // Create a new instance and store it
417
+ this.currentQuestion = new MultipleChoiceQuestion(questionData);
418
+ // Shuffle the answers on first creation
419
+ this.currentQuestion.shuffleAnswers();
420
+ this.questionInstances[this.currentQuestionIndex] = this.currentQuestion;
421
+ }
422
+
423
+ // Render the question
424
+ this.currentQuestion.render(`question-container-${this.uniqueId}`);
425
+
426
+ // Update navigation buttons and other UI elements
427
+ this.updateNavigationButtons();
428
+ }
429
+
430
+ submitAnswer() {
431
+ // Disable the submit button to prevent multiple clicks
432
+ const submitButton = document.getElementById(`submit-answer-${this.uniqueId}`);
433
+ submitButton.disabled = true;
434
+
435
+ const isCorrect = this.currentQuestion.checkAnswers(false); // Pass 'false' to suppress alerts
436
+
437
+ if (isCorrect) {
438
+ this.correctlyAnsweredQuestions.add(this.currentQuestionIndex); // Track correct answer
439
+ this.showToast('success');
440
+ // Mark the question as correctly answered
441
+ this.currentQuestion.markAsCorrectlyAnswered();
442
+ // Save state
443
+ this.saveState();
444
+ // Update navigation buttons after a short delay
445
+ setTimeout(() => {
446
+ this.updateNavigationButtons();
447
+ }, 800); // Delay to allow the user to see the feedback
448
+ } else {
449
+ this.showToast('error');
450
+ // Re-enable the submit button so the user can try again
451
+ setTimeout(() => {
452
+ submitButton.disabled = false;
453
+ }, 1500); // Match the toast display time
454
+ }
455
+ }
456
+
457
+ showToast(type) {
458
+ const toastId = type === 'success' ? `toast-success-${this.uniqueId}` : `toast-error-${this.uniqueId}`;
459
+ const toast = document.getElementById(toastId);
460
+
461
+ if (!toast) {
462
+ console.error(`Toast element with ID ${toastId} not found.`);
463
+ return;
464
+ }
465
+
466
+ // Ensure the container is positioned relatively
467
+ if (getComputedStyle(this.container).position === 'static') {
468
+ this.container.style.position = 'relative';
469
+ }
470
+
471
+ // Display the toast in the center of the container
472
+ toast.style.position = 'absolute';
473
+ toast.style.top = '50%';
474
+ toast.style.left = '50%';
475
+ toast.style.transform = 'translate(-50%, -50%)';
476
+ toast.style.display = 'block';
477
+
478
+ // Hide the toast after a delay
479
+ setTimeout(() => {
480
+ toast.style.display = 'none';
481
+ }, 1500); // Display for 1.5 seconds
482
+ }
483
+
484
+ finishQuiz() {
485
+ // Mark finished and show completion banner, keep UI for navigation
486
+ this.isFinished = true;
487
+ const banner = document.getElementById(`quiz-completion-${this.uniqueId}`);
488
+ if (banner) {
489
+ banner.style.display = 'block';
490
+ }
491
+ // Clear saved state after completion
492
+ this.clearState();
493
+ }
494
+
495
+ updateNavigationButtons() {
496
+ const prevButton = document.getElementById(`prev-question-${this.uniqueId}`);
497
+ const nextButton = document.getElementById(`next-question-${this.uniqueId}`);
498
+ const submitButton = document.getElementById(`submit-answer-${this.uniqueId}`);
499
+
500
+ const currentIndex = this.currentQuestionIndex;
501
+
502
+ // Show or hide the Previous button
503
+ if (currentIndex === 0 || !this.correctlyAnsweredQuestions.has(currentIndex - 1)) {
504
+ prevButton.style.display = 'none'; // Hide the button
505
+ } else {
506
+ prevButton.style.display = ''; // Show the button
507
+ }
508
+
509
+ // Show or hide the Next button
510
+ if (currentIndex === this.totalQuestions) {
511
+ nextButton.style.display = 'none'; // No next beyond completion card
512
+ } else if (this.correctlyAnsweredQuestions.has(currentIndex)) {
513
+ nextButton.style.display = ''; // Show the button (including on last real question)
514
+ } else {
515
+ nextButton.style.display = 'none'; // Hide the button
516
+ }
517
+
518
+ // Disable/hide the submit button on completion card; otherwise disable if already correct
519
+ if (currentIndex === this.totalQuestions) {
520
+ submitButton.style.display = 'none';
521
+ } else {
522
+ submitButton.style.display = '';
523
+ submitButton.disabled = this.correctlyAnsweredQuestions.has(currentIndex);
524
+ }
525
+ }
526
+
527
+ goToPreviousQuestion() {
528
+ if (this.currentQuestionIndex > 0 && this.correctlyAnsweredQuestions.has(this.currentQuestionIndex - 1)) {
529
+ this.currentQuestionIndex--;
530
+ this.saveState();
531
+ this.showQuestion();
532
+ // this.scrollToQuizContainer(); // Scroll to the quiz container
533
+ }
534
+ }
535
+
536
+ goToNextQuestion() {
537
+ if (this.currentQuestionIndex === this.totalQuestions) {
538
+ return; // Already at completion card
539
+ }
540
+ if (this.correctlyAnsweredQuestions.has(this.currentQuestionIndex)) {
541
+ if (this.currentQuestionIndex < this.totalQuestions - 1) {
542
+ this.currentQuestionIndex++;
543
+ this.saveState();
544
+ this.showQuestion();
545
+ } else if (this.currentQuestionIndex === this.totalQuestions - 1) {
546
+ // Move to virtual completion card
547
+ this.currentQuestionIndex = this.totalQuestions;
548
+ this.finishQuiz();
549
+ this.showQuestion();
550
+ }
551
+ // this.scrollToQuizContainer(); // Scroll to the quiz container
552
+ }
553
+ }
554
+
555
+ scrollToQuizContainer() {
556
+ this.container.scrollIntoView({
557
+ behavior: 'smooth',
558
+ block: 'center',
559
+ inline: 'nearest',
560
+ });
561
+ }
562
+ }
563
+
564
+ // Expose globally (for inline scripts)
565
+ window.SequentialMultipleChoiceQuiz = SequentialMultipleChoiceQuiz;
566
+ window.MultipleChoiceQuestion = MultipleChoiceQuestion;