docxodus 12.3.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 (50) hide show
  1. package/README.md +8 -6
  2. package/dist/editor.bundle.js +1206 -27
  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 +649 -57
  8. package/dist/embed.d.ts +5 -2
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +649 -57
  11. package/dist/embed.js +10 -3
  12. package/dist/embed.js.map +1 -1
  13. package/dist/export-assets.json +5 -5
  14. package/dist/history-checkpoints.d.ts +2 -0
  15. package/dist/history-checkpoints.d.ts.map +1 -1
  16. package/dist/history-checkpoints.js +2 -0
  17. package/dist/history-checkpoints.js.map +1 -1
  18. package/dist/history-controls.d.ts +6 -3
  19. package/dist/history-controls.d.ts.map +1 -1
  20. package/dist/history-controls.js +71 -29
  21. package/dist/history-controls.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/ribbon-chrome.d.ts +1 -1
  27. package/dist/ribbon-chrome.d.ts.map +1 -1
  28. package/dist/ribbon-chrome.js +6 -3
  29. package/dist/ribbon-chrome.js.map +1 -1
  30. package/dist/ribbon-history.d.ts +85 -0
  31. package/dist/ribbon-history.d.ts.map +1 -0
  32. package/dist/ribbon-history.js +508 -0
  33. package/dist/ribbon-history.js.map +1 -0
  34. package/dist/ribbon.d.ts +6 -0
  35. package/dist/ribbon.d.ts.map +1 -1
  36. package/dist/ribbon.js +47 -18
  37. package/dist/ribbon.js.map +1 -1
  38. package/dist/viewport.d.ts +3 -1
  39. package/dist/viewport.d.ts.map +1 -1
  40. package/dist/viewport.js +14 -0
  41. package/dist/viewport.js.map +1 -1
  42. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  43. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  44. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  45. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  46. package/dist/wasm/_framework/dotnet.boot.js +4 -4
  47. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  48. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  49. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  50. package/package.json +7 -8
@@ -2347,6 +2347,10 @@ var HistoryCheckpoints = class _HistoryCheckpoints {
2347
2347
  get hasPending() {
2348
2348
  return this.request !== null;
2349
2349
  }
2350
+ /** Snapshot of the exact request a retry will recover, including its original operation kind. */
2351
+ get pendingRequest() {
2352
+ return structuredClone(this.request);
2353
+ }
2350
2354
  get needsRefresh() {
2351
2355
  return this.stale;
2352
2356
  }
@@ -2593,25 +2597,26 @@ var HistoryPanel = class {
2593
2597
  const doc = container.ownerDocument;
2594
2598
  this.element = doc.createElement("section");
2595
2599
  this.element.className = "dx-history";
2596
- this.element.setAttribute("aria-label", "Document history");
2600
+ this.element.setAttribute("aria-label", "Version history");
2597
2601
  const style = doc.createElement("style");
2598
2602
  style.textContent = HISTORY_CSS;
2599
2603
  const title = doc.createElement("h2");
2600
- title.textContent = "Document history";
2604
+ title.textContent = "Version history";
2601
2605
  this.status = doc.createElement("p");
2602
2606
  this.status.setAttribute("role", "status");
2603
2607
  this.status.setAttribute("aria-live", "polite");
2604
2608
  this.fieldset = doc.createElement("fieldset");
2605
2609
  const legend = doc.createElement("legend");
2606
- legend.textContent = "Versions and checkpoints";
2610
+ legend.textContent = "Saved versions";
2607
2611
  this.fieldset.append(legend);
2608
2612
  this.element.append(style, title, this.status, this.fieldset);
2609
2613
  container.append(this.element);
2610
2614
  const note = doc.createElement("p");
2615
+ note.dataset.historyExisting = "";
2611
2616
  note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2612
2617
  this.fieldset.append(note);
2613
2618
  this.button("refresh", "Refresh history", () => this.load(true));
2614
- this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2619
+ this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2615
2620
  this.versions = this.select("Version");
2616
2621
  this.versions.size = 6;
2617
2622
  this.detail = doc.createElement("p");
@@ -2619,56 +2624,80 @@ var HistoryPanel = class {
2619
2624
  this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2620
2625
  this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2621
2626
  this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2622
- this.before = this.select("Compare from");
2627
+ const comparison = this.disclosure("Compare versions");
2628
+ this.before = this.select("Compare from", comparison);
2623
2629
  this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2624
2630
  const compareNote = doc.createElement("p");
2625
2631
  compareNote.textContent = "Compare from this version to the selected version above.";
2626
- this.fieldset.append(compareNote);
2632
+ comparison.append(compareNote);
2627
2633
  this.button("compare", "Compare versions", async () => {
2628
2634
  const before = this.records[Number(this.before.value)];
2629
2635
  await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2630
- });
2636
+ }, comparison);
2631
2637
  this.button("more", "Load older versions", async () => {
2632
2638
  if (this.next) await this.page(this.next, false);
2633
2639
  });
2634
- this.author = this.input("Your name", "text", options.author ?? "You");
2635
- this.label = this.input("Checkpoint name (optional)", "text");
2640
+ const saveGroup = doc.createElement("div");
2641
+ saveGroup.className = "dx-history-save";
2642
+ this.fieldset.insertBefore(saveGroup, note);
2643
+ const attribution = doc.createElement("details");
2644
+ const attributionTitle = doc.createElement("summary");
2645
+ attributionTitle.textContent = "Saved by";
2646
+ attribution.append(attributionTitle);
2647
+ saveGroup.append(attribution);
2648
+ this.author = this.input("Your name", "text", options.author ?? "You", attribution);
2649
+ this.label = this.input("Version name (optional)", "text", "", saveGroup);
2650
+ saveGroup.hidden = !options.checkpoints;
2636
2651
  this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2637
- this.button("save", "Save checkpoint", async () => {
2652
+ this.button("save", "Save version", async () => {
2638
2653
  const view = await options.checkpoints.save(await options.capture(), this.metadata());
2639
2654
  await options.onCheckpoint?.(view, "save");
2640
2655
  await this.load(false);
2641
2656
  this.label.value = "";
2642
- this.status.textContent = "Checkpoint saved. Your draft remains open.";
2643
- });
2657
+ this.status.textContent = "Version saved. Your draft remains open.";
2658
+ }, saveGroup);
2659
+ saveGroup.append(attribution);
2644
2660
  this.button("restore", "Restore selected", async () => {
2645
2661
  const version = this.selected();
2646
- if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
2662
+ const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
2647
2663
 
2648
- Your current draft and all later versions will be kept.`)) {
2664
+ Your current draft and all later versions will be kept.`);
2665
+ if (!confirmed) {
2649
2666
  this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2650
2667
  return;
2651
2668
  }
2652
2669
  const view = await options.checkpoints.restore(version.id, this.metadata());
2653
2670
  await options.onCheckpoint?.(view, "restore");
2654
2671
  await this.load(false);
2655
- this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
2672
+ 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.";
2656
2673
  });
