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.
- munchboka_edutools/__init__.py +184 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +389 -0
- munchboka_edutools/directives/cas_popup.py +428 -0
- munchboka_edutools/directives/clear.py +103 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/escape_room2.py +318 -0
- munchboka_edutools/directives/factor_tree.py +552 -0
- munchboka_edutools/directives/flashcards.py +233 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +308 -0
- munchboka_edutools/directives/horner.py +326 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/jeopardy2.py +636 -0
- munchboka_edutools/directives/multi_plot.py +2524 -0
- munchboka_edutools/directives/multi_plot2.py +252 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3758 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +346 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/quiz2.py +453 -0
- munchboka_edutools/directives/signchart.py +519 -0
- munchboka_edutools/directives/signchart2.py +1545 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +321 -0
- munchboka_edutools/static/css/flashcards.css +219 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +147 -0
- munchboka_edutools/static/css/github-light.css +155 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +553 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/popup.css +115 -0
- munchboka_edutools/static/css/quiz.css +377 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/flashcards.js +199 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +648 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +441 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/jeopardy.js +560 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/popup.js +85 -0
- munchboka_edutools/static/js/quiz.js +566 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.2.3.dist-info/METADATA +109 -0
- munchboka_edutools-0.2.3.dist-info/RECORD +157 -0
- munchboka_edutools-0.2.3.dist-info/WHEEL +4 -0
- munchboka_edutools-0.2.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAS Popup Directive
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
A Sphinx directive that creates a button which opens a GeoGebra CAS (Computer Algebra System)
|
|
6
|
+
window in a popup dialog. The dialog can be resized, dragged, and positioned anywhere on the page.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Customizable button text and dialog title
|
|
10
|
+
- Configurable width and height
|
|
11
|
+
- Sidebar layout option for floating the button to the side
|
|
12
|
+
- Scroll-locking when interacting with GeoGebra
|
|
13
|
+
- Responsive resizing
|
|
14
|
+
- Theme-aware styling (light/dark mode)
|
|
15
|
+
|
|
16
|
+
Dependencies:
|
|
17
|
+
- jQuery UI (for dialog functionality)
|
|
18
|
+
- GeoGebra API (loaded via geogebra-setup.js)
|
|
19
|
+
|
|
20
|
+
Usage Examples:
|
|
21
|
+
--------------
|
|
22
|
+
|
|
23
|
+
Basic usage (default 350x500 CAS window):
|
|
24
|
+
```
|
|
25
|
+
.. cas-popup::
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Custom size with 600x700 window:
|
|
29
|
+
```
|
|
30
|
+
.. cas-popup:: 600 700
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Custom button text and dialog title:
|
|
34
|
+
```
|
|
35
|
+
.. cas-popup:: 400 600 "Open Calculator" "My CAS Window"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Sidebar layout (floating button):
|
|
39
|
+
```
|
|
40
|
+
.. cas-popup:: 350 500 "CAS" "Calculator"
|
|
41
|
+
:layout: sidebar
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Author: Original implementation from matematikk_r1
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from docutils import nodes
|
|
48
|
+
from sphinx.util.docutils import SphinxDirective
|
|
49
|
+
import uuid
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CASPopUpDirective(SphinxDirective):
|
|
53
|
+
"""
|
|
54
|
+
Directive to create a popup GeoGebra CAS window.
|
|
55
|
+
|
|
56
|
+
Arguments:
|
|
57
|
+
1. width (optional): Width of the CAS window in pixels (default: 350)
|
|
58
|
+
2. height (optional): Height of the CAS window in pixels (default: 500)
|
|
59
|
+
3. button_text (optional): Text displayed on the button (default: "Åpne CAS‑vindu")
|
|
60
|
+
4. dialog_title (optional): Title of the popup dialog (default: "CAS‑vindu")
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
:layout: Set to "sidebar" to float the button to the right side of the page
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
required_arguments = 0
|
|
67
|
+
optional_arguments = 4 # width, height, button text, dialog title
|
|
68
|
+
final_argument_whitespace = True
|
|
69
|
+
has_content = False
|
|
70
|
+
option_spec = {
|
|
71
|
+
"layout": lambda arg: arg, # e.g., "sidebar"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def run(self):
|
|
75
|
+
# 1 » parse args -----------------------------------------------------
|
|
76
|
+
width = int(self.arguments[0]) if len(self.arguments) > 0 else 350
|
|
77
|
+
height = int(self.arguments[1]) if len(self.arguments) > 1 else 500
|
|
78
|
+
|
|
79
|
+
button_text = self.arguments[2] if len(self.arguments) > 2 else "Åpne CAS‑vindu"
|
|
80
|
+
dialog_title = self.arguments[3] if len(self.arguments) > 3 else "CAS‑vindu"
|
|
81
|
+
|
|
82
|
+
# 2 » unique IDs -----------------------------------------------------
|
|
83
|
+
cid = f"ggb-cas-{uuid.uuid4().hex[:8]}"
|
|
84
|
+
dialog_id = f"dialog-{cid}"
|
|
85
|
+
button_id = f"button-{cid}"
|
|
86
|
+
|
|
87
|
+
# 3 » layout option --------------------------------------------------
|
|
88
|
+
layout = self.options.get("layout", "").strip().lower()
|
|
89
|
+
use_sidebar = layout == "sidebar"
|
|
90
|
+
|
|
91
|
+
wrapper_start = '<div class="sidebar-cas">' if use_sidebar else ""
|
|
92
|
+
wrapper_end = "</div>" if use_sidebar else ""
|
|
93
|
+
|
|
94
|
+
# 4 » HTML content ---------------------------------------------------
|
|
95
|
+
html = f"""
|
|
96
|
+
{wrapper_start}
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
98
|
+
|
|
99
|
+
<button id="{button_id}" class="ggb-cas-button">{button_text}</button>
|
|
100
|
+
<div id="{dialog_id}" title="{dialog_title}" style="display:none;">
|
|
101
|
+
<div id="{cid}" class="ggb-window"></div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.ui-resizable-handle {{ min-width:16px;min-height:16px; }}
|
|
106
|
+
.ui-dialog-content{{padding:0!important;}}
|
|
107
|
+
.ggb-window {{width:100%!important;height:100%!important;box-sizing:border-box;}}
|
|
108
|
+
.ggb-cas-button {{margin-top: 1em; margin-bottom: 1em;}}
|
|
109
|
+
/* Ensure CAS dialog appears above jeopardy modal (which has z-index: 9999) */
|
|
110
|
+
.ui-dialog {{
|
|
111
|
+
z-index: 10000 !important;
|
|
112
|
+
}}
|
|
113
|
+
.ui-widget-overlay {{
|
|
114
|
+
z-index: 9999 !important;
|
|
115
|
+
}}
|
|
116
|
+
.ggb-reset-btn {{
|
|
117
|
+
position: absolute;
|
|
118
|
+
right: 2.5em;
|
|
119
|
+
top: 50%;
|
|
120
|
+
transform: translateY(-50%);
|
|
121
|
+
width: 2em;
|
|
122
|
+
height: 2em;
|
|
123
|
+
padding: 0.25em;
|
|
124
|
+
margin: 0;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
border: none;
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
background: transparent;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
transition: background-color 0.2s, opacity 0.2s;
|
|
133
|
+
opacity: 0.7;
|
|
134
|
+
}}
|
|
135
|
+
.ggb-reset-btn:hover {{
|
|
136
|
+
opacity: 1;
|
|
137
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
138
|
+
}}
|
|
139
|
+
[data-theme="dark"] .ggb-reset-btn:hover,
|
|
140
|
+
html[data-theme="dark"] .ggb-reset-btn:hover {{
|
|
141
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
142
|
+
}}
|
|
143
|
+
.ggb-reset-btn svg {{
|
|
144
|
+
color: inherit;
|
|
145
|
+
}}
|
|
146
|
+
</style>
|
|
147
|
+
|
|
148
|
+
<script>
|
|
149
|
+
(function() {{
|
|
150
|
+
$(function() {{
|
|
151
|
+
let ggbReady = false;
|
|
152
|
+
const storageKey = 'ggb-cas-state-{cid}';
|
|
153
|
+
|
|
154
|
+
function applySize() {{
|
|
155
|
+
if (!ggbReady) return;
|
|
156
|
+
const w = $("#{cid}").width(),
|
|
157
|
+
h = $("#{cid}").height();
|
|
158
|
+
window.ggbApplet.setSize(Math.round(w), Math.round(h));
|
|
159
|
+
}}
|
|
160
|
+
|
|
161
|
+
function saveState() {{
|
|
162
|
+
if (!ggbReady || !window.ggbApplet) return;
|
|
163
|
+
try {{
|
|
164
|
+
const state = window.ggbApplet.getBase64();
|
|
165
|
+
localStorage.setItem(storageKey, state);
|
|
166
|
+
// Update timestamp for this state
|
|
167
|
+
localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
|
|
168
|
+
}} catch (e) {{
|
|
169
|
+
// If quota exceeded, try cleaning up old states and retry
|
|
170
|
+
if (e.name === 'QuotaExceededError') {{
|
|
171
|
+
cleanupOldStates();
|
|
172
|
+
try {{
|
|
173
|
+
const state = window.ggbApplet.getBase64();
|
|
174
|
+
localStorage.setItem(storageKey, state);
|
|
175
|
+
localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
|
|
176
|
+
}} catch (retryError) {{
|
|
177
|
+
// Still failed after cleanup - silently give up
|
|
178
|
+
}}
|
|
179
|
+
}}
|
|
180
|
+
}}
|
|
181
|
+
}}
|
|
182
|
+
|
|
183
|
+
function cleanupOldStates() {{
|
|
184
|
+
try {{
|
|
185
|
+
// Find all GeoGebra CAS states with timestamps
|
|
186
|
+
const casStates = [];
|
|
187
|
+
for (let i = 0; i < localStorage.length; i++) {{
|
|
188
|
+
const key = localStorage.key(i);
|
|
189
|
+
if (key && key.startsWith('ggb-cas-state-') && key.endsWith('-timestamp')) {{
|
|
190
|
+
const stateKey = key.replace('-timestamp', '');
|
|
191
|
+
const timestamp = parseInt(localStorage.getItem(key) || '0', 10);
|
|
192
|
+
casStates.push({{ key: stateKey, timestamp: timestamp }});
|
|
193
|
+
}}
|
|
194
|
+
}}
|
|
195
|
+
|
|
196
|
+
// Sort by timestamp (oldest first)
|
|
197
|
+
casStates.sort((a, b) => a.timestamp - b.timestamp);
|
|
198
|
+
|
|
199
|
+
// Delete oldest 25% of states (minimum 1, maximum 10)
|
|
200
|
+
const numToDelete = Math.max(1, Math.min(10, Math.floor(casStates.length * 0.25)));
|
|
201
|
+
for (let i = 0; i < numToDelete && i < casStates.length; i++) {{
|
|
202
|
+
localStorage.removeItem(casStates[i].key);
|
|
203
|
+
localStorage.removeItem(casStates[i].key + '-timestamp');
|
|
204
|
+
}}
|
|
205
|
+
}} catch (e) {{
|
|
206
|
+
// Cleanup failed - silently continue
|
|
207
|
+
}}
|
|
208
|
+
}}
|
|
209
|
+
|
|
210
|
+
function restoreState() {{
|
|
211
|
+
if (!ggbReady || !window.ggbApplet) return;
|
|
212
|
+
try {{
|
|
213
|
+
const savedState = localStorage.getItem(storageKey);
|
|
214
|
+
if (savedState) {{
|
|
215
|
+
window.ggbApplet.setBase64(savedState);
|
|
216
|
+
// Re-apply custom toolbar after restoring state to prevent it from being overwritten
|
|
217
|
+
setTimeout(() => {{
|
|
218
|
+
if (window.ggbApplet && window.ggbApplet.setCustomToolBar) {{
|
|
219
|
+
window.ggbApplet.setCustomToolBar("1001 | 1002 | 1007 | 1010 | 6");
|
|
220
|
+
}}
|
|
221
|
+
}}, 50);
|
|
222
|
+
}}
|
|
223
|
+
}} catch (e) {{
|
|
224
|
+
// Silently fail if restore fails
|
|
225
|
+
}}
|
|
226
|
+
}}
|
|
227
|
+
|
|
228
|
+
const $dlg = $("#{dialog_id}").dialog({{
|
|
229
|
+
autoOpen: false,
|
|
230
|
+
width: {width+40}, height: {height+80},
|
|
231
|
+
resizable: true, draggable: true,
|
|
232
|
+
position: {{ my: "center", at: "center", of: window }},
|
|
233
|
+
zIndex: 10000,
|
|
234
|
+
resize: () => window.requestAnimationFrame(applySize),
|
|
235
|
+
open: function() {{
|
|
236
|
+
if (!ggbReady) {{
|
|
237
|
+
new GGBApplet({{
|
|
238
|
+
appName: "classic", id: "{cid}",
|
|
239
|
+
width: {width}, height: {height},
|
|
240
|
+
perspective: "C", language: "nb",
|
|
241
|
+
showToolBar: true, showAlgebraInput: false,
|
|
242
|
+
borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
|
|
243
|
+
customToolBar: "1001 | 1002 | 1007 | 1010 | 6",
|
|
244
|
+
appletOnLoad: () => {{
|
|
245
|
+
ggbReady = true;
|
|
246
|
+
applySize();
|
|
247
|
+
// Restore state after a short delay to ensure GeoGebra is fully initialized
|
|
248
|
+
setTimeout(restoreState, 100);
|
|
249
|
+
}}
|
|
250
|
+
}}, true).inject("{cid}");
|
|
251
|
+
}} else {{
|
|
252
|
+
applySize();
|
|
253
|
+
}}
|
|
254
|
+
}},
|
|
255
|
+
close: function() {{
|
|
256
|
+
// Save state when dialog is closed
|
|
257
|
+
saveState();
|
|
258
|
+
}}
|
|
259
|
+
}});
|
|
260
|
+
|
|
261
|
+
// Add refresh button to title bar
|
|
262
|
+
const titleBar = $dlg.parent().find('.ui-dialog-titlebar');
|
|
263
|
+
const refreshBtn = $('<button type="button" class="ggb-reset-btn" title="Start på nytt (slett lagret innhold)"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.65 2.35C12.2 0.9 10.21 0 8 0 3.58 0 0.01 3.58 0.01 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L9 7h7V0l-2.35 2.35z" fill="currentColor"/></svg></button>');
|
|
264
|
+
refreshBtn.on('click', function() {{
|
|
265
|
+
if (confirm('Er du sikker på at du vil slette lagret innhold og starte på nytt?')) {{
|
|
266
|
+
try {{
|
|
267
|
+
localStorage.removeItem(storageKey);
|
|
268
|
+
localStorage.removeItem(storageKey + '-timestamp');
|
|
269
|
+
if (ggbReady && window.ggbApplet) {{
|
|
270
|
+
// Clear the container
|
|
271
|
+
$("#{cid}").empty();
|
|
272
|
+
// Reset the flag
|
|
273
|
+
ggbReady = false;
|
|
274
|
+
// Recreate the applet from scratch
|
|
275
|
+
new GGBApplet({{
|
|
276
|
+
appName: "classic", id: "{cid}",
|
|
277
|
+
width: {width}, height: {height},
|
|
278
|
+
perspective: "C", language: "nb",
|
|
279
|
+
showToolBar: true, showAlgebraInput: false,
|
|
280
|
+
borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
|
|
281
|
+
customToolBar: "1001 | 1002 | 1007 | 1010 | 6",
|
|
282
|
+
appletOnLoad: () => {{
|
|
283
|
+
ggbReady = true;
|
|
284
|
+
applySize();
|
|
285
|
+
}}
|
|
286
|
+
}}, true).inject("{cid}");
|
|
287
|
+
}}
|
|
288
|
+
}} catch (e) {{
|
|
289
|
+
console.error('Failed to reset:', e);
|
|
290
|
+
}}
|
|
291
|
+
}}
|
|
292
|
+
}});
|
|
293
|
+
titleBar.append(refreshBtn);
|
|
294
|
+
|
|
295
|
+
// Save state when page is unloaded (refresh, navigate away, close tab)
|
|
296
|
+
$(window).on('beforeunload', function() {{
|
|
297
|
+
if ($dlg.dialog('isOpen')) {{
|
|
298
|
+
saveState();
|
|
299
|
+
}}
|
|
300
|
+
}});
|
|
301
|
+
|
|
302
|
+
// Prevent background scroll when GeoGebra applet is active
|
|
303
|
+
let scrollLocked = false;
|
|
304
|
+
let lockedScrollTop = 0;
|
|
305
|
+
let keyHandler = null;
|
|
306
|
+
let wheelHandler = null;
|
|
307
|
+
let scrollHandler = null;
|
|
308
|
+
|
|
309
|
+
function lockScroll() {{
|
|
310
|
+
if (scrollLocked) return;
|
|
311
|
+
scrollLocked = true;
|
|
312
|
+
lockedScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
|
|
313
|
+
|
|
314
|
+
// Instead of preventing keys, monitor and restore scroll position
|
|
315
|
+
scrollHandler = () => {{
|
|
316
|
+
if (scrollLocked) {{
|
|
317
|
+
const currentScroll = window.pageYOffset || document.documentElement.scrollTop || 0;
|
|
318
|
+
if (currentScroll !== lockedScrollTop) {{
|
|
319
|
+
window.scrollTo(0, lockedScrollTop);
|
|
320
|
+
}}
|
|
321
|
+
}}
|
|
322
|
+
}};
|
|
323
|
+
|
|
324
|
+
// Prevent wheel scrolling on the page background
|
|
325
|
+
wheelHandler = (e) => {{
|
|
326
|
+
const dialogElement = e.target.closest('.ui-dialog');
|
|
327
|
+
if (dialogElement) {{
|
|
328
|
+
// Allow scrolling within dialog elements that can scroll
|
|
329
|
+
let target = e.target;
|
|
330
|
+
while (target && target !== dialogElement) {{
|
|
331
|
+
const overflow = window.getComputedStyle(target).overflow;
|
|
332
|
+
if (overflow === 'auto' || overflow === 'scroll') {{
|
|
333
|
+
return; // Allow internal scrolling
|
|
334
|
+
}}
|
|
335
|
+
target = target.parentElement;
|
|
336
|
+
}}
|
|
337
|
+
// Prevent page scroll but don't stop the event from reaching GeoGebra
|
|
338
|
+
if (!e.target.closest('#{cid}')) {{
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
e.stopPropagation();
|
|
341
|
+
}}
|
|
342
|
+
}}
|
|
343
|
+
}};
|
|
344
|
+
|
|
345
|
+
// Monitor scroll events to maintain position
|
|
346
|
+
window.addEventListener('scroll', scrollHandler, {{ passive: true }});
|
|
347
|
+
document.addEventListener('wheel', wheelHandler, {{ capture: true, passive: false }});
|
|
348
|
+
}}
|
|
349
|
+
|
|
350
|
+
function unlockScroll() {{
|
|
351
|
+
if (!scrollLocked) return;
|
|
352
|
+
scrollLocked = false;
|
|
353
|
+
|
|
354
|
+
if (scrollHandler) {{
|
|
355
|
+
window.removeEventListener('scroll', scrollHandler);
|
|
356
|
+
scrollHandler = null;
|
|
357
|
+
}}
|
|
358
|
+
if (wheelHandler) {{
|
|
359
|
+
document.removeEventListener('wheel', wheelHandler, {{ capture: true }});
|
|
360
|
+
wheelHandler = null;
|
|
361
|
+
}}
|
|
362
|
+
}}
|
|
363
|
+
|
|
364
|
+
// Monitor GeoGebra applet focus/interaction to control scroll lock
|
|
365
|
+
function bindScrollLock() {{
|
|
366
|
+
const ggbContainer = document.getElementById('{cid}');
|
|
367
|
+
if (!ggbContainer) {{
|
|
368
|
+
setTimeout(bindScrollLock, 300);
|
|
369
|
+
return;
|
|
370
|
+
}}
|
|
371
|
+
|
|
372
|
+
// Look for the actual GeoGebra canvas/applet elements
|
|
373
|
+
const ggbApplet = ggbContainer.querySelector('canvas') ||
|
|
374
|
+
ggbContainer.querySelector('.ggbApplet') ||
|
|
375
|
+
ggbContainer.querySelector('[id^="ggbApplet"]') ||
|
|
376
|
+
ggbContainer;
|
|
377
|
+
|
|
378
|
+
if (ggbApplet) {{
|
|
379
|
+
// Lock scroll when interacting with GeoGebra
|
|
380
|
+
ggbApplet.addEventListener('mousedown', lockScroll);
|
|
381
|
+
ggbApplet.addEventListener('focus', lockScroll);
|
|
382
|
+
ggbApplet.addEventListener('click', lockScroll);
|
|
383
|
+
|
|
384
|
+
// Also monitor the container for any focus events
|
|
385
|
+
ggbContainer.addEventListener('focusin', lockScroll);
|
|
386
|
+
ggbContainer.addEventListener('mousedown', lockScroll);
|
|
387
|
+
|
|
388
|
+
// Unlock when clicking outside or losing focus
|
|
389
|
+
document.addEventListener('click', (e) => {{
|
|
390
|
+
if (!ggbContainer.contains(e.target) && !e.target.closest('.ui-dialog')) {{
|
|
391
|
+
unlockScroll();
|
|
392
|
+
}}
|
|
393
|
+
}});
|
|
394
|
+
|
|
395
|
+
ggbContainer.addEventListener('focusout', (e) => {{
|
|
396
|
+
// Small delay to check if focus moved outside the container
|
|
397
|
+
setTimeout(() => {{
|
|
398
|
+
if (!ggbContainer.contains(document.activeElement)) {{
|
|
399
|
+
unlockScroll();
|
|
400
|
+
}}
|
|
401
|
+
}}, 100);
|
|
402
|
+
}});
|
|
403
|
+
}}
|
|
404
|
+
}}
|
|
405
|
+
|
|
406
|
+
// Start monitoring after GeoGebra is loaded
|
|
407
|
+
setTimeout(bindScrollLock, 1000);
|
|
408
|
+
|
|
409
|
+
// Cleanup on dialog close
|
|
410
|
+
$dlg.on('dialogclose', unlockScroll);
|
|
411
|
+
|
|
412
|
+
$("#{button_id}").button()
|
|
413
|
+
.on("click touchend pointerup", e => {{
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
$("#{dialog_id}").dialog("open");
|
|
416
|
+
}});
|
|
417
|
+
}});
|
|
418
|
+
}})();
|
|
419
|
+
</script>
|
|
420
|
+
{wrapper_end}
|
|
421
|
+
"""
|
|
422
|
+
return [nodes.raw("", html, format="html")]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def setup(app):
|
|
426
|
+
"""Register the directive with Sphinx."""
|
|
427
|
+
app.add_directive("cas-popup", CASPopUpDirective)
|
|
428
|
+
return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clear Directive for Munchboka Edutools
|
|
3
|
+
======================================
|
|
4
|
+
|
|
5
|
+
This directive inserts a CSS clear:both element to clear floating elements.
|
|
6
|
+
Useful after floated images or other content to ensure proper layout flow.
|
|
7
|
+
|
|
8
|
+
Usage in MyST Markdown:
|
|
9
|
+
```{clear}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Purpose:
|
|
13
|
+
The clear directive is commonly used in web layouts to clear CSS floats.
|
|
14
|
+
When you have floated elements (like images with float:left or float:right),
|
|
15
|
+
subsequent content can wrap around them. The clear directive ensures that
|
|
16
|
+
content after it starts on a new line below all floated elements.
|
|
17
|
+
|
|
18
|
+
Common Use Cases:
|
|
19
|
+
- After floating images to prevent text wrapping
|
|
20
|
+
- Between sections to ensure proper spacing
|
|
21
|
+
- To reset layout flow after floated elements
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- No arguments or options needed
|
|
25
|
+
- Simply inserts a clearing div
|
|
26
|
+
- Works with all CSS float properties
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
{.float-left}
|
|
30
|
+
|
|
31
|
+
Some text that wraps around the image...
|
|
32
|
+
|
|
33
|
+
```{clear}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This text starts below the image, not wrapped around it.
|
|
37
|
+
|
|
38
|
+
Author: René Aasen (ported from matematikk_r1)
|
|
39
|
+
Date: November 2025
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from docutils import nodes
|
|
43
|
+
from docutils.parsers.rst import Directive
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ClearDirective(Directive):
|
|
47
|
+
"""
|
|
48
|
+
Sphinx directive for clearing CSS floats.
|
|
49
|
+
|
|
50
|
+
This directive inserts a div with `clear: both` CSS property,
|
|
51
|
+
which forces content to start below any floated elements.
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
None
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
None
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
After a floated image:
|
|
61
|
+
```{clear}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or between sections:
|
|
65
|
+
```{clear}
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
has_content = False
|
|
70
|
+
required_arguments = 0
|
|
71
|
+
optional_arguments = 0
|
|
72
|
+
|
|
73
|
+
def run(self):
|
|
74
|
+
"""
|
|
75
|
+
Generate the HTML for the clear element.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
list: List of docutils nodes (raw HTML node with clear div)
|
|
79
|
+
"""
|
|
80
|
+
html = '<div style="clear: both;"></div>'
|
|
81
|
+
return [nodes.raw("", html, format="html")]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def setup(app):
|
|
85
|
+
"""
|
|
86
|
+
Setup function to register the directive with Sphinx.
|
|
87
|
+
|
|
88
|
+
This function is called automatically by Sphinx when the extension is loaded.
|
|
89
|
+
It registers the 'clear' directive for use in documentation.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
app: The Sphinx application instance
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: Extension metadata including version and parallel processing flags
|
|
96
|
+
"""
|
|
97
|
+
app.add_directive("clear", ClearDirective)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"version": "0.1",
|
|
101
|
+
"parallel_read_safe": True,
|
|
102
|
+
"parallel_write_safe": True,
|
|
103
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dialogue directive for creating conversation-style content.
|
|
3
|
+
|
|
4
|
+
This directive creates a dialogue between two speakers with chat-bubble styling.
|
|
5
|
+
Each speaker's messages are aligned left or right and styled with different colors.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
```{dialogue}
|
|
9
|
+
:name1: Alice
|
|
10
|
+
:name2: Bob
|
|
11
|
+
:speaker1: left
|
|
12
|
+
:speaker2: right
|
|
13
|
+
|
|
14
|
+
Alice: Hello! How are you?
|
|
15
|
+
Bob: I'm great, thanks for asking!
|
|
16
|
+
Alice: That's wonderful to hear.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
- name1 (required): Name of the first speaker
|
|
21
|
+
- name2 (required): Name of the second speaker
|
|
22
|
+
- speaker1 (required): Alignment for first speaker ("left" or "right")
|
|
23
|
+
- speaker2 (required): Alignment for second speaker ("left" or "right")
|
|
24
|
+
|
|
25
|
+
Content:
|
|
26
|
+
Each line should be in the format: "SpeakerName: Message text"
|
|
27
|
+
Lines not matching this format are ignored.
|
|
28
|
+
Messages can contain math notation and will be parsed as normal content.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from docutils import nodes
|
|
32
|
+
from docutils.parsers.rst import Directive, directives
|
|
33
|
+
from sphinx.directives import optional_int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DialogueWrapperNode(nodes.General, nodes.Element):
|
|
37
|
+
"""Container node for the entire dialogue."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DialogueEntryNode(nodes.Admonition, nodes.Element):
|
|
43
|
+
"""Node for a single dialogue entry (one message from one speaker)."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def visit_dialogue_wrapper_node_html(self, node):
|
|
49
|
+
"""Open the dialogue container div."""
|
|
50
|
+
self.body.append('<div class="dialogue">')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def depart_dialogue_wrapper_node_html(self, node):
|
|
54
|
+
"""Close the dialogue container div."""
|
|
55
|
+
self.body.append("</div>")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def visit_dialogue_entry_node_html(self, node):
|
|
59
|
+
"""Open a dialogue entry with speaker name."""
|
|
60
|
+
self.body.append(f'<div class="dialogue-entry {node["css_class"]}">')
|
|
61
|
+
self.body.append(f'<div class="speaker-name">{node["speaker_name"]}</div>')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def depart_dialogue_entry_node_html(self, node):
|
|
65
|
+
"""Close the dialogue entry div."""
|
|
66
|
+
self.body.append("</div>")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DialogueDirective(Directive):
|
|
70
|
+
"""
|
|
71
|
+
Directive for creating dialogue between two speakers.
|
|
72
|
+
|
|
73
|
+
Creates chat-bubble styled conversations with left/right alignment.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
has_content = True
|
|
77
|
+
required_arguments = 0
|
|
78
|
+
option_spec = {
|
|
79
|
+
"name1": directives.unchanged_required,
|
|
80
|
+
"name2": directives.unchanged_required,
|
|
81
|
+
"speaker1": lambda s: directives.choice(s, ("left", "right")),
|
|
82
|
+
"speaker2": lambda s: directives.choice(s, ("left", "right")),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def run(self):
|
|
86
|
+
"""Parse dialogue content and create dialogue nodes."""
|
|
87
|
+
name1 = self.options.get("name1")
|
|
88
|
+
name2 = self.options.get("name2")
|
|
89
|
+
class1 = "speaker1" if self.options.get("speaker1") == "left" else "speaker2"
|
|
90
|
+
class2 = "speaker1" if self.options.get("speaker2") == "left" else "speaker2"
|
|
91
|
+
|
|
92
|
+
wrapper_node = DialogueWrapperNode()
|
|
93
|
+
|
|
94
|
+
for line in self.content:
|
|
95
|
+
line = line.strip()
|
|
96
|
+
if not line or ":" not in line:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
speaker_name, message = line.split(":", 1)
|
|
100
|
+
speaker_name = speaker_name.strip()
|
|
101
|
+
message = message.strip()
|
|
102
|
+
|
|
103
|
+
if speaker_name == name1:
|
|
104
|
+
css_class = class1
|
|
105
|
+
elif speaker_name == name2:
|
|
106
|
+
css_class = class2
|
|
107
|
+
else:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
entry_node = DialogueEntryNode()
|
|
111
|
+
entry_node["css_class"] = css_class
|
|
112
|
+
entry_node["speaker_name"] = speaker_name
|
|
113
|
+
|
|
114
|
+
# Parse the message line like normal content so math works
|
|
115
|
+
self.state.nested_parse([message], self.content_offset, entry_node)
|
|
116
|
+
wrapper_node += entry_node
|
|
117
|
+
|
|
118
|
+
return [wrapper_node]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def setup(app):
|
|
122
|
+
"""Register the dialogue directive and nodes with Sphinx."""
|
|
123
|
+
app.add_node(
|
|
124
|
+
DialogueWrapperNode,
|
|
125
|
+
html=(visit_dialogue_wrapper_node_html, depart_dialogue_wrapper_node_html),
|
|
126
|
+
)
|
|
127
|
+
app.add_node(
|
|
128
|
+
DialogueEntryNode,
|
|
129
|
+
html=(visit_dialogue_entry_node_html, depart_dialogue_entry_node_html),
|
|
130
|
+
)
|
|
131
|
+
app.add_directive("dialogue", DialogueDirective)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"version": "0.1.0",
|
|
135
|
+
"parallel_read_safe": True,
|
|
136
|
+
"parallel_write_safe": True,
|
|
137
|
+
}
|