docxodus 12.2.0 → 12.4.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 (58) hide show
  1. package/README.md +11 -4
  2. package/dist/editor.bundle.js +1208 -28
  3. package/dist/editor.d.ts +10 -1
  4. package/dist/editor.d.ts.map +1 -1
  5. package/dist/editor.js +48 -16
  6. package/dist/editor.js.map +1 -1
  7. package/dist/embed.bundle.js +1212 -31
  8. package/dist/embed.d.ts +5 -2
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +1212 -31
  11. package/dist/embed.js +10 -3
  12. package/dist/embed.js.map +1 -1
  13. package/dist/export-assets.json +9 -9
  14. package/dist/history-checkpoints.d.ts +50 -0
  15. package/dist/history-checkpoints.d.ts.map +1 -0
  16. package/dist/history-checkpoints.js +102 -0
  17. package/dist/history-checkpoints.js.map +1 -0
  18. package/dist/history-controls.d.ts +33 -0
  19. package/dist/history-controls.d.ts.map +1 -0
  20. package/dist/history-controls.js +375 -0
  21. package/dist/history-controls.js.map +1 -0
  22. package/dist/history-indexeddb.d.ts +12 -0
  23. package/dist/history-indexeddb.d.ts.map +1 -0
  24. package/dist/history-indexeddb.js +154 -0
  25. package/dist/history-indexeddb.js.map +1 -0
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/ribbon-chrome.d.ts +1 -1
  31. package/dist/ribbon-chrome.d.ts.map +1 -1
  32. package/dist/ribbon-chrome.js +6 -3
  33. package/dist/ribbon-chrome.js.map +1 -1
  34. package/dist/ribbon-history.d.ts +85 -0
  35. package/dist/ribbon-history.d.ts.map +1 -0
  36. package/dist/ribbon-history.js +508 -0
  37. package/dist/ribbon-history.js.map +1 -0
  38. package/dist/ribbon.d.ts +6 -0
  39. package/dist/ribbon.d.ts.map +1 -1
  40. package/dist/ribbon.js +50 -19
  41. package/dist/ribbon.js.map +1 -1
  42. package/dist/viewport.d.ts +3 -1
  43. package/dist/viewport.d.ts.map +1 -1
  44. package/dist/viewport.js +14 -0
  45. package/dist/viewport.js.map +1 -1
  46. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  47. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  48. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  49. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  50. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  51. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  52. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  53. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  54. package/dist/wasm/_framework/dotnet.boot.js +6 -6
  55. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  56. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  57. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  58. package/package.json +8 -7
