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,560 @@
1
+ /* Faithful Jeopardy runtime from the legacy project with math/code rendering, teams, turns, and scoring. */
2
+ (function(){
3
+ function renderMathIfAvailable(root){
4
+ if (typeof renderMathInElement === 'function') {
5
+ try { renderMathInElement(root, {delimiters: [
6
+ {left: '$$', right: '$$', display: true},
7
+ {left: '$', right: '$', display: false},
8
+ {left: '\\(', right: '\\)', display: false},
9
+ {left: '\\[', right: '\\]', display: true}
10
+ ]}); } catch(e){}
11
+ }
12
+ }
13
+
14
+ function highlightCodeIfAvailable(root){
15
+ if (typeof hljs !== 'undefined' && root) {
16
+ try {
17
+ root.querySelectorAll('pre code').forEach(function(block){
18
+ hljs.highlightElement(block);
19
+ });
20
+ } catch(e){}
21
+ }
22
+ }
23
+
24
+ function executeScripts(root){
25
+ // When content is inserted via innerHTML, script tags don't execute
26
+ // This function finds and re-executes them
27
+ if (!root) return;
28
+ const scripts = root.querySelectorAll('script');
29
+ scripts.forEach(function(oldScript){
30
+ const newScript = document.createElement('script');
31
+ if (oldScript.src) {
32
+ newScript.src = oldScript.src;
33
+ } else {
34
+ newScript.textContent = oldScript.textContent;
35
+ }
36
+ // Copy attributes
37
+ Array.from(oldScript.attributes).forEach(function(attr){
38
+ newScript.setAttribute(attr.name, attr.value);
39
+ });
40
+ oldScript.parentNode.replaceChild(newScript, oldScript);
41
+ });
42
+ }
43
+
44
+ function initJeopardy(container){
45
+ let cfg = null;
46
+ try {
47
+ const dataNode = container.querySelector('script.jeopardy-data[type="application/json"]');
48
+ let raw = dataNode ? (dataNode.textContent || dataNode.innerText || '') : '';
49
+ if (!raw || !raw.trim()) {
50
+ raw = container.getAttribute('data-config') || '{}';
51
+ }
52
+ if (raw && raw.indexOf('&') !== -1 && raw.indexOf('{') === -1) {
53
+ const ta = document.createElement('textarea'); ta.innerHTML = raw; raw = ta.value;
54
+ }
55
+ cfg = JSON.parse(raw);
56
+ } catch(e){ cfg = {}; }
57
+ const nTeams = Math.max(1, parseInt(cfg.teams||2,10));
58
+ const categories = cfg.categories||[];
59
+ const values = (cfg.values||[]).slice().sort(function(a,b){return a-b;});
60
+
61
+ const tileStates = Object.create(null);
62
+ const categoryStats = categories.map(()=>({correct:0, wrong:0}));
63
+ let totalPlayableTiles = 0;
64
+ let scoreboardShown = false;
65
+ const storageKey = 'jeopardy:' + (container && container.id ? container.id : 'default');
66
+ function buildState(){
67
+ return {
68
+ started,
69
+ gameMode,
70
+ timerMs,
71
+ currentTurn,
72
+ scoreboardShown,
73
+ teams: teams.map(t=>({name:t.name, score:t.score})),
74
+ teamCategoryPoints: teamCategoryPoints.map(row=> row.slice()),
75
+ categoryStats: categoryStats.map(s=>({correct:s.correct, wrong:s.wrong})),
76
+ tileStates: Object.fromEntries(Object.entries(tileStates).map(([k,v])=>[k,{locked:!!(v&&v.locked)}]))
77
+ };
78
+ }
79
+ function saveState(){ try { localStorage.setItem(storageKey, JSON.stringify(buildState())); } catch(e){} }
80
+ function loadState(){
81
+ try { const raw = localStorage.getItem(storageKey); if(!raw) return null; const obj = JSON.parse(raw); if (!obj || typeof obj !== 'object') return null; return obj; } catch(e){ return null; }
82
+ }
83
+ function clearState(){ try { localStorage.removeItem(storageKey); } catch(e){} }
84
+ let gameMode = 'duel';
85
+ let timerMs = 0;
86
+ let currentTurn = 0;
87
+ let started = false;
88
+
89
+ const scorebar = document.createElement('div');
90
+ scorebar.className = 'jeopardy-scorebar';
91
+ scorebar.style.display = 'none';
92
+ const turnIndicator = document.createElement('div');
93
+ turnIndicator.className = 'jeopardy-turn-indicator';
94
+ turnIndicator.style.display = 'none';
95
+ const topbar = document.createElement('div');
96
+ topbar.className = 'jeopardy-topbar';
97
+ const scoreWrap = document.createElement('div');
98
+ scoreWrap.className = 'jeopardy-scorebar-wrap';
99
+ scoreWrap.appendChild(scorebar);
100
+ const topbarRight = document.createElement('div');
101
+ topbarRight.className = 'jeopardy-topbar-right';
102
+ const resetBtn = document.createElement('button');
103
+ resetBtn.className = 'j-btn accent jeopardy-reset-button';
104
+ resetBtn.textContent = 'Reset spill';
105
+ resetBtn.style.display = 'none';
106
+ topbarRight.appendChild(resetBtn);
107
+ topbar.appendChild(scoreWrap);
108
+ topbar.appendChild(topbarRight);
109
+
110
+ let teams = [];
111
+ let teamCategoryPoints = Array.from({length: nTeams}, () => Array.from({length: categories.length}, () => 0));
112
+ function updateActiveTeamHighlight(){
113
+ teams.forEach((t,i)=>{
114
+ if (!t._el) return;
115
+ if (gameMode==='turn' && i===currentTurn) t._el.classList.add('active');
116
+ else t._el.classList.remove('active');
117
+ });
118
+ if (turnIndicator) {
119
+ if (gameMode==='turn' && started && teams.length>0) {
120
+ turnIndicator.style.display = '';
121
+ turnIndicator.textContent = `Tur: ${teams[currentTurn].name}`;
122
+ } else {
123
+ turnIndicator.style.display = 'none';
124
+ turnIndicator.textContent = '';
125
+ }
126
+ }
127
+ }
128
+ function rebuildTeams(newN, names){
129
+ teams = [];
130
+ scorebar.innerHTML = '';
131
+ teamCategoryPoints = Array.from({length: newN}, () => Array.from({length: categories.length}, () => 0));
132
+ for(let i=0;i<newN;i++){
133
+ const team = { name: names && names[i] ? names[i] : `Lag ${i+1}`, score: 0 };
134
+ teams.push(team);
135
+ const el = document.createElement('div');
136
+ el.className = 'jeopardy-team';
137
+ const nameSpan = document.createElement('span');
138
+ nameSpan.className = 'team-name';
139
+ nameSpan.textContent = team.name;
140
+ const scoreSpan = document.createElement('span');
141
+ scoreSpan.className = 'score';
142
+ scoreSpan.textContent = '0';
143
+ el.appendChild(nameSpan);
144
+ el.appendChild(scoreSpan);
145
+ scorebar.appendChild(el);
146
+ team._elScore = scoreSpan;
147
+ team._el = el;
148
+ }
149
+ updateActiveTeamHighlight();
150
+ }
151
+ function applySavedState(state){
152
+ try {
153
+ started = !!state.started;
154
+ gameMode = state.gameMode || gameMode;
155
+ timerMs = typeof state.timerMs === 'number' ? state.timerMs : timerMs;
156
+ currentTurn = typeof state.currentTurn === 'number' ? state.currentTurn : 0;
157
+ scoreboardShown = !!state.scoreboardShown;
158
+ const savedTeams = Array.isArray(state.teams) ? state.teams : [];
159
+ const names = savedTeams.length ? savedTeams.map(t=> t && t.name ? String(t.name) : '') : null;
160
+ const newN = Math.max(1, names ? names.length : nTeams);
161
+ rebuildTeams(newN, names || undefined);
162
+ if (savedTeams.length){
163
+ savedTeams.forEach((t,i)=>{
164
+ if (teams[i]){ teams[i].score = Number(t.score)||0; if (teams[i]._elScore) teams[i]._elScore.textContent = String(teams[i].score); }
165
+ });
166
+ }
167
+ if (Array.isArray(state.teamCategoryPoints)){
168
+ teamCategoryPoints = state.teamCategoryPoints.map(row=> Array.isArray(row)? row.slice(): []);
169
+ }
170
+ if (Array.isArray(state.categoryStats)){
171
+ state.categoryStats.forEach((s,i)=>{ if (categoryStats[i]){ categoryStats[i].correct = Number(s.correct)||0; categoryStats[i].wrong = Number(s.wrong)||0; }});
172
+ }
173
+ if (state.tileStates && typeof state.tileStates === 'object'){
174
+ Object.keys(state.tileStates).forEach(k=>{ const v = state.tileStates[k]; if (v && v.locked){ tileStates[k] = {locked:true}; }});
175
+ try {
176
+ container.querySelectorAll('.jeopardy-tile').forEach(btn=>{
177
+ const k = btn && btn.dataset ? btn.dataset.key : null; if (!k) return;
178
+ if (tileStates[k] && tileStates[k].locked){ btn.disabled = true; btn.classList.add('used'); }
179
+ });
180
+ } catch(e){}
181
+ }
182
+ try { scorebar.style.display = ''; } catch(e){}
183
+ try { resetBtn.style.display = ''; } catch(e){}
184
+ try { setup.style.display = 'none'; } catch(e){}
185
+ updateActiveTeamHighlight();
186
+ } catch(e){}
187
+ }
188
+
189
+ const table = document.createElement('table');
190
+ table.className = 'jeopardy-grid';
191
+
192
+ const thead = document.createElement('thead');
193
+ const thr = document.createElement('tr');
194
+ categories.forEach(cat=>{
195
+ const th = document.createElement('th');
196
+ th.textContent = cat.name||'';
197
+ thr.appendChild(th);
198
+ });
199
+ thead.appendChild(thr);
200
+
201
+ const tbody = document.createElement('tbody');
202
+
203
+ const lookup = {};
204
+ categories.forEach((cat,ci)=>{
205
+ (cat.tiles||[]).forEach(t=>{
206
+ const key = ci+'|'+t.value;
207
+ lookup[key] = t;
208
+ });
209
+ });
210
+ totalPlayableTiles = Object.keys(lookup).length;
211
+
212
+ values.forEach(val=>{
213
+ const tr = document.createElement('tr');
214
+ categories.forEach((cat,ci)=>{
215
+ const td = document.createElement('td');
216
+ const tile = document.createElement('button');
217
+ tile.className = 'jeopardy-tile';
218
+ tile.textContent = val;
219
+ const key = ci+'|'+val;
220
+ tile.dataset.key = key;
221
+ const data = lookup[key] || null;
222
+ if(!data){ tile.disabled = true; tile.classList.add('used'); }
223
+ if (tileStates[key] && tileStates[key].locked) { tile.disabled = true; tile.classList.add('used'); }
224
+ tile.addEventListener('click', ()=>{
225
+ if(tile.classList.contains('used')||tile.disabled) return;
226
+ if (!started) return;
227
+ openModal(cat.name||'', val, data, tile, key);
228
+ });
229
+ td.appendChild(tile);
230
+ tr.appendChild(td);
231
+ });
232
+ tbody.appendChild(tr);
233
+ });
234
+
235
+ table.appendChild(thead); table.appendChild(tbody);
236
+
237
+ const backdrop = document.createElement('div');
238
+ backdrop.className = 'jeopardy-modal-backdrop';
239
+ const modal = document.createElement('div');
240
+ modal.className = 'jeopardy-modal';
241
+ const header = document.createElement('div'); header.className='jeopardy-modal-header';
242
+ const title = document.createElement('div');
243
+ const timerBox = document.createElement('div'); timerBox.className='jeopardy-timer'; timerBox.style.marginLeft='auto';
244
+ const closeBtn = document.createElement('button'); closeBtn.className='j-btn warn'; closeBtn.textContent='Lukk';
245
+ const body = document.createElement('div'); body.className='jeopardy-modal-body';
246
+ const footer = document.createElement('div'); footer.className='jeopardy-modal-footer';
247
+
248
+ header.appendChild(title); header.appendChild(timerBox); header.appendChild(closeBtn);
249
+ modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer);
250
+ backdrop.appendChild(modal);
251
+
252
+ function setScore(i, delta){ teams[i].score += delta; teams[i]._elScore.textContent = String(teams[i].score); }
253
+ let escHandler = null;
254
+ const hideModal = ()=>{
255
+ backdrop.style.display = 'none';
256
+ try { if (typeof stopTimer === 'function') stopTimer(); } catch(e){}
257
+ if (escHandler) {
258
+ try { document.removeEventListener('keydown', escHandler); } catch(e){}
259
+ escHandler = null;
260
+ }
261
+ };
262
+ function enableEscClose(){
263
+ if (escHandler) {
264
+ try { document.removeEventListener('keydown', escHandler); } catch(e){}
265
+ escHandler = null;
266
+ }
267
+ escHandler = function(e){
268
+ const key = e.key || e.code;
269
+ if (key === 'Escape' || key === 'Esc') {
270
+ try { e.preventDefault(); } catch(_){ }
271
+ hideModal();
272
+ }
273
+ };
274
+ try { document.addEventListener('keydown', escHandler); } catch(e){}
275
+ }
276
+ function checkCompletionAndShowWinner(){
277
+ if (scoreboardShown) return;
278
+ if (totalPlayableTiles <= 0) return;
279
+ let lockedCount = 0;
280
+ for (const k in tileStates) { if (tileStates[k] && tileStates[k].locked) lockedCount++; }
281
+ if (lockedCount >= totalPlayableTiles) {
282
+ scoreboardShown = true;
283
+ openWinner();
284
+ }
285
+ }
286
+ function openWinner(){
287
+ const sorted = teams.map((t,i)=>({name:t.name, score:t.score, idx:i}))
288
+ .sort((a,b)=> b.score - a.score);
289
+ const max = sorted.length ? sorted[0].score : 0;
290
+ const winners = sorted.filter(x=> x.score === max);
291
+ title.textContent = winners.length > 1 ? 'Scoreboard' : 'Scoreboard';
292
+ body.innerHTML = '';
293
+ footer.innerHTML = '';
294
+ const grid = document.createElement('div');
295
+ const cols = 2 + categories.length;
296
+ grid.style.display = 'grid';
297
+ grid.style.gridTemplateColumns = `1.5fr ${'auto '.repeat(categories.length)} auto`;
298
+ grid.style.gap = '0.5rem 1rem';
299
+ const hTeam = document.createElement('div'); hTeam.style.fontWeight = '700'; hTeam.textContent = 'Lag';
300
+ grid.appendChild(hTeam);
301
+ categories.forEach(cat => { const h = document.createElement('div'); h.style.fontWeight='700'; h.textContent = cat.name||''; grid.appendChild(h); });
302
+ const hScore = document.createElement('div'); hScore.style.fontWeight='700'; hScore.textContent = 'Score';
303
+ grid.appendChild(hScore);
304
+ sorted.forEach(t => {
305
+ const rowBold = (t.score === max);
306
+ const name = document.createElement('div'); name.textContent = t.name; if (rowBold) name.style.fontWeight = '700'; grid.appendChild(name);
307
+ const pointsRow = teamCategoryPoints[t.idx] || [];
308
+ categories.forEach((_, ci) => { const cell = document.createElement('div'); const val = pointsRow[ci] || 0; cell.textContent = String(val); if (rowBold) cell.style.fontWeight = '700'; grid.appendChild(cell); });
309
+ const sc = document.createElement('div'); sc.textContent = String(t.score); if (rowBold) sc.style.fontWeight = '700'; grid.appendChild(sc);
310
+ });
311
+ body.appendChild(grid);
312
+ backdrop.style.display = 'flex';
313
+ enableEscClose();
314
+ closeBtn.onclick = hideModal; backdrop.onclick = (e)=>{ if(e.target===backdrop) hideModal(); };
315
+ }
316
+
317
+ function resetGame(){
318
+ try { hideModal(); } catch(e){}
319
+ try { stopTimer(); } catch(e){}
320
+ started = false;
321
+ scoreboardShown = false;
322
+ currentTurn = 0;
323
+ for (const k in tileStates) { try { delete tileStates[k]; } catch(e){} }
324
+ for (let i=0;i<categoryStats.length;i++){ categoryStats[i].correct = 0; categoryStats[i].wrong = 0; }
325
+ teams = [];
326
+ teamCategoryPoints = Array.from({length: nTeams}, () => Array.from({length: categories.length}, () => 0));
327
+ scorebar.innerHTML = '';
328
+ try {
329
+ container.querySelectorAll('.jeopardy-tile').forEach(b=>{ b.disabled = false; b.classList.remove('used'); });
330
+ } catch(e){}
331
+ try { scorebar.style.display = 'none'; } catch(e){}
332
+ try { turnIndicator.style.display = 'none'; } catch(e){}
333
+ try { resetBtn.style.display = 'none'; } catch(e){}
334
+ try { setup.style.display = ''; } catch(e){}
335
+ clearState();
336
+ }
337
+ resetBtn.addEventListener('click', resetGame);
338
+
339
+ let countdownId = null;
340
+ function stopTimer(){ if (countdownId) { try { clearInterval(countdownId); } catch(e){} countdownId = null; } timerBox.textContent=''; }
341
+ function startTimer(onTimeout){
342
+ stopTimer();
343
+ if (!timerMs || timerMs <= 0) return;
344
+ let remaining = Math.floor(timerMs/1000);
345
+ const render = () => { timerBox.textContent = `${Math.floor(remaining/60)}:${String(remaining%60).padStart(2,'0')}`; };
346
+ render();
347
+ countdownId = setInterval(()=>{
348
+ remaining -= 1;
349
+ if (remaining <= 0){ stopTimer(); try { onTimeout && onTimeout(); } catch(e){} }
350
+ else render();
351
+ }, 1000);
352
+ }
353
+
354
+ function openModal(category, value, data, tile, key){
355
+ title.textContent = `${category} – ${value}`;
356
+ body.innerHTML = '';
357
+ footer.innerHTML = '';
358
+
359
+ const q = document.createElement('div'); q.className='jeopardy-q'; q.innerHTML = data && data.question ? data.question : '';
360
+ const a = document.createElement('div'); a.className='jeopardy-a'; a.innerHTML = data && data.answer ? data.answer : '';
361
+
362
+ const revealBtn = document.createElement('button'); revealBtn.className='j-btn success'; revealBtn.textContent='Fasit';
363
+ revealBtn.addEventListener('click', ()=>{
364
+ const showing = a.style.display !== 'block';
365
+ a.style.display = showing ? 'block' : 'none';
366
+ try { setTimeout(()=>{ if (typeof body.scrollTo === 'function') body.scrollTo({ top: body.scrollHeight, behavior: 'smooth' }); else body.scrollTop = body.scrollHeight; }, 0); } catch(e){ try { body.scrollTop = body.scrollHeight; } catch(_e){} }
367
+ });
368
+ body.appendChild(q); body.appendChild(a);
369
+
370
+ // Execute any scripts in the question and answer content
371
+ executeScripts(q);
372
+ executeScripts(a);
373
+
374
+ // Tracking for duel vs turn-based scoring
375
+ let scored = false;
376
+ let duelPendingTeams = new Set();
377
+ const teamActions = document.createElement('div'); teamActions.className='jeopardy-team-actions';
378
+ const disableTeamButtons = () => { teamActions.querySelectorAll('button').forEach(b=>{ b.disabled = true; }); };
379
+ const onTimeout = ()=>{
380
+ disableTeamButtons();
381
+ if (gameMode==='turn' && teams.length>0){ currentTurn = (currentTurn+1)%teams.length; updateActiveTeamHighlight(); }
382
+ try { saveState(); } catch(e){}
383
+ try { setTimeout(()=>{ hideModal(); }, 300); } catch(e){}
384
+ };
385
+ teams.forEach((t, i)=>{
386
+ // In turn-based mode, only the active team can be scored on
387
+ if (gameMode==='turn' && i!==currentTurn) return;
388
+ // Create a per-team column with label and buttons
389
+ const teamCol = document.createElement('div');
390
+ teamCol.className = 'jeopardy-team-column';
391
+ const teamLabel = document.createElement('div');
392
+ teamLabel.className = 'jeopardy-team-label';
393
+ teamLabel.textContent = t.name;
394
+ teamCol.appendChild(teamLabel);
395
+
396
+ const add = document.createElement('button');
397
+ add.className='j-btn primary';
398
+ add.textContent = `+${value} poeng`;
399
+
400
+ // Explicit 0-points option per team in both modes
401
+ let zeroBtn = document.createElement('button');
402
+ zeroBtn.className = 'j-btn secondary';
403
+ zeroBtn.textContent = '0 poeng';
404
+
405
+ // Register this team as pending in duel mode
406
+ if (gameMode === 'duel') {
407
+ duelPendingTeams.add(i);
408
+ }
409
+
410
+ const registerScore = (delta)=>{
411
+ if (tileStates[key] && tileStates[key].locked) return;
412
+ setScore(i, delta);
413
+ try {
414
+ const ci = parseInt(String(key).split('|')[0], 10);
415
+ if (!isNaN(ci) && categoryStats[ci]) {
416
+ if (delta > 0) categoryStats[ci].correct++;
417
+ }
418
+ if (!isNaN(ci) && teamCategoryPoints[i]) {
419
+ if (typeof teamCategoryPoints[i][ci] !== 'number') teamCategoryPoints[i][ci] = 0;
420
+ teamCategoryPoints[i][ci] += delta;
421
+ }
422
+ } catch(e){}
423
+ };
424
+
425
+ const finalizeIfNeeded = ()=>{
426
+ if (gameMode === 'duel') {
427
+ if (duelPendingTeams.size > 0) return;
428
+ } else {
429
+ // turn-based: single decision ends the question
430
+ }
431
+
432
+ scored = true;
433
+ tileStates[key] = { locked: true };
434
+ if (tile) { tile.classList.add('used'); tile.disabled = true; }
435
+ if (gameMode==='turn' && teams.length>0){ currentTurn = (currentTurn+1)%teams.length; updateActiveTeamHighlight(); }
436
+ try { saveState(); setTimeout(()=>{ hideModal(); checkCompletionAndShowWinner(); }, 300); } catch(e){}
437
+ };
438
+
439
+ const handleAdd = ()=>{
440
+ if (gameMode === 'turn') {
441
+ if (scored) return;
442
+ registerScore(value);
443
+ scored = true;
444
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
445
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
446
+ finalizeIfNeeded();
447
+ } else {
448
+ if (!duelPendingTeams.has(i)) return;
449
+ registerScore(value);
450
+ duelPendingTeams.delete(i);
451
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
452
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
453
+ finalizeIfNeeded();
454
+ }
455
+ };
456
+
457
+ const handleZero = ()=>{
458
+ if (gameMode === 'turn') {
459
+ if (scored) return;
460
+ // Explicitly choose 0 points: no score change, but consume the question
461
+ scored = true;
462
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
463
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
464
+ finalizeIfNeeded();
465
+ } else if (gameMode === 'duel') {
466
+ if (!duelPendingTeams.has(i)) return;
467
+ // Zero points: just clear pending state for this team
468
+ duelPendingTeams.delete(i);
469
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
470
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
471
+ finalizeIfNeeded();
472
+ }
473
+ };
474
+
475
+ add.addEventListener('click', handleAdd);
476
+ if (zeroBtn) zeroBtn.addEventListener('click', handleZero);
477
+
478
+ if (tileStates[key] && tileStates[key].locked) {
479
+ add.disabled = true;
480
+ if (zeroBtn) zeroBtn.disabled = true;
481
+ }
482
+
483
+ teamCol.appendChild(add);
484
+ if (zeroBtn) teamCol.appendChild(zeroBtn);
485
+ teamActions.appendChild(teamCol);
486
+ });
487
+ const footerRight = document.createElement('div');
488
+ footerRight.className = 'jeopardy-footer-right';
489
+ footerRight.appendChild(revealBtn);
490
+ footer.appendChild(teamActions);
491
+ footer.appendChild(footerRight);
492
+
493
+ renderMathIfAvailable(q); renderMathIfAvailable(a);
494
+ highlightCodeIfAvailable(q); highlightCodeIfAvailable(a);
495
+
496
+ backdrop.style.display = 'flex';
497
+ enableEscClose();
498
+ startTimer(onTimeout);
499
+ closeBtn.onclick = hideModal; backdrop.onclick = (e)=>{ if(e.target===backdrop) hideModal(); };
500
+ }
501
+
502
+ const setup = document.createElement('div'); setup.className='jeopardy-setup';
503
+ const fTeams = document.createElement('div'); fTeams.className='jp-field';
504
+ const lTeams = document.createElement('label'); lTeams.textContent='Antall lag:'; const sTeams=document.createElement('select');
505
+ [1,2,3,4,5,6].forEach(n=>{ const opt=document.createElement('option'); opt.value=String(n); opt.textContent=String(n); if(n===nTeams) opt.selected=true; sTeams.appendChild(opt); });
506
+ fTeams.appendChild(lTeams); fTeams.appendChild(sTeams);
507
+ const namesWrap = document.createElement('div'); namesWrap.className='jp-names';
508
+ function renderNames(){ namesWrap.innerHTML=''; const n=parseInt(sTeams.value,10)||1; for(let i=0;i<n;i++){ const row=document.createElement('div'); row.className='jp-name-row'; const lbl=document.createElement('label'); lbl.textContent=`Lagnavn ${i+1}`; const inp=document.createElement('input'); inp.type='text'; inp.value=`Lag ${i+1}`; row.appendChild(lbl); row.appendChild(inp); namesWrap.appendChild(row);} }
509
+ sTeams.addEventListener('change', renderNames); renderNames();
510
+ const fTimer=document.createElement('div'); fTimer.className='jp-field'; const lTimer=document.createElement('label'); lTimer.textContent='Timer:'; const sTimer=document.createElement('select'); [{label:'∞',ms:0},{label:'30s',ms:30000},{label:'1 min',ms:60000},{label:'2 min',ms:120000}].forEach((t,i)=>{ const opt=document.createElement('option'); opt.value=String(t.ms); opt.textContent=t.label; if(i===0) opt.selected=true; sTimer.appendChild(opt);}); fTimer.appendChild(lTimer); fTimer.appendChild(sTimer);
511
+ const fMode=document.createElement('div'); fMode.className='jp-field'; const lMode=document.createElement('label'); lMode.textContent='Modus:'; const sMode=document.createElement('select'); [{v:'turn',t:'Turn-based'},{v:'duel',t:'Duell'}].forEach(m=>{ const opt=document.createElement('option'); opt.value=m.v; opt.textContent=m.t; if(m.v==='duel') opt.selected=true; sMode.appendChild(opt);}); fMode.appendChild(lMode); fMode.appendChild(sMode);
512
+ const startBtn=document.createElement('button'); startBtn.className='j-btn primary'; startBtn.textContent='Start spill';
513
+ startBtn.addEventListener('click', ()=>{
514
+ const newN = parseInt(sTeams.value,10)||1;
515
+ const names = Array.from(namesWrap.querySelectorAll('input')).map((inp,i)=> inp.value && inp.value.trim() ? inp.value.trim() : `Lag ${i+1}`);
516
+ gameMode = sMode.value==='turn' ? 'turn' : 'duel';
517
+ timerMs = parseInt(sTimer.value,10)||0;
518
+ currentTurn = Math.floor(Math.random()*Math.max(1,newN));
519
+ started = true;
520
+ try { if (typeof resumePrompt !== 'undefined' && resumePrompt && resumePrompt.parentNode) resumePrompt.remove(); } catch(e){}
521
+ rebuildTeams(newN, names);
522
+ try { scorebar.style.display = ''; } catch(e){}
523
+ try { resetBtn.style.display = ''; } catch(e){}
524
+ try { updateActiveTeamHighlight(); } catch(e){}
525
+ setup.style.display='none';
526
+ try { saveState(); } catch(e){}
527
+ });
528
+ setup.appendChild(fTeams); setup.appendChild(namesWrap); setup.appendChild(fTimer); setup.appendChild(fMode); setup.appendChild(startBtn);
529
+
530
+ container.innerHTML = '';
531
+ const saved = loadState();
532
+ let resumePrompt = null;
533
+ if (saved && (saved.started || (saved.tileStates && Object.keys(saved.tileStates).length>0) || (Array.isArray(saved.teams) && saved.teams.some(t=> (t&&Number(t.score)||0)!==0)))){
534
+ resumePrompt = document.createElement('div'); resumePrompt.className='jeopardy-resume-prompt';
535
+ const txt = document.createElement('div'); txt.className='jeopardy-resume-text'; txt.textContent = 'Fortsett der du slapp?';
536
+ const actions = document.createElement('div'); actions.className='jeopardy-resume-actions';
537
+ const btnStart = document.createElement('button'); btnStart.className='j-btn accent'; btnStart.textContent='Start fra begynnelsen';
538
+ const btnResume = document.createElement('button'); btnResume.className='j-btn primary'; btnResume.textContent='Fortsett';
539
+ actions.appendChild(btnStart); actions.appendChild(btnResume);
540
+ resumePrompt.appendChild(txt); resumePrompt.appendChild(actions);
541
+ btnStart.addEventListener('click', ()=>{
542
+ try { clearState(); } catch(e){}
543
+ try { resumePrompt.remove(); } catch(e){}
544
+ try { setup.style.display = ''; } catch(e){}
545
+ });
546
+ btnResume.addEventListener('click', ()=>{ try { applySavedState(saved); } catch(e){}; try { resumePrompt.remove(); } catch(e){}; });
547
+ container.appendChild(resumePrompt);
548
+ try { setup.style.display = 'none'; } catch(e){}
549
+ }
550
+ container.appendChild(setup);
551
+ container.appendChild(topbar);
552
+ container.appendChild(turnIndicator);
553
+ container.appendChild(table);
554
+ container.appendChild(backdrop);
555
+ }
556
+
557
+ document.addEventListener('DOMContentLoaded', function(){
558
+ document.querySelectorAll('.jeopardy-container[data-config]').forEach(initJeopardy);
559
+ });
560
+ })();
@@ -0,0 +1,64 @@
1
+ class DraggableItem {
2
+ constructor(id, content, pairId) {
3
+ this.id = id;
4
+ this.content = content;
5
+ this.pairId = pairId;
6
+ this.element = null;
7
+ }
8
+
9
+ createElement() {
10
+ const div = document.createElement('div');
11
+ div.classList.add('draggable-item');
12
+ div.setAttribute('draggable', 'true');
13
+ div.dataset.id = this.id;
14
+ div.dataset.pairId = this.pairId;
15
+
16
+ // Set the inner HTML content directly
17
+ div.innerHTML = this.content;
18
+ this.element = div;
19
+
20
+ // Apply syntax highlighting if there is a <pre><code> block
21
+ if (this.containsCodeBlock(this.content)) {
22
+ // Only try to highlight if hljs (highlight.js) is available
23
+ if (typeof hljs !== 'undefined') {
24
+ hljs.highlightElement(div.querySelector('code'));
25
+ }
26
+ }
27
+
28
+ // Render LaTeX math
29
+ this.renderMath();
30
+
31
+ this.addDragEvents();
32
+ return div;
33
+ }
34
+
35
+ containsCodeBlock(content) {
36
+ // Check if the content contains a <pre><code> block
37
+ return /<pre><code[\s\S]*<\/code><\/pre>/.test(content);
38
+ }
39
+
40
+ renderMath() {
41
+ // Ensure KaTeX renders LaTeX inside the item
42
+ renderMathInElement(this.element, {
43
+ delimiters: [
44
+ { left: "$$", right: "$$", display: true },
45
+ { left: "$", right: "$", display: false },
46
+ { left: "\\[", right: "\\]", display: true },
47
+ { left: "\\(", right: "\\)", display: false }
48
+ ]
49
+ });
50
+ }
51
+
52
+ addDragEvents() {
53
+ this.element.addEventListener('dragstart', (e) => {
54
+ e.dataTransfer.setData('text/plain', this.id);
55
+ setTimeout(() => {
56
+ this.element.style.opacity = '0'; // Hide the item temporarily while dragging
57
+ }, 0);
58
+ });
59
+
60
+ this.element.addEventListener('dragend', () => {
61
+ this.element.style.opacity = '1'; // Show the item again when dragging ends
62
+ });
63
+ }
64
+ }