docxodus 12.2.0 → 12.3.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 (38) hide show
  1. package/README.md +9 -4
  2. package/dist/editor.bundle.js +2 -1
  3. package/dist/embed.bundle.js +592 -3
  4. package/dist/embed.d.ts +1 -1
  5. package/dist/embed.iife.js +592 -3
  6. package/dist/embed.js +1 -1
  7. package/dist/export-assets.json +9 -9
  8. package/dist/history-checkpoints.d.ts +48 -0
  9. package/dist/history-checkpoints.d.ts.map +1 -0
  10. package/dist/history-checkpoints.js +100 -0
  11. package/dist/history-checkpoints.js.map +1 -0
  12. package/dist/history-controls.d.ts +30 -0
  13. package/dist/history-controls.d.ts.map +1 -0
  14. package/dist/history-controls.js +333 -0
  15. package/dist/history-controls.js.map +1 -0
  16. package/dist/history-indexeddb.d.ts +12 -0
  17. package/dist/history-indexeddb.d.ts.map +1 -0
  18. package/dist/history-indexeddb.js +154 -0
  19. package/dist/history-indexeddb.js.map +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/ribbon.js +3 -1
  25. package/dist/ribbon.js.map +1 -1
  26. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  27. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  28. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  29. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  30. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  31. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  32. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  33. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  34. package/dist/wasm/_framework/dotnet.boot.js +6 -6
  35. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  36. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  37. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  38. package/package.json +3 -1
