hyperbook 0.84.4 → 0.85.0

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 (39) hide show
  1. package/dist/assets/bootstrap.js +248 -0
  2. package/dist/assets/cloud.js +28 -17
  3. package/dist/assets/directive-abc-music/client.js +11 -3
  4. package/dist/assets/directive-archive/client.js +17 -4
  5. package/dist/assets/directive-audio/client.js +67 -28
  6. package/dist/assets/directive-bookmarks/client.js +9 -1
  7. package/dist/assets/directive-download/client.js +10 -2
  8. package/dist/assets/directive-embed/client.js +112 -0
  9. package/dist/assets/directive-embed/style.css +70 -1
  10. package/dist/assets/directive-excalidraw/client.js +9 -0
  11. package/dist/assets/directive-excalidraw/hyperbook-excalidraw.umd.js +1 -1
  12. package/dist/assets/directive-geogebra/client.js +16 -3
  13. package/dist/assets/directive-h5p/client.js +32 -3
  14. package/dist/assets/directive-learningmap/client.js +11 -3
  15. package/dist/assets/directive-mermaid/client.js +11 -1
  16. package/dist/assets/directive-multievent/multievent.js +2 -2
  17. package/dist/assets/directive-onlineide/client.js +7 -0
  18. package/dist/assets/directive-onlineide/include/online-ide-embedded.js +43 -43
  19. package/dist/assets/directive-p5/client.js +39 -7
  20. package/dist/assets/directive-protect/client.js +11 -3
  21. package/dist/assets/directive-pyide/client.js +20 -9
  22. package/dist/assets/directive-scratchblock/client.js +9 -0
  23. package/dist/assets/directive-slideshow/client.js +12 -4
  24. package/dist/assets/directive-sqlide/client.js +7 -0
  25. package/dist/assets/directive-sqlide/include/sql-ide-embedded.js +3 -3
  26. package/dist/assets/directive-tabs/client.js +14 -3
  27. package/dist/assets/directive-textinput/client.js +14 -3
  28. package/dist/assets/directive-webide/client.js +45 -10
  29. package/dist/assets/directive-youtube/client.js +99 -0
  30. package/dist/assets/directive-youtube/style.css +63 -0
  31. package/dist/assets/hyperbook.types.js +209 -0
  32. package/dist/assets/i18n.js +15 -1
  33. package/dist/assets/store.js +174 -139
  34. package/dist/assets/ui.js +279 -0
  35. package/dist/index.js +632 -413
  36. package/dist/locales/de.json +9 -1
  37. package/dist/locales/en.json +9 -1
  38. package/package.json +4 -4
  39. package/dist/assets/client.js +0 -506
@@ -1,166 +1,201 @@
1
+ /// <reference path="./hyperbook.types.js" />
2
+ window.hyperbook = window.hyperbook || {};
3
+
1
4
  /**
2
- * @type {import("dexie").Dexie}
5
+ * Persistent store backed by Dexie (IndexedDB).
6
+ * Provides table access, import/export, and reset functionality.
7
+ * @type {HyperbookStore}
8
+ * @memberof hyperbook
9
+ * @see hyperbook.i18n
10
+ * @see hyperbook.cloud
3
11
  */
