munchboka-edutools 0.1.0__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.
Files changed (150) hide show
  1. munchboka_edutools/__init__.py +182 -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 +356 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/dialogue.py +137 -0
  8. munchboka_edutools/directives/escape_room.py +296 -0
  9. munchboka_edutools/directives/factor_tree.py +549 -0
  10. munchboka_edutools/directives/ggb.py +209 -0
  11. munchboka_edutools/directives/ggb_icon.py +62 -0
  12. munchboka_edutools/directives/ggb_popup.py +165 -0
  13. munchboka_edutools/directives/horner.py +324 -0
  14. munchboka_edutools/directives/interactive_code.py +75 -0
  15. munchboka_edutools/directives/jeopardy.py +252 -0
  16. munchboka_edutools/directives/multi_plot.py +1126 -0
  17. munchboka_edutools/directives/pair_puzzle.py +191 -0
  18. munchboka_edutools/directives/parsons.py +109 -0
  19. munchboka_edutools/directives/plot.py +3012 -0
  20. munchboka_edutools/directives/poly_icon.py +91 -0
  21. munchboka_edutools/directives/polydiv.py +344 -0
  22. munchboka_edutools/directives/quiz.py +291 -0
  23. munchboka_edutools/directives/signchart.py +474 -0
  24. munchboka_edutools/directives/timed_quiz.py +436 -0
  25. munchboka_edutools/directives/turtle.py +157 -0
  26. munchboka_edutools/static/css/admonitions.css +2012 -0
  27. munchboka_edutools/static/css/cas_popup.css +242 -0
  28. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  29. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  33. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  34. munchboka_edutools/static/css/dialogue.css +92 -0
  35. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  36. munchboka_edutools/static/css/figures.css +274 -0
  37. munchboka_edutools/static/css/general_style.css +74 -0
  38. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  39. munchboka_edutools/static/css/github-dark.css +112 -0
  40. munchboka_edutools/static/css/github-light.css +120 -0
  41. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  42. munchboka_edutools/static/css/interactive_code.css +582 -0
  43. munchboka_edutools/static/css/jeopardy.css +476 -0
  44. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  45. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  46. munchboka_edutools/static/css/quiz.css +312 -0
  47. munchboka_edutools/static/css/timedQuiz.css +375 -0
  48. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  49. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  50. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  51. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  55. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  56. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  57. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  58. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  74. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  75. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  76. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  81. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  82. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  88. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  91. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  92. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  93. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  94. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  96. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  97. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  98. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  108. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  109. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  110. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  112. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  113. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  115. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  116. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  118. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  119. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  120. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  121. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  122. munchboka_edutools/static/js/casThemeManager.js +99 -0
  123. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  124. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  125. munchboka_edutools/static/js/highlight-init.js +6 -0
  126. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  127. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  128. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  129. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  130. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  131. munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
  132. munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
  133. munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
  134. munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
  135. munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
  136. munchboka_edutools/static/js/jeopardy.js +457 -0
  137. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  138. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  139. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  140. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  141. munchboka_edutools/static/js/quiz.js +422 -0
  142. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  143. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  144. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  145. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  146. munchboka_edutools/static/js/utils.js +3 -0
  147. munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
  148. munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
  149. munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
  150. munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,641 @@
