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.

Files changed (149) 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 +272 -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/factor_tree.py +549 -0
  11. munchboka_edutools/directives/ggb.py +209 -0
  12. munchboka_edutools/directives/ggb_icon.py +105 -0
  13. munchboka_edutools/directives/ggb_popup.py +165 -0
  14. munchboka_edutools/directives/horner.py +324 -0
  15. munchboka_edutools/directives/interactive_code.py +75 -0
  16. munchboka_edutools/directives/jeopardy.py +252 -0
  17. munchboka_edutools/directives/multi_plot.py +1126 -0
  18. munchboka_edutools/directives/pair_puzzle.py +191 -0
  19. munchboka_edutools/directives/parsons.py +109 -0
  20. munchboka_edutools/directives/plot.py +3105 -0
  21. munchboka_edutools/directives/poly_icon.py +111 -0
  22. munchboka_edutools/directives/polydiv.py +344 -0
  23. munchboka_edutools/directives/popup.py +245 -0
  24. munchboka_edutools/directives/quiz.py +291 -0
  25. munchboka_edutools/directives/signchart.py +516 -0
  26. munchboka_edutools/directives/timed_quiz.py +436 -0
  27. munchboka_edutools/directives/turtle.py +157 -0
  28. munchboka_edutools/static/css/admonitions.css +2012 -0
  29. munchboka_edutools/static/css/cas_popup.css +242 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  33. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  34. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  35. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  36. munchboka_edutools/static/css/dialogue.css +92 -0
  37. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  38. munchboka_edutools/static/css/figures.css +274 -0
  39. munchboka_edutools/static/css/general_style.css +74 -0
  40. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  41. munchboka_edutools/static/css/github-dark.css +112 -0
  42. munchboka_edutools/static/css/github-light.css +120 -0
  43. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  44. munchboka_edutools/static/css/interactive_code.css +582 -0
  45. munchboka_edutools/static/css/jeopardy.css +529 -0
  46. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  47. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  48. munchboka_edutools/static/css/popup.css +115 -0
  49. munchboka_edutools/static/css/quiz.css +312 -0
  50. munchboka_edutools/static/css/timedQuiz.css +375 -0
  51. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  55. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  56. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  57. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  58. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  81. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  82. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  84. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  85. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  92. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  96. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  97. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  98. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  112. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  113. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  115. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  116. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  119. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  121. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  123. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  124. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  125. munchboka_edutools/static/js/casThemeManager.js +99 -0
  126. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  127. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  128. munchboka_edutools/static/js/highlight-init.js +6 -0
  129. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  130. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  131. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  132. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  133. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  134. munchboka_edutools/static/js/jeopardy.js +523 -0
  135. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  136. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  137. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  138. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  139. munchboka_edutools/static/js/popup.js +85 -0
  140. munchboka_edutools/static/js/quiz.js +422 -0
  141. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  142. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  143. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  144. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  145. munchboka_edutools/static/js/utils.js +3 -0
  146. munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
  147. munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
  148. munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
  149. munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,523 @@
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 initJeopardy(container){
25
+ let cfg = null;
26
+ try {
27
+ const dataNode = container.querySelector('script.jeopardy-data[type="application/json"]');
28
+ let raw = dataNode ? (dataNode.textContent || dataNode.innerText || '') : '';
29
+ if (!raw || !raw.trim()) {
30
+ raw = container.getAttribute('data-config') || '{}';
31
+ }
32
+ if (raw && raw.indexOf('&') !== -1 && raw.indexOf('{') === -1) {
33
+ const ta = document.createElement('textarea'); ta.innerHTML = raw; raw = ta.value;
34
+ }
35
+ cfg = JSON.parse(raw);
36
+ } catch(e){ cfg = {}; }
37
+ const nTeams = Math.max(1, parseInt(cfg.teams||2,10));
38
+ const categories = cfg.categories||[];
39
+ const values = (cfg.values||[]).slice().sort(function(a,b){return a-b;});
40
+
41
+ const tileStates = Object.create(null);
42
+ const categoryStats = categories.map(()=>({correct:0, wrong:0}));
43
+ let totalPlayableTiles = 0;
44
+ let scoreboardShown = false;
45
+ const storageKey = 'jeopardy:' + (container && container.id ? container.id : 'default');
46
+ function buildState(){
47
+ return {
48
+ started,
49
+ gameMode,
50
+ timerMs,
51
+ currentTurn,
52
+ scoreboardShown,
53
+ teams: teams.map(t=>({name:t.name, score:t.score})),
54
+ teamCategoryPoints: teamCategoryPoints.map(row=> row.slice()),
55
+ categoryStats: categoryStats.map(s=>({correct:s.correct, wrong:s.wrong})),
56
+ tileStates: Object.fromEntries(Object.entries(tileStates).map(([k,v])=>[k,{locked:!!(v&&v.locked)}]))
57
+ };
58
+ }
59
+ function saveState(){ try { localStorage.setItem(storageKey, JSON.stringify(buildState())); } catch(e){} }
60
+ function loadState(){
61
+ 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; }
62
+ }
63
+ function clearState(){ try { localStorage.removeItem(storageKey); } catch(e){} }
64
+ let gameMode = 'duel';
65
+ let timerMs = 0;
66
+ let currentTurn = 0;
67
+ let started = false;
68
+
69
+ const scorebar = document.createElement('div');
70
+ scorebar.className = 'jeopardy-scorebar';
71
+ scorebar.style.display = 'none';
72
+ const turnIndicator = document.createElement('div');
73
+ turnIndicator.className = 'jeopardy-turn-indicator';
74
+ turnIndicator.style.display = 'none';
75
+ const topbar = document.createElement('div');
76
+ topbar.className = 'jeopardy-topbar';
77
+ const scoreWrap = document.createElement('div');
78
+ scoreWrap.className = 'jeopardy-scorebar-wrap';
79
+ scoreWrap.appendChild(scorebar);
80
+ const topbarRight = document.createElement('div');
81
+ topbarRight.className = 'jeopardy-topbar-right';
82
+ const resetBtn = document.createElement('button');
83
+ resetBtn.className = 'j-btn accent jeopardy-reset-button';
84
+ resetBtn.textContent = 'Reset spill';
85
+ resetBtn.style.display = 'none';
86
+ topbarRight.appendChild(resetBtn);
87
+ topbar.appendChild(scoreWrap);
88
+ topbar.appendChild(topbarRight);
89
+
90
+ let teams = [];
91
+ let teamCategoryPoints = Array.from({length: nTeams}, () => Array.from({length: categories.length}, () => 0));
92
+ function updateActiveTeamHighlight(){
93
+ teams.forEach((t,i)=>{
94
+ if (!t._el) return;
95
+ if (gameMode==='turn' && i===currentTurn) t._el.classList.add('active');
96
+ else t._el.classList.remove('active');
97
+ });
98
+ if (turnIndicator) {
99
+ if (gameMode==='turn' && started && teams.length>0) {
100
+ turnIndicator.style.display = '';
101
+ turnIndicator.textContent = `Tur: ${teams[currentTurn].name}`;
102
+ } else {
103
+ turnIndicator.style.display = 'none';
104
+ turnIndicator.textContent = '';
105
+ }
106
+ }
107
+ }
108
+ function rebuildTeams(newN, names){
109
+ teams = [];
110
+ scorebar.innerHTML = '';
111
+ teamCategoryPoints = Array.from({length: newN}, () => Array.from({length: categories.length}, () => 0));
112
+ for(let i=0;i<newN;i++){
113
+ const team = { name: names && names[i] ? names[i] : `Lag ${i+1}`, score: 0 };
114
+ teams.push(team);
115
+ const el = document.createElement('div');
116
+ el.className = 'jeopardy-team';
117
+ const nameSpan = document.createElement('span');
118
+ nameSpan.className = 'team-name';
119
+ nameSpan.textContent = team.name;
120
+ const scoreSpan = document.createElement('span');
121
+ scoreSpan.className = 'score';
122
+ scoreSpan.textContent = '0';
123
+ el.appendChild(nameSpan);
124
+ el.appendChild(scoreSpan);
125
+ scorebar.appendChild(el);
126
+ team._elScore = scoreSpan;
127
+ team._el = el;
128
+ }
129
+ updateActiveTeamHighlight();
130
+ }
131
+ function applySavedState(state){
132
+ try {
133
+ started = !!state.started;
134
+ gameMode = state.gameMode || gameMode;
135
+ timerMs = typeof state.timerMs === 'number' ? state.timerMs : timerMs;
136
+ currentTurn = typeof state.currentTurn === 'number' ? state.currentTurn : 0;
137
+ scoreboardShown = !!state.scoreboardShown;
138
+ const savedTeams = Array.isArray(state.teams) ? state.teams : [];
139
+ const names = savedTeams.length ? savedTeams.map(t=> t && t.name ? String(t.name) : '') : null;
140
+ const newN = Math.max(1, names ? names.length : nTeams);
141
+ rebuildTeams(newN, names || undefined);
142
+ if (savedTeams.length){
143
+ savedTeams.forEach((t,i)=>{
144
+ if (teams[i]){ teams[i].score = Number(t.score)||0; if (teams[i]._elScore) teams[i]._elScore.textContent = String(teams[i].score); }
145
+ });
146
+ }
147
+ if (Array.isArray(state.teamCategoryPoints)){
148
+ teamCategoryPoints = state.teamCategoryPoints.map(row=> Array.isArray(row)? row.slice(): []);
149
+ }
150
+ if (Array.isArray(state.categoryStats)){
151
+ state.categoryStats.forEach((s,i)=>{ if (categoryStats[i]){ categoryStats[i].correct = Number(s.correct)||0; categoryStats[i].wrong = Number(s.wrong)||0; }});
152
+ }
153
+ if (state.tileStates && typeof state.tileStates === 'object'){
154
+ Object.keys(state.tileStates).forEach(k=>{ const v = state.tileStates[k]; if (v && v.locked){ tileStates[k] = {locked:true}; }});
155
+ try {
156
+ container.querySelectorAll('.jeopardy-tile').forEach(btn=>{
157
+ const k = btn && btn.dataset ? btn.dataset.key : null; if (!k) return;
158
+ if (tileStates[k] && tileStates[k].locked){ btn.disabled = true; btn.classList.add('used'); }
159
+ });
160
+ } catch(e){}
161
+ }
162
+ try { scorebar.style.display = ''; } catch(e){}
163
+ try { resetBtn.style.display = ''; } catch(e){}
164
+ try { setup.style.display = 'none'; } catch(e){}
165
+ updateActiveTeamHighlight();
166
+ } catch(e){}
167
+ }
168
+
169
+ const table = document.createElement('table');
170
+ table.className = 'jeopardy-grid';
171
+
172
+ const thead = document.createElement('thead');
173
+ const thr = document.createElement('tr');
174
+ categories.forEach(cat=>{
175
+ const th = document.createElement('th');
176
+ th.textContent = cat.name||'';
177
+ thr.appendChild(th);
178
+ });
179
+ thead.appendChild(thr);
180
+
181
+ const tbody = document.createElement('tbody');
182
+
183
+ const lookup = {};
184
+ categories.forEach((cat,ci)=>{
185
+ (cat.tiles||[]).forEach(t=>{
186
+ const key = ci+'|'+t.value;
187
+ lookup[key] = t;
188
+ });
189
+ });
190
+ totalPlayableTiles = Object.keys(lookup).length;
191
+
192
+ values.forEach(val=>{
193
+ const tr = document.createElement('tr');
194
+ categories.forEach((cat,ci)=>{
195
+ const td = document.createElement('td');
196
+ const tile = document.createElement('button');
197
+ tile.className = 'jeopardy-tile';
198
+ tile.textContent = val;
199
+ const key = ci+'|'+val;
200
+ tile.dataset.key = key;
201
+ const data = lookup[key] || null;
202
+ if(!data){ tile.disabled = true; tile.classList.add('used'); }
203
+ if (tileStates[key] && tileStates[key].locked) { tile.disabled = true; tile.classList.add('used'); }
204
+ tile.addEventListener('click', ()=>{
205
+ if(tile.classList.contains('used')||tile.disabled) return;
206
+ if (!started) return;
207
+ openModal(cat.name||'', val, data, tile, key);
208
+ });
209
+ td.appendChild(tile);
210
+ tr.appendChild(td);
211
+ });
212
+ tbody.appendChild(tr);
213
+ });
214
+
215
+ table.appendChild(thead); table.appendChild(tbody);
216
+
217
+ const backdrop = document.createElement('div');
218
+ backdrop.className = 'jeopardy-modal-backdrop';
219
+ const modal = document.createElement('div');
220
+ modal.className = 'jeopardy-modal';
221
+ const header = document.createElement('div'); header.className='jeopardy-modal-header';
222
+ const title = document.createElement('div');
223
+ const timerBox = document.createElement('div'); timerBox.className='jeopardy-timer'; timerBox.style.marginLeft='auto';
224
+ const closeBtn = document.createElement('button'); closeBtn.className='j-btn warn'; closeBtn.textContent='Lukk';
225
+ const body = document.createElement('div'); body.className='jeopardy-modal-body';
226
+ const footer = document.createElement('div'); footer.className='jeopardy-modal-footer';
227
+
228
+ header.appendChild(title); header.appendChild(timerBox); header.appendChild(closeBtn);
229
+ modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer);
230
+ backdrop.appendChild(modal);
231
+
232
+ function setScore(i, delta){ teams[i].score += delta; teams[i]._elScore.textContent = String(teams[i].score); }
233
+ let escHandler = null;
234
+ const hideModal = ()=>{
235
+ backdrop.style.display = 'none';
236
+ try { if (typeof stopTimer === 'function') stopTimer(); } catch(e){}
237
+ if (escHandler) {
238
+ try { document.removeEventListener('keydown', escHandler); } catch(e){}
239
+ escHandler = null;
240
+ }
241
+ };
242
+ function enableEscClose(){
243
+ if (escHandler) {
244
+ try { document.removeEventListener('keydown', escHandler); } catch(e){}
245
+ escHandler = null;
246
+ }
247
+ escHandler = function(e){
248
+ const key = e.key || e.code;
249
+ if (key === 'Escape' || key === 'Esc') {
250
+ try { e.preventDefault(); } catch(_){ }
251
+ hideModal();
252
+ }
253
+ };
254
+ try { document.addEventListener('keydown', escHandler); } catch(e){}
255
+ }
256
+ function checkCompletionAndShowWinner(){
257
+ if (scoreboardShown) return;
258
+ if (totalPlayableTiles <= 0) return;
259
+ let lockedCount = 0;
260
+ for (const k in tileStates) { if (tileStates[k] && tileStates[k].locked) lockedCount++; }
261
+ if (lockedCount >= totalPlayableTiles) {
262
+ scoreboardShown = true;
263
+ openWinner();
264
+ }
265
+ }
266
+ function openWinner(){
267
+ const sorted = teams.map((t,i)=>({name:t.name, score:t.score, idx:i}))
268
+ .sort((a,b)=> b.score - a.score);
269
+ const max = sorted.length ? sorted[0].score : 0;
270
+ const winners = sorted.filter(x=> x.score === max);
271
+ title.textContent = winners.length > 1 ? 'Scoreboard' : 'Scoreboard';
272
+ body.innerHTML = '';
273
+ footer.innerHTML = '';
274
+ const grid = document.createElement('div');
275
+ const cols = 2 + categories.length;
276
+ grid.style.display = 'grid';
277
+ grid.style.gridTemplateColumns = `1.5fr ${'auto '.repeat(categories.length)} auto`;
278
+ grid.style.gap = '0.5rem 1rem';
279
+ const hTeam = document.createElement('div'); hTeam.style.fontWeight = '700'; hTeam.textContent = 'Lag';
280
+ grid.appendChild(hTeam);
281
+ categories.forEach(cat => { const h = document.createElement('div'); h.style.fontWeight='700'; h.textContent = cat.name||''; grid.appendChild(h); });
282
+ const hScore = document.createElement('div'); hScore.style.fontWeight='700'; hScore.textContent = 'Score';
283
+ grid.appendChild(hScore);
284
+ sorted.forEach(t => {
285
+ const rowBold = (t.score === max);
286
+ const name = document.createElement('div'); name.textContent = t.name; if (rowBold) name.style.fontWeight = '700'; grid.appendChild(name);
287
+ const pointsRow = teamCategoryPoints[t.idx] || [];
288
+ 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); });
289
+ const sc = document.createElement('div'); sc.textContent = String(t.score); if (rowBold) sc.style.fontWeight = '700'; grid.appendChild(sc);
290
+ });
291
+ body.appendChild(grid);
292
+ backdrop.style.display = 'flex';
293
+ enableEscClose();
294
+ closeBtn.onclick = hideModal; backdrop.onclick = (e)=>{ if(e.target===backdrop) hideModal(); };
295
+ }
296
+
297
+ function resetGame(){
298
+ try { hideModal(); } catch(e){}
299
+ try { stopTimer(); } catch(e){}
300
+ started = false;
301
+ scoreboardShown = false;
302
+ currentTurn = 0;
303
+ for (const k in tileStates) { try { delete tileStates[k]; } catch(e){} }
304
+ for (let i=0;i<categoryStats.length;i++){ categoryStats[i].correct = 0; categoryStats[i].wrong = 0; }
305
+ teams = [];
306
+ teamCategoryPoints = Array.from({length: nTeams}, () => Array.from({length: categories.length}, () => 0));
307
+ scorebar.innerHTML = '';
308
+ try {
309
+ container.querySelectorAll('.jeopardy-tile').forEach(b=>{ b.disabled = false; b.classList.remove('used'); });
310
+ } catch(e){}
311
+ try { scorebar.style.display = 'none'; } catch(e){}
312
+ try { turnIndicator.style.display = 'none'; } catch(e){}
313
+ try { resetBtn.style.display = 'none'; } catch(e){}
314
+ try { setup.style.display = ''; } catch(e){}
315
+ clearState();
316
+ }
317
+ resetBtn.addEventListener('click', resetGame);
318
+
319
+ let countdownId = null;
320
+ function stopTimer(){ if (countdownId) { try { clearInterval(countdownId); } catch(e){} countdownId = null; } timerBox.textContent=''; }
321
+ function startTimer(onTimeout){
322
+ stopTimer();
323
+ if (!timerMs || timerMs <= 0) return;
324
+ let remaining = Math.floor(timerMs/1000);
325
+ const render = () => { timerBox.textContent = `${Math.floor(remaining/60)}:${String(remaining%60).padStart(2,'0')}`; };
326
+ render();
327
+ countdownId = setInterval(()=>{
328
+ remaining -= 1;
329
+ if (remaining <= 0){ stopTimer(); try { onTimeout && onTimeout(); } catch(e){} }
330
+ else render();
331
+ }, 1000);
332
+ }
333
+
334
+ function openModal(category, value, data, tile, key){
335
+ title.textContent = `${category} – ${value}`;
336
+ body.innerHTML = '';
337
+ footer.innerHTML = '';
338
+
339
+ const q = document.createElement('div'); q.className='jeopardy-q'; q.innerHTML = data && data.question ? data.question : '';
340
+ const a = document.createElement('div'); a.className='jeopardy-a'; a.innerHTML = data && data.answer ? data.answer : '';
341
+
342
+ const revealBtn = document.createElement('button'); revealBtn.className='j-btn success'; revealBtn.textContent='Fasit';
343
+ revealBtn.addEventListener('click', ()=>{
344
+ const showing = a.style.display !== 'block';
345
+ a.style.display = showing ? 'block' : 'none';
346
+ 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){} }
347
+ });
348
+ body.appendChild(q); body.appendChild(a);
349
+
350
+ // Tracking for duel vs turn-based scoring
351
+ let scored = false;
352
+ let duelPendingTeams = new Set();
353
+ const teamActions = document.createElement('div'); teamActions.className='jeopardy-team-actions';
354
+ const disableTeamButtons = () => { teamActions.querySelectorAll('button').forEach(b=>{ b.disabled = true; }); };
355
+ const onTimeout = ()=>{
356
+ disableTeamButtons();
357
+ if (gameMode==='turn' && teams.length>0){ currentTurn = (currentTurn+1)%teams.length; updateActiveTeamHighlight(); }
358
+ try { saveState(); } catch(e){}
359
+ try { setTimeout(()=>{ hideModal(); }, 300); } catch(e){}
360
+ };
361
+ teams.forEach((t, i)=>{
362
+ // In turn-based mode, only the active team can be scored on
363
+ if (gameMode==='turn' && i!==currentTurn) return;
364
+
365
+ const add = document.createElement('button');
366
+ add.className='j-btn primary';
367
+ add.textContent = `+${value} ${t.name}`;
368
+
369
+ // In duel mode, we need an explicit "0 points" option per team
370
+ let zeroBtn = null;
371
+ if (gameMode === 'duel') {
372
+ zeroBtn = document.createElement('button');
373
+ zeroBtn.className = 'j-btn secondary';
374
+ zeroBtn.textContent = `0 ${t.name}`;
375
+ }
376
+
377
+ // Register this team as pending in duel mode
378
+ if (gameMode === 'duel') {
379
+ duelPendingTeams.add(i);
380
+ }
381
+
382
+ const registerScore = (delta)=>{
383
+ if (tileStates[key] && tileStates[key].locked) return;
384
+ setScore(i, delta);
385
+ try {
386
+ const ci = parseInt(String(key).split('|')[0], 10);
387
+ if (!isNaN(ci) && categoryStats[ci]) {
388
+ if (delta > 0) categoryStats[ci].correct++;
389
+ }
390
+ if (!isNaN(ci) && teamCategoryPoints[i]) {
391
+ if (typeof teamCategoryPoints[i][ci] !== 'number') teamCategoryPoints[i][ci] = 0;
392
+ teamCategoryPoints[i][ci] += delta;
393
+ }
394
+ } catch(e){}
395
+ };
396
+
397
+ const finalizeIfNeeded = ()=>{
398
+ if (gameMode === 'duel') {
399
+ if (duelPendingTeams.size > 0) return;
400
+ } else {
401
+ // turn-based: single decision ends the question
402
+ }
403
+
404
+ scored = true;
405
+ tileStates[key] = { locked: true };
406
+ if (tile) { tile.classList.add('used'); tile.disabled = true; }
407
+ if (gameMode==='turn' && teams.length>0){ currentTurn = (currentTurn+1)%teams.length; updateActiveTeamHighlight(); }
408
+ try { saveState(); setTimeout(()=>{ hideModal(); checkCompletionAndShowWinner(); }, 300); } catch(e){}
409
+ };
410
+
411
+ const handleAdd = ()=>{
412
+ if (gameMode === 'turn') {
413
+ if (scored) return;
414
+ registerScore(value);
415
+ scored = true;
416
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
417
+ finalizeIfNeeded();
418
+ } else {
419
+ if (!duelPendingTeams.has(i)) return;
420
+ registerScore(value);
421
+ duelPendingTeams.delete(i);
422
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
423
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
424
+ finalizeIfNeeded();
425
+ }
426
+ };
427
+
428
+ const handleZero = ()=>{
429
+ if (gameMode === 'duel') {
430
+ if (!duelPendingTeams.has(i)) return;
431
+ // Zero points: just clear pending state for this team
432
+ duelPendingTeams.delete(i);
433
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
434
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
435
+ finalizeIfNeeded();
436
+ }
437
+ };
438
+
439
+ add.addEventListener('click', handleAdd);
440
+ if (zeroBtn) zeroBtn.addEventListener('click', handleZero);
441
+
442
+ if (tileStates[key] && tileStates[key].locked) {
443
+ add.disabled = true;
444
+ if (zeroBtn) zeroBtn.disabled = true;
445
+ }
446
+
447
+ teamActions.appendChild(add);
448
+ if (zeroBtn) teamActions.appendChild(zeroBtn);
449
+ });
450
+ const footerRight = document.createElement('div');
451
+ footerRight.className = 'jeopardy-footer-right';
452
+ footerRight.appendChild(revealBtn);
453
+ footer.appendChild(teamActions);
454
+ footer.appendChild(footerRight);
455
+
456
+ renderMathIfAvailable(q); renderMathIfAvailable(a);
457
+ highlightCodeIfAvailable(q); highlightCodeIfAvailable(a);
458
+
459
+ backdrop.style.display = 'flex';
460
+ enableEscClose();
461
+ startTimer(onTimeout);
462
+ closeBtn.onclick = hideModal; backdrop.onclick = (e)=>{ if(e.target===backdrop) hideModal(); };
463
+ }
464
+
465
+ const setup = document.createElement('div'); setup.className='jeopardy-setup';
466
+ const fTeams = document.createElement('div'); fTeams.className='jp-field';
467
+ const lTeams = document.createElement('label'); lTeams.textContent='Antall lag:'; const sTeams=document.createElement('select');
468
+ [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); });
469
+ fTeams.appendChild(lTeams); fTeams.appendChild(sTeams);
470
+ const namesWrap = document.createElement('div'); namesWrap.className='jp-names';
471
+ 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);} }
472
+ sTeams.addEventListener('change', renderNames); renderNames();
473
+ 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);
474
+ 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);
475
+ const startBtn=document.createElement('button'); startBtn.className='j-btn primary'; startBtn.textContent='Start spill';
476
+ startBtn.addEventListener('click', ()=>{
477
+ const newN = parseInt(sTeams.value,10)||1;
478
+ const names = Array.from(namesWrap.querySelectorAll('input')).map((inp,i)=> inp.value && inp.value.trim() ? inp.value.trim() : `Lag ${i+1}`);
479
+ gameMode = sMode.value==='turn' ? 'turn' : 'duel';
480
+ timerMs = parseInt(sTimer.value,10)||0;
481
+ currentTurn = Math.floor(Math.random()*Math.max(1,newN));
482
+ started = true;
483
+ try { if (typeof resumePrompt !== 'undefined' && resumePrompt && resumePrompt.parentNode) resumePrompt.remove(); } catch(e){}
484
+ rebuildTeams(newN, names);
485
+ try { scorebar.style.display = ''; } catch(e){}
486
+ try { resetBtn.style.display = ''; } catch(e){}
487
+ try { updateActiveTeamHighlight(); } catch(e){}
488
+ setup.style.display='none';
489
+ try { saveState(); } catch(e){}
490
+ });
491
+ setup.appendChild(fTeams); setup.appendChild(namesWrap); setup.appendChild(fTimer); setup.appendChild(fMode); setup.appendChild(startBtn);
492
+
493
+ container.innerHTML = '';
494
+ const saved = loadState();
495
+ let resumePrompt = null;
496
+ 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)))){
497
+ resumePrompt = document.createElement('div'); resumePrompt.className='jeopardy-resume-prompt';
498
+ const txt = document.createElement('div'); txt.className='jeopardy-resume-text'; txt.textContent = 'Fortsett der du slapp?';
499
+ const actions = document.createElement('div'); actions.className='jeopardy-resume-actions';
500
+ const btnStart = document.createElement('button'); btnStart.className='j-btn accent'; btnStart.textContent='Start fra begynnelsen';
501
+ const btnResume = document.createElement('button'); btnResume.className='j-btn primary'; btnResume.textContent='Fortsett';
502
+ actions.appendChild(btnStart); actions.appendChild(btnResume);
503
+ resumePrompt.appendChild(txt); resumePrompt.appendChild(actions);
504
+ btnStart.addEventListener('click', ()=>{
505
+ try { clearState(); } catch(e){}
506
+ try { resumePrompt.remove(); } catch(e){}
507
+ try { setup.style.display = ''; } catch(e){}
508
+ });
509
+ btnResume.addEventListener('click', ()=>{ try { applySavedState(saved); } catch(e){}; try { resumePrompt.remove(); } catch(e){}; });
510
+ container.appendChild(resumePrompt);
511
+ try { setup.style.display = 'none'; } catch(e){}
512
+ }
513
+ container.appendChild(setup);
514
+ container.appendChild(topbar);
515
+ container.appendChild(turnIndicator);
516
+ container.appendChild(table);
517
+ container.appendChild(backdrop);
518
+ }
519
+
520
+ document.addEventListener('DOMContentLoaded', function(){
521
+ document.querySelectorAll('.jeopardy-container[data-config]').forEach(initJeopardy);
522
+ });
523
+ })();
@@ -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
+ }