4
- var store = new Dexie("Hyperbook");
5
- store.version(2).stores({
6
- currentState: `
7
- id,
8
- path,
9
- mouseX,
10
- mouseY,
11
- scrollX,
12
- scrollY,
13
- windowWidth,
14
- windowHeight
15
- `,
16
- collapsibles: `id`,
17
- abcMusic: `id,tune`,
18
- audio: `id,time`,
19
- bookmarks: `path,label`,
20
- p5: `id,sketch`,
21
- protect: `id,passwordHash`,
22
- pyide: `id,script`,
23
- slideshow: `id,active`,
24
- tabs: `id,active`,
25
- excalidraw: `id,excalidrawElements,appState,files`,
26
- webide: `id,html,css,js`,
27
- h5p: `id,userData`,
28
- geogebra: `id,state`,
29
- learningmap: `id,nodes,x,y,zoom`,
30
- textinput: `id,text`,
31
- custom: `id,payload`,
32
- onlineide: `scriptId,script`,
33
- sqlideScripts: `scriptId,script`,
34
- sqlideDatabases: `databaseId,database`,
35
- multievent: `id,state`,
36
- typst: `id,code`,
37
- });
38
-
39
- const initStore = async () => {
40
- store.currentState.put({
41
- id: 1,
42
- path: window.location.pathname,
43
- mouseX: 0,
44
- mouseY: 0,
45
- scrollX: window.scrollX,
46
- scrollY: window.scrollY,
47
- windowWidth: window.innerWidth,
48
- windowHeight: window.innerHeight,
49
- });
50
- window.addEventListener("mousemove", (e) => {
51
- store.currentState.update(1, { mouseX: e.clientX, mouseY: e.clientY });
12
+ hyperbook.store = (function () {
13
+ /** @type {import("dexie").Dexie} */
14
+ var db = new Dexie("Hyperbook");
15
+ db.version(3).stores({
16
+ consent: `id`,
17
+ currentState: `
18
+ id,
19
+ path,
20
+ mouseX,
21
+ mouseY,
22
+ scrollX,
23
+ scrollY,
24
+ windowWidth,
25
+ windowHeight
26
+ `,
27
+ collapsibles: `id`,
28
+ abcMusic: `id,tune`,
29
+ audio: `id,time`,
30
+ bookmarks: `path,label`,
31
+ p5: `id,sketch`,
32
+ protect: `id,passwordHash`,
33
+ pyide: `id,script`,
34
+ slideshow: `id,active`,
35
+ tabs: `id,active`,
36
+ excalidraw: `id,excalidrawElements,appState,files`,
37
+ webide: `id,html,css,js`,
38
+ h5p: `id,userData`,
39
+ geogebra: `id,state`,
40
+ learningmap: `id,nodes,x,y,zoom`,
41
+ textinput: `id,text`,
42
+ custom: `id,payload`,
43
+ onlineide: `scriptId,script`,
44
+ sqlideScripts: `scriptId,script`,
45
+ sqlideDatabases: `databaseId,database`,
46
+ multievent: `id,state`,
47
+ typst: `id,code`,
52
48
  });
53
- window.addEventListener("scroll", (e) => {
54
- store.currentState.update(1, {
49
+
50
+ /** @returns {Promise<void>} */
51
+ const init = async () => {
52
+ db.currentState.put({
53
+ id: 1,
54
+ path: window.location.pathname,
55
+ mouseX: 0,
56
+ mouseY: 0,
55
57
  scrollX: window.scrollX,
56
58
  scrollY: window.scrollY,
57
- });
58
- });
59
- window.addEventListener("resize", (e) => {
60
- store.currentState.update(1, {
61
59
  windowWidth: window.innerWidth,
62
60
  windowHeight: window.innerHeight,
63
61
  });
64
- });
65
- };
66
-
67
- initStore();
62
+ window.addEventListener("mousemove", (e) => {
63
+ db.currentState.update(1, { mouseX: e.clientX, mouseY: e.clientY });
64
+ });
65
+ window.addEventListener("scroll", (e) => {
66
+ db.currentState.update(1, {
67
+ scrollX: window.scrollX,
68
+ scrollY: window.scrollY,
69
+ });
70
+ });
71
+ window.addEventListener("resize", (e) => {
72
+ db.currentState.update(1, {
73
+ windowWidth: window.innerWidth,
74
+ windowHeight: window.innerHeight,
75
+ });
76
+ });
77
+ };
68
78
 
69
- async function hyperbookExport() {
70
- const hyperbook = await store.export({ prettyJson: true });
79
+ init();
80
+
81
+ /**
82
+ * Export all store data as a JSON file download.
83
+ * @returns {Promise<void>}
84
+ */
85
+ async function hyperbookExport() {
86
+ const exp = await db.export({ prettyJson: true });
87
+
88
+ const data = {
89
+ version: 1,
90
+ origin: window.location.origin,
91
+ data: {
92
+ hyperbook: JSON.parse(await exp.text()),
93
+ },
94
+ };
71
95
 
72
- const data = {
73
- version: 1,
74
- origin: window.location.origin,
75
- data: {
76
- hyperbook: JSON.parse(await hyperbook.text()),
77
- },
78
- };
96
+ const json = JSON.stringify(data);
97
+ const blob = new Blob([json], { type: "application/json" });
98
+ const url = URL.createObjectURL(blob);
99
+ const a = document.createElement("a");
100
+ const date = new Date().toISOString().split("T")[0];
101
+ a.href = url;
102
+ a.download = `hyperbook-export-${date}.json`;
103
+ a.click();
104
+ URL.revokeObjectURL(url);
105
+ }
79
106
 
80
- const json = JSON.stringify(data);
81
- const blob = new Blob([json], { type: "application/json" });
82
- const url = URL.createObjectURL(blob);
83
- const a = document.createElement("a");
84
- const date = new Date().toISOString().split("T")[0];
85
- a.href = url;
86
- a.download = `hyperbook-export-${date}.json`;
87
- a.click();
88
- URL.revokeObjectURL(url);
89
- }
90
-
91
- async function hyperbookReset() {
92
- async function clearTable(db) {
93
- for (const table of db.tables) {
94
- await table.clear();
107
+ /**
108
+ * Clear all store data after user confirmation.
109
+ * Syncs the reset to the cloud if connected.
110
+ * @returns {Promise<void>}
111
+ */
112
+ async function hyperbookReset() {
113
+ async function clearTable(database) {
114
+ for (const table of database.tables) {
115
+ await table.clear();
116
+ }
95
117
  }
96
- }
97
118
 
98
- if (!confirm(i18n.get("store-reset-confirm"))) {
99
- return;
100
- }
119
+ if (!confirm(hyperbook.i18n.get("store-reset-confirm"))) {
120
+ return;
121
+ }
101
122
 
102
- clearTable(store);
123
+ clearTable(db);
103
124
 
104
- // Send empty snapshot to cloud
105
- if (window.hyperbook && window.hyperbook.cloud) {
106
- try {
107
- await window.hyperbook.cloud.sendSnapshot();
108
- } catch (e) {
109
- console.error("Failed to sync reset to cloud:", e);
125
+ // Send empty snapshot to cloud
126
+ if (hyperbook.cloud) {
127
+ try {
128
+ await hyperbook.cloud.sendSnapshot();
129
+ } catch (e) {
130
+ console.error("Failed to sync reset to cloud:", e);
131
+ }
110
132
  }
133
+
134
+ alert(hyperbook.i18n.get("store-reset-sucessful"));
135
+ window.location.reload();
111
136
  }
112
137
 
113
- alert(i18n.get("store-reset-sucessful"));
114
- window.location.reload();
115
- }
116
-
117
- async function hyperbookImport() {
118
- const input = document.createElement("input");
119
- input.type = "file";
120
- input.accept = "application/json";
121
- input.onchange = async (event) => {
122
- const file = event.target.files[0];
123
- if (!file) return;
124
- const reader = new FileReader();
125
- reader.onload = async (e) => {
126
- const data = JSON.parse(e.target.result);
127
- if (data.origin !== window.location.origin) {
128
- if (
129
- !confirm(i18n.get("store-different-origin", { origin: data.origin }))
130
- ) {
138
+ /**
139
+ * Import store data from a user-selected JSON file.
140
+ * Syncs the import to the cloud if connected.
141
+ * @returns {Promise<void>}
142
+ */
143
+ async function hyperbookImport() {
144
+ const input = document.createElement("input");
145
+ input.type = "file";
146
+ input.accept = "application/json";
147
+ input.onchange = async (event) => {
148
+ const file = event.target.files[0];
149
+ if (!file) return;
150
+ const reader = new FileReader();
151
+ reader.onload = async (e) => {
152
+ const data = JSON.parse(e.target.result);
153
+ if (data.origin !== window.location.origin) {
154
+ if (
155
+ !confirm(hyperbook.i18n.get("store-different-origin", { origin: data.origin }))
156
+ ) {
157
+ return;
158
+ }
159
+ }
160
+ if (data.version !== 1) {
161
+ alert(
162
+ hyperbook.i18n.get("store-not-supported-file-version", {
163
+ version: data.version,
164
+ }),
165
+ );
131
166
  return;
132
167
  }
133
- }
134
- if (data.version !== 1) {
135
- alert(
136
- i18n.get("store-not-supported-file-version", {
137
- version: data.version,
138
- }),
139
- );
140
- return;
141
- }
142
168
 
143
- const { hyperbook } = data.data;
169
+ const { hyperbook: hyperbookData } = data.data;
144
170
 
145
- const hyperbookBlob = new Blob([JSON.stringify(hyperbook)], {
146
- type: "application/json",
147
- });
171
+ const hyperbookBlob = new Blob([JSON.stringify(hyperbookData)], {
172
+ type: "application/json",
173
+ });
148
174
 
149
- await store.import(hyperbookBlob, { clearTablesBeforeImport: true });
175
+ await db.import(hyperbookBlob, { clearTablesBeforeImport: true });
150
176
 
151
- // Send full snapshot to cloud after import
152
- if (window.hyperbook && window.hyperbook.cloud) {
153
- try {
154
- await window.hyperbook.cloud.sendSnapshot();
155
- } catch (e) {
156
- console.error("Failed to sync import to cloud:", e);
177
+ // Send full snapshot to cloud after import
178
+ if (hyperbook.cloud) {
179
+ try {
180
+ await hyperbook.cloud.sendSnapshot();
181
+ } catch (e) {
182
+ console.error("Failed to sync import to cloud:", e);
183
+ }
157
184
  }
158
- }
159
185
 
160
- alert(i18n.get("store-import-sucessful"));
161
- window.location.reload();
186
+ alert(hyperbook.i18n.get("store-import-sucessful"));
187
+ window.location.reload();
188
+ };
189
+ reader.readAsText(file);
162
190
  };
163
- reader.readAsText(file);
164
- };
165
- input.click();
166
- }
191
+ input.click();
192
+ }
193
+
194
+ // Expose the Dexie db instance properties and public API
195
+ return Object.assign(db, {
196
+ db,
197
+ export: hyperbookExport,
198
+ reset: hyperbookReset,
199
+ import: hyperbookImport,
200
+ });
201
+ })();
@@ -0,0 +1,279 @@
1
+ /// <reference path="./hyperbook.types.js" />
2
+ window.hyperbook = window.hyperbook || {};
3
+
4
+ /**
5
+ * Core UI functions called from onclick handlers in the generated HTML.
6
+ * @type {HyperbookUI}
7
+ * @memberof hyperbook
8
+ * @see hyperbook.store
9
+ */
10
+ hyperbook.ui = (function () {
11
+ /**
12
+ * Toggle the lightbox view of an element.
13
+ * @param {HTMLElement} el - The image element to display.
14
+ */
15
+ function toggleLightbox(el) {
16
+ const overlay = document.createElement("div");
17
+ overlay.classList.add("lightbox-overlay");
18
+
19
+ const captionText =
20
+ el.parentElement.querySelector("figcaption")?.textContent || "";
21
+
22
+ const content = document.createElement("div");
23
+ content.classList.add("lightbox-content");
24
+
25
+ const imgContainer = document.createElement("div");
26
+ imgContainer.classList.add("lightbox-image-container");
27
+
28
+ const lightboxImg = document.createElement("img");
29
+ lightboxImg.src = el.src;
30
+ imgContainer.appendChild(lightboxImg);
31
+
32
+ content.appendChild(imgContainer);
33
+
34
+ if (captionText) {
35
+ const caption = document.createElement("div");
36
+ caption.classList.add("caption");
37
+ caption.textContent = captionText;
38
+ content.appendChild(caption);
39
+ }
40
+
41
+ overlay.appendChild(content);
42
+
43
+ overlay.addEventListener("click", () => {
44
+ document.body.removeChild(overlay);
45
+ });
46
+
47
+ document.body.appendChild(overlay);
48
+ overlay.style.display = "flex";
49
+ }
50
+
51
+ /**
52
+ * Toggle a bookmark on/off.
53
+ * @param {string} key - The bookmark path key.
54
+ * @param {string} label - The bookmark label.
55
+ */
56
+ function toggleBookmark(key, label) {
57
+ const el = document.querySelectorAll(`.bookmark[data-key="${key}"]`);
58
+ hyperbook.store.bookmarks.get(key).then((bookmark) => {
59
+ if (!bookmark) {
60
+ hyperbook.store.bookmarks.add({ path: key, label }).then(() => {
61
+ el.forEach((e) => e.classList.add("active"));
62
+ hyperbook.bookmarks.update();
63
+ });
64
+ } else {
65
+ hyperbook.store.bookmarks.delete(key).then(() => {
66
+ el.forEach((e) => e.classList.remove("active"));
67
+ hyperbook.bookmarks.update();
68
+ });
69
+ }
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Toggle the navigation drawer.
75
+ */
76
+ function navToggle() {
77
+ const navDrawerEl = document.getElementById("nav-drawer");
78
+ navDrawerEl.open = !navDrawerEl.open;
79
+ }
80
+
81
+ /**
82
+ * Toggle the table of contents drawer.
83
+ */
84
+ function tocToggle() {
85
+ const tocDrawerEl = document.getElementById("toc-drawer");
86
+ tocDrawerEl.open = !tocDrawerEl.open;
87
+ }
88
+
89
+ /**
90
+ * Toggle the search drawer.
91
+ */
92
+ function searchToggle() {
93
+ const searchDrawerEl = document.getElementById("search-drawer");
94
+ searchDrawerEl.open = !searchDrawerEl.open;
95
+ }
96
+
97
+ /**
98
+ * Perform a search and display the results.
99
+ */
100
+ function search() {
101
+ const resultsEl = document.getElementById("search-results");
102
+ resultsEl.innerHTML = "";
103
+ const searchInputEl = document.querySelector("#search-input");
104
+ const query = searchInputEl.value;
105
+ const idx = window.lunr.Index.load(LUNR_INDEX);
106
+ const documents = SEARCH_DOCUMENTS;
107
+ const results = idx.search(query);
108
+ for (let result of results) {
109
+ const doc = documents[result.ref];
110
+
111
+ const container = document.createElement("a");
112
+ container.href = doc.href;
113
+ container.classList.add("search-result");
114
+ const heading = document.createElement("div");
115
+ heading.textContent = doc.heading;
116
+ heading.classList.add("search-result-heading");
117
+ const content = document.createElement("div");
118
+ content.classList.add("search-result-content");
119
+ const href = document.createElement("div");
120
+ href.classList.add("search-result-href");
121
+ href.textContent = doc.href;
122
+
123
+ let contentHTML = "";
124
+ const terms = Object.keys(result.matchData.metadata);
125
+ const term = terms[0];
126
+ if (result?.matchData?.metadata?.[term]?.content?.position?.length > 0) {
127
+ const pos = result.matchData.metadata[term].content.position[0];
128
+ const start = pos[0];
129
+ const len = pos[1];
130
+ let cutoffBefore = start - 50;
131
+ if (cutoffBefore < 0) {
132
+ cutoffBefore = 0;
133
+ } else {
134
+ contentHTML += "...";
135
+ }
136
+ contentHTML += doc.content.slice(cutoffBefore, start);
137
+
138
+ contentHTML += `<mark>${doc.content.slice(start, start + len)}</mark>`;
139
+ let cutoffAfter = start + len + 50;
140
+
141
+ contentHTML += doc.content.slice(start + len, cutoffAfter);
142
+ if (cutoffAfter < doc.content.length) {
143
+ contentHTML += "...";
144
+ }
145
+ }
146
+
147
+ content.innerHTML = contentHTML;
148
+
149
+ container.appendChild(heading);
150
+ container.appendChild(content);
151
+ container.appendChild(href);
152
+ resultsEl.appendChild(container);
153
+ }
154
+ }
155
+
156
+ return {
157
+ toggleLightbox,
158
+ toggleBookmark,
159
+ navToggle,
160
+ tocToggle,
161
+ searchToggle,
162
+ search,
163
+ };
164
+ })();
165
+
166
+ /**
167
+ * QR code dialog functions.
168
+ * @type {HyperbookQrcode}
169
+ * @memberof hyperbook
170
+ */
171
+ hyperbook.qrcode = (function () {
172
+ /**
173
+ * Open the QR code dialog.
174
+ */
175
+ function open() {
176
+ const qrCodeDialog = document.getElementById("qrcode-dialog");
177
+ const qrcodeEls = qrCodeDialog.getElementsByClassName("make-qrcode");
178
+ const urlEls = qrCodeDialog.getElementsByClassName("url");
179
+ const qrcodeEl = qrcodeEls[0];
180
+ const qrcode = new window.QRCode({
181
+ content: window.location.href,
182
+ padding: 0,
183
+ join: true,
184
+ color: "var(--color-text)",
185
+ container: "svg-viewbox",
186
+ background: "var(--color-background)",
187
+ ecl: "M",
188
+ });
189
+ qrcodeEl.innerHTML = qrcode.svg();
190
+ for (let urlEl of urlEls[0].children) {
191
+ const href = urlEl.getAttribute("data-href");
192
+ urlEl.innerHTML = `${window.location.origin}${href}`;
193
+ }
194
+
195
+ qrCodeDialog.showModal();
196
+ }
197
+
198
+ /**
199
+ * Close the QR code dialog.
200
+ */
201
+ function close() {
202
+ const qrCodeDialog = document.getElementById("qrcode-dialog");
203
+ qrCodeDialog.close();
204
+ }
205
+
206
+ return { open, close };
207
+ })();
208
+
209
+ /**
210
+ * Share dialog functions.
211
+ * @type {HyperbookShare}
212
+ * @memberof hyperbook
213
+ * @see hyperbook.i18n
214
+ */
215
+ hyperbook.share = (function () {
216
+ /**
217
+ * Open the share dialog.
218
+ */
219
+ function open() {
220
+ const shareDialog = document.getElementById("share-dialog");
221
+ updatePreview();
222
+ shareDialog.showModal();
223
+ }
224
+
225
+ /**
226
+ * Close the share dialog.
227
+ */
228
+ function close() {
229
+ const shareDialog = document.getElementById("share-dialog");
230
+ shareDialog.close();
231
+ }
232
+
233
+ /**
234
+ * Update the URL preview in the share dialog.
235
+ */
236
+ function updatePreview() {
237
+ const standaloneCheckbox = document.getElementById("share-standalone-checkbox");
238
+ const sectionCheckboxes = document.querySelectorAll("#share-dialog input[data-anchor]");
239
+ const previewEl = document.getElementById("share-url-preview");
240
+
241
+ const baseUrl = window.location.origin + window.location.pathname;
242
+ const params = new URLSearchParams();
243
+
244
+ if (standaloneCheckbox && standaloneCheckbox.checked) {
245
+ params.append("standalone", "true");
246
+ }
247
+
248
+ const selectedSections = Array.from(sectionCheckboxes)
249
+ .filter(cb => cb.checked)
250
+ .map(cb => cb.getAttribute("data-anchor"));
251
+
252
+ if (selectedSections.length > 0) {
253
+ params.append("sections", selectedSections.join(","));
254
+ }
255
+
256
+ const finalUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
257
+ previewEl.textContent = finalUrl;
258
+ }
259
+
260
+ /**
261
+ * Copy the shareable URL to clipboard.
262
+ */
263
+ function copyUrl() {
264
+ const previewEl = document.getElementById("share-url-preview");
265
+ const url = previewEl.textContent;
266
+
267
+ navigator.clipboard.writeText(url).then(() => {
268
+ const button = document.querySelector("#share-dialog .copy-button");
269
+ const originalText = button.textContent;
270
+ button.textContent = hyperbook.i18n.get("share-dialog-url-copied");
271
+
272
+ setTimeout(() => {
273
+ button.textContent = originalText;
274
+ }, 2000);
275
+ });
276
+ }
277
+
278
+ return { open, close, updatePreview, copyUrl };
279
+ })();