1
+ class ParsonsPuzzle {
2
+ constructor(puzzleContainerId, codeString, onSolvedCallback = null) {
3
+ this.puzzleContainerId = puzzleContainerId;
4
+ this.puzzleContainer = document.getElementById(puzzleContainerId);
5
+ this.codeString = codeString;
6
+ this.onSolvedCallback = onSolvedCallback;
7
+
8
+ this.generateHTML();
9
+
10
+ this.dropArea = document.getElementById(this.dropAreaId);
11
+ this.checkButton = document.getElementById(this.checkSolutionId);
12
+ this.resetButton = document.getElementById(this.resetButtonId);
13
+ this.draggableCodeContainer = document.getElementById(this.draggableId);
14
+ this.toast = document.getElementById(this.toastId);
15
+
16
+ this.solutionModal = this.createSolutionModal(puzzleContainerId);
17
+ this.fullCodeElement = this.solutionModal.querySelector(`#fullCode-${puzzleContainerId}`);
18
+ this.closeModalButton = this.solutionModal.querySelector('.close');
19
+ this.copyCodeButton = this.solutionModal.querySelector(`#copyCodeButton-${puzzleContainerId}`);
20
+
21
+ this.codeBlocks = this.preprocessCode(codeString);
22
+ this.shuffledCodeBlocks = this.shuffleArray(this.codeBlocks.slice());
23
+
24
+ this.renderDraggableCode(this.draggableCodeContainer, this.shuffledCodeBlocks);
25
+ this.createPlaceholder(this.dropArea);
26
+ this.enableDragAndDrop(this.draggableCodeContainer, this.dropArea);
27
+
28
+
29
+ this.isSolved = false;
30
+
31
+ this.addEventListeners();
32
+ }
33
+
34
+
35
+ addEventListeners() {
36
+ this.checkButton.addEventListener('click', () => this.checkSolution());
37
+ this.resetButton.addEventListener('click', () => {
38
+ this.reset();
39
+ this.reshuffle();
40
+ });
41
+ this.closeModalButton.addEventListener('click', () => this.solutionModal.style.display = 'none');
42
+ this.copyCodeButton.addEventListener('click', () => {
43
+ navigator.clipboard.writeText(this.fullCodeElement.textContent).then(() => {
44
+ alert('Du har kopiert koden!');
45
+ });
46
+ });
47
+ }
48
+
49
+ generateHTML() {
50
+ const container = document.getElementById(this.puzzleContainerId);
51
+ if (!container) {
52
+ console.error(`Container with ID ${this.puzzleContainerId} not found.`);
53
+ return;
54
+ }
55
+
56
+ const uniqueId = generateUUID();
57
+ this.dropAreaId = `drop-area-${uniqueId}`;
58
+ this.checkSolutionId = `check-solution-${uniqueId}`;
59
+ this.resetButtonId = `reset-button-${uniqueId}`;
60
+ this.draggableId = `draggable-code-${uniqueId}`;
61
+ this.toastSuccessId = `toast-success-${uniqueId}`;
62
+ this.toastErrorId = `toast-error-${uniqueId}`;
63
+
64
+ const checkSolutionIcon = `
65
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
66
+ <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
67
+ </svg>
68
+ `;
69
+
70
+ const resetIcon = `
71
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
72
+ <path fill-rule="evenodd" d="M9.53 2.47a.75.75 0 0 1 0 1.06L4.81 8.25H15a6.75 6.75 0 0 1 0 13.5h-3a.75.75 0 0 1 0-1.5h3a5.25 5.25 0 1 0 0-10.5H4.81l4.72 4.72a.75.75 0 1 1-1.06 1.06l-6-6a.75.75 0 0 1 0-1.06l6-6a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
73
+ </svg>
74
+ `;
75
+
76
+ const html = `
77
+ <!-- Toast Notifications -->
78
+ <div id="${this.toastSuccessId}" class="toast toast-success" style="display: none;">
79
+ <p>Riktig! 🎉</p>
80
+ </div>
81
+ <div id="${this.toastErrorId}" class="toast toast-error" style="display: none;">
82
+ <p>Prøv igjen!</p>
83
+ </div>
84
+ <div id="${this.draggableId}" class="draggable-code"></div>
85
+ <div id="${this.dropAreaId}" class="drop-area"></div>
86
+ <div class="button-container">
87
+ <button id="${this.checkSolutionId}" class="button button-check-solution">Sjekk løsning ${checkSolutionIcon}</button>
88
+ <button id="${this.resetButtonId}" class="button button-reset-puzzle">Reset puslespill ${resetIcon}</button>
89
+ </div>
90
+ `;
91
+
92
+ container.innerHTML = html;
93
+
94
+ // Get references to the toast elements
95
+ this.toastSuccess = document.getElementById(this.toastSuccessId);
96
+ this.toastError = document.getElementById(this.toastErrorId);
97
+ }
98
+
99
+
100
+ preprocessCode(codeString) {
101
+ const lines = codeString.split('\n');
102
+ return lines.map((line, index) => {
103
+ let trimmedLine = line.trim();
104
+ if (line.includes(';')) {
105
+ const parts = line.split(';');
106
+ trimmedLine = parts.map(part => part.trim() === '' ? '' : part).join('\n');
107
+ }
108
+ return {
109
+ block: line.includes(';') ? trimmedLine : line,
110
+ order: index,
111
+ isEmpty: line.trim() === ''
112
+ };
113
+ });
114
+ }
115
+
116
+ shuffleArray(array) {
117
+ for (let i = array.length - 1; i > 0; i--) {
118
+ const j = Math.floor(Math.random() * (i + 1));
119
+ [array[i], array[j]] = [array[j], array[i]];
120
+ }
121
+ return array;
122
+ }
123
+
124
+ renderDraggableCode(container, codeBlockObjects) {
125
+ container.innerHTML = '';
126
+ codeBlockObjects.forEach((obj) => {
127
+ if (!obj.isEmpty) {
128
+ const lineElement = document.createElement('div');
129
+ lineElement.className = 'draggable';
130
+ lineElement.draggable = true;
131
+ lineElement.dataset.order = obj.order;
132
+ lineElement.innerHTML = `<pre class="highlight python"><code>${this.escapeHTML(obj.block)}</code></pre>`;
133
+ container.appendChild(lineElement);
134
+ hljs.highlightElement(lineElement.querySelector('code'));
135
+ }
136
+ });
137
+ }
138
+
139
+ escapeHTML(str) {
140
+ return str.replace(/&/g, "&amp;")
141
+ .replace(/</g, "&lt;")
142
+ .replace(/>/g, "&gt;")
143
+ .replace(/"/g, "&quot;")
144
+ .replace(/'/g, "&#039;");
145
+ }
146
+
147
+ checkSolution() {
148
+ const droppedItems = Array.from(this.dropArea.children).filter(item => !item.classList.contains('placeholder'));
149
+ const droppedOrder = droppedItems.map(item => parseInt(item.dataset.order));
150
+ const fullCode = this.codeBlocks.sort((a, b) => a.order - b.order).map(obj => obj.block).join('\n');
151
+ this.fullCodeElement.textContent = fullCode;
152
+ console.log("fullCode: \n", fullCode);
153
+
154
+
155
+ hljs.highlightElement(this.fullCodeElement);
156
+ const correctOrder = this.codeBlocks.filter(obj => !obj.isEmpty).sort((a, b) => a.order - b.order).map(obj => obj.order);
157
+ if (JSON.stringify(droppedOrder) === JSON.stringify(correctOrder)) {
158
+ this.showToast('success');
159
+ console.log("onSolvedCallback: ", this.onSolvedCallback);
160
+ if (this.onSolvedCallback) {
161
+
162
+ console.log("Calling callback function now!");
163
+ setTimeout(() => {
164
+ this.onSolvedCallback(fullCode);
165
+ }, 1500); // Display for 2.5 seconds
166
+ }
167
+ else {
168
+ this.solutionModal.style.display = 'block';
169
+ }
170
+ return true;
171
+ } else {
172
+ this.showToast('error');
173
+ return false;
174
+ }
175
+ }
176
+
177
+ showToast(type) {
178
+ const toast = type === 'success' ? this.toastSuccess : this.toastError;
179
+
180
+ console.log("Toast: ", toast);
181
+ if (!toast) {
182
+ console.error(`Toast element not found for type ${type}.`);
183
+ return;
184
+ }
185
+
186
+ // Ensure the puzzle container is positioned relatively
187
+ const containerStyle = getComputedStyle(this.puzzleContainer);
188
+ if (containerStyle.position === 'static') {
189
+ this.puzzleContainer.style.position = 'relative';
190
+ }
191
+
192
+ // Position the toast in the center of the puzzle container
193
+ toast.style.position = 'absolute';
194
+ toast.style.top = '50%';
195
+ toast.style.left = '50%';
196
+ toast.style.transform = 'translate(-50%, -50%)';
197
+ toast.style.display = 'block';
198
+
199
+ // Hide the toast after a delay
200
+ setTimeout(() => {
201
+ toast.style.display = 'none';
202
+ }, 2500); // Display for 2.5 seconds
203
+ }
204
+
205
+
206
+ createSolutionModal(puzzleContainerId) {
207
+ const modal = document.createElement('div');
208
+ modal.id = `solutionModal-${puzzleContainerId}`;
209
+ modal.className = 'modal';
210
+ modal.innerHTML = `
211
+ <div class="modal-content">
212
+ <span class="close">&times;</span>
213
+ <pre><code id="fullCode-${puzzleContainerId}" class="highlight python"></code></pre>
214
+ <button id="copyCodeButton-${puzzleContainerId}" class="button button-check-solution">Riktig! 🔥 Kopier koden!</button>
215
+ </div>
216
+ `;
217
+ document.body.appendChild(modal);
218
+ return modal;
219
+ }
220
+
221
+ createPlaceholder(dropArea) {
222
+ const placeholder = document.createElement('div');
223
+ placeholder.className = 'placeholder';
224
+ placeholder.textContent = 'Dra og dropp kode her!';
225
+ dropArea.appendChild(placeholder);
226
+ }
227
+
228
+
229
+ enableDragAndDrop(draggableContainer, dropArea) {
230
+ const draggables = draggableContainer.querySelectorAll('.draggable');
231
+
232
+ // Event listeners for drag start and drag end
233
+ draggables.forEach(draggable => {
234
+ draggable.addEventListener('dragstart', (e) => this.dragStart(e));
235
+ draggable.addEventListener('dragend', (e) => this.dragEnd(e, dropArea)); // Always pass dropArea to manage the placeholder correctly
236
+ });
237
+
238
+ // Event listeners for drop area and draggable area
239
+ [dropArea, draggableContainer].forEach(container => {
240
+ container.addEventListener('dragover', (e) => this.dragOver(e, container));
241
+ container.addEventListener('drop', (e) => this.drop(e, container, this.dropArea.querySelector('.placeholder'))); // Always manage placeholder from dropArea
242
+ });
243
+
244
+ // New event listeners to ensure no lingering active state after a drop
245
+ dropArea.addEventListener('drop', () => {
246
+ // Temporarily disable pointer events on all draggables to clear active state
247
+ const allDraggables = document.querySelectorAll('.draggable');
248
+ allDraggables.forEach(item => {
249
+ item.style.pointerEvents = 'none';
250
+ });
251
+
252
+ // Re-enable pointer events after a short delay
253
+ setTimeout(() => {
254
+ allDraggables.forEach(item => {
255
+ item.style.pointerEvents = '';
256
+ });
257
+ }, 100); // Short delay to allow the active state to clear
258
+ });
259
+ }
260
+
261
+
262
+ dragStart(e) {
263
+ e.dataTransfer.setData('text/plain', e.target.dataset.order);
264
+ e.target.classList.add('dragging');
265
+
266
+
267
+ setTimeout(() => {
268
+ e.target.style.display = 'none';
269
+ }, 0);
270
+ }
271
+
272
+ // Ensure dragEnd method doesn't remove the placeholder incorrectly
273
+ dragEnd(e, dropArea) {
274
+ e.target.style.display = 'block';
275
+ e.target.classList.remove('dragging');
276
+
277
+ this.updatePlaceholderVisibility(this.dropArea, this.draggableCodeContainer); // Ensure placeholder visibility is updated based on both areas
278
+ }
279
+
280
+ dragOver(e, dropArea) {
281
+ e.preventDefault();
282
+ const draggable = document.querySelector('.dragging');
283
+ const afterElement = this.getDragAfterElement(e.clientY, dropArea);
284
+ if (afterElement == null) {
285
+ dropArea.insertBefore(draggable, dropArea.querySelector('.placeholder'));
286
+ } else {
287
+ const box = afterElement.getBoundingClientRect();
288
+ const offset = e.clientY - box.top;
289
+ if (offset < box.height / 2) {
290
+ dropArea.insertBefore(draggable, afterElement);
291
+ } else {
292
+ dropArea.insertBefore(draggable, afterElement.nextSibling);
293
+ }
294
+ }
295
+ }
296
+
297
+
298
+ // Update the drop method to handle placeholder correctly
299
+ drop(e, container, placeholder) {
300
+ e.preventDefault();
301
+ const draggableElement = document.querySelector('.dragging');
302
+ const targetDropArea = e.target.closest('.drop-area');
303
+ const targetDraggableArea = e.target.closest('.draggable-code'); // Handle re-adding to the draggable area
304
+ const afterElement = this.getDragAfterElement(e.clientY, targetDropArea || targetDraggableArea);
305
+
306
+ if (afterElement == null) {
307
+ (targetDropArea || targetDraggableArea).insertBefore(draggableElement, placeholder);
308
+ } else {
309
+ const box = afterElement.getBoundingClientRect();
310
+ const offset = e.clientY - box.top;
311
+ if (offset < box.height / 2) {
312
+ (targetDropArea || targetDraggableArea).insertBefore(draggableElement, afterElement);
313
+ } else {
314
+ (targetDropArea || targetDraggableArea).insertBefore(draggableElement, afterElement.nextSibling);
315
+ }
316
+ }
317
+ this.updatePlaceholderVisibility(this.dropArea, this.draggableCodeContainer); // Update placeholder visibility for both areas
318
+ }
319
+
320
+ // Updated updatePlaceholderVisibility to consider both areas
321
+ updatePlaceholderVisibility(dropArea, draggableCodeContainer) {
322
+ const dropAreaBlockCount = dropArea.querySelectorAll('.draggable').length;
323
+ const draggableBlockCount = draggableCodeContainer.querySelectorAll('.draggable').length;
324
+
325
+ console.log("dropAreaBlockCount: ", dropAreaBlockCount);
326
+ const placeholder = dropArea.querySelector('.placeholder');
327
+ if (draggableBlockCount === 0) {
328
+ if (placeholder) placeholder.style.display = 'none'; // Hide if no blocks in drop area
329
+ } else {
330
+ if (!placeholder) {
331
+ this.createPlaceholder(dropArea); // Create if missing
332
+ }
333
+ placeholder.style.display = ''; // Show if blocks exist
334
+ }
335
+
336
+ // Ensure the placeholder is always at the end of the drop area when visible
337
+ if (placeholder && dropAreaBlockCount > 0) {
338
+ dropArea.appendChild(placeholder);
339
+ }
340
+ }
341
+
342
+ getDragAfterElement(y, dropArea) {
343
+ const draggableElements = [...dropArea.querySelectorAll('.draggable:not(.dragging)')];
344
+ return draggableElements.reduce((closest, child) => {
345
+ const box = child.getBoundingClientRect();
346
+ const offset = y - box.top - box.height / 2;
347
+ if (offset < 0 && offset > closest.offset) {
348
+ return { offset: offset, element: child };
349
+ } else {
350
+ return closest;
351
+ }
352
+ }, { offset: Number.NEGATIVE_INFINITY }).element;
353
+ }
354
+
355
+ reset() {
356
+ this.feedback.textContent = '';
357
+ const draggableElements = this.dropArea.querySelectorAll('.draggable');
358
+ draggableElements.forEach(element => {
359
+ this.draggableCodeContainer.appendChild(element);
360
+ });
361
+ const originalShuffledOrder = Array.from(this.draggableCodeContainer.querySelectorAll('.draggable'));
362
+ this.shuffleArray(originalShuffledOrder).forEach(element => {
363
+ this.draggableCodeContainer.appendChild(element);
364
+ });
365
+ const placeholder = this.dropArea.querySelector('.placeholder');
366
+ if (placeholder) {
367
+ placeholder.style.display = '';
368
+ } else {
369
+ this.createPlaceholder(this.dropArea);
370
+ }
371
+ }
372
+
373
+ reshuffle() {
374
+ const originalShuffledOrder = Array.from(this.draggableCodeContainer.querySelectorAll('.draggable'));
375
+ this.shuffleArray(originalShuffledOrder).forEach(element => {
376
+ this.draggableCodeContainer.appendChild(element);
377
+ });
378
+ }
379
+ }
380
+
381
+
382
+ function makeParsonsPuzzle(puzzleContainerId, codeString) {
383
+ new ParsonsPuzzle(puzzleContainerId, codeString);
384
+ }
385
+
386
+
387
+ function makeCallbackFunction(puzzleContainerId, editorId) {
388
+ function callbackFunction(fullCode) {
389
+ document.getElementById(puzzleContainerId).style.display = 'none';
390
+ let editorContainer = document.getElementById(editorId);
391
+ editorContainer.style.display = 'block';
392
+
393
+ makeInteractiveCode(editorId, fullCode);
394
+ }
395
+
396
+ return callbackFunction;
397
+ }
398
+
399
+
400
+ class IndentationParsonsPuzzle extends ParsonsPuzzle {
401
+ constructor(puzzleContainerId, codeString, onSolvedCallback = null) {
402
+ super(puzzleContainerId, codeString, onSolvedCallback);
403
+
404
+ // Override codeBlocks with preprocessed code
405
+ this.codeBlocks = this.preprocessCodeWithIndentation(codeString);
406
+
407
+ // Determine the maximum indentation level required by the code
408
+ this.maxIndentationLevel = Math.max(...this.codeBlocks.map(obj => obj.expectedIndentation));
409
+
410
+ // Initialize indentation settings
411
+ this.indentationWidth = 40; // Pixels per indentation level
412
+
413
+ // Shuffle the code blocks
414
+ this.shuffledCodeBlocks = this.shuffleArray(this.codeBlocks.slice());
415
+
416
+ // Render the draggable code lines
417
+ this.renderDraggableCodeLines(this.draggableCodeContainer, this.shuffledCodeBlocks);
418
+
419
+ // Add visual guides for indentation levels
420
+ this.addIndentationGuides(this.dropArea);
421
+
422
+ // Enable custom drag-and-drop functionality
423
+ this.enableCustomDragAndDrop();
424
+ }
425
+
426
+ // Override the preprocessCode method to handle indentation
427
+ preprocessCodeWithIndentation(codeString) {
428
+ const lines = codeString.split('\n');
429
+ return lines.map((line, index) => {
430
+ const leadingSpaces = line.match(/^\s*/)[0].length;
431
+ const indentLevel = leadingSpaces / 4; // Assuming 4 spaces per indent level
432
+ const trimmedLine = line.trim();
433
+ return {
434
+ block: trimmedLine,
435
+ order: index,
436
+ expectedIndentation: indentLevel,
437
+ isEmpty: trimmedLine === ''
438
+ };
439
+ });
440
+ }
441
+
442
+ // Render draggable code lines
443
+ renderDraggableCodeLines(container, codeBlockObjects) {
444
+ container.innerHTML = '';
445
+ codeBlockObjects.forEach((obj) => {
446
+ if (!obj.isEmpty) {
447
+ const lineElement = document.createElement('div');
448
+ lineElement.className = 'code-line';
449
+ lineElement.dataset.order = obj.order;
450
+ lineElement.dataset.expectedIndentation = obj.expectedIndentation;
451
+ lineElement.dataset.currentIndentation = 0; // Initialize current indentation to 0
452
+
453
+ // Code content
454
+ const codeContent = document.createElement('pre');
455
+ codeContent.className = 'highlight python code-content';
456
+ codeContent.innerHTML = `<code>${this.escapeHTML(obj.block)}</code>`;
457
+
458
+ // Assemble line element
459
+ lineElement.appendChild(codeContent);
460
+
461
+ container.appendChild(lineElement);
462
+ hljs.highlightElement(codeContent.querySelector('code'));
463
+ }
464
+ });
465
+ }
466
+
467
+ // Add visual indentation guides to the drop area
468
+ addIndentationGuides(container) {
469
+ container.style.position = 'relative';
470
+ for (let i = 1; i <= this.maxIndentationLevel; i++) {
471
+ const guide = document.createElement('div');
472
+ guide.className = 'indentation-guide';
473
+ guide.style.left = `${i * this.indentationWidth}px`;
474
+ container.appendChild(guide);
475
+ }
476
+ }
477
+
478
+ // Enable custom drag-and-drop functionality
479
+ enableCustomDragAndDrop() {
480
+ const codeLines = this.puzzleContainer.querySelectorAll('.code-line');
481
+ codeLines.forEach(codeLine => {
482
+ codeLine.addEventListener('mousedown', (e) => this.dragStart(e, codeLine));
483
+ });
484
+ }
485
+
486
+ dragStart(e, codeLine) {
487
+ e.preventDefault();
488
+ this.currentCodeLine = codeLine;
489
+ this.startX = e.clientX;
490
+ this.startY = e.clientY;
491
+
492
+ this.originalParent = codeLine.parentElement;
493
+ this.placeholder = document.createElement('div');
494
+ this.placeholder.className = 'placeholder-code-line';
495
+ this.placeholder.style.height = `${codeLine.offsetHeight}px`;
496
+
497
+ // Insert placeholder
498
+ codeLine.parentElement.insertBefore(this.placeholder, codeLine.nextSibling);
499
+
500
+ // Move code line to body for absolute positioning
501
+ document.body.appendChild(codeLine);
502
+ codeLine.style.position = 'absolute';
503
+ codeLine.style.zIndex = 1000;
504
+ codeLine.classList.add('dragging');
505
+
506
+ this.moveAt(e.pageX, e.pageY);
507
+
508
+ document.addEventListener('mousemove', this.dragMove.bind(this));
509
+ document.addEventListener('mouseup', this.dragEnd.bind(this));
510
+ }
511
+
512
+ moveAt(pageX, pageY) {
513
+ this.currentCodeLine.style.left = pageX - this.currentCodeLine.offsetWidth / 2 + 'px';
514
+ this.currentCodeLine.style.top = pageY - this.currentCodeLine.offsetHeight / 2 + 'px';
515
+ }
516
+
517
+ dragMove(e) {
518
+ e.preventDefault();
519
+ this.moveAt(e.pageX, e.pageY);
520
+
521
+ // Check for potential drop targets
522
+ const elementsBelow = document.elementsFromPoint(e.clientX, e.clientY);
523
+ const dropArea = this.dropArea;
524
+ const draggableArea = this.draggableCodeContainer;
525
+
526
+ let newParent = null;
527
+ if (elementsBelow.includes(dropArea)) {
528
+ newParent = dropArea;
529
+ } else if (elementsBelow.includes(draggableArea)) {
530
+ newParent = draggableArea;
531
+ }
532
+
533
+ if (newParent && this.currentCodeLine.parentElement !== newParent) {
534
+ this.placeholder.remove();
535
+ newParent.appendChild(this.placeholder);
536
+ }
537
+
538
+ // Adjust placeholder position within new parent
539
+ const codeLines = Array.from(newParent.querySelectorAll('.code-line:not(.dragging)'));
540
+ let insertBeforeElement = null;
541
+ for (let codeLine of codeLines) {
542
+ const rect = codeLine.getBoundingClientRect();
543
+ if (e.clientY < rect.top + rect.height / 2) {
544
+ insertBeforeElement = codeLine;
545
+ break;
546
+ }
547
+ }
548
+
549
+ if (insertBeforeElement) {
550
+ newParent.insertBefore(this.placeholder, insertBeforeElement);
551
+ } else {
552
+ newParent.appendChild(this.placeholder);
553
+ }
554
+ }
555
+
556
+ dragEnd(e) {
557
+ e.preventDefault();
558
+ document.removeEventListener('mousemove', this.dragMove.bind(this));
559
+ document.removeEventListener('mouseup', this.dragEnd.bind(this));
560
+
561
+ // Snap to indentation level
562
+ const dropAreaRect = this.dropArea.getBoundingClientRect();
563
+ const relativeX = e.clientX - dropAreaRect.left;
564
+ let indentLevel = Math.round(relativeX / this.indentationWidth);
565
+ indentLevel = Math.max(0, Math.min(indentLevel, this.maxIndentationLevel));
566
+
567
+ this.currentCodeLine.dataset.currentIndentation = indentLevel;
568
+ this.currentCodeLine.style.paddingLeft = `${indentLevel * this.indentationWidth}px`;
569
+
570
+ // Remove styles added during dragging
571
+ this.currentCodeLine.style.position = '';
572
+ this.currentCodeLine.style.left = '';
573
+ this.currentCodeLine.style.top = '';
574
+ this.currentCodeLine.style.zIndex = '';
575
+ this.currentCodeLine.classList.remove('dragging');
576
+
577
+ // Insert the code line into the new parent
578
+ this.placeholder.parentElement.insertBefore(this.currentCodeLine, this.placeholder);
579
+ this.placeholder.remove();
580
+
581
+ this.currentCodeLine = null;
582
+ }
583
+
584
+ // Override the checkSolution method to include indentation
585
+ checkSolution() {
586
+ const droppedItems = Array.from(this.dropArea.querySelectorAll('.code-line'));
587
+ const droppedOrder = droppedItems.map(item => ({
588
+ order: parseInt(item.dataset.order),
589
+ indentation: parseInt(item.dataset.currentIndentation)
590
+ }));
591
+
592
+ // Get the expected order and indentation
593
+ const correctOrder = this.codeBlocks.filter(obj => !obj.isEmpty).map(obj => ({
594
+ order: obj.order,
595
+ indentation: obj.expectedIndentation
596
+ }));
597
+
598
+ // Compare the student's solution with the correct one
599
+ const isCorrect = this.compareSolutions(droppedOrder, correctOrder);
600
+
601
+ // Prepare the full code with student's indentation for display
602
+ const fullCode = droppedItems.map(item => {
603
+ const numSpaces = parseInt(item.dataset.currentIndentation) * 4; // Assuming 4 spaces per indent level
604
+ const indentation = ' '.repeat(numSpaces);
605
+ const codeLine = this.codeBlocks.find(obj => obj.order === parseInt(item.dataset.order)).block;
606
+ return indentation + codeLine;
607
+ }).join('\n');
608
+
609
+ this.fullCodeElement.textContent = fullCode;
610
+ hljs.highlightElement(this.fullCodeElement);
611
+
612
+ if (isCorrect) {
613
+ this.showToast('success');
614
+ if (this.onSolvedCallback) {
615
+ setTimeout(() => {
616
+ this.onSolvedCallback(fullCode);
617
+ }, 1500);
618
+ } else {
619
+ this.solutionModal.style.display = 'block';
620
+ }
621
+ return true;
622
+ } else {
623
+ this.showToast('error');
624
+ return false;
625
+ }
626
+ }
627
+
628
+ // Helper method to compare solutions
629
+ compareSolutions(droppedOrder, correctOrder) {
630
+ if (droppedOrder.length !== correctOrder.length) {
631
+ return false;
632
+ }
633
+ for (let i = 0; i < droppedOrder.length; i++) {
634
+ if (droppedOrder[i].order !== correctOrder[i].order ||
635
+ droppedOrder[i].indentation !== correctOrder[i].indentation) {
636
+ return false;
637
+ }
638
+ }
639
+ return true;
640
+ }
641
+ }