@@ -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,627 @@ 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
+ /** Snapshot of the exact request a retry will recover, including its original operation kind. */
2489
+ get pendingRequest() {
2490
+ return structuredClone(this.request);
2491
+ }
2492
+ get needsRefresh() {
2493
+ return this.stale;
2494
+ }
2495
+ async refresh() {
2496
+ return this.exclusive(async () => {
2497
+ this.current = await this.document.read();
2498
+ this.stale = false;
2499
+ return this.view;
2500
+ });
2501
+ }
2502
+ async save(bytes, metadata) {
2503
+ return this.start({
2504
+ kind: "save",
2505
+ head: this.current?.head ?? null,
2506
+ bytes,
2507
+ id: crypto.randomUUID(),
2508
+ documentId: this.document.documentId,
2509
+ metadata
2510
+ });
2511
+ }
2512
+ async restore(target, metadata) {
2513
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
2514
+ return this.start({
2515
+ kind: "restore",
2516
+ head: this.current.head,
2517
+ target,
2518
+ id: crypto.randomUUID(),
2519
+ documentId: this.document.documentId,
2520
+ metadata
2521
+ });
2522
+ }
2523
+ async retry() {
2524
+ return this.exclusive(async () => {
2525
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
2526
+ return this.publish();
2527
+ });
2528
+ }
2529
+ async start(request) {
2530
+ return this.exclusive(async () => {
2531
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
2532
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
2533
+ this.request = structuredClone(request);
2534
+ return this.publish();
2535
+ });
2536
+ }
2537
+ async publish() {
2538
+ const intended = this.request;
2539
+ const request = await this.journal.put(structuredClone(intended));
2540
+ this.request = structuredClone(request);
2541
+ if (request.id !== intended.id)
2542
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
2543
+ let view;
2544
+ try {
2545
+ 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);
2546
+ } catch (error) {
2547
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
2548
+ this.stale = true;
2549
+ await this.journal.remove(request.id);
2550
+ this.request = null;
2551
+ }
2552
+ throw error;
2553
+ }
2554
+ this.current = view;
2555
+ await this.journal.remove(request.id);
2556
+ this.request = null;
2557
+ return this.view;
2558
+ }
2559
+ async exclusive(action) {
2560
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
2561
+ this.running = true;
2562
+ try {
2563
+ return await action();
2564
+ } finally {
2565
+ this.running = false;
2566
+ }
2567
+ }
2568
+ };
2569
+
2570
+ // src/history-indexeddb.ts
2571
+ async function openIndexedDbHistoryStore(name) {
2572
+ const db = await new Promise((resolve, reject) => {
2573
+ let blocked = false;
2574
+ const request = indexedDB.open(name, 1);
2575
+ request.onupgradeneeded = () => {
2576
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
2577
+ };
2578
+ request.onsuccess = () => {
2579
+ if (blocked) request.result.close();
2580
+ else resolve(request.result);
2581
+ };
2582
+ request.onerror = () => reject(request.error);
2583
+ request.onblocked = () => {
2584
+ blocked = true;
2585
+ reject(new Error("Close other tabs using this history store, then try again."));
2586
+ };
2587
+ });
2588
+ db.onversionchange = () => db.close();
2589
+ function transaction(name2, mode, action) {
2590
+ return new Promise((resolve, reject) => {
2591
+ const tx = db.transaction(name2, mode);
2592
+ let value;
2593
+ tx.oncomplete = () => resolve(value);
2594
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
2595
+ try {
2596
+ action(tx.objectStore(name2), (result) => {
2597
+ value = result;
2598
+ });
2599
+ } catch (error) {
2600
+ tx.abort();
2601
+ reject(error);
2602
+ }
2603
+ });
2604
+ }
2605
+ const storage = {
2606
+ readBlob(reference) {
2607
+ const key = referenceKey(reference);
2608
+ return transaction("blobs", "readonly", (store, result) => {
2609
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
2610
+ });
2611
+ },
2612
+ async putBlob(reference, bytes) {
2613
+ const key = referenceKey(reference);
2614
+ const captured = bytes.slice();
2615
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
2616
+ const hash = Array.from(
2617
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2618
+ (byte) => byte.toString(16).padStart(2, "0")
2619
+ ).join("");
2620
+ if (hash !== key.split(":")[0])
2621
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
2622
+ await transaction("blobs", "readwrite", (store, result) => {
2623
+ store.put(captured, key);
2624
+ result();
2625
+ });
2626
+ },
2627
+ readHead(documentId) {
2628
+ return transaction("heads", "readonly", (store, result) => {
2629
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2630
+ });
2631
+ },
2632
+ advanceHead(documentId, expected, state) {
2633
+ referenceKey(state);
2634
+ const captured = structuredClone({ expected, state });
2635
+ if (expected) validateHead(expected);
2636
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
2637
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2638
+ return transaction("heads", "readwrite", (store, result) => {
2639
+ store.get(documentId).onsuccess = (event) => {
2640
+ const current = event.target.result ?? null;
2641
+ if (!equalHead(current, captured.expected)) {
2642
+ result(null);
2643
+ return;
2644
+ }
2645
+ const head = { revision: String(revision), state: captured.state };
2646
+ store.put(head, documentId);
2647
+ result(head);
2648
+ };
2649
+ });
2650
+ },
2651
+ initializeHead(documentId, head) {
2652
+ validateHead(head);
2653
+ const captured = structuredClone(head);
2654
+ return transaction("heads", "readwrite", (store, result) => {
2655
+ store.get(documentId).onsuccess = (event) => {
2656
+ const existing = event.target.result;
2657
+ if (existing) {
2658
+ result({ initialized: false, head: existing });
2659
+ return;
2660
+ }
2661
+ store.put(captured, documentId);
2662
+ result({ initialized: true, head: captured });
2663
+ };
2664
+ });
2665
+ }
2666
+ };
2667
+ return {
2668
+ storage,
2669
+ journal(documentId) {
2670
+ return {
2671
+ read: () => transaction("requests", "readonly", (store, result) => {
2672
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2673
+ }),
2674
+ put(request) {
2675
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
2676
+ const captured = structuredClone(request);
2677
+ return transaction("requests", "readwrite", (store, result) => {
2678
+ store.get(documentId).onsuccess = (event) => {
2679
+ const existing = event.target.result;
2680
+ if (existing) {
2681
+ result(existing);
2682
+ return;
2683
+ }
2684
+ store.put(captured, documentId);
2685
+ result(captured);
2686
+ };
2687
+ });
2688
+ },
2689
+ remove(requestId) {
2690
+ return transaction("requests", "readwrite", (store, result) => {
2691
+ store.get(documentId).onsuccess = (event) => {
2692
+ if (event.target.result?.id === requestId) store.delete(documentId);
2693
+ result();
2694
+ };
2695
+ });
2696
+ }
2697
+ };
2698
+ },
2699
+ close: () => db.close()
2700
+ };
2701
+ }
2702
+ function referenceKey(reference) {
2703
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
2704
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
2705
+ return `${reference.digest.value}:${reference.length}`;
2706
+ }
2707
+ function validateHead(head) {
2708
+ referenceKey(head.state);
2709
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2710
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
2711
+ }
2712
+ function equalHead(a, b) {
2713
+ 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;
2714
+ }
2715
+
2716
+ // src/history-controls.ts
2717
+ function mountHistoryControls(container, options) {
2718
+ const pageSize = options.pageSize ?? 25;
2719
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
2720
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
2721
+ throw new Error("History controls and checkpoints must use the same document.");
2722
+ return new HistoryPanel(container, options, pageSize);
2723
+ }
2724
+ var HistoryPanel = class {
2725
+ constructor(container, options, pageSize) {
2726
+ this.options = options;
2727
+ this.pageSize = pageSize;
2728
+ this.actions = /* @__PURE__ */ new Map();
2729
+ this.events = new AbortController();
2730
+ this.records = [];
2731
+ this.next = null;
2732
+ this.view = null;
2733
+ this.active = null;
2734
+ this.destroyed = false;
2735
+ const doc = container.ownerDocument;
2736
+ this.element = doc.createElement("section");
2737
+ this.element.className = "dx-history";
2738
+ this.element.setAttribute("aria-label", "Version history");
2739
+ const style = doc.createElement("style");
2740
+ style.textContent = HISTORY_CSS;
2741
+ const title = doc.createElement("h2");
2742
+ title.textContent = "Version history";
2743
+ this.status = doc.createElement("p");
2744
+ this.status.setAttribute("role", "status");
2745
+ this.status.setAttribute("aria-live", "polite");
2746
+ this.fieldset = doc.createElement("fieldset");
2747
+ const legend = doc.createElement("legend");
2748
+ legend.textContent = "Saved versions";
2749
+ this.fieldset.append(legend);
2750
+ this.element.append(style, title, this.status, this.fieldset);
2751
+ container.append(this.element);
2752
+ const note = doc.createElement("p");
2753
+ note.dataset.historyExisting = "";
2754
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2755
+ this.fieldset.append(note);
2756
+ this.button("refresh", "Refresh history", () => this.load(true));
2757
+ this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2758
+ this.versions = this.select("Version");
2759
+ this.versions.size = 6;
2760
+ this.detail = doc.createElement("p");
2761
+ this.fieldset.append(this.detail);
2762
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2763
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2764
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2765
+ const comparison = this.disclosure("Compare versions");
2766
+ this.before = this.select("Compare from", comparison);
2767
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2768
+ const compareNote = doc.createElement("p");
2769
+ compareNote.textContent = "Compare from this version to the selected version above.";
2770
+ comparison.append(compareNote);
2771
+ this.button("compare", "Compare versions", async () => {
2772
+ const before = this.records[Number(this.before.value)];
2773
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2774
+ }, comparison);
2775
+ this.button("more", "Load older versions", async () => {
2776
+ if (this.next) await this.page(this.next, false);
2777
+ });
2778
+ const saveGroup = doc.createElement("div");
2779
+ saveGroup.className = "dx-history-save";
2780
+ this.fieldset.insertBefore(saveGroup, note);
2781
+ const attribution = doc.createElement("details");
2782
+ const attributionTitle = doc.createElement("summary");
2783
+ attributionTitle.textContent = "Saved by";
2784
+ attribution.append(attributionTitle);
2785
+ saveGroup.append(attribution);
2786
+ this.author = this.input("Your name", "text", options.author ?? "You", attribution);
2787
+ this.label = this.input("Version name (optional)", "text", "", saveGroup);
2788
+ saveGroup.hidden = !options.checkpoints;
2789
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2790
+ this.button("save", "Save version", async () => {
2791
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
2792
+ await options.onCheckpoint?.(view, "save");
2793
+ await this.load(false);
2794
+ this.label.value = "";
2795
+ this.status.textContent = "Version saved. Your draft remains open.";
2796
+ }, saveGroup);
2797
+ saveGroup.append(attribution);
2798
+ this.button("restore", "Restore selected", async () => {
2799
+ const version = this.selected();
2800
+ const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
2801
+
2802
+ Your current draft and all later versions will be kept.`);
2803
+ if (!confirmed) {
2804
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2805
+ return;
2806
+ }
2807
+ const view = await options.checkpoints.restore(version.id, this.metadata());
2808
+ await options.onCheckpoint?.(view, "restore");
2809
+ await this.load(false);
2810
+ this.status.textContent = options.restoreUpdatesDraft ? "Version restored. All saved versions are kept." : "Restored as a new saved version. Your draft and later versions are kept.";
2811
+ });
2812
+ const restoreNote = doc.createElement("p");
2813
+ restoreNote.hidden = !options.checkpoints;
2814
+ if (options.checkpoints) restoreNote.dataset.historyExisting = "";
2815
+ restoreNote.textContent = options.restoreUpdatesDraft ? "Restore returns your document to this version and keeps every saved version." : "Restore creates a new saved version. Preview latest to view it; your draft stays open.";
2816
+ this.fieldset.append(restoreNote);
2817
+ this.button("retry", "Retry save", async () => {
2818
+ const request = options.checkpoints.pendingRequest;
2819
+ if (request?.kind === "restore" && options.confirmRestore) {
2820
+ const target = await options.reader.getVersion(request.target);
2821
+ if (!await options.confirmRestore(versionTitle(target))) {
2822
+ this.status.textContent = "Restore retry canceled. Your draft is unchanged; the saved action still needs recovery.";
2823
+ return;
2824
+ }
2825
+ }
2826
+ const view = await options.checkpoints.retry();
2827
+ await options.onCheckpoint?.(view, "retry", request ?? void 0);
2828
+ await this.load(false);
2829
+ this.status.textContent = request?.kind === "restore" && options.restoreUpdatesDraft ? "Version restored. All saved versions are kept. Refresh history to check for newer versions." : "Saved version recovered. Your draft remains open. Refresh history to check for newer versions.";
2830
+ });
2831
+ const sharing = this.disclosure("Download with history");
2832
+ const sharingNote = doc.createElement("p");
2833
+ 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.";
2834
+ sharing.append(sharingNote);
2835
+ this.button("archive", "Download with version history", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2836
+ const portability = doc.createElement("p");
2837
+ portability.textContent = "Saved versions stay on this device. Download a history file to keep a portable copy; clearing browser data removes local versions.";
2838
+ sharing.append(portability);
2839
+ const time = this.disclosure("Find a version by time");
2840
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2841
+ this.button("time", "Preview at time", async () => {
2842
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
2843
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
2844
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
2845
+ }, time);
2846
+ const activity = this.disclosure("Recorded collaboration");
2847
+ const decisions = doc.createElement("ol");
2848
+ let showOlderActivity = () => {
2849
+ };
2850
+ this.button("activity", "Load activity", async () => {
2851
+ const { operations } = await options.reader.readOperationsSince(null);
2852
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
2853
+ let shown = 0;
2854
+ showOlderActivity = () => {
2855
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
2856
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
2857
+ shown += page.length;
2858
+ this.actions.get("activity-more").hidden = shown >= operations.length;
2859
+ };
2860
+ decisions.replaceChildren();
2861
+ showOlderActivity();
2862
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
2863
+ }, activity);
2864
+ activity.append(decisions);
2865
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
2866
+ this.actions.get("activity-more").hidden = true;
2867
+ this.ready = this.run("Loading history", () => this.load(false));
2868
+ }
2869
+ refresh() {
2870
+ return this.run("Loading history", () => this.load(true));
2871
+ }
2872
+ async destroy() {
2873
+ this.destroyed = true;
2874
+ this.events.abort();
2875
+ this.element.remove();
2876
+ await this.active?.catch(() => {
2877
+ });
2878
+ }
2879
+ async load(refresh) {
2880
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
2881
+ if (this.view) await this.page(this.view.version.id, true);
2882
+ else {
2883
+ this.records = [];
2884
+ this.next = null;
2885
+ this.versions.replaceChildren();
2886
+ this.before.replaceChildren();
2887
+ }
2888
+ this.status.textContent = this.options.checkpoints?.hasPending ? "A save needs recovery. Retry it before saving more changes." : this.view ? "Select a version to preview, download or restore." : "No saved versions yet. Save your first version when you are ready.";
2889
+ }
2890
+ async page(cursor, reset) {
2891
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
2892
+ if (reset) {
2893
+ this.records = [];
2894
+ this.versions.replaceChildren();
2895
+ this.before.replaceChildren();
2896
+ }
2897
+ const start2 = this.records.length;
2898
+ this.records.push(...page.versions);
2899
+ this.next = page.next;
2900
+ for (let index = start2; index < this.records.length; index++) {
2901
+ for (const select of [this.versions, this.before]) {
2902
+ const option = select.ownerDocument.createElement("option");
2903
+ option.value = String(index);
2904
+ option.textContent = option.title = versionTitle(this.records[index]);
2905
+ select.append(option);
2906
+ }
2907
+ }
2908
+ if (reset) {
2909
+ this.versions.value = "0";
2910
+ this.before.value = this.records.length > 1 ? "1" : "0";
2911
+ }
2912
+ }
2913
+ update() {
2914
+ const hasVersion = this.records.length > 0;
2915
+ const pending = this.options.checkpoints?.hasPending ?? false;
2916
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
2917
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
2918
+ this.actions.get("more").hidden = !this.next;
2919
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
2920
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
2921
+ this.actions.get("save").disabled = pending || stale;
2922
+ this.actions.get("restore").hidden = !this.options.checkpoints;
2923
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
2924
+ this.actions.get("retry").hidden = !pending;
2925
+ this.versions.parentElement.hidden = this.detail.hidden = !hasVersion;
2926
+ this.versions.size = Math.max(2, Math.min(6, this.records.length));
2927
+ for (const key of ["latest", "preview", "download"]) this.actions.get(key).hidden = !hasVersion;
2928
+ this.actions.get("restore").hidden = !hasVersion || !this.options.checkpoints;
2929
+ for (const element of Array.from(this.element.querySelectorAll("[data-history-existing]"))) element.hidden = !hasVersion;
2930
+ this.detail.textContent = hasVersion ? [
2931
+ versionTitle(this.selected()),
2932
+ this.selected().record.metadata.message,
2933
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
2934
+ ].filter(Boolean).join(" \u2014 ") : "";
2935
+ }
2936
+ selected() {
2937
+ return this.records[Number(this.versions.value)];
2938
+ }
2939
+ activityItem(op, resolved) {
2940
+ const doc = this.element.ownerDocument;
2941
+ const item = doc.createElement("li");
2942
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
2943
+ const label = doc.createElement("p");
2944
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
2945
+ item.append(label);
2946
+ const description = doc.createElement("p");
2947
+ this.commandButton("View decision", async () => {
2948
+ const decision = await this.options.reader.getOperation(op.id);
2949
+ const { kind, metadata } = decision.input.request;
2950
+ description.textContent = [
2951
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
2952
+ metadata.label,
2953
+ metadata.message,
2954
+ decision.record.conflict
2955
+ ].filter(Boolean).join(" \u2014 ");
2956
+ }, item);
2957
+ item.append(description);
2958
+ this.commandButton("Download proposal", async () => {
2959
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
2960
+ }, item);
2961
+ return item;
2962
+ }
2963
+ metadata() {
2964
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
2965
+ }
2966
+ async preview(bytes, title) {
2967
+ if (!this.destroyed) await this.options.preview(bytes, title);
2968
+ }
2969
+ async download(bytes, suffix) {
2970
+ if (this.destroyed) return;
2971
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
2972
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
2973
+ if (this.options.download) {
2974
+ await this.options.download(bytes, name);
2975
+ return;
2976
+ }
2977
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
2978
+ const link = this.element.ownerDocument.createElement("a");
2979
+ link.href = url;
2980
+ link.download = name;
2981
+ link.click();
2982
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2983
+ }
2984
+ async run(label, action) {
2985
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
2986
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
2987
+ this.fieldset.disabled = true;
2988
+ this.element.setAttribute("aria-busy", "true");
2989
+ this.status.textContent = `${label}\u2026`;
2990
+ const work = Promise.resolve().then(action);
2991
+ this.active = work;
2992
+ try {
2993
+ await work;
2994
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
2995
+ } catch (error) {
2996
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
2997
+ throw error;
2998
+ } finally {
2999
+ this.active = null;
3000
+ if (!this.destroyed) {
3001
+ this.fieldset.disabled = false;
3002
+ this.element.setAttribute("aria-busy", "false");
3003
+ this.update();
3004
+ }
3005
+ }
3006
+ }
3007
+ button(key, title, action, parent = this.fieldset) {
3008
+ const button = this.commandButton(title, action, parent);
3009
+ button.dataset.historyAction = key;
3010
+ this.actions.set(key, button);
3011
+ }
3012
+ commandButton(title, action, parent) {
3013
+ const button = parent.ownerDocument.createElement("button");
3014
+ button.type = "button";
3015
+ button.textContent = title;
3016
+ button.addEventListener("click", () => {
3017
+ void this.run(title, action).catch(() => {
3018
+ });
3019
+ }, { signal: this.events.signal });
3020
+ parent.append(button);
3021
+ return button;
3022
+ }
3023
+ select(title, parent = this.fieldset) {
3024
+ const label = this.fieldset.ownerDocument.createElement("label");
3025
+ label.textContent = title;
3026
+ const select = label.ownerDocument.createElement("select");
3027
+ select.setAttribute("aria-label", title);
3028
+ label.append(select);
3029
+ parent.append(label);
3030
+ return select;
3031
+ }
3032
+ input(title, type, value = "", parent = this.fieldset) {
3033
+ const label = parent.ownerDocument.createElement("label");
3034
+ label.textContent = title;
3035
+ const input = label.ownerDocument.createElement("input");
3036
+ input.type = type;
3037
+ input.value = value;
3038
+ label.append(input);
3039
+ parent.append(label);
3040
+ return input;
3041
+ }
3042
+ disclosure(title) {
3043
+ const details = this.fieldset.ownerDocument.createElement("details");
3044
+ details.dataset.historyExisting = "";
3045
+ const summary = details.ownerDocument.createElement("summary");
3046
+ summary.textContent = title;
3047
+ details.append(summary);
3048
+ this.fieldset.append(details);
3049
+ return details;
3050
+ }
3051
+ };
3052
+ function historyControlError(error, pending = false) {
3053
+ const code = error instanceof DocxHistoryError ? error.code : "";
3054
+ if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
3055
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
3056
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
3057
+ if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
3058
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
3059
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
3060
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
3061
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
3062
+ }
3063
+ function versionTitle(version) {
3064
+ const { metadata } = version.record;
3065
+ return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
3066
+ }
3067
+ function formatTime(value) {
3068
+ const time = new Date(value);
3069
+ return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
3070
+ }
3071
+ var HISTORY_CSS = `
3072
+ .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}
3073
+ .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}
3074
+ .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%)}
3075
+ .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}
3076
+ .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}
3077
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
3078
+ .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}
3079
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
3080
+ `;
3081
+
2457
3082
  // src/page-number-format.ts
2458
3083
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2459
3084
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -5680,6 +6305,19 @@ var Docxodus = (() => {
5680
6305
  scale: options.scale ?? 1
5681
6306
  };
5682
6307
  }
6308
+ /** Retarget the mounted document to an equivalent host after its DOM is adopted. */
6309
+ adoptHost(host) {
6310
+ if (host === this.host) return;
6311
+ this.observer?.disconnect();
6312
+ this.observer = null;
6313
+ this.host.style.removeProperty("--docx-sheet-width");
6314
+ this.host = host;
6315
+ this.refresh();
6316
+ if (this.root && typeof ResizeObserver !== "undefined") {
6317
+ this.observer = new ResizeObserver(() => this.refresh());
6318
+ this.observer.observe(this.host);
6319
+ }
6320
+ }
5683
6321
  /**
5684
6322
  * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
5685
6323
  * Safe to call on every remount; the previous root is released first.
@@ -10613,15 +11251,20 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10613
11251
  revisionAuthor: opts.revisionAuthor
10614
11252
  }));
10615
11253
  const editor = new _DocxEditor(container, exports, handle, opts);
10616
- editor.refreshAnchorMap();
10617
- if (opts.headerFooter) editor.createRegion();
10618
- const fullHtml = editor.renderFullHtml(bytes);
10619
- if (opts.paginated) editor.mountPaginated(fullHtml);
10620
- else editor.mountHtml(fullHtml);
10621
- editor.syncRegionToBody();
10622
- editor.setupBlockDrag();
10623
- if (opts.comments) editor.createGutter();
10624
- return editor;
11254
+ try {
11255
+ editor.refreshAnchorMap();
11256
+ if (opts.headerFooter) editor.createRegion();
11257
+ const fullHtml = editor.renderFullHtml(bytes);
11258
+ if (opts.paginated) editor.mountPaginated(fullHtml);
11259
+ else editor.mountHtml(fullHtml);
11260
+ editor.syncRegionToBody();
11261
+ editor.setupBlockDrag();
11262
+ if (opts.comments) editor.createGutter();
11263
+ return editor;
11264
+ } catch (error) {
11265
+ editor.close();
11266
+ throw error;
11267
+ }
10625
11268
  }
10626
11269
  /**
10627
11270
  * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
@@ -10636,6 +11279,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10636
11279
  this.assertOpen();
10637
11280
  return this.exports.DocxSessionBridge.Save(this.handle);
10638
11281
  }
11282
+ /** Monotonic committed document version, when supported by the loaded engine. */
11283
+ get version() {
11284
+ this.assertOpen();
11285
+ const value = this.exports.DocxSessionBridge.GetVersion?.(this.handle);
11286
+ return value ? JSON.parse(value).version : null;
11287
+ }
10639
11288
  /** Release the underlying WASM session. The editor is unusable afterward. */
10640
11289
  close() {
10641
11290
  if (this.closed) return;
@@ -10670,6 +11319,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10670
11319
  get root() {
10671
11320
  return this.container;
10672
11321
  }
11322
+ /**
11323
+ * Move a fully rendered candidate into the host's stable public surface.
11324
+ * Internal editor chrome that binds directly to the container is recreated there;
11325
+ * document blocks and their listeners move with the DOM nodes.
11326
+ */
11327
+ adoptContainer(container) {
11328
+ this.assertOpen();
11329
+ if (container === this.container) return;
11330
+ this.teardownBlockDrag();
11331
+ this.gutter?.dispose();
11332
+ this.gutter = null;
11333
+ const previous = this.container;
11334
+ container.replaceChildren(...Array.from(previous.childNodes));
11335
+ this.container = container;
11336
+ this.viewport.adoptHost(container);
11337
+ this.setupBlockDrag();
11338
+ if (this.options.comments) this.createGutter();
11339
+ }
10673
11340
  /**
10674
11341
  * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
10675
11342
  * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
@@ -13829,6 +14496,481 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13829
14496
  (doc.head ?? doc.documentElement).appendChild(style);
13830
14497
  }
13831
14498
 
14499
+ // src/ribbon-history.ts
14500
+ var RibbonHistory = class {
14501
+ constructor(ribbon, options) {
14502
+ this.ribbon = ribbon;
14503
+ this.options = options;
14504
+ this.events = new AbortController();
14505
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14506
+ this.destroyed = false;
14507
+ this.installing = false;
14508
+ this.dirty = false;
14509
+ this.generation = 0;
14510
+ this.captured = -1;
14511
+ this.savedVersion = null;
14512
+ this.observedVersion = null;
14513
+ this.capturedVersion = null;
14514
+ const doc = ribbon.element.ownerDocument;
14515
+ this.prefix = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:document:`;
14516
+ this.lastKey = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:workspace:${options.workspaceId ?? crypto.randomUUID()}`;
14517
+ const style = doc.createElement("style");
14518
+ style.textContent = HISTORY_DRAWER_CSS;
14519
+ this.dialog = doc.createElement("dialog");
14520
+ this.dialog.className = "dxr-history-dialog";
14521
+ this.dialog.setAttribute("aria-label", "Version history");
14522
+ const header = doc.createElement("div");
14523
+ header.className = "dxr-history-header";
14524
+ this.dialog.append(header);
14525
+ const close = this.button("Close version history", () => this.dialog.close(), header);
14526
+ close.className = "dxr-history-close";
14527
+ close.textContent = "Close \u2715";
14528
+ this.source = doc.createElement("p");
14529
+ this.source.className = "dxr-history-source";
14530
+ this.status = doc.createElement("p");
14531
+ this.status.setAttribute("role", "status");
14532
+ this.status.className = "dxr-history-status";
14533
+ const recentLabel = doc.createElement("label");
14534
+ recentLabel.textContent = "Saved documents on this device";
14535
+ this.recent = doc.createElement("select");
14536
+ this.recent.setAttribute("aria-label", recentLabel.textContent);
14537
+ recentLabel.append(this.recent);
14538
+ this.recent.addEventListener("change", () => this.command(async () => {
14539
+ const selectedId = this.recent.value;
14540
+ this.recent.value = this.identity.id;
14541
+ const selected = this.readIdentity(localStorage.getItem(this.prefix + selectedId));
14542
+ if (!selected || !this.confirmReplace()) return;
14543
+ await this.loadSaved(selected);
14544
+ await this.mountPanel();
14545
+ this.updateRecent();
14546
+ }), { signal: this.events.signal });
14547
+ this.dialog.append(this.source, this.status, recentLabel);
14548
+ this.resumeButton = this.button("Continue editing this document", () => this.command(() => this.importArchive()), this.dialog);
14549
+ this.back = this.button("Back to my document", () => this.command(async () => {
14550
+ await this.closeArchive();
14551
+ await this.mountPanel();
14552
+ }), this.dialog);
14553
+ this.body = doc.createElement("div");
14554
+ this.dialog.append(this.body);
14555
+ this.previewDialog = doc.createElement("dialog");
14556
+ this.previewDialog.className = "dxr-version-preview";
14557
+ this.previewDialog.setAttribute("aria-label", "Version preview");
14558
+ this.panelObserver = new MutationObserver(() => this.syncBusy());
14559
+ ribbon.element.append(style, this.dialog, this.previewDialog);
14560
+ for (const dialog of [this.dialog, this.previewDialog]) {
14561
+ dialog.addEventListener("keydown", (event) => event.stopPropagation(), { signal: this.events.signal });
14562
+ dialog.addEventListener("click", (event) => {
14563
+ if (event.target === dialog) {
14564
+ const rect = dialog.getBoundingClientRect();
14565
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
14566
+ }
14567
+ }, { signal: this.events.signal });
14568
+ }
14569
+ this.dialog.addEventListener("close", () => ribbon.control("history")?.focus(), { signal: this.events.signal });
14570
+ ribbon.control("history")?.addEventListener("click", () => {
14571
+ void this.show();
14572
+ }, { signal: this.events.signal });
14573
+ doc.defaultView?.addEventListener("beforeunload", (event) => {
14574
+ if (this.hasUnsavedChanges || this.busy) event.preventDefault();
14575
+ }, { signal: this.events.signal });
14576
+ doc.defaultView?.addEventListener("pagehide", (event) => {
14577
+ if (!event.persisted) void this.destroy();
14578
+ }, { signal: this.events.signal });
14579
+ }
14580
+ get busy() {
14581
+ return !!this.active || this.panel?.element.getAttribute("aria-busy") === "true";
14582
+ }
14583
+ get hasUnsavedChanges() {
14584
+ return this.dirty || (this.ribbon.editor?.version ?? null) !== this.savedVersion;
14585
+ }
14586
+ /** Called by the ribbon before a programmatic replacement. */
14587
+ beforeOpen() {
14588
+ if (!this.installing && this.busy) throw new DocxHistoryError("Busy", "Finish the version action before opening another document.");
14589
+ }
14590
+ /** A new DOCX is a new document identity, even if its filename matches another document. */
14591
+ documentOpened(name) {
14592
+ if (this.installing) return;
14593
+ this.identity = { id: crypto.randomUUID(), name };
14594
+ this.draft = void 0;
14595
+ this.dirty = false;
14596
+ this.generation++;
14597
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14598
+ this.dialog.close();
14599
+ this.previewDialog.close();
14600
+ void this.closeArchive();
14601
+ }
14602
+ edited() {
14603
+ const version = this.ribbon.editor?.version ?? null;
14604
+ if (version !== null && version === this.observedVersion) return;
14605
+ this.observedVersion = version;
14606
+ this.dirty = true;
14607
+ this.generation++;
14608
+ }
14609
+ /** Restores a saved workspace only when requested by the host, before exposing the editor. */
14610
+ async resume() {
14611
+ if (!this.options.workspaceId) return;
14612
+ try {
14613
+ const previous = this.readIdentity(localStorage.getItem(this.lastKey));
14614
+ if (previous) await this.run(() => this.loadSaved(previous));
14615
+ } catch (error) {
14616
+ this.ribbon.setStatus("Your document is open. Version history is unavailable on this device.");
14617
+ this.status.textContent = this.explain(error);
14618
+ }
14619
+ }
14620
+ async show() {
14621
+ if (this.destroyed) return;
14622
+ if (!this.dialog.open) this.dialog.showModal();
14623
+ if (this.busy) return;
14624
+ await this.run(async () => {
14625
+ await this.mountPanel();
14626
+ this.updateRecent();
14627
+ }).catch(() => {
14628
+ });
14629
+ }
14630
+ async openFile(file) {
14631
+ await this.run(async () => {
14632
+ if (/\.docxhistory$/i.test(file.name)) {
14633
+ if (file.size > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History file is too large.");
14634
+ const bytes = new Uint8Array(await file.arrayBuffer());
14635
+ const reader = await this.options.openArchive(bytes);
14636
+ try {
14637
+ await reader.read();
14638
+ } catch (error) {
14639
+ reader.close();
14640
+ throw error;
14641
+ }
14642
+ await this.closeArchive();
14643
+ this.archive = { reader, bytes, name: file.name };
14644
+ if (!this.dialog.open) this.dialog.showModal();
14645
+ await this.mountPanel();
14646
+ this.updateRecent();
14647
+ } else {
14648
+ if (!/\.docx$/i.test(file.name)) throw new Error("Choose a Word document or a document with version history.");
14649
+ if (!this.confirmReplace()) return;
14650
+ const bytes = new Uint8Array(await file.arrayBuffer());
14651
+ this.install(bytes, { id: crypto.randomUUID(), name: file.name });
14652
+ this.draft = void 0;
14653
+ this.dirty = true;
14654
+ await this.closeArchive();
14655
+ this.dialog.close();
14656
+ }
14657
+ }).catch(() => {
14658
+ });
14659
+ }
14660
+ async newDocument() {
14661
+ await this.run(async () => {
14662
+ if (!this.confirmReplace()) return;
14663
+ this.installing = true;
14664
+ try {
14665
+ this.ribbon.openBlank("Untitled.docx");
14666
+ } finally {
14667
+ this.installing = false;
14668
+ }
14669
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14670
+ this.draft = void 0;
14671
+ this.dirty = false;
14672
+ this.generation++;
14673
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14674
+ try {
14675
+ localStorage.removeItem(this.lastKey);
14676
+ } catch {
14677
+ }
14678
+ await this.closeArchive();
14679
+ this.dialog.close();
14680
+ }).catch(() => {
14681
+ });
14682
+ }
14683
+ async destroy() {
14684
+ if (this.destroyed) return;
14685
+ this.destroyed = true;
14686
+ this.events.abort();
14687
+ this.dialog.remove();
14688
+ this.previewDialog.remove();
14689
+ this.panelObserver.disconnect();
14690
+ await this.active?.catch(() => {
14691
+ });
14692
+ await this.closeArchive();
14693
+ this.preview?.destroy();
14694
+ this.client?.close();
14695
+ this.store?.close();
14696
+ }
14697
+ async ensureStorage() {
14698
+ if (this.client) return;
14699
+ const store = await openIndexedDbHistoryStore(this.options.storageName ?? "docxodus-editor");
14700
+ try {
14701
+ this.client = this.options.openHistory(store.storage);
14702
+ this.store = store;
14703
+ } catch (error) {
14704
+ store.close();
14705
+ throw error;
14706
+ }
14707
+ }
14708
+ async ensureDraft() {
14709
+ await this.ensureStorage();
14710
+ if (!this.draft) {
14711
+ const document2 = this.client.document(this.identity.id);
14712
+ this.draft = { ...this.identity, checkpoints: await HistoryCheckpoints.open(document2, this.store.journal(this.identity.id)) };
14713
+ }
14714
+ return this.draft;
14715
+ }
14716
+ async loadSaved(identity) {
14717
+ await this.ensureStorage();
14718
+ const document2 = this.client.document(identity.id);
14719
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id));
14720
+ const pending = checkpoints.pendingRequest;
14721
+ const view = checkpoints.view;
14722
+ if (!view && pending?.kind !== "save") throw new Error("This saved document is no longer on this device.");
14723
+ const bytes = pending?.kind === "save" ? pending.bytes : await document2.exportDocx(view.version.id);
14724
+ this.install(bytes, identity);
14725
+ this.draft = { ...identity, checkpoints };
14726
+ this.dirty = pending?.kind === "save";
14727
+ if (pending?.kind === "save") this.captureState(pending.bytes);
14728
+ await this.closeArchive();
14729
+ this.remember();
14730
+ }
14731
+ install(bytes, identity) {
14732
+ if (this.destroyed) throw new DocxHistoryError("Closed", "The editor is closed.");
14733
+ this.installing = true;
14734
+ try {
14735
+ this.ribbon.open(bytes, identity.name);
14736
+ } finally {
14737
+ this.installing = false;
14738
+ }
14739
+ this.identity = identity;
14740
+ this.dirty = false;
14741
+ this.generation++;
14742
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14743
+ }
14744
+ async mountPanel() {
14745
+ const current = this.archive ?? await this.ensureDraft();
14746
+ const writable = "checkpoints" in current;
14747
+ const holder = this.body.ownerDocument.createElement("div");
14748
+ const panel = mountHistoryControls(holder, {
14749
+ reader: writable ? current.checkpoints.document : current.reader,
14750
+ checkpoints: writable ? current.checkpoints : void 0,
14751
+ author: this.options.author,
14752
+ documentName: current.name,
14753
+ capture: writable ? () => {
14754
+ const bytes = this.ribbon.save();
14755
+ if (!bytes) throw new Error("Open a document first.");
14756
+ this.remember();
14757
+ this.captureState(bytes);
14758
+ return bytes;
14759
+ } : void 0,
14760
+ confirmRestore: writable ? (title) => this.confirm(`Restore ${title}?${this.hasUnsavedChanges ? "\n\nYour unsaved changes will be replaced." : ""}
14761
+
14762
+ All saved versions will be kept.`) : void 0,
14763
+ onCheckpoint: async (view, action, request) => {
14764
+ if (this.destroyed) return;
14765
+ const savedCapture = action === "save" || action === "retry" && request?.kind === "save" && this.capturedBytes && sameBytes(this.capturedBytes, request.bytes);
14766
+ if (savedCapture && this.captured === this.generation && this.capturedVersion === (this.ribbon.editor?.version ?? null)) {
14767
+ this.dirty = false;
14768
+ this.savedVersion = this.capturedVersion;
14769
+ }
14770
+ this.captured = -1;
14771
+ this.capturedBytes = void 0;
14772
+ if ((action === "restore" || action === "retry" && request?.kind === "restore") && writable)
14773
+ this.install(await current.checkpoints.document.exportDocx(view.version.id), this.identity);
14774
+ this.remember();
14775
+ this.updateRecent();
14776
+ },
14777
+ restoreUpdatesDraft: true,
14778
+ preview: (bytes, title) => this.showPreview(bytes, title)
14779
+ });
14780
+ try {
14781
+ await panel.ready;
14782
+ } catch (error) {
14783
+ await panel.destroy();
14784
+ throw error;
14785
+ }
14786
+ if (this.destroyed) {
14787
+ await panel.destroy();
14788
+ return;
14789
+ }
14790
+ await this.panel?.destroy();
14791
+ this.panel = panel;
14792
+ this.body.replaceChildren(holder);
14793
+ this.panelObserver.disconnect();
14794
+ this.panelObserver.observe(panel.element, { attributes: true, attributeFilter: ["aria-busy"] });
14795
+ this.source.textContent = current.name;
14796
+ this.resumeButton.hidden = this.back.hidden = writable;
14797
+ this.status.textContent = writable ? "Your saved versions stay on this device." : "You\u2019re browsing a history file. Your open document is safe.";
14798
+ }
14799
+ async showPreview(bytes, title) {
14800
+ const doc = this.previewDialog.ownerDocument;
14801
+ const holder = doc.createElement("div");
14802
+ holder.className = "dxr-version-paper";
14803
+ const preview = await this.options.preview(holder, bytes);
14804
+ if (this.destroyed) {
14805
+ preview.destroy();
14806
+ return;
14807
+ }
14808
+ const heading = doc.createElement("h2");
14809
+ heading.textContent = title;
14810
+ const actions = doc.createElement("div");
14811
+ actions.className = "dxr-version-actions";
14812
+ this.button("Back to version history", () => this.previewDialog.close(), actions);
14813
+ if (!this.archive) this.button("Use as draft", () => this.command(async () => {
14814
+ if (!this.confirmReplace()) return;
14815
+ this.install(bytes, this.identity);
14816
+ this.dirty = true;
14817
+ this.previewDialog.close();
14818
+ this.dialog.close();
14819
+ }), actions);
14820
+ this.button("Download this version", () => download(bytes, "version.docx", doc), actions);
14821
+ this.preview?.destroy();
14822
+ this.preview = preview;
14823
+ this.previewDialog.replaceChildren(heading, actions, holder);
14824
+ if (!this.previewDialog.open) this.previewDialog.showModal();
14825
+ }
14826
+ async importArchive() {
14827
+ if (!this.archive || !this.confirmReplace()) return;
14828
+ await this.ensureStorage();
14829
+ const imported = await this.client.importHistoryArchive(this.archive.bytes);
14830
+ const identity = { id: imported.archive.documentId, name: this.archive.name.replace(/\.docxhistory$/i, ".docx") };
14831
+ const document2 = this.client.document(identity.id);
14832
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id), imported.view);
14833
+ this.install(await document2.exportDocx(imported.view.version.id), identity);
14834
+ this.draft = { ...identity, checkpoints };
14835
+ this.remember();
14836
+ await this.closeArchive();
14837
+ await this.mountPanel();
14838
+ this.updateRecent();
14839
+ }
14840
+ async closeArchive() {
14841
+ this.panelObserver.disconnect();
14842
+ const panel = this.panel;
14843
+ this.panel = void 0;
14844
+ const archive = this.archive;
14845
+ this.archive = void 0;
14846
+ await panel?.destroy();
14847
+ archive?.reader.close();
14848
+ }
14849
+ remember() {
14850
+ localStorage.setItem(this.prefix + this.identity.id, JSON.stringify(this.identity));
14851
+ localStorage.setItem(this.lastKey, JSON.stringify(this.identity));
14852
+ }
14853
+ readIdentity(value) {
14854
+ if (!value) return null;
14855
+ const identity = JSON.parse(value);
14856
+ if (typeof identity?.id !== "string" || !identity.id || typeof identity?.name !== "string") return null;
14857
+ return { id: identity.id, name: identity.name };
14858
+ }
14859
+ updateRecent() {
14860
+ this.recent.replaceChildren(new Option("Choose a saved document\u2026", ""));
14861
+ for (let i = 0; i < localStorage.length; i++) {
14862
+ const key = localStorage.key(i);
14863
+ if (!key.startsWith(this.prefix)) continue;
14864
+ try {
14865
+ const identity = this.readIdentity(localStorage.getItem(key));
14866
+ if (identity) this.recent.append(new Option(identity.name, identity.id));
14867
+ } catch {
14868
+ }
14869
+ }
14870
+ this.recent.value = this.identity.id;
14871
+ this.recent.parentElement.hidden = this.recent.options.length <= 1;
14872
+ }
14873
+ confirm(message) {
14874
+ return this.dialog.ownerDocument.defaultView?.confirm(message) ?? false;
14875
+ }
14876
+ captureState(bytes) {
14877
+ this.captured = this.generation;
14878
+ this.capturedVersion = this.ribbon.editor?.version ?? null;
14879
+ this.capturedBytes = bytes.slice();
14880
+ }
14881
+ confirmReplace() {
14882
+ return !this.hasUnsavedChanges || this.confirm("Replace your open document? Save a version or download it first to keep your unsaved changes.");
14883
+ }
14884
+ explain(error) {
14885
+ return historyControlError(error, this.draft?.checkpoints.hasPending);
14886
+ }
14887
+ command(action) {
14888
+ void this.run(action).catch(() => {
14889
+ });
14890
+ }
14891
+ syncBusy() {
14892
+ const busy = this.busy;
14893
+ this.ribbon.surface.inert = busy;
14894
+ const chrome = this.ribbon.element.querySelector(".dxr-chrome");
14895
+ if (chrome) chrome.inert = busy;
14896
+ this.recent.disabled = this.resumeButton.disabled = this.back.disabled = busy;
14897
+ const file = this.ribbon.control("file");
14898
+ if (file) file.disabled = busy;
14899
+ const create = this.ribbon.control("new");
14900
+ if (create) create.disabled = busy;
14901
+ }
14902
+ async run(action) {
14903
+ if (this.destroyed || this.busy) return;
14904
+ const work = Promise.resolve().then(action);
14905
+ this.active = work;
14906
+ this.dialog.setAttribute("aria-busy", "true");
14907
+ this.body.inert = true;
14908
+ this.syncBusy();
14909
+ this.ribbon.element.querySelector(".dxr-chrome").inert = true;
14910
+ this.ribbon.surface.inert = true;
14911
+ try {
14912
+ await work;
14913
+ } catch (error) {
14914
+ this.status.textContent = this.explain(error);
14915
+ this.ribbon.setStatus(this.explain(error));
14916
+ throw error;
14917
+ } finally {
14918
+ this.active = void 0;
14919
+ this.body.inert = false;
14920
+ this.dialog.setAttribute("aria-busy", "false");
14921
+ this.syncBusy();
14922
+ this.ribbon.element.querySelector(".dxr-chrome")?.removeAttribute("inert");
14923
+ this.ribbon.surface.inert = false;
14924
+ }
14925
+ }
14926
+ button(label, action, parent) {
14927
+ const button = parent.ownerDocument.createElement("button");
14928
+ button.type = "button";
14929
+ button.textContent = label;
14930
+ button.setAttribute("aria-label", label);
14931
+ button.addEventListener("click", action, { signal: this.events.signal });
14932
+ parent.append(button);
14933
+ return button;
14934
+ }
14935
+ };
14936
+ function download(bytes, name, doc) {
14937
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
14938
+ const link = doc.createElement("a");
14939
+ link.href = url;
14940
+ link.download = name;
14941
+ link.click();
14942
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
14943
+ }
14944
+ function sameBytes(left2, right2) {
14945
+ return left2.length === right2.length && left2.every((value, index) => value === right2[index]);
14946
+ }
14947
+ var HISTORY_DRAWER_CSS = `
14948
+ .dxr-history-dialog,.dxr-version-preview{box-sizing:border-box;color:#243b42;background:#fff;border:1px solid #dce5e5;box-shadow:0 24px 80px #163f4033;font:14px/1.5 system-ui,sans-serif;padding:24px;overscroll-behavior:contain}
14949
+ .dxr-history-dialog{width:min(420px,calc(100vw - 24px));max-height:calc(100dvh - 24px);margin:12px 12px 12px auto;border-radius:18px}
14950
+ .dxr-history-dialog::backdrop,.dxr-version-preview::backdrop{background:#16323333;backdrop-filter:blur(2px)}
14951
+ .dxr-history-dialog [hidden],.dxr-version-preview [hidden]{display:none!important}
14952
+ .dxr-history-dialog button,.dxr-version-preview button{font:inherit;color:inherit;border:1px solid #cbd9d9;background:#fff;border-radius:9px;min-height:40px;padding:8px 12px;cursor:pointer}
14953
+ .dxr-history-dialog button:hover,.dxr-version-preview button:hover{background:#eff8f6}
14954
+ .dxr-history-dialog :focus-visible,.dxr-version-preview :focus-visible{outline:3px solid #0f766e;outline-offset:3px}
14955
+ .dxr-history-header{display:flex;justify-content:flex-end;position:sticky;top:0;z-index:2;background:#fff;box-shadow:0 0 0 6px #fff}
14956
+ .dxr-history-dialog .dxr-history-close{border:0;font-size:13px;background:#fff}
14957
+ .dxr-history-source{font-weight:600;overflow-wrap:anywhere;margin:12px 0 4px}
14958
+ .dxr-history-status{color:#597273;margin:4px 0 18px}
14959
+ .dxr-history-dialog>label{display:block;font-size:12px;color:#597273;margin:12px 0}
14960
+ .dxr-history-dialog>label select{display:block;width:100%;font:inherit;padding:8px;border:1px solid #cbd9d9;border-radius:8px;background:#fff;color:#243b42}
14961
+ .dxr-history-dialog .dx-history{padding:0;border:0;border-radius:0;background:transparent}
14962
+ .dxr-history-dialog .dx-history h2{font-size:24px;letter-spacing:-.6px;margin:12px 0}
14963
+ .dxr-history-dialog .dx-history button[data-history-action=save]{background:#0f766e;border-color:#0f766e;color:#fff;font-weight:600}
14964
+ .dxr-history-dialog .dx-history select{border-radius:9px;background:#f8fbfa}
14965
+ .dxr-history-dialog .dx-history option{padding:10px;font-size:13px}
14966
+ .dxr-version-preview{width:min(1080px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border-radius:18px}
14967
+ .dxr-version-preview h2{font-size:20px;margin:0 0 12px;overflow-wrap:anywhere}
14968
+ .dxr-version-actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
14969
+ .dxr-version-paper{max-height:70dvh;overflow:auto;background:#f2f6f5;padding:20px;border-radius:10px}
14970
+ @media(max-width:520px){.dxr-history-dialog,.dxr-version-preview{padding:16px}.dxr-version-paper{padding:8px}}
14971
+ @media(prefers-reduced-motion:no-preference){.dxr-history-dialog[open]{animation:dxr-history-in .16s ease-out}@keyframes dxr-history-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:translateX(0)}}}
14972
+ `;
14973
+
13832
14974
  // src/ribbon-chrome.ts
13833
14975
  var RIBBON_STYLE_VERSION = "11";
13834
14976
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
@@ -14279,9 +15421,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14279
15421
  Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
14280
15422
  .dxr-surface .docx-hf-band {
14281
15423
  /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
14282
- custom property the viewport publishes rather than stretching to the whole surface. */
15424
+ custom property the viewport publishes, including when the page exceeds the surface. */
14283
15425
  position: relative;
14284
- width: min(100%, var(--docx-sheet-width, 100%));
15426
+ width: var(--docx-sheet-width, 100%);
14285
15427
  margin: 0 auto;
14286
15428
  padding: 34px 72px 12px;
14287
15429
  background: var(--dxr-sheet);
@@ -14340,7 +15482,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14340
15482
  vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
14341
15483
  has zoomed to fit stays centered at its scaled width. */
14342
15484
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
14343
- max-width: 100%;
15485
+ /* CSS zoom scales the authored page width. A percentage cap would shrink only the
15486
+ paper to the host while its fixed-width section and content keep magnifying. */
15487
+ max-width: none;
14344
15488
  margin: 0 auto;
14345
15489
  padding: 56px 0;
14346
15490
  border-radius: 3px;
@@ -14707,6 +15851,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14707
15851
  <label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
14708
15852
  <button type="button" data-dxr="save" disabled>Save</button>
14709
15853
  </div>
15854
+ <button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
14710
15855
  <div class="dxr-quick">
14711
15856
  <button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">&#8630;</button>
14712
15857
  <button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">&#8631;</button>
@@ -15541,6 +16686,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15541
16686
  this.resizeObserver = new ResizeObserver(() => this.applyChrome());
15542
16687
  this.resizeObserver.observe(root);
15543
16688
  }
16689
+ this.history = options.history ? new RibbonHistory(this, options.history) : null;
15544
16690
  }
15545
16691
  // ── element lookup ──────────────────────────────────────────────────────────
15546
16692
  control(name) {
@@ -15565,6 +16711,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15565
16711
  this.element.querySelector("[data-dxr-files]")?.remove();
15566
16712
  }
15567
16713
  if (!this.loaderOptions) this.control("loader")?.remove();
16714
+ if (!this.options.history) this.control("history")?.remove();
16715
+ else {
16716
+ const file = this.control("file");
16717
+ if (file) {
16718
+ file.accept = ".docx,.docxhistory";
16719
+ file.setAttribute("aria-label", "Open a document or history file");
16720
+ }
16721
+ }
15568
16722
  this.require("docname").textContent = this.documentName;
15569
16723
  this.require("paginated").checked = this.options.paginated ?? false;
15570
16724
  this.require("headerfooter").checked = this.headerFooter;
@@ -15746,22 +16900,13 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15746
16900
  }
15747
16901
  open(bytes, name) {
15748
16902
  if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
15749
- if (this.live) {
15750
- try {
15751
- this.live.close();
15752
- } catch {
15753
- }
15754
- this.live = null;
15755
- }
15756
- if (name) this.documentName = name;
15757
- this.require("docname").textContent = this.documentName;
16903
+ this.history?.beforeOpen();
15758
16904
  const paginated = this.require("paginated").checked;
15759
- this.surface.dataset.view = paginated ? "paginated" : "continuous";
15760
- this.surface.replaceChildren();
15761
- this.closeFindBar();
16905
+ const candidateSurface = this.surface.cloneNode(false);
16906
+ candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
15762
16907
  const started = performance.now();
15763
16908
  const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
15764
- this.live = DocxEditor.open(this.surface, bytes, this.exports, {
16909
+ const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
15765
16910
  cssPrefix: this.options.cssPrefix,
15766
16911
  fabricateClasses: this.options.fabricateClasses,
15767
16912
  editable: this.options.editable,
@@ -15769,10 +16914,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15769
16914
  columnWidth: this.options.columnWidth,
15770
16915
  fitToWidth: this.options.fitToWidth,
15771
16916
  onEdit: (info) => {
16917
+ this.history?.edited();
15772
16918
  this.options.onEdit?.(info);
15773
16919
  this.scheduleStats();
15774
16920
  },
15775
- onMove: this.options.onMove,
16921
+ onMove: (info) => {
16922
+ this.history?.edited();
16923
+ this.options.onMove?.(info);
16924
+ },
15776
16925
  onStoryChange: (which) => this.onStoryChange(which),
15777
16926
  onCommentsChange: (info) => this.onCommentsChange(info),
15778
16927
  paginated,
@@ -15783,7 +16932,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15783
16932
  comments: this.options.comments,
15784
16933
  commentAuthor: this.author
15785
16934
  });
15786
- this.require("save").disabled = false;
16935
+ this.live?.close();
16936
+ candidate.adoptContainer(this.surface);
16937
+ this.surface.dataset.view = paginated ? "paginated" : "continuous";
16938
+ this.live = candidate;
16939
+ if (name) this.documentName = name;
16940
+ this.require("docname").textContent = this.documentName;
16941
+ this.closeFindBar();
16942
+ this.history?.documentOpened(this.documentName);
16943
+ const saveButton = this.control("save");
16944
+ if (saveButton) saveButton.disabled = false;
15787
16945
  this.require("ribbon").setAttribute("aria-disabled", "false");
15788
16946
  this.setState("ready");
15789
16947
  this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
@@ -15829,6 +16987,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15829
16987
  destroy() {
15830
16988
  if (this.destroyed) return;
15831
16989
  this.destroyed = true;
16990
+ void this.history?.destroy();
15832
16991
  const doc = this.element.ownerDocument ?? document;
15833
16992
  doc.removeEventListener("selectionchange", this.onSelectionChange);
15834
16993
  doc.removeEventListener("mousedown", this.onDocumentMouseDown);
@@ -15865,6 +17024,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15865
17024
  const el = this.control("railOp");
15866
17025
  if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
15867
17026
  this.options.onCommand?.(label, ms);
17027
+ this.history?.edited();
15868
17028
  this.refreshRailCounts();
15869
17029
  this.refreshRailAnchor();
15870
17030
  this.scheduleStats();
@@ -16064,11 +17224,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16064
17224
  file?.addEventListener("change", async () => {
16065
17225
  const chosen = file.files?.[0];
16066
17226
  if (!chosen) return;
17227
+ file.value = "";
17228
+ if (this.history) {
17229
+ await this.history.openFile(chosen);
17230
+ return;
17231
+ }
16067
17232
  this.setStatus(`Loading ${chosen.name}\u2026`);
16068
- this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17233
+ try {
17234
+ this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17235
+ } catch (error) {
17236
+ this.setStatus(`Could not open this document. ${String(error)}`);
17237
+ }
16069
17238
  file.value = "";
16070
17239
  });
16071
17240
  this.control("new")?.addEventListener("click", () => {
17241
+ if (this.history) {
17242
+ void this.history.newDocument();
17243
+ return;
17244
+ }
16072
17245
  if (this.exports) this.openBlank("untitled.docx");
16073
17246
  });
16074
17247
  this.control("save")?.addEventListener("click", () => this.download());
@@ -16079,6 +17252,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16079
17252
  if (!chosen || !this.live) return;
16080
17253
  const started = performance.now();
16081
17254
  const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
17255
+ if (ok) this.history?.edited();
16082
17256
  const ms = performance.now() - started;
16083
17257
  const el = this.control("railOp");
16084
17258
  if (el) el.textContent = `picture ${Math.round(ms)} ms`;
@@ -18543,7 +19717,7 @@ ${parsed.documentElement.outerHTML}`;
18543
19717
  }
18544
19718
  async function createRibbonEditor(container, source, options = {}) {
18545
19719
  const el = resolveContainer2(container);
18546
- const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
19720
+ const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
18547
19721
  const mount2 = createScopedMount(el);
18548
19722
  mount2.root.style.height = "100%";
18549
19723
  mount2.root.style.minHeight = "0";
@@ -18553,6 +19727,12 @@ ${parsed.documentElement.outerHTML}`;
18553
19727
  // boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
18554
19728
  frame: "card",
18555
19729
  ...ribbonOptions,
19730
+ history: history ? {
19731
+ ...typeof history === "object" ? history : {},
19732
+ openHistory: openDocxHistory,
19733
+ openArchive: openDocxHistoryArchive,
19734
+ preview: (container2, bytes) => createViewer(container2, bytes, { wasmBasePath: wasmBasePath2, renderTrackedChanges: true })
19735
+ } : void 0,
18556
19736
  // Exports arrive after the runtime boots; the loader covers that gap.
18557
19737
  exports: void 0
18558
19738
  });
@@ -18570,6 +19750,7 @@ ${parsed.documentElement.outerHTML}`;
18570
19750
  ribbon.loader.stage(2);
18571
19751
  if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
18572
19752
  else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
19753
+ await ribbon.history?.resume();
18573
19754
  ribbon.loader.stage(3);
18574
19755
  ribbon.loader.done();
18575
19756
  return ribbon;