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
@@ -2485,6 +2485,10 @@ var Docxodus = (() => {
2485
2485
  get hasPending() {
2486
2486
  return this.request !== null;
2487
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
+ }
2488
2492
  get needsRefresh() {
2489
2493
  return this.stale;
2490
2494
  }
@@ -2731,25 +2735,26 @@ var Docxodus = (() => {
2731
2735
  const doc = container.ownerDocument;
2732
2736
  this.element = doc.createElement("section");
2733
2737
  this.element.className = "dx-history";
2734
- this.element.setAttribute("aria-label", "Document history");
2738
+ this.element.setAttribute("aria-label", "Version history");
2735
2739
  const style = doc.createElement("style");
2736
2740
  style.textContent = HISTORY_CSS;
2737
2741
  const title = doc.createElement("h2");
2738
- title.textContent = "Document history";
2742
+ title.textContent = "Version history";
2739
2743
  this.status = doc.createElement("p");
2740
2744
  this.status.setAttribute("role", "status");
2741
2745
  this.status.setAttribute("aria-live", "polite");
2742
2746
  this.fieldset = doc.createElement("fieldset");
2743
2747
  const legend = doc.createElement("legend");
2744
- legend.textContent = "Versions and checkpoints";
2748
+ legend.textContent = "Saved versions";
2745
2749
  this.fieldset.append(legend);
2746
2750
  this.element.append(style, title, this.status, this.fieldset);
2747
2751
  container.append(this.element);
2748
2752
  const note = doc.createElement("p");
2753
+ note.dataset.historyExisting = "";
2749
2754
  note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2750
2755
  this.fieldset.append(note);
2751
2756
  this.button("refresh", "Refresh history", () => this.load(true));
2752
- this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2757
+ this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2753
2758
  this.versions = this.select("Version");
2754
2759
  this.versions.size = 6;
2755
2760
  this.detail = doc.createElement("p");
@@ -2757,56 +2762,80 @@ var Docxodus = (() => {
2757
2762
  this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2758
2763
  this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2759
2764
  this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2760
- this.before = this.select("Compare from");
2765
+ const comparison = this.disclosure("Compare versions");
2766
+ this.before = this.select("Compare from", comparison);
2761
2767
  this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2762
2768
  const compareNote = doc.createElement("p");
2763
2769
  compareNote.textContent = "Compare from this version to the selected version above.";
2764
- this.fieldset.append(compareNote);
2770
+ comparison.append(compareNote);
2765
2771
  this.button("compare", "Compare versions", async () => {
2766
2772
  const before = this.records[Number(this.before.value)];
2767
2773
  await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2768
- });
2774
+ }, comparison);
2769
2775
  this.button("more", "Load older versions", async () => {
2770
2776
  if (this.next) await this.page(this.next, false);
2771
2777
  });
2772
- this.author = this.input("Your name", "text", options.author ?? "You");
2773
- this.label = this.input("Checkpoint name (optional)", "text");
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;
2774
2789
  this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2775
- this.button("save", "Save checkpoint", async () => {
2790
+ this.button("save", "Save version", async () => {
2776
2791
  const view = await options.checkpoints.save(await options.capture(), this.metadata());
2777
2792
  await options.onCheckpoint?.(view, "save");
2778
2793
  await this.load(false);
2779
2794
  this.label.value = "";
2780
- this.status.textContent = "Checkpoint saved. Your draft remains open.";
2781
- });
2795
+ this.status.textContent = "Version saved. Your draft remains open.";
2796
+ }, saveGroup);
2797
+ saveGroup.append(attribution);
2782
2798
  this.button("restore", "Restore selected", async () => {
2783
2799
  const version = this.selected();
2784
- if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
2800
+ const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
2785
2801
 
2786
- Your current draft and all later versions will be kept.`)) {
2802
+ Your current draft and all later versions will be kept.`);
2803
+ if (!confirmed) {
2787
2804
  this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2788
2805
  return;
2789
2806
  }
2790
2807
  const view = await options.checkpoints.restore(version.id, this.metadata());
2791
2808
  await options.onCheckpoint?.(view, "restore");
2792
2809
  await this.load(false);
2793
- this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
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.";
2794
2811
  });
2795
2812
  const restoreNote = doc.createElement("p");
2796
2813
  restoreNote.hidden = !options.checkpoints;
2797
- restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
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.";
2798
2816
  this.fieldset.append(restoreNote);
2799
- this.button("retry", "Retry checkpoint", async () => {
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
+ }
2800
2826
  const view = await options.checkpoints.retry();
2801
- await options.onCheckpoint?.(view, "retry");
2827
+ await options.onCheckpoint?.(view, "retry", request ?? void 0);
2802
2828
  await this.load(false);
2803
- this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
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.";
2804
2830
  });
2805
2831
  const sharing = this.disclosure("Download with history");
2806
2832
  const sharingNote = doc.createElement("p");
2807
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.";
2808
2834
  sharing.append(sharingNote);
2809
- this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
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);
2810
2839
  const time = this.disclosure("Find a version by time");
2811
2840
  const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2812
2841
  this.button("time", "Preview at time", async () => {
@@ -2856,7 +2885,7 @@ Your current draft and all later versions will be kept.`)) {
2856
2885
  this.versions.replaceChildren();
2857
2886
  this.before.replaceChildren();
2858
2887
  }
2859
- this.status.textContent = this.options.checkpoints?.hasPending ? "A checkpoint needs recovery. Retry it before saving more changes." : this.view ? "History loaded. Select a version to preview, download or restore." : "No checkpoints yet. Save your first checkpoint when you are ready.";
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.";
2860
2889
  }
2861
2890
  async page(cursor, reset) {
2862
2891
  const page = await this.options.reader.listVersions(cursor, this.pageSize);
@@ -2893,6 +2922,11 @@ Your current draft and all later versions will be kept.`)) {
2893
2922
  this.actions.get("restore").hidden = !this.options.checkpoints;
2894
2923
  this.actions.get("restore").disabled = !hasVersion || pending || stale;
2895
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;
2896
2930
  this.detail.textContent = hasVersion ? [
2897
2931
  versionTitle(this.selected()),
2898
2932
  this.selected().record.metadata.message,
@@ -2971,7 +3005,9 @@ Your current draft and all later versions will be kept.`)) {
2971
3005
  }
2972
3006
  }
2973
3007
  button(key, title, action, parent = this.fieldset) {
2974
- this.actions.set(key, this.commandButton(title, action, parent));
3008
+ const button = this.commandButton(title, action, parent);
3009
+ button.dataset.historyAction = key;
3010
+ this.actions.set(key, button);
2975
3011
  }
2976
3012
  commandButton(title, action, parent) {
2977
3013
  const button = parent.ownerDocument.createElement("button");
@@ -2984,13 +3020,13 @@ Your current draft and all later versions will be kept.`)) {
2984
3020
  parent.append(button);
2985
3021
  return button;
2986
3022
  }
2987
- select(title) {
3023
+ select(title, parent = this.fieldset) {
2988
3024
  const label = this.fieldset.ownerDocument.createElement("label");
2989
3025
  label.textContent = title;
2990
3026
  const select = label.ownerDocument.createElement("select");
2991
3027
  select.setAttribute("aria-label", title);
2992
3028
  label.append(select);
2993
- this.fieldset.append(label);
3029
+ parent.append(label);
2994
3030
  return select;
2995
3031
  }
2996
3032
  input(title, type, value = "", parent = this.fieldset) {
@@ -3005,6 +3041,7 @@ Your current draft and all later versions will be kept.`)) {
3005
3041
  }
3006
3042
  disclosure(title) {
3007
3043
  const details = this.fieldset.ownerDocument.createElement("details");
3044
+ details.dataset.historyExisting = "";
3008
3045
  const summary = details.ownerDocument.createElement("summary");
3009
3046
  summary.textContent = title;
3010
3047
  details.append(summary);
@@ -3014,22 +3051,22 @@ Your current draft and all later versions will be kept.`)) {
3014
3051
  };
3015
3052
  function historyControlError(error, pending = false) {
3016
3053
  const code = error instanceof DocxHistoryError ? error.code : "";
3017
- if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
3054
+ if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
3018
3055
  if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
3019
3056
  if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
3020
- if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
3057
+ if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
3021
3058
  if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
3022
3059
  if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
3023
3060
  if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
3024
3061
  return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
3025
3062
  }
3026
3063
  function versionTitle(version) {
3027
- const { metadata, sequence } = version.record;
3028
- return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
3064
+ const { metadata } = version.record;
3065
+ return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
3029
3066
  }
3030
3067
  function formatTime(value) {
3031
3068
  const time = new Date(value);
3032
- return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
3069
+ return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
3033
3070
  }
3034
3071
  var HISTORY_CSS = `
3035
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}
@@ -6268,6 +6305,19 @@ Your current draft and all later versions will be kept.`)) {
6268
6305
  scale: options.scale ?? 1
6269
6306
  };
