hyperbook 0.84.3 → 0.84.5

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 (34) hide show
  1. package/dist/assets/bootstrap.js +248 -0
  2. package/dist/assets/cloud.js +217 -169
  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-excalidraw/client.js +9 -0
  9. package/dist/assets/directive-excalidraw/hyperbook-excalidraw.umd.js +1 -1
  10. package/dist/assets/directive-geogebra/client.js +16 -3
  11. package/dist/assets/directive-h5p/client.js +32 -3
  12. package/dist/assets/directive-learningmap/client.js +11 -3
  13. package/dist/assets/directive-mermaid/client.js +11 -1
  14. package/dist/assets/directive-multievent/multievent.js +2 -2
  15. package/dist/assets/directive-onlineide/client.js +7 -0
  16. package/dist/assets/directive-onlineide/include/online-ide-embedded.js +43 -43
  17. package/dist/assets/directive-p5/client.js +39 -7
  18. package/dist/assets/directive-protect/client.js +11 -3
  19. package/dist/assets/directive-pyide/client.js +20 -9
  20. package/dist/assets/directive-scratchblock/client.js +9 -0
  21. package/dist/assets/directive-slideshow/client.js +12 -4
  22. package/dist/assets/directive-sqlide/client.js +7 -0
  23. package/dist/assets/directive-sqlide/include/includeIDE.js +22 -11
  24. package/dist/assets/directive-sqlide/include/sql-ide-embedded.js +30 -30
  25. package/dist/assets/directive-tabs/client.js +14 -3
  26. package/dist/assets/directive-textinput/client.js +14 -3
  27. package/dist/assets/directive-webide/client.js +45 -10
  28. package/dist/assets/hyperbook.types.js +209 -0
  29. package/dist/assets/i18n.js +15 -1
  30. package/dist/assets/store.js +174 -266
  31. package/dist/assets/ui.js +279 -0
  32. package/dist/index.js +53 -29
  33. package/package.json +4 -4
  34. package/dist/assets/client.js +0 -506
@@ -1,8 +1,20 @@
1
+ /// <reference path="./hyperbook.types.js" />
1
2
  window.hyperbook = window.hyperbook || {};