2657
2674
  const restoreNote = doc.createElement("p");
2658
2675
  restoreNote.hidden = !options.checkpoints;
2659
- restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
2676
+ if (options.checkpoints) restoreNote.dataset.historyExisting = "";
2677
+ 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.";
2660
2678
  this.fieldset.append(restoreNote);
2661
- this.button("retry", "Retry checkpoint", async () => {
2679
+ this.button("retry", "Retry save", async () => {
2680
+ const request = options.checkpoints.pendingRequest;
2681
+ if (request?.kind === "restore" && options.confirmRestore) {
2682
+ const target = await options.reader.getVersion(request.target);
2683
+ if (!await options.confirmRestore(versionTitle(target))) {
2684
+ this.status.textContent = "Restore retry canceled. Your draft is unchanged; the saved action still needs recovery.";
2685
+ return;
2686
+ }
2687
+ }
2662
2688
  const view = await options.checkpoints.retry();
2663
- await options.onCheckpoint?.(view, "retry");
2689
+ await options.onCheckpoint?.(view, "retry", request ?? void 0);
2664
2690
  await this.load(false);
2665
- this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
2691
+ 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.";
2666
2692
  });
2667
2693
  const sharing = this.disclosure("Download with history");
2668
2694
  const sharingNote = doc.createElement("p");
2669
2695
  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.";
2670
2696
  sharing.append(sharingNote);
2671
- this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2697
+ this.button("archive", "Download with version history", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2698
+ const portability = doc.createElement("p");
2699
+ portability.textContent = "Saved versions stay on this device. Download a history file to keep a portable copy; clearing browser data removes local versions.";
2700
+ sharing.append(portability);
2672
2701
  const time = this.disclosure("Find a version by time");
2673
2702
  const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2674
2703
  this.button("time", "Preview at time", async () => {
@@ -2718,7 +2747,7 @@ Your current draft and all later versions will be kept.`)) {
2718
2747
  this.versions.replaceChildren();
2719
2748
  this.before.replaceChildren();
2720
2749
  }
2721
- 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.";
2750
+ 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.";
2722
2751
  }
2723
2752
  async page(cursor, reset) {
2724
2753
  const page = await this.options.reader.listVersions(cursor, this.pageSize);
@@ -2755,6 +2784,11 @@ Your current draft and all later versions will be kept.`)) {
2755
2784
  this.actions.get("restore").hidden = !this.options.checkpoints;
2756
2785
  this.actions.get("restore").disabled = !hasVersion || pending || stale;
2757
2786
  this.actions.get("retry").hidden = !pending;
2787
+ this.versions.parentElement.hidden = this.detail.hidden = !hasVersion;
2788
+ this.versions.size = Math.max(2, Math.min(6, this.records.length));
2789
+ for (const key of ["latest", "preview", "download"]) this.actions.get(key).hidden = !hasVersion;
2790
+ this.actions.get("restore").hidden = !hasVersion || !this.options.checkpoints;
2791
+ for (const element of Array.from(this.element.querySelectorAll("[data-history-existing]"))) element.hidden = !hasVersion;
2758
2792
  this.detail.textContent = hasVersion ? [
2759
2793
  versionTitle(this.selected()),
2760
2794
  this.selected().record.metadata.message,
@@ -2833,7 +2867,9 @@ Your current draft and all later versions will be kept.`)) {
2833
2867
  }
2834
2868
  }
2835
2869
  button(key, title, action, parent = this.fieldset) {
2836
- this.actions.set(key, this.commandButton(title, action, parent));
2870
+ const button = this.commandButton(title, action, parent);
2871
+ button.dataset.historyAction = key;
2872
+ this.actions.set(key, button);
2837
2873
  }
2838
2874
  commandButton(title, action, parent) {
2839
2875
  const button = parent.ownerDocument.createElement("button");
@@ -2846,13 +2882,13 @@ Your current draft and all later versions will be kept.`)) {
2846
2882
  parent.append(button);
2847
2883
  return button;
2848
2884
  }
2849
- select(title) {
2885
+ select(title, parent = this.fieldset) {
2850
2886
  const label = this.fieldset.ownerDocument.createElement("label");
2851
2887
  label.textContent = title;
2852
2888
  const select = label.ownerDocument.createElement("select");
2853
2889
  select.setAttribute("aria-label", title);
2854
2890
  label.append(select);
2855
- this.fieldset.append(label);
2891
+ parent.append(label);
2856
2892
  return select;
2857
2893
  }
2858
2894
  input(title, type, value = "", parent = this.fieldset) {
@@ -2867,6 +2903,7 @@ Your current draft and all later versions will be kept.`)) {
2867
2903
  }
2868
2904
  disclosure(title) {
2869
2905
  const details = this.fieldset.ownerDocument.createElement("details");
2906
+ details.dataset.historyExisting = "";
2870
2907
  const summary = details.ownerDocument.createElement("summary");
2871
2908
  summary.textContent = title;
2872
2909
  details.append(summary);
@@ -2876,22 +2913,22 @@ Your current draft and all later versions will be kept.`)) {
2876
2913
  };
2877
2914
  function historyControlError(error, pending = false) {
2878
2915
  const code = error instanceof DocxHistoryError ? error.code : "";
2879
- if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
2916
+ if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
2880
2917
  if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
2881
2918
  if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
2882
- if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
2919
+ if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
2883
2920
  if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
2884
2921
  if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
2885
2922
  if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
2886
2923
  return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
2887
2924
  }
2888
2925
  function versionTitle(version) {
2889
- const { metadata, sequence } = version.record;
2890
- return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
2926
+ const { metadata } = version.record;
2927
+ return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
2891
2928
  }
2892
2929
  function formatTime(value) {
2893
2930
  const time = new Date(value);
2894
- return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
2931
+ return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
2895
2932
  }
2896
2933
  var HISTORY_CSS = `
2897
2934
  .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}
@@ -6130,6 +6167,19 @@ var DocumentViewport = class {
6130
6167
  scale: options.scale ?? 1
6131
6168
  };
6132
6169
  }
6170
+ /** Retarget the mounted document to an equivalent host after its DOM is adopted. */
6171
+ adoptHost(host) {
6172
+ if (host === this.host) return;
6173
+ this.observer?.disconnect();
6174
+ this.observer = null;
6175
+ this.host.style.removeProperty("--docx-sheet-width");
6176
+ this.host = host;
6177
+ this.refresh();
6178
+ if (this.root && typeof ResizeObserver !== "undefined") {
6179
+ this.observer = new ResizeObserver(() => this.refresh());
6180
+ this.observer.observe(this.host);
6181
+ }
6182
+ }
6133
6183
  /**
6134
6184
  * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
6135
6185
  * Safe to call on every remount; the previous root is released first.
@@ -11063,15 +11113,20 @@ var DocxEditor = class _DocxEditor {
11063
11113
  revisionAuthor: opts.revisionAuthor
11064
11114
  }));
11065
11115
  const editor = new _DocxEditor(container, exports, handle, opts);
11066
- editor.refreshAnchorMap();
11067
- if (opts.headerFooter) editor.createRegion();
11068
- const fullHtml = editor.renderFullHtml(bytes);
11069
- if (opts.paginated) editor.mountPaginated(fullHtml);
11070
- else editor.mountHtml(fullHtml);
11071
- editor.syncRegionToBody();
11072
- editor.setupBlockDrag();
11073
- if (opts.comments) editor.createGutter();
11074
- return editor;
11116
+ try {
11117
+ editor.refreshAnchorMap();
11118
+ if (opts.headerFooter) editor.createRegion();
11119
+ const fullHtml = editor.renderFullHtml(bytes);
11120
+ if (opts.paginated) editor.mountPaginated(fullHtml);
11121
+ else editor.mountHtml(fullHtml);
11122
+ editor.syncRegionToBody();
11123
+ editor.setupBlockDrag();
11124
+ if (opts.comments) editor.createGutter();
11125
+ return editor;
11126
+ } catch (error) {
11127
+ editor.close();
11128
+ throw error;
11129
+ }
11075
11130
  }
11076
11131
  /**
11077
11132
  * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
@@ -11086,6 +11141,12 @@ var DocxEditor = class _DocxEditor {
11086
11141
  this.assertOpen();
11087
11142
  return this.exports.DocxSessionBridge.Save(this.handle);
11088
11143
  }
11144
+ /** Monotonic committed document version, when supported by the loaded engine. */
11145
+ get version() {
11146
+ this.assertOpen();
11147
+ const value = this.exports.DocxSessionBridge.GetVersion?.(this.handle);
11148
+ return value ? JSON.parse(value).version : null;
11149
+ }
11089
11150
  /** Release the underlying WASM session. The editor is unusable afterward. */
11090
11151
  close() {
11091
11152
  if (this.closed) return;
@@ -11120,6 +11181,24 @@ var DocxEditor = class _DocxEditor {
11120
11181
  get root() {
11121
11182
  return this.container;
11122
11183
  }
11184
+ /**
11185
+ * Move a fully rendered candidate into the host's stable public surface.
11186
+ * Internal editor chrome that binds directly to the container is recreated there;
11187
+ * document blocks and their listeners move with the DOM nodes.
11188
+ */
11189
+ adoptContainer(container) {
11190
+ this.assertOpen();
11191
+ if (container === this.container) return;
11192
+ this.teardownBlockDrag();
11193
+ this.gutter?.dispose();
11194
+ this.gutter = null;
11195
+ const previous = this.container;
11196
+ container.replaceChildren(...Array.from(previous.childNodes));
11197
+ this.container = container;
11198
+ this.viewport.adoptHost(container);
11199
+ this.setupBlockDrag();
11200
+ if (this.options.comments) this.createGutter();
11201
+ }
11123
11202
  /**
11124
11203
  * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
11125
11204
  * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
@@ -14279,6 +14358,481 @@ function ensureCommentGutterStyles(doc) {
14279
14358
  (doc.head ?? doc.documentElement).appendChild(style);
14280
14359
  }
14281
14360
 
14361
+ // src/ribbon-history.ts
14362
+ var RibbonHistory = class {
14363
+ constructor(ribbon, options) {
14364
+ this.ribbon = ribbon;
14365
+ this.options = options;
14366
+ this.events = new AbortController();
14367
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14368
+ this.destroyed = false;
14369
+ this.installing = false;
14370
+ this.dirty = false;
14371
+ this.generation = 0;
14372
+ this.captured = -1;
14373
+ this.savedVersion = null;
14374
+ this.observedVersion = null;
14375
+ this.capturedVersion = null;
14376
+ const doc = ribbon.element.ownerDocument;
14377
+ this.prefix = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:document:`;
14378
+ this.lastKey = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:workspace:${options.workspaceId ?? crypto.randomUUID()}`;
14379
+ const style = doc.createElement("style");
14380
+ style.textContent = HISTORY_DRAWER_CSS;
14381
+ this.dialog = doc.createElement("dialog");
14382
+ this.dialog.className = "dxr-history-dialog";
14383
+ this.dialog.setAttribute("aria-label", "Version history");
14384
+ const header = doc.createElement("div");
14385
+ header.className = "dxr-history-header";
14386
+ this.dialog.append(header);
14387
+ const close = this.button("Close version history", () => this.dialog.close(), header);
14388
+ close.className = "dxr-history-close";
14389
+ close.textContent = "Close \u2715";
14390
+ this.source = doc.createElement("p");
14391
+ this.source.className = "dxr-history-source";
14392
+ this.status = doc.createElement("p");
14393
+ this.status.setAttribute("role", "status");
14394
+ this.status.className = "dxr-history-status";
14395
+ const recentLabel = doc.createElement("label");
14396
+ recentLabel.textContent = "Saved documents on this device";
14397
+ this.recent = doc.createElement("select");
14398
+ this.recent.setAttribute("aria-label", recentLabel.textContent);
14399
+ recentLabel.append(this.recent);
14400
+ this.recent.addEventListener("change", () => this.command(async () => {
14401
+ const selectedId = this.recent.value;
14402
+ this.recent.value = this.identity.id;
14403
+ const selected = this.readIdentity(localStorage.getItem(this.prefix + selectedId));
14404
+ if (!selected || !this.confirmReplace()) return;
14405
+ await this.loadSaved(selected);
14406
+ await this.mountPanel();
14407
+ this.updateRecent();
14408
+ }), { signal: this.events.signal });
14409
+ this.dialog.append(this.source, this.status, recentLabel);
14410
+ this.resumeButton = this.button("Continue editing this document", () => this.command(() => this.importArchive()), this.dialog);
14411
+ this.back = this.button("Back to my document", () => this.command(async () => {
14412
+ await this.closeArchive();
14413
+ await this.mountPanel();
14414
+ }), this.dialog);
14415
+ this.body = doc.createElement("div");
14416
+ this.dialog.append(this.body);
14417
+ this.previewDialog = doc.createElement("dialog");
14418
+ this.previewDialog.className = "dxr-version-preview";
14419
+ this.previewDialog.setAttribute("aria-label", "Version preview");
14420
+ this.panelObserver = new MutationObserver(() => this.syncBusy());
14421
+ ribbon.element.append(style, this.dialog, this.previewDialog);
14422
+ for (const dialog of [this.dialog, this.previewDialog]) {
14423
+ dialog.addEventListener("keydown", (event) => event.stopPropagation(), { signal: this.events.signal });
14424
+ dialog.addEventListener("click", (event) => {
14425
+ if (event.target === dialog) {
14426
+ const rect = dialog.getBoundingClientRect();
14427
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
14428
+ }
14429
+ }, { signal: this.events.signal });
14430
+ }
14431
+ this.dialog.addEventListener("close", () => ribbon.control("history")?.focus(), { signal: this.events.signal });
14432
+ ribbon.control("history")?.addEventListener("click", () => {
14433
+ void this.show();
14434
+ }, { signal: this.events.signal });
14435
+ doc.defaultView?.addEventListener("beforeunload", (event) => {
14436
+ if (this.hasUnsavedChanges || this.busy) event.preventDefault();
14437
+ }, { signal: this.events.signal });
14438
+ doc.defaultView?.addEventListener("pagehide", (event) => {
14439
+ if (!event.persisted) void this.destroy();
14440
+ }, { signal: this.events.signal });
14441
+ }
14442
+ get busy() {
14443
+ return !!this.active || this.panel?.element.getAttribute("aria-busy") === "true";
14444
+ }
14445
+ get hasUnsavedChanges() {
14446
+ return this.dirty || (this.ribbon.editor?.version ?? null) !== this.savedVersion;
14447
+ }
14448
+ /** Called by the ribbon before a programmatic replacement. */
14449
+ beforeOpen() {
14450
+ if (!this.installing && this.busy) throw new DocxHistoryError("Busy", "Finish the version action before opening another document.");
14451
+ }
14452
+ /** A new DOCX is a new document identity, even if its filename matches another document. */
14453
+ documentOpened(name) {
14454
+ if (this.installing) return;
14455
+ this.identity = { id: crypto.randomUUID(), name };
14456
+ this.draft = void 0;
14457
+ this.dirty = false;
14458
+ this.generation++;
14459
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14460
+ this.dialog.close();
14461
+ this.previewDialog.close();
14462
+ void this.closeArchive();
14463
+ }
14464
+ edited() {
14465
+ const version = this.ribbon.editor?.version ?? null;
14466
+ if (version !== null && version === this.observedVersion) return;
14467
+ this.observedVersion = version;
14468
+ this.dirty = true;
14469
+ this.generation++;
14470
+ }
14471
+ /** Restores a saved workspace only when requested by the host, before exposing the editor. */
14472
+ async resume() {
14473
+ if (!this.options.workspaceId) return;
14474
+ try {
14475
+ const previous = this.readIdentity(localStorage.getItem(this.lastKey));
14476
+ if (previous) await this.run(() => this.loadSaved(previous));
14477
+ } catch (error) {
14478
+ this.ribbon.setStatus("Your document is open. Version history is unavailable on this device.");
14479
+ this.status.textContent = this.explain(error);
14480
+ }
14481
+ }
14482
+ async show() {
14483
+ if (this.destroyed) return;
14484
+ if (!this.dialog.open) this.dialog.showModal();
14485
+ if (this.busy) return;
14486
+ await this.run(async () => {
14487
+ await this.mountPanel();
14488
+ this.updateRecent();
14489
+ }).catch(() => {
14490
+ });
14491
+ }
14492
+ async openFile(file) {
14493
+ await this.run(async () => {
14494
+ if (/\.docxhistory$/i.test(file.name)) {
14495
+ if (file.size > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History file is too large.");
14496
+ const bytes = new Uint8Array(await file.arrayBuffer());
14497
+ const reader = await this.options.openArchive(bytes);
14498
+ try {
14499
+ await reader.read();
14500
+ } catch (error) {
14501
+ reader.close();
14502
+ throw error;
14503
+ }
14504
+ await this.closeArchive();
14505
+ this.archive = { reader, bytes, name: file.name };
14506
+ if (!this.dialog.open) this.dialog.showModal();
14507
+ await this.mountPanel();
14508
+ this.updateRecent();
14509
+ } else {
14510
+ if (!/\.docx$/i.test(file.name)) throw new Error("Choose a Word document or a document with version history.");
14511
+ if (!this.confirmReplace()) return;
14512
+ const bytes = new Uint8Array(await file.arrayBuffer());
14513
+ this.install(bytes, { id: crypto.randomUUID(), name: file.name });
14514
+ this.draft = void 0;
14515
+ this.dirty = true;
14516
+ await this.closeArchive();
14517
+ this.dialog.close();
14518
+ }
14519
+ }).catch(() => {
14520
+ });
14521
+ }
14522
+ async newDocument() {
14523
+ await this.run(async () => {
14524
+ if (!this.confirmReplace()) return;
14525
+ this.installing = true;
14526
+ try {
14527
+ this.ribbon.openBlank("Untitled.docx");
14528
+ } finally {
14529
+ this.installing = false;
14530
+ }
14531
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14532
+ this.draft = void 0;
14533
+ this.dirty = false;
14534
+ this.generation++;
14535
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14536
+ try {
14537
+ localStorage.removeItem(this.lastKey);
14538
+ } catch {
14539
+ }
14540
+ await this.closeArchive();
14541
+ this.dialog.close();
14542
+ }).catch(() => {
14543
+ });
14544
+ }
14545
+ async destroy() {
14546
+ if (this.destroyed) return;
14547
+ this.destroyed = true;
14548
+ this.events.abort();
14549
+ this.dialog.remove();
14550
+ this.previewDialog.remove();
14551
+ this.panelObserver.disconnect();
14552
+ await this.active?.catch(() => {
14553
+ });
14554
+ await this.closeArchive();
14555
+ this.preview?.destroy();
14556
+ this.client?.close();
14557
+ this.store?.close();
14558
+ }
14559
+ async ensureStorage() {
14560
+ if (this.client) return;
14561
+ const store = await openIndexedDbHistoryStore(this.options.storageName ?? "docxodus-editor");
14562
+ try {
14563
+ this.client = this.options.openHistory(store.storage);
14564
+ this.store = store;
14565
+ } catch (error) {
14566
+ store.close();
14567
+ throw error;
14568
+ }
14569
+ }
14570
+ async ensureDraft() {
14571
+ await this.ensureStorage();
14572
+ if (!this.draft) {
14573
+ const document2 = this.client.document(this.identity.id);
14574
+ this.draft = { ...this.identity, checkpoints: await HistoryCheckpoints.open(document2, this.store.journal(this.identity.id)) };
14575
+ }
14576
+ return this.draft;
14577
+ }
14578
+ async loadSaved(identity) {
14579
+ await this.ensureStorage();
14580
+ const document2 = this.client.document(identity.id);
14581
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id));
14582
+ const pending = checkpoints.pendingRequest;
14583
+ const view = checkpoints.view;
14584
+ if (!view && pending?.kind !== "save") throw new Error("This saved document is no longer on this device.");
14585
+ const bytes = pending?.kind === "save" ? pending.bytes : await document2.exportDocx(view.version.id);
14586
+ this.install(bytes, identity);
14587
+ this.draft = { ...identity, checkpoints };
14588
+ this.dirty = pending?.kind === "save";
14589
+ if (pending?.kind === "save") this.captureState(pending.bytes);
14590
+ await this.closeArchive();
14591
+ this.remember();
14592
+ }
14593
+ install(bytes, identity) {
14594
+ if (this.destroyed) throw new DocxHistoryError("Closed", "The editor is closed.");
14595
+ this.installing = true;
14596
+ try {
14597
+ this.ribbon.open(bytes, identity.name);
14598
+ } finally {
14599
+ this.installing = false;
14600
+ }
14601
+ this.identity = identity;
14602
+ this.dirty = false;
14603
+ this.generation++;
14604
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14605
+ }
14606
+ async mountPanel() {
14607
+ const current = this.archive ?? await this.ensureDraft();
14608
+ const writable = "checkpoints" in current;
14609
+ const holder = this.body.ownerDocument.createElement("div");
14610
+ const panel = mountHistoryControls(holder, {
14611
+ reader: writable ? current.checkpoints.document : current.reader,
14612
+ checkpoints: writable ? current.checkpoints : void 0,
14613
+ author: this.options.author,
14614
+ documentName: current.name,
14615
+ capture: writable ? () => {
14616
+ const bytes = this.ribbon.save();
14617
+ if (!bytes) throw new Error("Open a document first.");
14618
+ this.remember();
14619
+ this.captureState(bytes);
14620
+ return bytes;
14621
+ } : void 0,
14622
+ confirmRestore: writable ? (title) => this.confirm(`Restore ${title}?${this.hasUnsavedChanges ? "\n\nYour unsaved changes will be replaced." : ""}
14623
+
14624
+ All saved versions will be kept.`) : void 0,
14625
+ onCheckpoint: async (view, action, request) => {
14626
+ if (this.destroyed) return;
14627
+ const savedCapture = action === "save" || action === "retry" && request?.kind === "save" && this.capturedBytes && sameBytes(this.capturedBytes, request.bytes);
14628
+ if (savedCapture && this.captured === this.generation && this.capturedVersion === (this.ribbon.editor?.version ?? null)) {
14629
+ this.dirty = false;
14630
+ this.savedVersion = this.capturedVersion;
14631
+ }
14632
+ this.captured = -1;
14633
+ this.capturedBytes = void 0;
14634
+ if ((action === "restore" || action === "retry" && request?.kind === "restore") && writable)
14635
+ this.install(await current.checkpoints.document.exportDocx(view.version.id), this.identity);
14636
+ this.remember();
14637
+ this.updateRecent();
14638
+ },
14639
+ restoreUpdatesDraft: true,
14640
+ preview: (bytes, title) => this.showPreview(bytes, title)
14641
+ });
14642
+ try {
14643
+ await panel.ready;
14644
+ } catch (error) {
14645
+ await panel.destroy();
14646
+ throw error;
14647
+ }
14648
+ if (this.destroyed) {
14649
+ await panel.destroy();
14650
+ return;
14651
+ }
14652
+ await this.panel?.destroy();
14653
+ this.panel = panel;
14654
+ this.body.replaceChildren(holder);
14655
+ this.panelObserver.disconnect();
14656
+ this.panelObserver.observe(panel.element, { attributes: true, attributeFilter: ["aria-busy"] });
14657
+ this.source.textContent = current.name;
14658
+ this.resumeButton.hidden = this.back.hidden = writable;
14659
+ this.status.textContent = writable ? "Your saved versions stay on this device." : "You\u2019re browsing a history file. Your open document is safe.";
14660
+ }
14661
+ async showPreview(bytes, title) {
14662
+ const doc = this.previewDialog.ownerDocument;
14663
+ const holder = doc.createElement("div");
14664
+ holder.className = "dxr-version-paper";
14665
+ const preview = await this.options.preview(holder, bytes);
14666
+ if (this.destroyed) {
14667
+ preview.destroy();
14668
+ return;
14669
+ }
14670
+ const heading = doc.createElement("h2");
14671
+ heading.textContent = title;
14672
+ const actions = doc.createElement("div");
14673
+ actions.className = "dxr-version-actions";
14674
+ this.button("Back to version history", () => this.previewDialog.close(), actions);
14675
+ if (!this.archive) this.button("Use as draft", () => this.command(async () => {
14676
+ if (!this.confirmReplace()) return;
14677
+ this.install(bytes, this.identity);
14678
+ this.dirty = true;
14679
+ this.previewDialog.close();
14680
+ this.dialog.close();
14681
+ }), actions);
14682
+ this.button("Download this version", () => download(bytes, "version.docx", doc), actions);
14683
+ this.preview?.destroy();
14684
+ this.preview = preview;
14685
+ this.previewDialog.replaceChildren(heading, actions, holder);
14686
+ if (!this.previewDialog.open) this.previewDialog.showModal();
14687
+ }
14688
+ async importArchive() {
14689
+ if (!this.archive || !this.confirmReplace()) return;
14690
+ await this.ensureStorage();
14691
+ const imported = await this.client.importHistoryArchive(this.archive.bytes);
14692
+ const identity = { id: imported.archive.documentId, name: this.archive.name.replace(/\.docxhistory$/i, ".docx") };
14693
+ const document2 = this.client.document(identity.id);
14694
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id), imported.view);
14695
+ this.install(await document2.exportDocx(imported.view.version.id), identity);
14696
+ this.draft = { ...identity, checkpoints };
14697
+ this.remember();
14698
+ await this.closeArchive();
14699
+ await this.mountPanel();
14700
+ this.updateRecent();
14701
+ }
14702
+ async closeArchive() {
14703
+ this.panelObserver.disconnect();
14704
+ const panel = this.panel;
14705
+ this.panel = void 0;
14706
+ const archive = this.archive;
14707
+ this.archive = void 0;
14708
+ await panel?.destroy();
14709
+ archive?.reader.close();
14710
+ }
14711
+ remember() {
14712
+ localStorage.setItem(this.prefix + this.identity.id, JSON.stringify(this.identity));
14713
+ localStorage.setItem(this.lastKey, JSON.stringify(this.identity));
14714
+ }
14715
+ readIdentity(value) {
14716
+ if (!value) return null;
14717
+ const identity = JSON.parse(value);
14718
+ if (typeof identity?.id !== "string" || !identity.id || typeof identity?.name !== "string") return null;
14719
+ return { id: identity.id, name: identity.name };
14720
+ }
14721
+ updateRecent() {
14722
+ this.recent.replaceChildren(new Option("Choose a saved document\u2026", ""));
14723
+ for (let i = 0; i < localStorage.length; i++) {
14724
+ const key = localStorage.key(i);
14725
+ if (!key.startsWith(this.prefix)) continue;
14726
+ try {
14727
+ const identity = this.readIdentity(localStorage.getItem(key));
14728
+ if (identity) this.recent.append(new Option(identity.name, identity.id));
14729
+ } catch {
14730
+ }
14731
+ }
14732
+ this.recent.value = this.identity.id;
14733
+ this.recent.parentElement.hidden = this.recent.options.length <= 1;
14734
+ }
14735
+ confirm(message) {
14736
+ return this.dialog.ownerDocument.defaultView?.confirm(message) ?? false;
14737
+ }
14738
+ captureState(bytes) {
14739
+ this.captured = this.generation;
14740
+ this.capturedVersion = this.ribbon.editor?.version ?? null;
14741
+ this.capturedBytes = bytes.slice();
14742
+ }
14743
+ confirmReplace() {
14744
+ return !this.hasUnsavedChanges || this.confirm("Replace your open document? Save a version or download it first to keep your unsaved changes.");
14745
+ }
14746
+ explain(error) {
14747
+ return historyControlError(error, this.draft?.checkpoints.hasPending);
14748
+ }
14749
+ command(action) {
14750
+ void this.run(action).catch(() => {
14751
+ });
14752
+ }
14753
+ syncBusy() {
14754
+ const busy = this.busy;
14755
+ this.ribbon.surface.inert = busy;
14756
+ const chrome = this.ribbon.element.querySelector(".dxr-chrome");
14757
+ if (chrome) chrome.inert = busy;
14758
+ this.recent.disabled = this.resumeButton.disabled = this.back.disabled = busy;
14759
+ const file = this.ribbon.control("file");
14760
+ if (file) file.disabled = busy;
14761
+ const create = this.ribbon.control("new");
14762
+ if (create) create.disabled = busy;
14763
+ }
14764
+ async run(action) {
14765
+ if (this.destroyed || this.busy) return;
14766
+ const work = Promise.resolve().then(action);
14767
+ this.active = work;
14768
+ this.dialog.setAttribute("aria-busy", "true");
14769
+ this.body.inert = true;
14770
+ this.syncBusy();
14771
+ this.ribbon.element.querySelector(".dxr-chrome").inert = true;
14772
+ this.ribbon.surface.inert = true;
14773
+ try {
14774
+ await work;
14775
+ } catch (error) {
14776
+ this.status.textContent = this.explain(error);
14777
+ this.ribbon.setStatus(this.explain(error));
14778
+ throw error;
14779
+ } finally {
14780
+ this.active = void 0;
14781
+ this.body.inert = false;
14782
+ this.dialog.setAttribute("aria-busy", "false");
14783
+ this.syncBusy();
14784
+ this.ribbon.element.querySelector(".dxr-chrome")?.removeAttribute("inert");
14785
+ this.ribbon.surface.inert = false;
14786
+ }
14787
+ }
14788
+ button(label, action, parent) {
14789
+ const button = parent.ownerDocument.createElement("button");
14790
+ button.type = "button";
14791
+ button.textContent = label;
14792
+ button.setAttribute("aria-label", label);
14793
+ button.addEventListener("click", action, { signal: this.events.signal });
14794
+ parent.append(button);
14795
+ return button;
14796
+ }
14797
+ };
14798
+ function download(bytes, name, doc) {
14799
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
14800
+ const link = doc.createElement("a");
14801
+ link.href = url;
14802
+ link.download = name;
14803
+ link.click();
14804
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
14805
+ }
14806
+ function sameBytes(left2, right2) {
14807
+ return left2.length === right2.length && left2.every((value, index) => value === right2[index]);
14808
+ }
14809
+ var HISTORY_DRAWER_CSS = `
14810
+ .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}
14811
+ .dxr-history-dialog{width:min(420px,calc(100vw - 24px));max-height:calc(100dvh - 24px);margin:12px 12px 12px auto;border-radius:18px}
14812
+ .dxr-history-dialog::backdrop,.dxr-version-preview::backdrop{background:#16323333;backdrop-filter:blur(2px)}
14813
+ .dxr-history-dialog [hidden],.dxr-version-preview [hidden]{display:none!important}
14814
+ .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}
14815
+ .dxr-history-dialog button:hover,.dxr-version-preview button:hover{background:#eff8f6}
14816
+ .dxr-history-dialog :focus-visible,.dxr-version-preview :focus-visible{outline:3px solid #0f766e;outline-offset:3px}
14817
+ .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}
14818
+ .dxr-history-dialog .dxr-history-close{border:0;font-size:13px;background:#fff}
14819
+ .dxr-history-source{font-weight:600;overflow-wrap:anywhere;margin:12px 0 4px}
14820
+ .dxr-history-status{color:#597273;margin:4px 0 18px}
14821
+ .dxr-history-dialog>label{display:block;font-size:12px;color:#597273;margin:12px 0}
14822
+ .dxr-history-dialog>label select{display:block;width:100%;font:inherit;padding:8px;border:1px solid #cbd9d9;border-radius:8px;background:#fff;color:#243b42}
14823
+ .dxr-history-dialog .dx-history{padding:0;border:0;border-radius:0;background:transparent}
14824
+ .dxr-history-dialog .dx-history h2{font-size:24px;letter-spacing:-.6px;margin:12px 0}
14825
+ .dxr-history-dialog .dx-history button[data-history-action=save]{background:#0f766e;border-color:#0f766e;color:#fff;font-weight:600}
14826
+ .dxr-history-dialog .dx-history select{border-radius:9px;background:#f8fbfa}
14827
+ .dxr-history-dialog .dx-history option{padding:10px;font-size:13px}
14828
+ .dxr-version-preview{width:min(1080px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border-radius:18px}
14829
+ .dxr-version-preview h2{font-size:20px;margin:0 0 12px;overflow-wrap:anywhere}
14830
+ .dxr-version-actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
14831
+ .dxr-version-paper{max-height:70dvh;overflow:auto;background:#f2f6f5;padding:20px;border-radius:10px}
14832
+ @media(max-width:520px){.dxr-history-dialog,.dxr-version-preview{padding:16px}.dxr-version-paper{padding:8px}}
14833
+ @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)}}}
14834
+ `;
14835
+
14282
14836
  // src/ribbon-chrome.ts
14283
14837
  var RIBBON_STYLE_VERSION = "11";
14284
14838
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
@@ -14729,9 +15283,9 @@ var RIBBON_CSS = `
14729
15283
  Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
14730
15284
  .dxr-surface .docx-hf-band {
14731
15285
  /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
14732
- custom property the viewport publishes rather than stretching to the whole surface. */
15286
+ custom property the viewport publishes, including when the page exceeds the surface. */
14733
15287
  position: relative;
14734
- width: min(100%, var(--docx-sheet-width, 100%));
15288
+ width: var(--docx-sheet-width, 100%);
14735
15289
  margin: 0 auto;
14736
15290
  padding: 34px 72px 12px;
14737
15291
  background: var(--dxr-sheet);
@@ -14790,7 +15344,9 @@ var RIBBON_CSS = `
14790
15344
  vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
14791
15345
  has zoomed to fit stays centered at its scaled width. */
14792
15346
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
14793
- max-width: 100%;
15347
+ /* CSS zoom scales the authored page width. A percentage cap would shrink only the
15348
+ paper to the host while its fixed-width section and content keep magnifying. */
15349
+ max-width: none;
14794
15350
  margin: 0 auto;
14795
15351
  padding: 56px 0;
14796
15352
  border-radius: 3px;
@@ -15157,6 +15713,7 @@ var RIBBON_HTML = `
15157
15713
  <label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
15158
15714
  <button type="button" data-dxr="save" disabled>Save</button>
15159
15715
  </div>
15716
+ <button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
15160
15717
  <div class="dxr-quick">
15161
15718
  <button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">&#8630;</button>
15162
15719
  <button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">&#8631;</button>
@@ -15991,6 +16548,7 @@ var RibbonSurface = class {
15991
16548
  this.resizeObserver = new ResizeObserver(() => this.applyChrome());
15992
16549
  this.resizeObserver.observe(root);
15993
16550
  }
16551
+ this.history = options.history ? new RibbonHistory(this, options.history) : null;
15994
16552
  }
15995
16553
  // ── element lookup ──────────────────────────────────────────────────────────
15996
16554
  control(name) {
@@ -16015,6 +16573,14 @@ var RibbonSurface = class {
16015
16573
  this.element.querySelector("[data-dxr-files]")?.remove();
16016
16574
  }
16017
16575
  if (!this.loaderOptions) this.control("loader")?.remove();
16576
+ if (!this.options.history) this.control("history")?.remove();
16577
+ else {
16578
+ const file = this.control("file");
16579
+ if (file) {
16580
+ file.accept = ".docx,.docxhistory";
16581
+ file.setAttribute("aria-label", "Open a document or history file");
16582
+ }
16583
+ }
16018
16584
  this.require("docname").textContent = this.documentName;
16019
16585
  this.require("paginated").checked = this.options.paginated ?? false;
16020
16586
  this.require("headerfooter").checked = this.headerFooter;
@@ -16196,22 +16762,13 @@ var RibbonSurface = class {
16196
16762
  }
16197
16763
  open(bytes, name) {
16198
16764
  if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
16199
- if (this.live) {
16200
- try {
16201
- this.live.close();
16202
- } catch {
16203
- }
16204
- this.live = null;
16205
- }
16206
- if (name) this.documentName = name;
16207
- this.require("docname").textContent = this.documentName;
16765
+ this.history?.beforeOpen();
16208
16766
  const paginated = this.require("paginated").checked;
16209
- this.surface.dataset.view = paginated ? "paginated" : "continuous";
16210
- this.surface.replaceChildren();
16211
- this.closeFindBar();
16767
+ const candidateSurface = this.surface.cloneNode(false);
16768
+ candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
16212
16769
  const started = performance.now();
16213
16770
  const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
16214
- this.live = DocxEditor.open(this.surface, bytes, this.exports, {
16771
+ const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
16215
16772
  cssPrefix: this.options.cssPrefix,
16216
16773
  fabricateClasses: this.options.fabricateClasses,
16217
16774
  editable: this.options.editable,
@@ -16219,10 +16776,14 @@ var RibbonSurface = class {
16219
16776
  columnWidth: this.options.columnWidth,
16220
16777
  fitToWidth: this.options.fitToWidth,
16221
16778
  onEdit: (info) => {
16779
+ this.history?.edited();
16222
16780
  this.options.onEdit?.(info);
16223
16781
  this.scheduleStats();
16224
16782
  },
16225
- onMove: this.options.onMove,
16783
+ onMove: (info) => {
16784
+ this.history?.edited();
16785
+ this.options.onMove?.(info);
16786
+ },
16226
16787
  onStoryChange: (which) => this.onStoryChange(which),
16227
16788
  onCommentsChange: (info) => this.onCommentsChange(info),
16228
16789
  paginated,
@@ -16233,6 +16794,14 @@ var RibbonSurface = class {
16233
16794
  comments: this.options.comments,
16234
16795
  commentAuthor: this.author
16235
16796
  });
16797
+ this.live?.close();
16798
+ candidate.adoptContainer(this.surface);
16799
+ this.surface.dataset.view = paginated ? "paginated" : "continuous";
16800
+ this.live = candidate;
16801
+ if (name) this.documentName = name;
16802
+ this.require("docname").textContent = this.documentName;
16803
+ this.closeFindBar();
16804
+ this.history?.documentOpened(this.documentName);
16236
16805
  const saveButton = this.control("save");
16237
16806
  if (saveButton) saveButton.disabled = false;
16238
16807
  this.require("ribbon").setAttribute("aria-disabled", "false");
@@ -16280,6 +16849,7 @@ var RibbonSurface = class {
16280
16849
  destroy() {
16281
16850
  if (this.destroyed) return;
16282
16851
  this.destroyed = true;
16852
+ void this.history?.destroy();
16283
16853
  const doc = this.element.ownerDocument ?? document;
16284
16854
  doc.removeEventListener("selectionchange", this.onSelectionChange);
16285
16855
  doc.removeEventListener("mousedown", this.onDocumentMouseDown);
@@ -16316,6 +16886,7 @@ var RibbonSurface = class {
16316
16886
  const el = this.control("railOp");
16317
16887
  if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
16318
16888
  this.options.onCommand?.(label, ms);
16889
+ this.history?.edited();
16319
16890
  this.refreshRailCounts();
16320
16891
  this.refreshRailAnchor();
16321
16892
  this.scheduleStats();
@@ -16515,11 +17086,24 @@ var RibbonSurface = class {
16515
17086
  file?.addEventListener("change", async () => {
16516
17087
  const chosen = file.files?.[0];
16517
17088
  if (!chosen) return;
17089
+ file.value = "";
17090
+ if (this.history) {
17091
+ await this.history.openFile(chosen);
17092
+ return;
17093
+ }
16518
17094
  this.setStatus(`Loading ${chosen.name}\u2026`);
16519
- this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17095
+ try {
17096
+ this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17097
+ } catch (error) {
17098
+ this.setStatus(`Could not open this document. ${String(error)}`);
17099
+ }
16520
17100
  file.value = "";
16521
17101
  });
16522
17102
  this.control("new")?.addEventListener("click", () => {
17103
+ if (this.history) {
17104
+ void this.history.newDocument();
17105
+ return;
17106
+ }
16523
17107
  if (this.exports) this.openBlank("untitled.docx");
16524
17108
  });
16525
17109
  this.control("save")?.addEventListener("click", () => this.download());
@@ -16530,6 +17114,7 @@ var RibbonSurface = class {
16530
17114
  if (!chosen || !this.live) return;
16531
17115
  const started = performance.now();
16532
17116
  const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
17117
+ if (ok) this.history?.edited();
16533
17118
  const ms = performance.now() - started;
16534
17119
  const el = this.control("railOp");
16535
17120
  if (el) el.textContent = `picture ${Math.round(ms)} ms`;
@@ -18992,7 +19577,7 @@ function nameFromSource(source) {
18992
19577
  }
18993
19578
  async function createRibbonEditor(container, source, options = {}) {
18994
19579
  const el = resolveContainer2(container);
18995
- const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
19580
+ const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
18996
19581
  const mount2 = createScopedMount(el);
18997
19582
  mount2.root.style.height = "100%";
18998
19583
  mount2.root.style.minHeight = "0";
@@ -19002,6 +19587,12 @@ async function createRibbonEditor(container, source, options = {}) {
19002
19587
  // boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
19003
19588
  frame: "card",
19004
19589
  ...ribbonOptions,
19590
+ history: history ? {
19591
+ ...typeof history === "object" ? history : {},
19592
+ openHistory: openDocxHistory,
19593
+ openArchive: openDocxHistoryArchive,
19594
+ preview: (container2, bytes) => createViewer(container2, bytes, { wasmBasePath: wasmBasePath2, renderTrackedChanges: true })
19595
+ } : void 0,
19005
19596
  // Exports arrive after the runtime boots; the loader covers that gap.
19006
19597
  exports: void 0
19007
19598
  });
@@ -19019,6 +19610,7 @@ async function createRibbonEditor(container, source, options = {}) {
19019
19610
  ribbon.loader.stage(2);
19020
19611
  if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
19021
19612
  else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
19613
+ await ribbon.history?.resume();
19022
19614
  ribbon.loader.stage(3);
19023
19615
  ribbon.loader.done();
19024
19616
  return ribbon;