6270
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
+ }
6271
6321
  /**
6272
6322
  * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
6273
6323
  * Safe to call on every remount; the previous root is released first.
@@ -11201,15 +11251,20 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11201
11251
  revisionAuthor: opts.revisionAuthor
11202
11252
  }));
11203
11253
  const editor = new _DocxEditor(container, exports, handle, opts);
11204
- editor.refreshAnchorMap();
11205
- if (opts.headerFooter) editor.createRegion();
11206
- const fullHtml = editor.renderFullHtml(bytes);
11207
- if (opts.paginated) editor.mountPaginated(fullHtml);
11208
- else editor.mountHtml(fullHtml);
11209
- editor.syncRegionToBody();
11210
- editor.setupBlockDrag();
11211
- if (opts.comments) editor.createGutter();
11212
- 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
+ }
11213
11268
  }
11214
11269
  /**
11215
11270
  * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
@@ -11224,6 +11279,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11224
11279
  this.assertOpen();
11225
11280
  return this.exports.DocxSessionBridge.Save(this.handle);
11226
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
+ }
11227
11288
  /** Release the underlying WASM session. The editor is unusable afterward. */
11228
11289
  close() {
11229
11290
  if (this.closed) return;
@@ -11258,6 +11319,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11258
11319
  get root() {
11259
11320
  return this.container;
11260
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
+ }
11261
11340
  /**
11262
11341
  * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
11263
11342
  * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
@@ -14417,6 +14496,481 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14417
14496
  (doc.head ?? doc.documentElement).appendChild(style);
14418
14497
  }
14419
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
+
14420
14974
  // src/ribbon-chrome.ts
14421
14975
  var RIBBON_STYLE_VERSION = "11";
14422
14976
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
@@ -14867,9 +15421,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14867
15421
  Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
14868
15422
  .dxr-surface .docx-hf-band {
14869
15423
  /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
14870
- 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. */
14871
15425
  position: relative;
14872
- width: min(100%, var(--docx-sheet-width, 100%));
15426
+ width: var(--docx-sheet-width, 100%);
14873
15427
  margin: 0 auto;
14874
15428
  padding: 34px 72px 12px;
14875
15429
  background: var(--dxr-sheet);
@@ -14928,7 +15482,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
14928
15482
  vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
14929
15483
  has zoomed to fit stays centered at its scaled width. */
14930
15484
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
14931
- 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;
14932
15488
  margin: 0 auto;
14933
15489
  padding: 56px 0;
14934
15490
  border-radius: 3px;
@@ -15295,6 +15851,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15295
15851
  <label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
15296
15852
  <button type="button" data-dxr="save" disabled>Save</button>
15297
15853
  </div>
15854
+ <button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
15298
15855
  <div class="dxr-quick">
15299
15856
  <button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">&#8630;</button>
15300
15857
  <button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">&#8631;</button>
@@ -16129,6 +16686,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16129
16686
  this.resizeObserver = new ResizeObserver(() => this.applyChrome());
16130
16687
  this.resizeObserver.observe(root);
16131
16688
  }