@@ -148,6 +148,7 @@ var Docxodus = (() => {
148
148
  DocxHistoryReader: () => DocxHistoryReader,
149
149
  DocxSession: () => DocxSession,
150
150
  EmptyParagraphMode: () => EmptyParagraphMode,
151
+ HistoryCheckpoints: () => HistoryCheckpoints,
151
152
  MAX_HISTORY_ARCHIVE_BYTES: () => MAX_HISTORY_ARCHIVE_BYTES,
152
153
  PaginationEngine: () => PaginationEngine,
153
154
  PaginationMode: () => PaginationMode,
@@ -207,6 +208,7 @@ var Docxodus = (() => {
207
208
  getVersion: () => getVersion,
208
209
  getWasmExports: () => getWasmExports,
209
210
  hasAnnotations: () => hasAnnotations,
211
+ historyControlError: () => historyControlError,
210
212
  initialize: () => initialize,
211
213
  installHistoryStorageImports: () => installHistoryStorageImports,
212
214
  isDeletion: () => isDeletion,
@@ -214,11 +216,13 @@ var Docxodus = (() => {
214
216
  isInitialized: () => isInitialized,
215
217
  isInsertion: () => isInsertion,
216
218
  isMove: () => isMove,
219
+ mountHistoryControls: () => mountHistoryControls,
217
220
  mountRibbon: () => mountRibbon,
218
221
  navigateToPageCitation: () => navigateToPageCitation,
219
222
  openDocxHistory: () => openDocxHistory,
220
223
  openDocxHistoryArchive: () => openDocxHistoryArchive,
221
224
  openDocxSession: () => openDocxSession2,
225
+ openIndexedDbHistoryStore: () => openIndexedDbHistoryStore,
222
226
  paginateHtml: () => paginateHtml,
223
227
  parseSectionDimensions: () => parseSectionDimensions,
224
228
  projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
@@ -2412,7 +2416,7 @@ var Docxodus = (() => {
2412
2416
  return `${reference.digest.value}:${reference.length}`;
2413
2417
  };
2414
2418
  const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
2415
- const equalHead = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2419
+ const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2416
2420
  return {
2417
2421
  async readBlob(reference) {
2418
2422
  return blobs.get(key(reference))?.slice() ?? null;
@@ -2434,7 +2438,7 @@ var Docxodus = (() => {
2434
2438
  async advanceHead(documentId, expected, state) {
2435
2439
  key(state);
2436
2440
  const current = heads.get(documentId) ?? null;
2437
- if (!equalHead(current, expected)) return null;
2441
+ if (!equalHead2(current, expected)) return null;
2438
2442
  const revision = BigInt(current?.revision ?? "0") + 1n;
2439
2443
  if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2440
2444
  const head = { revision: String(revision), state: copy(state) };
@@ -2454,6 +2458,590 @@ var Docxodus = (() => {
2454
2458
  };
2455
2459
  }
2456
2460
 
2461
+ // src/history-checkpoints.ts
2462
+ var HistoryCheckpoints = class _HistoryCheckpoints {
2463
+ constructor(document2, journal) {
2464
+ this.document = document2;
2465
+ this.journal = journal;
2466
+ this.current = null;
2467
+ this.request = null;
2468
+ this.running = false;
2469
+ this.stale = false;
2470
+ }
2471
+ /** Pass an already captured view when opening its exact version in an editor (for example, after import). */
2472
+ static async open(document2, journal, view) {
2473
+ const controls = new _HistoryCheckpoints(document2, journal);
2474
+ controls.request = structuredClone(await journal.read());
2475
+ if (controls.request && controls.request.documentId !== document2.documentId)
2476
+ throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
2477
+ controls.current = view === void 0 ? await document2.read() : structuredClone(view);
2478
+ if (controls.current && controls.current.state.documentId !== document2.documentId)
2479
+ throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
2480
+ return controls;
2481
+ }
2482
+ get view() {
2483
+ return structuredClone(this.current);
2484
+ }
2485
+ get hasPending() {
2486
+ return this.request !== null;
2487
+ }
2488
+ get needsRefresh() {
2489
+ return this.stale;
2490
+ }
2491
+ async refresh() {
2492
+ return this.exclusive(async () => {
2493
+ this.current = await this.document.read();
2494
+ this.stale = false;
2495
+ return this.view;
2496
+ });
2497
+ }
2498
+ async save(bytes, metadata) {
2499
+ return this.start({
2500
+ kind: "save",
2501
+ head: this.current?.head ?? null,
2502
+ bytes,
2503
+ id: crypto.randomUUID(),
2504
+ documentId: this.document.documentId,
2505
+ metadata
2506
+ });
2507
+ }
2508
+ async restore(target, metadata) {
2509
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
2510
+ return this.start({
2511
+ kind: "restore",
2512
+ head: this.current.head,
2513
+ target,
2514
+ id: crypto.randomUUID(),
2515
+ documentId: this.document.documentId,
2516
+ metadata
2517
+ });
2518
+ }
2519
+ async retry() {
2520
+ return this.exclusive(async () => {
2521
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
2522
+ return this.publish();
2523
+ });
2524
+ }
2525
+ async start(request) {
2526
+ return this.exclusive(async () => {
2527
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
2528
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
2529
+ this.request = structuredClone(request);
2530
+ return this.publish();
2531
+ });
2532
+ }
2533
+ async publish() {
2534
+ const intended = this.request;
2535
+ const request = await this.journal.put(structuredClone(intended));
2536
+ this.request = structuredClone(request);
2537
+ if (request.id !== intended.id)
2538
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
2539
+ let view;
2540
+ try {
2541
+ view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
2542
+ } catch (error) {
2543
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
2544
+ this.stale = true;
2545
+ await this.journal.remove(request.id);
2546
+ this.request = null;
2547
+ }
2548
+ throw error;
2549
+ }
2550
+ this.current = view;
2551
+ await this.journal.remove(request.id);
2552
+ this.request = null;
2553
+ return this.view;
2554
+ }
2555
+ async exclusive(action) {
2556
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
2557
+ this.running = true;
2558
+ try {
2559
+ return await action();
2560
+ } finally {
2561
+ this.running = false;
2562
+ }
2563
+ }
2564
+ };
2565
+
2566
+ // src/history-indexeddb.ts
2567
+ async function openIndexedDbHistoryStore(name) {
2568
+ const db = await new Promise((resolve, reject) => {
2569
+ let blocked = false;
2570
+ const request = indexedDB.open(name, 1);
2571
+ request.onupgradeneeded = () => {
2572
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
2573
+ };
2574
+ request.onsuccess = () => {
2575
+ if (blocked) request.result.close();
2576
+ else resolve(request.result);
2577
+ };
2578
+ request.onerror = () => reject(request.error);
2579
+ request.onblocked = () => {
2580
+ blocked = true;
2581
+ reject(new Error("Close other tabs using this history store, then try again."));
2582
+ };
2583
+ });
2584
+ db.onversionchange = () => db.close();
2585
+ function transaction(name2, mode, action) {
2586
+ return new Promise((resolve, reject) => {
2587
+ const tx = db.transaction(name2, mode);
2588
+ let value;
2589
+ tx.oncomplete = () => resolve(value);
2590
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
2591
+ try {
2592
+ action(tx.objectStore(name2), (result) => {
2593
+ value = result;
2594
+ });
2595
+ } catch (error) {
2596
+ tx.abort();
2597
+ reject(error);
2598
+ }
2599
+ });
2600
+ }
2601
+ const storage = {
2602
+ readBlob(reference) {
2603
+ const key = referenceKey(reference);
2604
+ return transaction("blobs", "readonly", (store, result) => {
2605
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
2606
+ });
2607
+ },
2608
+ async putBlob(reference, bytes) {
2609
+ const key = referenceKey(reference);
2610
+ const captured = bytes.slice();
2611
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
2612
+ const hash = Array.from(
2613
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2614
+ (byte) => byte.toString(16).padStart(2, "0")
2615
+ ).join("");
2616
+ if (hash !== key.split(":")[0])
2617
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
2618
+ await transaction("blobs", "readwrite", (store, result) => {
2619
+ store.put(captured, key);
2620
+ result();
2621
+ });
2622
+ },
2623
+ readHead(documentId) {
2624
+ return transaction("heads", "readonly", (store, result) => {
2625
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2626
+ });
2627
+ },
2628
+ advanceHead(documentId, expected, state) {
2629
+ referenceKey(state);
2630
+ const captured = structuredClone({ expected, state });
2631
+ if (expected) validateHead(expected);
2632
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
2633
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2634
+ return transaction("heads", "readwrite", (store, result) => {
2635
+ store.get(documentId).onsuccess = (event) => {
2636
+ const current = event.target.result ?? null;
2637
+ if (!equalHead(current, captured.expected)) {
2638
+ result(null);
2639
+ return;
2640
+ }
2641
+ const head = { revision: String(revision), state: captured.state };
2642
+ store.put(head, documentId);
2643
+ result(head);
2644
+ };
2645
+ });
2646
+ },
2647
+ initializeHead(documentId, head) {
2648
+ validateHead(head);
2649
+ const captured = structuredClone(head);
2650
+ return transaction("heads", "readwrite", (store, result) => {
2651
+ store.get(documentId).onsuccess = (event) => {
2652
+ const existing = event.target.result;
2653
+ if (existing) {
2654
+ result({ initialized: false, head: existing });
2655
+ return;
2656
+ }
2657
+ store.put(captured, documentId);
2658
+ result({ initialized: true, head: captured });
2659
+ };
2660
+ });
2661
+ }
2662
+ };
2663
+ return {
2664
+ storage,
2665
+ journal(documentId) {
2666
+ return {
2667
+ read: () => transaction("requests", "readonly", (store, result) => {
2668
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2669
+ }),
2670
+ put(request) {
2671
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
2672
+ const captured = structuredClone(request);
2673
+ return transaction("requests", "readwrite", (store, result) => {
2674
+ store.get(documentId).onsuccess = (event) => {
2675
+ const existing = event.target.result;
2676
+ if (existing) {
2677
+ result(existing);
2678
+ return;
2679
+ }
2680
+ store.put(captured, documentId);
2681
+ result(captured);
2682
+ };
2683
+ });
2684
+ },
2685
+ remove(requestId) {
2686
+ return transaction("requests", "readwrite", (store, result) => {
2687
+ store.get(documentId).onsuccess = (event) => {
2688
+ if (event.target.result?.id === requestId) store.delete(documentId);
2689
+ result();
2690
+ };
2691
+ });
2692
+ }
2693
+ };
2694
+ },
2695
+ close: () => db.close()
2696
+ };
2697
+ }
2698
+ function referenceKey(reference) {
2699
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
2700
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
2701
+ return `${reference.digest.value}:${reference.length}`;
2702
+ }
2703
+ function validateHead(head) {
2704
+ referenceKey(head.state);
2705
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2706
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
2707
+ }
2708
+ function equalHead(a, b) {
2709
+ return a === null || b === null ? a === b : a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
2710
+ }
2711
+
2712
+ // src/history-controls.ts
2713
+ function mountHistoryControls(container, options) {
2714
+ const pageSize = options.pageSize ?? 25;
2715
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
2716
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
2717
+ throw new Error("History controls and checkpoints must use the same document.");
2718
+ return new HistoryPanel(container, options, pageSize);
2719
+ }
2720
+ var HistoryPanel = class {
2721
+ constructor(container, options, pageSize) {
2722
+ this.options = options;
2723
+ this.pageSize = pageSize;
2724
+ this.actions = /* @__PURE__ */ new Map();
2725
+ this.events = new AbortController();
2726
+ this.records = [];
2727
+ this.next = null;
2728
+ this.view = null;
2729
+ this.active = null;
2730
+ this.destroyed = false;
2731
+ const doc = container.ownerDocument;
2732
+ this.element = doc.createElement("section");
2733
+ this.element.className = "dx-history";
2734
+ this.element.setAttribute("aria-label", "Document history");
2735
+ const style = doc.createElement("style");
2736
+ style.textContent = HISTORY_CSS;
2737
+ const title = doc.createElement("h2");
2738
+ title.textContent = "Document history";
2739
+ this.status = doc.createElement("p");
2740
+ this.status.setAttribute("role", "status");
2741
+ this.status.setAttribute("aria-live", "polite");
2742
+ this.fieldset = doc.createElement("fieldset");
2743
+ const legend = doc.createElement("legend");
2744
+ legend.textContent = "Versions and checkpoints";
2745
+ this.fieldset.append(legend);
2746
+ this.element.append(style, title, this.status, this.fieldset);
2747
+ container.append(this.element);
2748
+ const note = doc.createElement("p");
2749
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2750
+ this.fieldset.append(note);
2751
+ this.button("refresh", "Refresh history", () => this.load(true));
2752
+ this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2753
+ this.versions = this.select("Version");
2754
+ this.versions.size = 6;
2755
+ this.detail = doc.createElement("p");
2756
+ this.fieldset.append(this.detail);
2757
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2758
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2759
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2760
+ this.before = this.select("Compare from");
2761
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2762
+ const compareNote = doc.createElement("p");
2763
+ compareNote.textContent = "Compare from this version to the selected version above.";
2764
+ this.fieldset.append(compareNote);
2765
+ this.button("compare", "Compare versions", async () => {
2766
+ const before = this.records[Number(this.before.value)];
2767
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2768
+ });
2769
+ this.button("more", "Load older versions", async () => {
2770
+ if (this.next) await this.page(this.next, false);
2771
+ });
2772
+ this.author = this.input("Your name", "text", options.author ?? "You");
2773
+ this.label = this.input("Checkpoint name (optional)", "text");
2774
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2775
+ this.button("save", "Save checkpoint", async () => {
2776
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
2777
+ await options.onCheckpoint?.(view, "save");
2778
+ await this.load(false);
2779
+ this.label.value = "";
2780
+ this.status.textContent = "Checkpoint saved. Your draft remains open.";
2781
+ });
2782
+ this.button("restore", "Restore selected", async () => {
2783
+ const version = this.selected();
2784
+ if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
2785
+
2786
+ Your current draft and all later versions will be kept.`)) {
2787
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2788
+ return;
2789
+ }
2790
+ const view = await options.checkpoints.restore(version.id, this.metadata());
2791
+ await options.onCheckpoint?.(view, "restore");
2792
+ await this.load(false);
2793
+ this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
2794
+ });
2795
+ const restoreNote = doc.createElement("p");
2796
+ restoreNote.hidden = !options.checkpoints;
2797
+ restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
2798
+ this.fieldset.append(restoreNote);
2799
+ this.button("retry", "Retry checkpoint", async () => {
2800
+ const view = await options.checkpoints.retry();
2801
+ await options.onCheckpoint?.(view, "retry");
2802
+ await this.load(false);
2803
+ this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
2804
+ });
2805
+ const sharing = this.disclosure("Download with history");
2806
+ const sharingNote = doc.createElement("p");
2807
+ sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
2808
+ sharing.append(sharingNote);
2809
+ this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2810
+ const time = this.disclosure("Find a version by time");
2811
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2812
+ this.button("time", "Preview at time", async () => {
2813
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
2814
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
2815
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
2816
+ }, time);
2817
+ const activity = this.disclosure("Recorded collaboration");
2818
+ const decisions = doc.createElement("ol");
2819
+ let showOlderActivity = () => {
2820
+ };
2821
+ this.button("activity", "Load activity", async () => {
2822
+ const { operations } = await options.reader.readOperationsSince(null);
2823
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
2824
+ let shown = 0;
2825
+ showOlderActivity = () => {
2826
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
2827
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
2828
+ shown += page.length;
2829
+ this.actions.get("activity-more").hidden = shown >= operations.length;
2830
+ };
2831
+ decisions.replaceChildren();
2832
+ showOlderActivity();
2833
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
2834
+ }, activity);
2835
+ activity.append(decisions);
2836
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
2837
+ this.actions.get("activity-more").hidden = true;
2838
+ this.ready = this.run("Loading history", () => this.load(false));
2839
+ }
2840
+ refresh() {
2841
+ return this.run("Loading history", () => this.load(true));
2842
+ }
2843
+ async destroy() {
2844
+ this.destroyed = true;
2845
+ this.events.abort();
2846
+ this.element.remove();
2847
+ await this.active?.catch(() => {
2848
+ });
2849
+ }
2850
+ async load(refresh) {
2851
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
2852
+ if (this.view) await this.page(this.view.version.id, true);
2853
+ else {
2854
+ this.records = [];
2855
+ this.next = null;
2856
+ this.versions.replaceChildren();
2857
+ this.before.replaceChildren();
2858
+ }
2859
+ this.status.textContent = this.options.checkpoints?.hasPending ? "A checkpoint needs recovery. Retry it before saving more changes." : this.view ? "History loaded. Select a version to preview, download or restore." : "No checkpoints yet. Save your first checkpoint when you are ready.";
2860
+ }
2861
+ async page(cursor, reset) {
2862
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
2863
+ if (reset) {
2864
+ this.records = [];
2865
+ this.versions.replaceChildren();
2866
+ this.before.replaceChildren();
2867
+ }
2868
+ const start2 = this.records.length;
2869
+ this.records.push(...page.versions);
2870
+ this.next = page.next;
2871
+ for (let index = start2; index < this.records.length; index++) {
2872
+ for (const select of [this.versions, this.before]) {
2873
+ const option = select.ownerDocument.createElement("option");
2874
+ option.value = String(index);
2875
+ option.textContent = option.title = versionTitle(this.records[index]);
2876
+ select.append(option);
2877
+ }
2878
+ }
2879
+ if (reset) {
2880
+ this.versions.value = "0";
2881
+ this.before.value = this.records.length > 1 ? "1" : "0";
2882
+ }
2883
+ }
2884
+ update() {
2885
+ const hasVersion = this.records.length > 0;
2886
+ const pending = this.options.checkpoints?.hasPending ?? false;
2887
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
2888
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
2889
+ this.actions.get("more").hidden = !this.next;
2890
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
2891
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
2892
+ this.actions.get("save").disabled = pending || stale;
2893
+ this.actions.get("restore").hidden = !this.options.checkpoints;
2894
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
2895
+ this.actions.get("retry").hidden = !pending;
2896
+ this.detail.textContent = hasVersion ? [
2897
+ versionTitle(this.selected()),
2898
+ this.selected().record.metadata.message,
2899
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
2900
+ ].filter(Boolean).join(" \u2014 ") : "";
2901
+ }
2902
+ selected() {
2903
+ return this.records[Number(this.versions.value)];
2904
+ }
2905
+ activityItem(op, resolved) {
2906
+ const doc = this.element.ownerDocument;
2907
+ const item = doc.createElement("li");
2908
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
2909
+ const label = doc.createElement("p");
2910
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
2911
+ item.append(label);
2912
+ const description = doc.createElement("p");
2913
+ this.commandButton("View decision", async () => {
2914
+ const decision = await this.options.reader.getOperation(op.id);
2915
+ const { kind, metadata } = decision.input.request;
2916
+ description.textContent = [
2917
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
2918
+ metadata.label,
2919
+ metadata.message,
2920
+ decision.record.conflict
2921
+ ].filter(Boolean).join(" \u2014 ");
2922
+ }, item);
2923
+ item.append(description);
2924
+ this.commandButton("Download proposal", async () => {
2925
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
2926
+ }, item);
2927
+ return item;
2928
+ }
2929
+ metadata() {
2930
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
2931
+ }
2932
+ async preview(bytes, title) {
2933
+ if (!this.destroyed) await this.options.preview(bytes, title);
2934
+ }
2935
+ async download(bytes, suffix) {
2936
+ if (this.destroyed) return;
2937
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
2938
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
2939
+ if (this.options.download) {
2940
+ await this.options.download(bytes, name);
2941
+ return;
2942
+ }
2943
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
2944
+ const link = this.element.ownerDocument.createElement("a");
2945
+ link.href = url;
2946
+ link.download = name;
2947
+ link.click();
2948
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2949
+ }
2950
+ async run(label, action) {
2951
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
2952
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
2953
+ this.fieldset.disabled = true;
2954
+ this.element.setAttribute("aria-busy", "true");
2955
+ this.status.textContent = `${label}\u2026`;
2956
+ const work = Promise.resolve().then(action);
2957
+ this.active = work;
2958
+ try {
2959
+ await work;
2960
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
2961
+ } catch (error) {
2962
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
2963
+ throw error;
2964
+ } finally {
2965
+ this.active = null;
2966
+ if (!this.destroyed) {
2967
+ this.fieldset.disabled = false;
2968
+ this.element.setAttribute("aria-busy", "false");
2969
+ this.update();
2970
+ }
2971
+ }
2972
+ }
2973
+ button(key, title, action, parent = this.fieldset) {
2974
+ this.actions.set(key, this.commandButton(title, action, parent));
2975
+ }
2976
+ commandButton(title, action, parent) {
2977
+ const button = parent.ownerDocument.createElement("button");
2978
+ button.type = "button";
2979
+ button.textContent = title;
2980
+ button.addEventListener("click", () => {
2981
+ void this.run(title, action).catch(() => {
2982
+ });
2983
+ }, { signal: this.events.signal });
2984
+ parent.append(button);
2985
+ return button;
2986
+ }
2987
+ select(title) {
2988
+ const label = this.fieldset.ownerDocument.createElement("label");
2989
+ label.textContent = title;
2990
+ const select = label.ownerDocument.createElement("select");
2991
+ select.setAttribute("aria-label", title);
2992
+ label.append(select);
2993
+ this.fieldset.append(label);
2994
+ return select;
2995
+ }
2996
+ input(title, type, value = "", parent = this.fieldset) {
2997
+ const label = parent.ownerDocument.createElement("label");
2998
+ label.textContent = title;
2999
+ const input = label.ownerDocument.createElement("input");
3000
+ input.type = type;
3001
+ input.value = value;
3002
+ label.append(input);
3003
+ parent.append(label);
3004
+ return input;
3005
+ }
3006
+ disclosure(title) {
3007
+ const details = this.fieldset.ownerDocument.createElement("details");
3008
+ const summary = details.ownerDocument.createElement("summary");
3009
+ summary.textContent = title;
3010
+ details.append(summary);
3011
+ this.fieldset.append(details);
3012
+ return details;
3013
+ }
3014
+ };
3015
+ function historyControlError(error, pending = false) {
3016
+ const code = error instanceof DocxHistoryError ? error.code : "";
3017
+ if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
3018
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
3019
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
3020
+ if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
3021
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
3022
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
3023
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
3024
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
3025
+ }
3026
+ function versionTitle(version) {
3027
+ const { metadata, sequence } = version.record;
3028
+ return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
3029
+ }
3030
+ function formatTime(value) {
3031
+ const time = new Date(value);
3032
+ return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
3033
+ }
3034
+ var HISTORY_CSS = `
3035
+ .dx-history{font:14px/1.5 system-ui,sans-serif;color:#203047;background:#fff;border:1px solid #cbd5e1;border-radius:12px;padding:18px;min-width:0}
3036
+ .dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
3037
+ .dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
3038
+ .dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
3039
+ .dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
3040
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
3041
+ .dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
3042
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
3043
+ `;
3044
+
2457
3045
  // src/page-number-format.ts
2458
3046
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2459
3047
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -15783,7 +16371,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15783
16371
  comments: this.options.comments,
15784
16372
  commentAuthor: this.author
15785
16373
  });
15786
- this.require("save").disabled = false;
16374
+ const saveButton = this.control("save");
16375
+ if (saveButton) saveButton.disabled = false;
15787
16376
  this.require("ribbon").setAttribute("aria-disabled", "false");
15788
16377
  this.setState("ready");
15789
16378
  this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
package/dist/embed.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * ```html
8
8
  * <div id="doc"></div>
9
9
  * <script type="module">
10
- * import { createViewer } from "https://cdn.jsdelivr.net/npm/docxodus@12.1.0/dist/embed.bundle.js";
10
+ * import { createViewer } from "https://cdn.jsdelivr.net/npm/docxodus@12.2.0/dist/embed.bundle.js";
11
11
  * await createViewer("#doc", "./contract.docx");
12
12
  * </script>
13
13
  * ```