2
- window.hyperbook.cloud = (function () {
3
+
4
+ /**
5
+ * Cloud sync integration for hyperbook store data.
6
+ * Handles authentication, event-based sync, and snapshot sync.
7
+ * @type {HyperbookCloud}
8
+ * @memberof hyperbook
9
+ * @see hyperbook.store
10
+ * @see hyperbook.i18n
11
+ */
12
+ hyperbook.cloud = (function () {
3
13
  // ===== Cloud Integration =====
4
14
  const AUTH_TOKEN_KEY = "hyperbook_auth_token";
5
15
  const AUTH_USER_KEY = "hyperbook_auth_user";
16
+ const LAST_EVENT_ID_KEY = "hyperbook_last_event_id";
17
+ const EVENT_BATCH_MAX_SIZE = 512 * 1024; // 512KB
6
18
  let isLoadingFromCloud = false;
7
19
  let syncManager = null;
8
20
 
@@ -41,7 +53,6 @@ window.hyperbook.cloud = (function () {
41
53
  this.maxWaitTime = options.maxWaitTime || 10000;
42
54
  this.minSaveInterval = options.minSaveInterval || 1000;
43
55
 
44
- this.isDirty = false;
45
56
  this.lastSaveTime = 0;
46
57
  this.lastChangeTime = 0;
47
58
  this.debounceTimer = null;
@@ -50,110 +61,25 @@ window.hyperbook.cloud = (function () {
50
61
  this.saveMutex = new Mutex();
51
62
  this.retryCount = 0;
52
63
 
53
- this.dirtyStores = new Set();
64
+ this.pendingEvents = [];
65
+ this.lastEventId = parseInt(
66
+ localStorage.getItem(LAST_EVENT_ID_KEY) || "0",
67
+ 10,
68
+ );
54
69
 
55
70
  this.offlineQueue = [];
56
71
  this.isOnline = navigator.onLine;
57
72
 
58
- // Snapshots for external DB polling
59
- this.externalDBs = options.externalDBs || [];
60
- this.externalSnapshots = {};
61
- this.pollInterval = options.pollInterval || 5000;
62
- this.pollTimer = null;
63
-
64
73
  this.setupEventListeners();
65
74
  }
66
75
 
67
- /**
68
- * Get a fingerprint of an external DB by hashing all row data.
69
- */
70
- _snapshotExternalDB(dbName) {
71
- return new Promise((resolve) => {
72
- const request = indexedDB.open(dbName);
73
- request.onerror = () => resolve(null);
74
- request.onsuccess = (event) => {
75
- const db = event.target.result;
76
- const storeNames = Array.from(db.objectStoreNames);
77
- if (storeNames.length === 0) {
78
- db.close();
79
- resolve(null);
80
- return;
81
- }
82
-
83
- const parts = [];
84
- const tx = db.transaction(storeNames, "readonly");
85
- let pending = storeNames.length;
86
- storeNames.forEach((name) => {
87
- const rows = [];
88
- const cursorReq = tx.objectStore(name).openCursor();
89
- cursorReq.onsuccess = (e) => {
90
- const cursor = e.target.result;
91
- if (cursor) {
92
- rows.push(JSON.stringify(cursor.value));
93
- cursor.continue();
94
- } else {
95
- parts.push(name + ":" + rows.join(","));
96
- pending--;
97
- if (pending === 0) {
98
- db.close();
99
- resolve(parts.join("|"));
100
- }
101
- }
102
- };
103
- cursorReq.onerror = () => {
104
- pending--;
105
- if (pending === 0) {
106
- db.close();
107
- resolve(parts.join("|"));
108
- }
109
- };
110
- });
111
- };
112
- });
113
- }
114
-
115
- /**
116
- * Take initial snapshots and start polling external DBs for changes.
117
- */
118
- async startPolling() {
119
- for (const dbName of this.externalDBs) {
120
- this.externalSnapshots[dbName] = await this._snapshotExternalDB(dbName);
121
- }
122
- this.pollTimer = setInterval(
123
- () => this._pollExternalDBs(),
124
- this.pollInterval,
125
- );
126
- }
127
-
128
- /**
129
- * Stop polling external DBs.
130
- */
131
- stopPolling() {
132
- if (this.pollTimer) {
133
- clearInterval(this.pollTimer);
134
- this.pollTimer = null;
135
- }
136
- }
137
-
138
- async _pollExternalDBs() {
139
- if (isLoadingFromCloud || this.saveInProgress) return;
140
- for (const dbName of this.externalDBs) {
141
- const current = await this._snapshotExternalDB(dbName);
142
- const prev = this.externalSnapshots[dbName];
143
-
144
- if (current !== prev) {
145
- this.externalSnapshots[dbName] = current;
146
- this.markDirty(dbName);
147
- }
148
- }
76
+ get isDirty() {
77
+ return this.pendingEvents.length > 0;
149
78
  }
150
79
 
151
- markDirty(storeName = null) {
80
+ addEvent(event) {
152
81
  if (isLoadingFromCloud || isReadOnlyMode()) return;
153
- if (storeName) {
154
- this.dirtyStores.add(storeName);
155
- }
156
- this.isDirty = true;
82
+ this.pendingEvents.push(event);
157
83
  this.lastChangeTime = Date.now();
158
84
  this.updateUI("unsaved");
159
85
  this.scheduleSave();
@@ -196,24 +122,46 @@ window.hyperbook.cloud = (function () {
196
122
  this.updateUI("saving");
197
123
 
198
124
  try {
199
- const dataToSave = await this.exportStores();
125
+ // Take a snapshot of pending events
126
+ const eventsToSend = this.pendingEvents.slice();
127
+ const serialized = JSON.stringify(eventsToSend);
200
128
 
201
129
  if (!this.isOnline) {
202
130
  this.offlineQueue.push({
203
- data: dataToSave,
131
+ events: eventsToSend,
132
+ afterEventId: this.lastEventId,
204
133
  timestamp: Date.now(),
205
134
  });
135
+ this.pendingEvents = [];
206
136
  this.updateUI("offline-queued");
207
137
  return;
208
138
  }
209
139
 
210
- await apiRequest(`/api/store/${HYPERBOOK_CLOUD.id}`, {
211
- method: "POST",
212
- body: JSON.stringify({ data: dataToSave }),
213
- });
140
+ let result;
141
+
142
+ if (serialized.length > EVENT_BATCH_MAX_SIZE) {
143
+ // Large batch — fall back to full snapshot
144
+ result = await this.sendSnapshot();
145
+ } else {
146
+ // Normal path — send events
147
+ result = await this.sendEvents(eventsToSend);
148
+ }
214
149
 
215
- this.isDirty = false;
216
- this.dirtyStores.clear();
150
+ if (result.conflict) {
151
+ // 409 — stale state, re-fetch
152
+ console.log("⚠ Stale state detected, re-fetching from cloud...");
153
+ await loadFromCloud();
154
+ this.pendingEvents = [];
155
+ window.location.reload();
156
+ return;
157
+ }
158
+
159
+ this.pendingEvents = [];
160
+ this.lastEventId = result.lastEventId;
161
+ localStorage.setItem(
162
+ LAST_EVENT_ID_KEY,
163
+ String(this.lastEventId),
164
+ );
217
165
  this.lastSaveTime = Date.now();
218
166
  this.retryCount = 0;
219
167
  this.updateUI("saved");
@@ -228,19 +176,46 @@ window.hyperbook.cloud = (function () {
228
176
  });
229
177
  }
230
178
 
231
- async exportStores() {
232
- const hyperbookExport = await store.export({ prettyJson: false });
233
- const sqlIdeExport = await exportExternalDB("SQL-IDE");
234
- const learnJExport = await exportExternalDB("LearnJ");
235
- return {
236
- version: 1,
237
- origin: window.location.origin,
238
- data: {
239
- hyperbook: JSON.parse(await hyperbookExport.text()),
240
- sqlIde: sqlIdeExport || {},
241
- learnJ: learnJExport || {},
179
+ async sendEvents(events, afterEventId) {
180
+ var effectiveAfterId = afterEventId !== undefined ? afterEventId : this.lastEventId;
181
+ try {
182
+ const data = await apiRequest(
183
+ `/api/store/${HYPERBOOK_CLOUD.id}/events`,
184
+ {
185
+ method: "POST",
186
+ body: JSON.stringify({
187
+ events: events,
188
+ afterEventId: effectiveAfterId,
189
+ }),
190
+ },
191
+ );
192
+ return { lastEventId: data.lastEventId, conflict: false };
193
+ } catch (error) {
194
+ if (error.status === 409) {
195
+ return { conflict: true };
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ async sendSnapshot() {
202
+ const storeExport = await hyperbook.store.db.export({ prettyJson: false });
203
+ const exportData = JSON.parse(await storeExport.text());
204
+
205
+ const data = await apiRequest(
206
+ `/api/store/${HYPERBOOK_CLOUD.id}/snapshot`,
207
+ {
208
+ method: "POST",
209
+ body: JSON.stringify({
210
+ data: {
211
+ version: 1,
212
+ origin: window.location.origin,
213
+ data: { hyperbook: exportData },
214
+ },
215
+ }),
242
216
  },
243
- };
217
+ );
218
+ return { lastEventId: data.lastEventId, conflict: false };
244
219
  }
245
220
 
246
221
  scheduleRetry() {
@@ -272,7 +247,6 @@ window.hyperbook.cloud = (function () {
272
247
 
273
248
  window.addEventListener("beforeunload", (e) => {
274
249
  if (this.isDirty) {
275
- this.performSave("unload");
276
250
  e.preventDefault();
277
251
  e.returnValue = "";
278
252
  }
@@ -289,21 +263,38 @@ window.hyperbook.cloud = (function () {
289
263
  if (this.offlineQueue.length === 0) return;
290
264
 
291
265
  console.log(`Processing ${this.offlineQueue.length} queued saves...`);
292
- this.offlineQueue.sort((a, b) => a.timestamp - b.timestamp);
293
266
 
294
- // Only send the latest queued save
295
- const latest = this.offlineQueue[this.offlineQueue.length - 1];
296
- try {
297
- await apiRequest(`/api/store/${HYPERBOOK_CLOUD.id}`, {
298
- method: "POST",
299
- body: JSON.stringify({ data: latest.data }),
300
- });
301
- this.offlineQueue = [];
302
- this.lastSaveTime = Date.now();
303
- console.log("✓ Offline queue processed");
304
- } catch (error) {
305
- console.error("Failed to process offline queue:", error);
267
+ // Send queued events in order
268
+ for (let i = 0; i < this.offlineQueue.length; i++) {
269
+ const queued = this.offlineQueue[i];
270
+ try {
271
+ const result = await this.sendEvents(queued.events, queued.afterEventId);
272
+
273
+ if (result.conflict) {
274
+ // Conflict — discard remaining queue, re-fetch
275
+ console.log("⚠ Offline queue conflict, re-fetching...");
276
+ this.offlineQueue = [];
277
+ await loadFromCloud();
278
+ window.location.reload();
279
+ return;
280
+ }
281
+
282
+ this.lastEventId = result.lastEventId;
283
+ localStorage.setItem(
284
+ LAST_EVENT_ID_KEY,
285
+ String(this.lastEventId),
286
+ );
287
+ } catch (error) {
288
+ console.error("Failed to process offline queue:", error);
289
+ // Keep remaining items in queue
290
+ this.offlineQueue = this.offlineQueue.slice(i);
291
+ return;
292
+ }
306
293
  }
294
+
295
+ this.offlineQueue = [];
296
+ this.lastSaveTime = Date.now();
297
+ console.log("✓ Offline queue processed");
307
298
  }
308
299
 
309
300
  clearTimers() {
@@ -327,16 +318,31 @@ window.hyperbook.cloud = (function () {
327
318
  }
328
319
 
329
320
  async manualSave() {
330
- this.isDirty = true;
321
+ if (this.pendingEvents.length === 0) {
322
+ // No pending events — send full snapshot
323
+ this.clearTimers();
324
+ try {
325
+ this.updateUI("saving");
326
+ const result = await this.sendSnapshot();
327
+ this.lastEventId = result.lastEventId;
328
+ localStorage.setItem(
329
+ LAST_EVENT_ID_KEY,
330
+ String(this.lastEventId),
331
+ );
332
+ this.updateUI("saved");
333
+ } catch (error) {
334
+ console.error("Manual save failed:", error);
335
+ this.updateUI("error");
336
+ }
337
+ return;
338
+ }
331
339
  this.clearTimers();
332
340
  await this.performSave("manual");
333
341
  }
334
342
 
335
343
  reset() {
336
- this.isDirty = false;
337
- this.dirtyStores.clear();
344
+ this.pendingEvents = [];
338
345
  this.clearTimers();
339
- this.stopPolling();
340
346
  this.offlineQueue = [];
341
347
  this.retryCount = 0;
342
348
  }
@@ -400,6 +406,7 @@ window.hyperbook.cloud = (function () {
400
406
  function clearAuthToken() {
401
407
  localStorage.removeItem(AUTH_TOKEN_KEY);
402
408
  localStorage.removeItem(AUTH_USER_KEY);
409
+ localStorage.removeItem(LAST_EVENT_ID_KEY);
403
410
  }
404
411
 
405
412
  /**
@@ -467,12 +474,16 @@ window.hyperbook.cloud = (function () {
467
474
  const data = await response.json();
468
475
 
469
476
  if (!response.ok) {
470
- throw new Error(data.error || "Request failed");
477
+ const err = new Error(data.error || "Request failed");
478
+ err.status = response.status;
479
+ throw err;
471
480
  }
472
481
 
473
482
  return data;
474
483
  } catch (error) {
475
- console.error("Cloud API error:", error);
484
+ if (!error.status) {
485
+ console.error("Cloud API error:", error);
486
+ }
476
487
  throw error;
477
488
  }
478
489
  }
@@ -524,24 +535,26 @@ window.hyperbook.cloud = (function () {
524
535
  try {
525
536
  const data = await apiRequest(`/api/store/${HYPERBOOK_CLOUD.id}`);
526
537
 
527
- if (data && data.data) {
528
- // Import data into local stores
529
- const storeData = data.data.data || data.data;
530
- const { hyperbook, sqlIde, learnJ } = storeData;
538
+ if (data && data.snapshot) {
539
+ const storeData = data.snapshot.data || data.snapshot;
540
+ const { hyperbook } = storeData;
531
541
 
532
542
  if (hyperbook) {
533
543
  const blob = new Blob([JSON.stringify(hyperbook)], {
534
544
  type: "application/json",
535
545
  });
536
- await store.import(blob, { clearTablesBeforeImport: true });
546
+ await hyperbook.store.db.import(blob, { clearTablesBeforeImport: true });
537
547
  }
538
548
 
539
- if (sqlIde) {
540
- await importExternalDB("SQL-IDE", sqlIde);
541
- }
542
-
543
- if (learnJ) {
544
- await importExternalDB("LearnJ", learnJ);
549
+ // Track the server's lastEventId
550
+ if (data.lastEventId !== undefined) {
551
+ localStorage.setItem(
552
+ LAST_EVENT_ID_KEY,
553
+ String(data.lastEventId),
554
+ );
555
+ if (syncManager) {
556
+ syncManager.lastEventId = data.lastEventId;
557
+ }
545
558
  }
546
559
 
547
560
  console.log("✓ Store loaded from cloud");
@@ -584,20 +597,39 @@ window.hyperbook.cloud = (function () {
584
597
  debounceDelay: 2000,
585
598
  maxWaitTime: 10000,
586
599
  minSaveInterval: 1000,
587
- externalDBs: ["SQL-IDE", "LearnJ"],
588
- pollInterval: 5000,
589
600
  });
590
601
 
591
- // Hook Dexie tables to track changes (skip currentState — ephemeral UI data)
592
- store.tables.forEach((table) => {
602
+ // Hook Dexie tables to capture granular events (skip currentState — ephemeral UI data)
603
+ hyperbook.store.tables.forEach((table) => {
593
604
  if (table.name === "currentState") return;
594
- table.hook("creating", () => syncManager.markDirty(table.name));
595
- table.hook("updating", () => syncManager.markDirty(table.name));
596
- table.hook("deleting", () => syncManager.markDirty(table.name));
597
- });
598
605
 
599
- // Start polling external DBs for changes
600
- syncManager.startPolling();
606
+ table.hook("creating", function (primKey, obj) {
607
+ syncManager.addEvent({
608
+ table: table.name,
609
+ op: "create",
610
+ primKey: primKey,
611
+ data: obj,
612
+ });
613
+ });
614
+
615
+ table.hook("updating", function (modifications, primKey) {
616
+ syncManager.addEvent({
617
+ table: table.name,
618
+ op: "update",
619
+ primKey: primKey,
620
+ data: modifications,
621
+ });
622
+ });
623
+
624
+ table.hook("deleting", function (primKey) {
625
+ syncManager.addEvent({
626
+ table: table.name,
627
+ op: "delete",
628
+ primKey: primKey,
629
+ data: null,
630
+ });
631
+ });
632
+ });
601
633
  }
602
634
  }
603
635
 
@@ -635,29 +667,29 @@ window.hyperbook.cloud = (function () {
635
667
  statusEl.className = status;
636
668
 
637
669
  if (status === "unsaved") {
638
- statusEl.textContent = i18n.get("user-unsaved", {}, "Unsaved changes");
670
+ statusEl.textContent = hyperbook.i18n.get("user-unsaved", {}, "Unsaved changes");
639
671
  updateUserIconState("logged-in");
640
672
  } else if (status === "saving") {
641
- statusEl.textContent = i18n.get("user-saving", {}, "Saving...");
673
+ statusEl.textContent = hyperbook.i18n.get("user-saving", {}, "Saving...");
642
674
  updateUserIconState("syncing");
643
675
  } else if (status === "saved") {
644
- statusEl.textContent = i18n.get("user-saved", {}, "Saved");
676
+ statusEl.textContent = hyperbook.i18n.get("user-saved", {}, "Saved");
645
677
  updateUserIconState("synced");
646
678
  } else if (status === "error") {
647
- statusEl.textContent = i18n.get("user-save-error", {}, "Save Error");
679
+ statusEl.textContent = hyperbook.i18n.get("user-save-error", {}, "Save Error");
648
680
  updateUserIconState("unsynced");
649
681
  } else if (status === "offline") {
650
- statusEl.textContent = i18n.get("user-offline", {}, "Offline");
682
+ statusEl.textContent = hyperbook.i18n.get("user-offline", {}, "Offline");
651
683
  updateUserIconState("unsynced");
652
684
  } else if (status === "offline-queued") {
653
- statusEl.textContent = i18n.get(
685
+ statusEl.textContent = hyperbook.i18n.get(
654
686
  "user-offline-queued",
655
687
  {},
656
688
  "Saved locally",
657
689
  );
658
690
  updateUserIconState("logged-in");
659
691
  } else if (status === "readonly") {
660
- statusEl.textContent = i18n.get("user-readonly", {}, "Read-Only Mode");
692
+ statusEl.textContent = hyperbook.i18n.get("user-readonly", {}, "Read-Only Mode");
661
693
  statusEl.className = "readonly";
662
694
  updateUserIconState("synced");
663
695
  }
@@ -686,7 +718,7 @@ window.hyperbook.cloud = (function () {
686
718
  const errorEl = document.getElementById("user-login-error");
687
719
 
688
720
  if (!username || !password) {
689
- errorEl.textContent = i18n.get(
721
+ errorEl.textContent = hyperbook.i18n.get(
690
722
  "user-login-required",
691
723
  {},
692
724
  "Username and password required",
@@ -700,14 +732,14 @@ window.hyperbook.cloud = (function () {
700
732
  errorEl.textContent = "";
701
733
  } catch (error) {
702
734
  errorEl.textContent =
703
- error.message || i18n.get("user-login-failed", {}, "Login failed");
735
+ error.message || hyperbook.i18n.get("user-login-failed", {}, "Login failed");
704
736
  }
705
737
  };
706
738
 
707
739
  const logout = () => {
708
740
  if (
709
741
  confirm(
710
- i18n.get("user-logout-confirm", {}, "Are you sure you want to logout?"),
742
+ hyperbook.i18n.get("user-logout-confirm", {}, "Are you sure you want to logout?"),
711
743
  )
712
744
  ) {
713
745
  hyperbookLogout();
@@ -727,13 +759,21 @@ window.hyperbook.cloud = (function () {
727
759
  updateUserUI(user);
728
760
  }
729
761
 
762
+ // Hide local export/import/reset when logged into cloud
763
+ if (HYPERBOOK_CLOUD && getAuthToken()) {
764
+ document.querySelectorAll(".export-icon, .import-icon, .reset-icon").forEach((el) => {
765
+ const link = el.closest("a");
766
+ if (link) link.style.display = "none";
767
+ });
768
+ }
769
+
730
770
  // Show impersonation banner if in readonly mode
731
771
  if (isReadOnlyMode()) {
732
772
  const banner = document.createElement("div");
733
773
  banner.id = "impersonation-banner";
734
774
  banner.innerHTML = `
735
- <span>${i18n.get("user-impersonating", {}, "Impersonating")}: <strong>${user ? user.username : ""}</strong> — ${i18n.get("user-readonly", {}, "Read-Only Mode")}</span>
736
- <a href="#" id="exit-impersonation">${i18n.get("user-exit-impersonation", {}, "Exit Impersonation")}</a>
775
+ <span>${hyperbook.i18n.get("user-impersonating", {}, "Impersonating")}: <strong>${user ? user.username : ""}</strong> — ${hyperbook.i18n.get("user-readonly", {}, "Read-Only Mode")}</span>
776
+ <a href="#" id="exit-impersonation">${hyperbook.i18n.get("user-exit-impersonation", {}, "Exit Impersonation")}</a>
737
777
  `;
738
778
  document.body.prepend(banner);
739
779
 
@@ -752,6 +792,14 @@ window.hyperbook.cloud = (function () {
752
792
 
753
793
  return {
754
794
  save: () => syncManager?.manualSave(),
795
+ sendSnapshot: async () => {
796
+ if (!syncManager || !HYPERBOOK_CLOUD || !getAuthToken() || isReadOnlyMode()) return;
797
+ syncManager.pendingEvents = [];
798
+ syncManager.clearTimers();
799
+ const result = await syncManager.sendSnapshot();
800
+ syncManager.lastEventId = result.lastEventId;
801
+ localStorage.setItem(LAST_EVENT_ID_KEY, String(result.lastEventId));
802
+ },
755
803
  userToggle,
756
804
  login,
757
805
  logout,
@@ -1,3 +1,11 @@
1
+ /// <reference path="../hyperbook.types.js" />
2
+
3
+ /**
4
+ * ABC music notation rendering and editing.
5
+ * @type {HyperbookAbc}
6
+ * @memberof hyperbook
7
+ * @see hyperbook.store
8
+ */
1
9
  hyperbook.abc = (function () {
2
10
  window.codeInput?.registerTemplate(
3
11
  "abc-highlighted",
@@ -33,7 +41,7 @@ hyperbook.abc = (function () {
33
41
  });
34
42
 
35
43
  resetEl?.addEventListener("click", () => {
36
- store.abcMusic.delete(id);
44
+ hyperbook.store.abcMusic.delete(id);
37
45
  window.location.reload();
38
46
  });
39
47
 
@@ -46,7 +54,7 @@ hyperbook.abc = (function () {
46
54
  });
47
55
 
48
56
  editorEl.addEventListener("code-input_load", async () => {
49
- const storeResult = await store.abcMusic
57
+ const storeResult = await hyperbook.store.abcMusic
50
58
  .where("id")
51
59
  .equals(editorEl.id)
52
60
  .first();
@@ -70,7 +78,7 @@ hyperbook.abc = (function () {
70
78
  });
71
79
 
72
80
  editorEl.addEventListener("change", () => {
73
- store.abcMusic.put({
81
+ hyperbook.store.abcMusic.put({
74
82
  id: editorEl.id,
75
83
  tune: editorEl.value,
76
84
  });
@@ -1,4 +1,12 @@
1
- hyperbook.download = (function () {
1
+ /// <reference path="../hyperbook.types.js" />
2
+
3
+ /**
4
+ * Archive download management.
5
+ * @type {HyperbookArchive}
6
+ * @memberof hyperbook
7
+ * @see hyperbook.i18n
8
+ */
9
+ hyperbook.archive = (function () {
2
10
  function initElement(el) {
3
11
  const labelEl = el.getElementsByClassName("label")[0];
4
12
  const src = el.href;
@@ -7,10 +15,10 @@ hyperbook.download = (function () {
7
15
  const isOnline = r.ok;
8
16
  if (isOnline) {
9
17
  labelEl.classList.remove("offline");
10
- labelEl.innerHTML = labelEl.innerHTML.replace(`(${i18n.get("archive-offline")})`, "");
18
+ labelEl.innerHTML = labelEl.innerHTML.replace(`(${hyperbook.i18n.get("archive-offline")})`, "");
11
19
  } else {
12
20
  labelEl.classList.add("offline");
13
- labelEl.innerHTML += ` (${i18n.get("archive-offline")})`;
21
+ labelEl.innerHTML += ` (${hyperbook.i18n.get("archive-offline")})`;
14
22
  }
15
23
  });
16
24
  }
@@ -36,5 +44,10 @@ hyperbook.download = (function () {
36
44
 
37
45
  observer.observe(document.body, { childList: true, subtree: true });
38
46
 
39
- init();
47
+ // Initialize existing elements on document load
48
+ document.addEventListener("DOMContentLoaded", () => {
49
+ init();
50
+ });
51
+
52
+ return { init };
40
53
  })();