16689
+ this.history = options.history ? new RibbonHistory(this, options.history) : null;
16132
16690
  }
16133
16691
  // ── element lookup ──────────────────────────────────────────────────────────
16134
16692
  control(name) {
@@ -16153,6 +16711,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16153
16711
  this.element.querySelector("[data-dxr-files]")?.remove();
16154
16712
  }
16155
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
+ }
16156
16722
  this.require("docname").textContent = this.documentName;
16157
16723
  this.require("paginated").checked = this.options.paginated ?? false;
16158
16724
  this.require("headerfooter").checked = this.headerFooter;
@@ -16334,22 +16900,13 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16334
16900
  }
16335
16901
  open(bytes, name) {
16336
16902
  if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
16337
- if (this.live) {
16338
- try {
16339
- this.live.close();
16340
- } catch {
16341
- }
16342
- this.live = null;
16343
- }
16344
- if (name) this.documentName = name;
16345
- this.require("docname").textContent = this.documentName;
16903
+ this.history?.beforeOpen();
16346
16904
  const paginated = this.require("paginated").checked;
16347
- this.surface.dataset.view = paginated ? "paginated" : "continuous";
16348
- this.surface.replaceChildren();
16349
- this.closeFindBar();
16905
+ const candidateSurface = this.surface.cloneNode(false);
16906
+ candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
16350
16907
  const started = performance.now();
16351
16908
  const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
16352
- this.live = DocxEditor.open(this.surface, bytes, this.exports, {
16909
+ const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
16353
16910
  cssPrefix: this.options.cssPrefix,
16354
16911
  fabricateClasses: this.options.fabricateClasses,
16355
16912
  editable: this.options.editable,
@@ -16357,10 +16914,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16357
16914
  columnWidth: this.options.columnWidth,
16358
16915
  fitToWidth: this.options.fitToWidth,
16359
16916
  onEdit: (info) => {
16917
+ this.history?.edited();
16360
16918
  this.options.onEdit?.(info);
16361
16919
  this.scheduleStats();
16362
16920
  },
16363
- onMove: this.options.onMove,
16921
+ onMove: (info) => {
16922
+ this.history?.edited();
16923
+ this.options.onMove?.(info);
16924
+ },
16364
16925
  onStoryChange: (which) => this.onStoryChange(which),
16365
16926
  onCommentsChange: (info) => this.onCommentsChange(info),
16366
16927
  paginated,
@@ -16371,6 +16932,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16371
16932
  comments: this.options.comments,
16372
16933
  commentAuthor: this.author
16373
16934
  });
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);
16374
16943
  const saveButton = this.control("save");
16375
16944
  if (saveButton) saveButton.disabled = false;
16376
16945
  this.require("ribbon").setAttribute("aria-disabled", "false");
@@ -16418,6 +16987,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16418
16987
  destroy() {
16419
16988
  if (this.destroyed) return;
16420
16989
  this.destroyed = true;
16990
+ void this.history?.destroy();
16421
16991
  const doc = this.element.ownerDocument ?? document;
16422
16992
  doc.removeEventListener("selectionchange", this.onSelectionChange);
16423
16993
  doc.removeEventListener("mousedown", this.onDocumentMouseDown);
@@ -16454,6 +17024,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16454
17024
  const el = this.control("railOp");
16455
17025
  if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
16456
17026
  this.options.onCommand?.(label, ms);
17027
+ this.history?.edited();
16457
17028
  this.refreshRailCounts();
16458
17029
  this.refreshRailAnchor();
16459
17030
  this.scheduleStats();
@@ -16653,11 +17224,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16653
17224
  file?.addEventListener("change", async () => {
16654
17225
  const chosen = file.files?.[0];
16655
17226
  if (!chosen) return;
17227
+ file.value = "";
17228
+ if (this.history) {
17229
+ await this.history.openFile(chosen);
17230
+ return;
17231
+ }
16656
17232
  this.setStatus(`Loading ${chosen.name}\u2026`);
16657
- 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
+ }
16658
17238
  file.value = "";
16659
17239
  });
16660
17240
  this.control("new")?.addEventListener("click", () => {
17241
+ if (this.history) {
17242
+ void this.history.newDocument();
17243
+ return;
17244
+ }
16661
17245
  if (this.exports) this.openBlank("untitled.docx");
16662
17246
  });
16663
17247
  this.control("save")?.addEventListener("click", () => this.download());
@@ -16668,6 +17252,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16668
17252
  if (!chosen || !this.live) return;
16669
17253
  const started = performance.now();
16670
17254
  const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
17255
+ if (ok) this.history?.edited();
16671
17256
  const ms = performance.now() - started;
16672
17257
  const el = this.control("railOp");
16673
17258
  if (el) el.textContent = `picture ${Math.round(ms)} ms`;
@@ -19132,7 +19717,7 @@ ${parsed.documentElement.outerHTML}`;
19132
19717
  }
19133
19718
  async function createRibbonEditor(container, source, options = {}) {
19134
19719
  const el = resolveContainer2(container);
19135
- const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
19720
+ const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
19136
19721
  const mount2 = createScopedMount(el);
19137
19722
  mount2.root.style.height = "100%";
19138
19723
  mount2.root.style.minHeight = "0";
@@ -19142,6 +19727,12 @@ ${parsed.documentElement.outerHTML}`;
19142
19727
  // boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
19143
19728
  frame: "card",
19144
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,
19145
19736
  // Exports arrive after the runtime boots; the loader covers that gap.
19146
19737
  exports: void 0
19147
19738
  });
@@ -19159,6 +19750,7 @@ ${parsed.documentElement.outerHTML}`;
19159
19750
  ribbon.loader.stage(2);
19160
19751
  if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
19161
19752
  else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
19753
+ await ribbon.history?.resume();
19162
19754
  ribbon.loader.stage(3);
19163
19755
  ribbon.loader.done();
19164
19756
  return ribbon;