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
@@ -3269,6 +3269,19 @@ var DocxodusEditor = (() => {
3269
3269
  scale: options.scale ?? 1
3270
3270
  };
3271
3271
  }
3272
+ /** Retarget the mounted document to an equivalent host after its DOM is adopted. */
3273
+ adoptHost(host) {
3274
+ if (host === this.host) return;
3275
+ this.observer?.disconnect();
3276
+ this.observer = null;
3277
+ this.host.style.removeProperty("--docx-sheet-width");
3278
+ this.host = host;
3279
+ this.refresh();
3280
+ if (this.root && typeof ResizeObserver !== "undefined") {
3281
+ this.observer = new ResizeObserver(() => this.refresh());
3282
+ this.observer.observe(this.host);
3283
+ }
3284
+ }
3272
3285
  /**
3273
3286
  * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
3274
3287
  * Safe to call on every remount; the previous root is released first.
@@ -8202,15 +8215,20 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8202
8215
  revisionAuthor: opts.revisionAuthor
8203
8216
  }));
8204
8217
  const editor = new _DocxEditor(container, exports, handle, opts);
8205
- editor.refreshAnchorMap();
8206
- if (opts.headerFooter) editor.createRegion();
8207
- const fullHtml = editor.renderFullHtml(bytes);
8208
- if (opts.paginated) editor.mountPaginated(fullHtml);
8209
- else editor.mountHtml(fullHtml);
8210
- editor.syncRegionToBody();
8211
- editor.setupBlockDrag();
8212
- if (opts.comments) editor.createGutter();
8213
- return editor;
8218
+ try {
8219
+ editor.refreshAnchorMap();
8220
+ if (opts.headerFooter) editor.createRegion();
8221
+ const fullHtml = editor.renderFullHtml(bytes);
8222
+ if (opts.paginated) editor.mountPaginated(fullHtml);
8223
+ else editor.mountHtml(fullHtml);
8224
+ editor.syncRegionToBody();
8225
+ editor.setupBlockDrag();
8226
+ if (opts.comments) editor.createGutter();
8227
+ return editor;
8228
+ } catch (error) {
8229
+ editor.close();
8230
+ throw error;
8231
+ }
8214
8232
  }
8215
8233
  /**
8216
8234
  * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
@@ -8225,6 +8243,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8225
8243
  this.assertOpen();
8226
8244
  return this.exports.DocxSessionBridge.Save(this.handle);
8227
8245
  }
8246
+ /** Monotonic committed document version, when supported by the loaded engine. */
8247
+ get version() {
8248
+ this.assertOpen();
8249
+ const value = this.exports.DocxSessionBridge.GetVersion?.(this.handle);
8250
+ return value ? JSON.parse(value).version : null;
8251
+ }
8228
8252
  /** Release the underlying WASM session. The editor is unusable afterward. */
8229
8253
  close() {
8230
8254
  if (this.closed) return;
@@ -8259,6 +8283,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8259
8283
  get root() {
8260
8284
  return this.container;
8261
8285
  }
8286
+ /**
8287
+ * Move a fully rendered candidate into the host's stable public surface.
8288
+ * Internal editor chrome that binds directly to the container is recreated there;
8289
+ * document blocks and their listeners move with the DOM nodes.
8290
+ */
8291
+ adoptContainer(container) {
8292
+ this.assertOpen();
8293
+ if (container === this.container) return;
8294
+ this.teardownBlockDrag();
8295
+ this.gutter?.dispose();
8296
+ this.gutter = null;
8297
+ const previous = this.container;
8298
+ container.replaceChildren(...Array.from(previous.childNodes));
8299
+ this.container = container;
8300
+ this.viewport.adoptHost(container);
8301
+ this.setupBlockDrag();
8302
+ if (this.options.comments) this.createGutter();
8303
+ }
8262
8304
  /**
8263
8305
  * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
8264
8306
  * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
@@ -11418,6 +11460,1112 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11418
11460
  (doc.head ?? doc.documentElement).appendChild(style);
11419
11461
  }
11420
11462
 
11463
+ // src/history.ts
11464
+ var MAX_HISTORY_ARCHIVE_BYTES = 64 * 1024 * 1024;
11465
+ var DocxHistoryError = class extends Error {
11466
+ constructor(code, message) {
11467
+ super(message);
11468
+ this.code = code;
11469
+ this.name = "DocxHistoryError";
11470
+ }
11471
+ };
11472
+
11473
+ // src/history-checkpoints.ts
11474
+ var HistoryCheckpoints = class _HistoryCheckpoints {
11475
+ constructor(document2, journal) {
11476
+ this.document = document2;
11477
+ this.journal = journal;
11478
+ this.current = null;
11479
+ this.request = null;
11480
+ this.running = false;
11481
+ this.stale = false;
11482
+ }
11483
+ /** Pass an already captured view when opening its exact version in an editor (for example, after import). */
11484
+ static async open(document2, journal, view) {
11485
+ const controls = new _HistoryCheckpoints(document2, journal);
11486
+ controls.request = structuredClone(await journal.read());
11487
+ if (controls.request && controls.request.documentId !== document2.documentId)
11488
+ throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
11489
+ controls.current = view === void 0 ? await document2.read() : structuredClone(view);
11490
+ if (controls.current && controls.current.state.documentId !== document2.documentId)
11491
+ throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
11492
+ return controls;
11493
+ }
11494
+ get view() {
11495
+ return structuredClone(this.current);
11496
+ }
11497
+ get hasPending() {
11498
+ return this.request !== null;
11499
+ }
11500
+ /** Snapshot of the exact request a retry will recover, including its original operation kind. */
11501
+ get pendingRequest() {
11502
+ return structuredClone(this.request);
11503
+ }
11504
+ get needsRefresh() {
11505
+ return this.stale;
11506
+ }
11507
+ async refresh() {
11508
+ return this.exclusive(async () => {
11509
+ this.current = await this.document.read();
11510
+ this.stale = false;
11511
+ return this.view;
11512
+ });
11513
+ }
11514
+ async save(bytes, metadata) {
11515
+ return this.start({
11516
+ kind: "save",
11517
+ head: this.current?.head ?? null,
11518
+ bytes,
11519
+ id: crypto.randomUUID(),
11520
+ documentId: this.document.documentId,
11521
+ metadata
11522
+ });
11523
+ }
11524
+ async restore(target, metadata) {
11525
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
11526
+ return this.start({
11527
+ kind: "restore",
11528
+ head: this.current.head,
11529
+ target,
11530
+ id: crypto.randomUUID(),
11531
+ documentId: this.document.documentId,
11532
+ metadata
11533
+ });
11534
+ }
11535
+ async retry() {
11536
+ return this.exclusive(async () => {
11537
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
11538
+ return this.publish();
11539
+ });
11540
+ }
11541
+ async start(request) {
11542
+ return this.exclusive(async () => {
11543
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
11544
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
11545
+ this.request = structuredClone(request);
11546
+ return this.publish();
11547
+ });
11548
+ }
11549
+ async publish() {
11550
+ const intended = this.request;
11551
+ const request = await this.journal.put(structuredClone(intended));
11552
+ this.request = structuredClone(request);
11553
+ if (request.id !== intended.id)
11554
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
11555
+ let view;
11556
+ try {
11557
+ view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
11558
+ } catch (error) {
11559
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
11560
+ this.stale = true;
11561
+ await this.journal.remove(request.id);
11562
+ this.request = null;
11563
+ }
11564
+ throw error;
11565
+ }
11566
+ this.current = view;
11567
+ await this.journal.remove(request.id);
11568
+ this.request = null;
11569
+ return this.view;
11570
+ }
11571
+ async exclusive(action) {
11572
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
11573
+ this.running = true;
11574
+ try {
11575
+ return await action();
11576
+ } finally {
11577
+ this.running = false;
11578
+ }
11579
+ }
11580
+ };
11581
+
11582
+ // src/history-controls.ts
11583
+ function mountHistoryControls(container, options) {
11584
+ const pageSize = options.pageSize ?? 25;
11585
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
11586
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
11587
+ throw new Error("History controls and checkpoints must use the same document.");
11588
+ return new HistoryPanel(container, options, pageSize);
11589
+ }
11590
+ var HistoryPanel = class {
11591
+ constructor(container, options, pageSize) {
11592
+ this.options = options;
11593
+ this.pageSize = pageSize;
11594
+ this.actions = /* @__PURE__ */ new Map();
11595
+ this.events = new AbortController();
11596
+ this.records = [];
11597
+ this.next = null;
11598
+ this.view = null;
11599
+ this.active = null;
11600
+ this.destroyed = false;
11601
+ const doc = container.ownerDocument;
11602
+ this.element = doc.createElement("section");
11603
+ this.element.className = "dx-history";
11604
+ this.element.setAttribute("aria-label", "Version history");
11605
+ const style = doc.createElement("style");
11606
+ style.textContent = HISTORY_CSS;
11607
+ const title = doc.createElement("h2");
11608
+ title.textContent = "Version history";
11609
+ this.status = doc.createElement("p");
11610
+ this.status.setAttribute("role", "status");
11611
+ this.status.setAttribute("aria-live", "polite");
11612
+ this.fieldset = doc.createElement("fieldset");
11613
+ const legend = doc.createElement("legend");
11614
+ legend.textContent = "Saved versions";
11615
+ this.fieldset.append(legend);
11616
+ this.element.append(style, title, this.status, this.fieldset);
11617
+ container.append(this.element);
11618
+ const note = doc.createElement("p");
11619
+ note.dataset.historyExisting = "";
11620
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
11621
+ this.fieldset.append(note);
11622
+ this.button("refresh", "Refresh history", () => this.load(true));
11623
+ this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
11624
+ this.versions = this.select("Version");
11625
+ this.versions.size = 6;
11626
+ this.detail = doc.createElement("p");
11627
+ this.fieldset.append(this.detail);
11628
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
11629
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
11630
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
11631
+ const comparison = this.disclosure("Compare versions");
11632
+ this.before = this.select("Compare from", comparison);
11633
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
11634
+ const compareNote = doc.createElement("p");
11635
+ compareNote.textContent = "Compare from this version to the selected version above.";
11636
+ comparison.append(compareNote);
11637
+ this.button("compare", "Compare versions", async () => {
11638
+ const before = this.records[Number(this.before.value)];
11639
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
11640
+ }, comparison);
11641
+ this.button("more", "Load older versions", async () => {
11642
+ if (this.next) await this.page(this.next, false);
11643
+ });
11644
+ const saveGroup = doc.createElement("div");
11645
+ saveGroup.className = "dx-history-save";
11646
+ this.fieldset.insertBefore(saveGroup, note);
11647
+ const attribution = doc.createElement("details");
11648
+ const attributionTitle = doc.createElement("summary");
11649
+ attributionTitle.textContent = "Saved by";
11650
+ attribution.append(attributionTitle);
11651
+ saveGroup.append(attribution);
11652
+ this.author = this.input("Your name", "text", options.author ?? "You", attribution);
11653
+ this.label = this.input("Version name (optional)", "text", "", saveGroup);
11654
+ saveGroup.hidden = !options.checkpoints;
11655
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
11656
+ this.button("save", "Save version", async () => {
11657
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
11658
+ await options.onCheckpoint?.(view, "save");
11659
+ await this.load(false);
11660
+ this.label.value = "";
11661
+ this.status.textContent = "Version saved. Your draft remains open.";
11662
+ }, saveGroup);
11663
+ saveGroup.append(attribution);
11664
+ this.button("restore", "Restore selected", async () => {
11665
+ const version = this.selected();
11666
+ const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
11667
+
11668
+ Your current draft and all later versions will be kept.`);
11669
+ if (!confirmed) {
11670
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
11671
+ return;
11672
+ }
11673
+ const view = await options.checkpoints.restore(version.id, this.metadata());
11674
+ await options.onCheckpoint?.(view, "restore");
11675
+ await this.load(false);
11676
+ 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.";
11677
+ });
11678
+ const restoreNote = doc.createElement("p");
11679
+ restoreNote.hidden = !options.checkpoints;
11680
+ if (options.checkpoints) restoreNote.dataset.historyExisting = "";
11681
+ 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.";
11682
+ this.fieldset.append(restoreNote);
11683
+ this.button("retry", "Retry save", async () => {
11684
+ const request = options.checkpoints.pendingRequest;
11685
+ if (request?.kind === "restore" && options.confirmRestore) {
11686
+ const target = await options.reader.getVersion(request.target);
11687
+ if (!await options.confirmRestore(versionTitle(target))) {
11688
+ this.status.textContent = "Restore retry canceled. Your draft is unchanged; the saved action still needs recovery.";
11689
+ return;
11690
+ }
11691
+ }
11692
+ const view = await options.checkpoints.retry();
11693
+ await options.onCheckpoint?.(view, "retry", request ?? void 0);
11694
+ await this.load(false);
11695
+ 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.";
11696
+ });
11697
+ const sharing = this.disclosure("Download with history");
11698
+ const sharingNote = doc.createElement("p");
11699
+ 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.";
11700
+ sharing.append(sharingNote);
11701
+ this.button("archive", "Download with version history", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
11702
+ const portability = doc.createElement("p");
11703
+ portability.textContent = "Saved versions stay on this device. Download a history file to keep a portable copy; clearing browser data removes local versions.";
11704
+ sharing.append(portability);
11705
+ const time = this.disclosure("Find a version by time");
11706
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
11707
+ this.button("time", "Preview at time", async () => {
11708
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
11709
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
11710
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
11711
+ }, time);
11712
+ const activity = this.disclosure("Recorded collaboration");
11713
+ const decisions = doc.createElement("ol");
11714
+ let showOlderActivity = () => {
11715
+ };
11716
+ this.button("activity", "Load activity", async () => {
11717
+ const { operations } = await options.reader.readOperationsSince(null);
11718
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
11719
+ let shown = 0;
11720
+ showOlderActivity = () => {
11721
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
11722
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
11723
+ shown += page.length;
11724
+ this.actions.get("activity-more").hidden = shown >= operations.length;
11725
+ };
11726
+ decisions.replaceChildren();
11727
+ showOlderActivity();
11728
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
11729
+ }, activity);
11730
+ activity.append(decisions);
11731
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
11732
+ this.actions.get("activity-more").hidden = true;
11733
+ this.ready = this.run("Loading history", () => this.load(false));
11734
+ }
11735
+ refresh() {
11736
+ return this.run("Loading history", () => this.load(true));
11737
+ }
11738
+ async destroy() {
11739
+ this.destroyed = true;
11740
+ this.events.abort();
11741
+ this.element.remove();
11742
+ await this.active?.catch(() => {
11743
+ });
11744
+ }
11745
+ async load(refresh) {
11746
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
11747
+ if (this.view) await this.page(this.view.version.id, true);
11748
+ else {
11749
+ this.records = [];
11750
+ this.next = null;
11751
+ this.versions.replaceChildren();
11752
+ this.before.replaceChildren();
11753
+ }
11754
+ 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.";
11755
+ }
11756
+ async page(cursor, reset) {
11757
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
11758
+ if (reset) {
11759
+ this.records = [];
11760
+ this.versions.replaceChildren();
11761
+ this.before.replaceChildren();
11762
+ }
11763
+ const start2 = this.records.length;
11764
+ this.records.push(...page.versions);
11765
+ this.next = page.next;
11766
+ for (let index = start2; index < this.records.length; index++) {
11767
+ for (const select of [this.versions, this.before]) {
11768
+ const option = select.ownerDocument.createElement("option");
11769
+ option.value = String(index);
11770
+ option.textContent = option.title = versionTitle(this.records[index]);
11771
+ select.append(option);
11772
+ }
11773
+ }
11774
+ if (reset) {
11775
+ this.versions.value = "0";
11776
+ this.before.value = this.records.length > 1 ? "1" : "0";
11777
+ }
11778
+ }
11779
+ update() {
11780
+ const hasVersion = this.records.length > 0;
11781
+ const pending = this.options.checkpoints?.hasPending ?? false;
11782
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
11783
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
11784
+ this.actions.get("more").hidden = !this.next;
11785
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
11786
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
11787
+ this.actions.get("save").disabled = pending || stale;
11788
+ this.actions.get("restore").hidden = !this.options.checkpoints;
11789
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
11790
+ this.actions.get("retry").hidden = !pending;
11791
+ this.versions.parentElement.hidden = this.detail.hidden = !hasVersion;
11792
+ this.versions.size = Math.max(2, Math.min(6, this.records.length));
11793
+ for (const key of ["latest", "preview", "download"]) this.actions.get(key).hidden = !hasVersion;
11794
+ this.actions.get("restore").hidden = !hasVersion || !this.options.checkpoints;
11795
+ for (const element of Array.from(this.element.querySelectorAll("[data-history-existing]"))) element.hidden = !hasVersion;
11796
+ this.detail.textContent = hasVersion ? [
11797
+ versionTitle(this.selected()),
11798
+ this.selected().record.metadata.message,
11799
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
11800
+ ].filter(Boolean).join(" \u2014 ") : "";
11801
+ }
11802
+ selected() {
11803
+ return this.records[Number(this.versions.value)];
11804
+ }
11805
+ activityItem(op, resolved) {
11806
+ const doc = this.element.ownerDocument;
11807
+ const item = doc.createElement("li");
11808
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
11809
+ const label = doc.createElement("p");
11810
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
11811
+ item.append(label);
11812
+ const description = doc.createElement("p");
11813
+ this.commandButton("View decision", async () => {
11814
+ const decision = await this.options.reader.getOperation(op.id);
11815
+ const { kind, metadata } = decision.input.request;
11816
+ description.textContent = [
11817
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
11818
+ metadata.label,
11819
+ metadata.message,
11820
+ decision.record.conflict
11821
+ ].filter(Boolean).join(" \u2014 ");
11822
+ }, item);
11823
+ item.append(description);
11824
+ this.commandButton("Download proposal", async () => {
11825
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
11826
+ }, item);
11827
+ return item;
11828
+ }
11829
+ metadata() {
11830
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
11831
+ }
11832
+ async preview(bytes, title) {
11833
+ if (!this.destroyed) await this.options.preview(bytes, title);
11834
+ }
11835
+ async download(bytes, suffix) {
11836
+ if (this.destroyed) return;
11837
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
11838
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
11839
+ if (this.options.download) {
11840
+ await this.options.download(bytes, name);
11841
+ return;
11842
+ }
11843
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
11844
+ const link = this.element.ownerDocument.createElement("a");
11845
+ link.href = url;
11846
+ link.download = name;
11847
+ link.click();
11848
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
11849
+ }
11850
+ async run(label, action) {
11851
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
11852
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
11853
+ this.fieldset.disabled = true;
11854
+ this.element.setAttribute("aria-busy", "true");
11855
+ this.status.textContent = `${label}\u2026`;
11856
+ const work = Promise.resolve().then(action);
11857
+ this.active = work;
11858
+ try {
11859
+ await work;
11860
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
11861
+ } catch (error) {
11862
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
11863
+ throw error;
11864
+ } finally {
11865
+ this.active = null;
11866
+ if (!this.destroyed) {
11867
+ this.fieldset.disabled = false;
11868
+ this.element.setAttribute("aria-busy", "false");
11869
+ this.update();
11870
+ }
11871
+ }
11872
+ }
11873
+ button(key, title, action, parent = this.fieldset) {
11874
+ const button = this.commandButton(title, action, parent);
11875
+ button.dataset.historyAction = key;
11876
+ this.actions.set(key, button);
11877
+ }
11878
+ commandButton(title, action, parent) {
11879
+ const button = parent.ownerDocument.createElement("button");
11880
+ button.type = "button";
11881
+ button.textContent = title;
11882
+ button.addEventListener("click", () => {
11883
+ void this.run(title, action).catch(() => {
11884
+ });
11885
+ }, { signal: this.events.signal });
11886
+ parent.append(button);
11887
+ return button;
11888
+ }
11889
+ select(title, parent = this.fieldset) {
11890
+ const label = this.fieldset.ownerDocument.createElement("label");
11891
+ label.textContent = title;
11892
+ const select = label.ownerDocument.createElement("select");
11893
+ select.setAttribute("aria-label", title);
11894
+ label.append(select);
11895
+ parent.append(label);
11896
+ return select;
11897
+ }
11898
+ input(title, type, value = "", parent = this.fieldset) {
11899
+ const label = parent.ownerDocument.createElement("label");
11900
+ label.textContent = title;
11901
+ const input = label.ownerDocument.createElement("input");
11902
+ input.type = type;
11903
+ input.value = value;
11904
+ label.append(input);
11905
+ parent.append(label);
11906
+ return input;
11907
+ }
11908
+ disclosure(title) {
11909
+ const details = this.fieldset.ownerDocument.createElement("details");
11910
+ details.dataset.historyExisting = "";
11911
+ const summary = details.ownerDocument.createElement("summary");
11912
+ summary.textContent = title;
11913
+ details.append(summary);
11914
+ this.fieldset.append(details);
11915
+ return details;
11916
+ }
11917
+ };
11918
+ function historyControlError(error, pending = false) {
11919
+ const code = error instanceof DocxHistoryError ? error.code : "";
11920
+ if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
11921
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
11922
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
11923
+ if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
11924
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
11925
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
11926
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
11927
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
11928
+ }
11929
+ function versionTitle(version) {
11930
+ const { metadata } = version.record;
11931
+ return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
11932
+ }
11933
+ function formatTime(value) {
11934
+ const time = new Date(value);
11935
+ return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
11936
+ }
11937
+ var HISTORY_CSS = `
11938
+ .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}
11939
+ .dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
11940
+ .dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
11941
+ .dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
11942
+ .dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
11943
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
11944
+ .dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
11945
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
11946
+ `;
11947
+
11948
+ // src/history-indexeddb.ts
11949
+ async function openIndexedDbHistoryStore(name) {
11950
+ const db = await new Promise((resolve, reject) => {
11951
+ let blocked = false;
11952
+ const request = indexedDB.open(name, 1);
11953
+ request.onupgradeneeded = () => {
11954
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
11955
+ };
11956
+ request.onsuccess = () => {
11957
+ if (blocked) request.result.close();
11958
+ else resolve(request.result);
11959
+ };
11960
+ request.onerror = () => reject(request.error);
11961
+ request.onblocked = () => {
11962
+ blocked = true;
11963
+ reject(new Error("Close other tabs using this history store, then try again."));
11964
+ };
11965
+ });
11966
+ db.onversionchange = () => db.close();
11967
+ function transaction(name2, mode, action) {
11968
+ return new Promise((resolve, reject) => {
11969
+ const tx = db.transaction(name2, mode);
11970
+ let value;
11971
+ tx.oncomplete = () => resolve(value);
11972
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
11973
+ try {
11974
+ action(tx.objectStore(name2), (result) => {
11975
+ value = result;
11976
+ });
11977
+ } catch (error) {
11978
+ tx.abort();
11979
+ reject(error);
11980
+ }
11981
+ });
11982
+ }
11983
+ const storage = {
11984
+ readBlob(reference) {
11985
+ const key = referenceKey(reference);
11986
+ return transaction("blobs", "readonly", (store, result) => {
11987
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
11988
+ });
11989
+ },
11990
+ async putBlob(reference, bytes) {
11991
+ const key = referenceKey(reference);
11992
+ const captured = bytes.slice();
11993
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
11994
+ const hash = Array.from(
11995
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
11996
+ (byte) => byte.toString(16).padStart(2, "0")
11997
+ ).join("");
11998
+ if (hash !== key.split(":")[0])
11999
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
12000
+ await transaction("blobs", "readwrite", (store, result) => {
12001
+ store.put(captured, key);
12002
+ result();
12003
+ });
12004
+ },
12005
+ readHead(documentId) {
12006
+ return transaction("heads", "readonly", (store, result) => {
12007
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
12008
+ });
12009
+ },
12010
+ advanceHead(documentId, expected, state) {
12011
+ referenceKey(state);
12012
+ const captured = structuredClone({ expected, state });
12013
+ if (expected) validateHead(expected);
12014
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
12015
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
12016
+ return transaction("heads", "readwrite", (store, result) => {
12017
+ store.get(documentId).onsuccess = (event) => {
12018
+ const current = event.target.result ?? null;
12019
+ if (!equalHead(current, captured.expected)) {
12020
+ result(null);
12021
+ return;
12022
+ }
12023
+ const head = { revision: String(revision), state: captured.state };
12024
+ store.put(head, documentId);
12025
+ result(head);
12026
+ };
12027
+ });
12028
+ },
12029
+ initializeHead(documentId, head) {
12030
+ validateHead(head);
12031
+ const captured = structuredClone(head);
12032
+ return transaction("heads", "readwrite", (store, result) => {
12033
+ store.get(documentId).onsuccess = (event) => {
12034
+ const existing = event.target.result;
12035
+ if (existing) {
12036
+ result({ initialized: false, head: existing });
12037
+ return;
12038
+ }
12039
+ store.put(captured, documentId);
12040
+ result({ initialized: true, head: captured });
12041
+ };
12042
+ });
12043
+ }
12044
+ };
12045
+ return {
12046
+ storage,
12047
+ journal(documentId) {
12048
+ return {
12049
+ read: () => transaction("requests", "readonly", (store, result) => {
12050
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
12051
+ }),
12052
+ put(request) {
12053
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
12054
+ const captured = structuredClone(request);
12055
+ return transaction("requests", "readwrite", (store, result) => {
12056
+ store.get(documentId).onsuccess = (event) => {
12057
+ const existing = event.target.result;
12058
+ if (existing) {
12059
+ result(existing);
12060
+ return;
12061
+ }
12062
+ store.put(captured, documentId);
12063
+ result(captured);
12064
+ };
12065
+ });
12066
+ },
12067
+ remove(requestId) {
12068
+ return transaction("requests", "readwrite", (store, result) => {
12069
+ store.get(documentId).onsuccess = (event) => {
12070
+ if (event.target.result?.id === requestId) store.delete(documentId);
12071
+ result();
12072
+ };
12073
+ });
12074
+ }
12075
+ };
12076
+ },
12077
+ close: () => db.close()
12078
+ };
12079
+ }
12080
+ function referenceKey(reference) {
12081
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
12082
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
12083
+ return `${reference.digest.value}:${reference.length}`;
12084
+ }
12085
+ function validateHead(head) {
12086
+ referenceKey(head.state);
12087
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
12088
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
12089
+ }
12090
+ function equalHead(a, b) {
12091
+ return a === null || b === null ? a === b : a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
12092
+ }
12093
+
12094
+ // src/ribbon-history.ts
12095
+ var RibbonHistory = class {
12096
+ constructor(ribbon, options) {
12097
+ this.ribbon = ribbon;
12098
+ this.options = options;
12099
+ this.events = new AbortController();
12100
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
12101
+ this.destroyed = false;
12102
+ this.installing = false;
12103
+ this.dirty = false;
12104
+ this.generation = 0;
12105
+ this.captured = -1;
12106
+ this.savedVersion = null;
12107
+ this.observedVersion = null;
12108
+ this.capturedVersion = null;
12109
+ const doc = ribbon.element.ownerDocument;
12110
+ this.prefix = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:document:`;
12111
+ this.lastKey = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:workspace:${options.workspaceId ?? crypto.randomUUID()}`;
12112
+ const style = doc.createElement("style");
12113
+ style.textContent = HISTORY_DRAWER_CSS;
12114
+ this.dialog = doc.createElement("dialog");
12115
+ this.dialog.className = "dxr-history-dialog";
12116
+ this.dialog.setAttribute("aria-label", "Version history");
12117
+ const header = doc.createElement("div");
12118
+ header.className = "dxr-history-header";
12119
+ this.dialog.append(header);
12120
+ const close = this.button("Close version history", () => this.dialog.close(), header);
12121
+ close.className = "dxr-history-close";
12122
+ close.textContent = "Close \u2715";
12123
+ this.source = doc.createElement("p");
12124
+ this.source.className = "dxr-history-source";
12125
+ this.status = doc.createElement("p");
12126
+ this.status.setAttribute("role", "status");
12127
+ this.status.className = "dxr-history-status";
12128
+ const recentLabel = doc.createElement("label");
12129
+ recentLabel.textContent = "Saved documents on this device";
12130
+ this.recent = doc.createElement("select");
12131
+ this.recent.setAttribute("aria-label", recentLabel.textContent);
12132
+ recentLabel.append(this.recent);
12133
+ this.recent.addEventListener("change", () => this.command(async () => {
12134
+ const selectedId = this.recent.value;
12135
+ this.recent.value = this.identity.id;
12136
+ const selected = this.readIdentity(localStorage.getItem(this.prefix + selectedId));
12137
+ if (!selected || !this.confirmReplace()) return;
12138
+ await this.loadSaved(selected);
12139
+ await this.mountPanel();
12140
+ this.updateRecent();
12141
+ }), { signal: this.events.signal });
12142
+ this.dialog.append(this.source, this.status, recentLabel);
12143
+ this.resumeButton = this.button("Continue editing this document", () => this.command(() => this.importArchive()), this.dialog);
12144
+ this.back = this.button("Back to my document", () => this.command(async () => {
12145
+ await this.closeArchive();
12146
+ await this.mountPanel();
12147
+ }), this.dialog);
12148
+ this.body = doc.createElement("div");
12149
+ this.dialog.append(this.body);
12150
+ this.previewDialog = doc.createElement("dialog");
12151
+ this.previewDialog.className = "dxr-version-preview";
12152
+ this.previewDialog.setAttribute("aria-label", "Version preview");
12153
+ this.panelObserver = new MutationObserver(() => this.syncBusy());
12154
+ ribbon.element.append(style, this.dialog, this.previewDialog);
12155
+ for (const dialog of [this.dialog, this.previewDialog]) {
12156
+ dialog.addEventListener("keydown", (event) => event.stopPropagation(), { signal: this.events.signal });
12157
+ dialog.addEventListener("click", (event) => {
12158
+ if (event.target === dialog) {
12159
+ const rect = dialog.getBoundingClientRect();
12160
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
12161
+ }
12162
+ }, { signal: this.events.signal });
12163
+ }
12164
+ this.dialog.addEventListener("close", () => ribbon.control("history")?.focus(), { signal: this.events.signal });
12165
+ ribbon.control("history")?.addEventListener("click", () => {
12166
+ void this.show();
12167
+ }, { signal: this.events.signal });
12168
+ doc.defaultView?.addEventListener("beforeunload", (event) => {
12169
+ if (this.hasUnsavedChanges || this.busy) event.preventDefault();
12170
+ }, { signal: this.events.signal });
12171
+ doc.defaultView?.addEventListener("pagehide", (event) => {
12172
+ if (!event.persisted) void this.destroy();
12173
+ }, { signal: this.events.signal });
12174
+ }
12175
+ get busy() {
12176
+ return !!this.active || this.panel?.element.getAttribute("aria-busy") === "true";
12177
+ }
12178
+ get hasUnsavedChanges() {
12179
+ return this.dirty || (this.ribbon.editor?.version ?? null) !== this.savedVersion;
12180
+ }
12181
+ /** Called by the ribbon before a programmatic replacement. */
12182
+ beforeOpen() {
12183
+ if (!this.installing && this.busy) throw new DocxHistoryError("Busy", "Finish the version action before opening another document.");
12184
+ }
12185
+ /** A new DOCX is a new document identity, even if its filename matches another document. */
12186
+ documentOpened(name) {
12187
+ if (this.installing) return;
12188
+ this.identity = { id: crypto.randomUUID(), name };
12189
+ this.draft = void 0;
12190
+ this.dirty = false;
12191
+ this.generation++;
12192
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
12193
+ this.dialog.close();
12194
+ this.previewDialog.close();
12195
+ void this.closeArchive();
12196
+ }
12197
+ edited() {
12198
+ const version = this.ribbon.editor?.version ?? null;
12199
+ if (version !== null && version === this.observedVersion) return;
12200
+ this.observedVersion = version;
12201
+ this.dirty = true;
12202
+ this.generation++;
12203
+ }
12204
+ /** Restores a saved workspace only when requested by the host, before exposing the editor. */
12205
+ async resume() {
12206
+ if (!this.options.workspaceId) return;
12207
+ try {
12208
+ const previous = this.readIdentity(localStorage.getItem(this.lastKey));
12209
+ if (previous) await this.run(() => this.loadSaved(previous));
12210
+ } catch (error) {
12211
+ this.ribbon.setStatus("Your document is open. Version history is unavailable on this device.");
12212
+ this.status.textContent = this.explain(error);
12213
+ }
12214
+ }
12215
+ async show() {
12216
+ if (this.destroyed) return;
12217
+ if (!this.dialog.open) this.dialog.showModal();
12218
+ if (this.busy) return;
12219
+ await this.run(async () => {
12220
+ await this.mountPanel();
12221
+ this.updateRecent();
12222
+ }).catch(() => {
12223
+ });
12224
+ }
12225
+ async openFile(file) {
12226
+ await this.run(async () => {
12227
+ if (/\.docxhistory$/i.test(file.name)) {
12228
+ if (file.size > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History file is too large.");
12229
+ const bytes = new Uint8Array(await file.arrayBuffer());
12230
+ const reader = await this.options.openArchive(bytes);
12231
+ try {
12232
+ await reader.read();
12233
+ } catch (error) {
12234
+ reader.close();
12235
+ throw error;
12236
+ }
12237
+ await this.closeArchive();
12238
+ this.archive = { reader, bytes, name: file.name };
12239
+ if (!this.dialog.open) this.dialog.showModal();
12240
+ await this.mountPanel();
12241
+ this.updateRecent();
12242
+ } else {
12243
+ if (!/\.docx$/i.test(file.name)) throw new Error("Choose a Word document or a document with version history.");
12244
+ if (!this.confirmReplace()) return;
12245
+ const bytes = new Uint8Array(await file.arrayBuffer());
12246
+ this.install(bytes, { id: crypto.randomUUID(), name: file.name });
12247
+ this.draft = void 0;
12248
+ this.dirty = true;
12249
+ await this.closeArchive();
12250
+ this.dialog.close();
12251
+ }
12252
+ }).catch(() => {
12253
+ });
12254
+ }
12255
+ async newDocument() {
12256
+ await this.run(async () => {
12257
+ if (!this.confirmReplace()) return;
12258
+ this.installing = true;
12259
+ try {
12260
+ this.ribbon.openBlank("Untitled.docx");
12261
+ } finally {
12262
+ this.installing = false;
12263
+ }
12264
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
12265
+ this.draft = void 0;
12266
+ this.dirty = false;
12267
+ this.generation++;
12268
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
12269
+ try {
12270
+ localStorage.removeItem(this.lastKey);
12271
+ } catch {
12272
+ }
12273
+ await this.closeArchive();
12274
+ this.dialog.close();
12275
+ }).catch(() => {
12276
+ });
12277
+ }
12278
+ async destroy() {
12279
+ if (this.destroyed) return;
12280
+ this.destroyed = true;
12281
+ this.events.abort();
12282
+ this.dialog.remove();
12283
+ this.previewDialog.remove();
12284
+ this.panelObserver.disconnect();
12285
+ await this.active?.catch(() => {
12286
+ });
12287
+ await this.closeArchive();
12288
+ this.preview?.destroy();
12289
+ this.client?.close();
12290
+ this.store?.close();
12291
+ }
12292
+ async ensureStorage() {
12293
+ if (this.client) return;
12294
+ const store = await openIndexedDbHistoryStore(this.options.storageName ?? "docxodus-editor");
12295
+ try {
12296
+ this.client = this.options.openHistory(store.storage);
12297
+ this.store = store;
12298
+ } catch (error) {
12299
+ store.close();
12300
+ throw error;
12301
+ }
12302
+ }
12303
+ async ensureDraft() {
12304
+ await this.ensureStorage();
12305
+ if (!this.draft) {
12306
+ const document2 = this.client.document(this.identity.id);
12307
+ this.draft = { ...this.identity, checkpoints: await HistoryCheckpoints.open(document2, this.store.journal(this.identity.id)) };
12308
+ }
12309
+ return this.draft;
12310
+ }
12311
+ async loadSaved(identity) {
12312
+ await this.ensureStorage();
12313
+ const document2 = this.client.document(identity.id);
12314
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id));
12315
+ const pending = checkpoints.pendingRequest;
12316
+ const view = checkpoints.view;
12317
+ if (!view && pending?.kind !== "save") throw new Error("This saved document is no longer on this device.");
12318
+ const bytes = pending?.kind === "save" ? pending.bytes : await document2.exportDocx(view.version.id);
12319
+ this.install(bytes, identity);
12320
+ this.draft = { ...identity, checkpoints };
12321
+ this.dirty = pending?.kind === "save";
12322
+ if (pending?.kind === "save") this.captureState(pending.bytes);
12323
+ await this.closeArchive();
12324
+ this.remember();
12325
+ }
12326
+ install(bytes, identity) {
12327
+ if (this.destroyed) throw new DocxHistoryError("Closed", "The editor is closed.");
12328
+ this.installing = true;
12329
+ try {
12330
+ this.ribbon.open(bytes, identity.name);
12331
+ } finally {
12332
+ this.installing = false;
12333
+ }
12334
+ this.identity = identity;
12335
+ this.dirty = false;
12336
+ this.generation++;
12337
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
12338
+ }
12339
+ async mountPanel() {
12340
+ const current = this.archive ?? await this.ensureDraft();
12341
+ const writable = "checkpoints" in current;
12342
+ const holder = this.body.ownerDocument.createElement("div");
12343
+ const panel = mountHistoryControls(holder, {
12344
+ reader: writable ? current.checkpoints.document : current.reader,
12345
+ checkpoints: writable ? current.checkpoints : void 0,
12346
+ author: this.options.author,
12347
+ documentName: current.name,
12348
+ capture: writable ? () => {
12349
+ const bytes = this.ribbon.save();
12350
+ if (!bytes) throw new Error("Open a document first.");
12351
+ this.remember();
12352
+ this.captureState(bytes);
12353
+ return bytes;
12354
+ } : void 0,
12355
+ confirmRestore: writable ? (title) => this.confirm(`Restore ${title}?${this.hasUnsavedChanges ? "\n\nYour unsaved changes will be replaced." : ""}
12356
+
12357
+ All saved versions will be kept.`) : void 0,
12358
+ onCheckpoint: async (view, action, request) => {
12359
+ if (this.destroyed) return;
12360
+ const savedCapture = action === "save" || action === "retry" && request?.kind === "save" && this.capturedBytes && sameBytes(this.capturedBytes, request.bytes);
12361
+ if (savedCapture && this.captured === this.generation && this.capturedVersion === (this.ribbon.editor?.version ?? null)) {
12362
+ this.dirty = false;
12363
+ this.savedVersion = this.capturedVersion;
12364
+ }
12365
+ this.captured = -1;
12366
+ this.capturedBytes = void 0;
12367
+ if ((action === "restore" || action === "retry" && request?.kind === "restore") && writable)
12368
+ this.install(await current.checkpoints.document.exportDocx(view.version.id), this.identity);
12369
+ this.remember();
12370
+ this.updateRecent();
12371
+ },
12372
+ restoreUpdatesDraft: true,
12373
+ preview: (bytes, title) => this.showPreview(bytes, title)
12374
+ });
12375
+ try {
12376
+ await panel.ready;
12377
+ } catch (error) {
12378
+ await panel.destroy();
12379
+ throw error;
12380
+ }
12381
+ if (this.destroyed) {
12382
+ await panel.destroy();
12383
+ return;
12384
+ }
12385
+ await this.panel?.destroy();
12386
+ this.panel = panel;
12387
+ this.body.replaceChildren(holder);
12388
+ this.panelObserver.disconnect();
12389
+ this.panelObserver.observe(panel.element, { attributes: true, attributeFilter: ["aria-busy"] });
12390
+ this.source.textContent = current.name;
12391
+ this.resumeButton.hidden = this.back.hidden = writable;
12392
+ this.status.textContent = writable ? "Your saved versions stay on this device." : "You\u2019re browsing a history file. Your open document is safe.";
12393
+ }
12394
+ async showPreview(bytes, title) {
12395
+ const doc = this.previewDialog.ownerDocument;
12396
+ const holder = doc.createElement("div");
12397
+ holder.className = "dxr-version-paper";
12398
+ const preview = await this.options.preview(holder, bytes);
12399
+ if (this.destroyed) {
12400
+ preview.destroy();
12401
+ return;
12402
+ }
12403
+ const heading = doc.createElement("h2");
12404
+ heading.textContent = title;
12405
+ const actions = doc.createElement("div");
12406
+ actions.className = "dxr-version-actions";
12407
+ this.button("Back to version history", () => this.previewDialog.close(), actions);
12408
+ if (!this.archive) this.button("Use as draft", () => this.command(async () => {
12409
+ if (!this.confirmReplace()) return;
12410
+ this.install(bytes, this.identity);
12411
+ this.dirty = true;
12412
+ this.previewDialog.close();
12413
+ this.dialog.close();
12414
+ }), actions);
12415
+ this.button("Download this version", () => download(bytes, "version.docx", doc), actions);
12416
+ this.preview?.destroy();
12417
+ this.preview = preview;
12418
+ this.previewDialog.replaceChildren(heading, actions, holder);
12419
+ if (!this.previewDialog.open) this.previewDialog.showModal();
12420
+ }
12421
+ async importArchive() {
12422
+ if (!this.archive || !this.confirmReplace()) return;
12423
+ await this.ensureStorage();
12424
+ const imported = await this.client.importHistoryArchive(this.archive.bytes);
12425
+ const identity = { id: imported.archive.documentId, name: this.archive.name.replace(/\.docxhistory$/i, ".docx") };
12426
+ const document2 = this.client.document(identity.id);
12427
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id), imported.view);
12428
+ this.install(await document2.exportDocx(imported.view.version.id), identity);
12429
+ this.draft = { ...identity, checkpoints };
12430
+ this.remember();
12431
+ await this.closeArchive();
12432
+ await this.mountPanel();
12433
+ this.updateRecent();
12434
+ }
12435
+ async closeArchive() {
12436
+ this.panelObserver.disconnect();
12437
+ const panel = this.panel;
12438
+ this.panel = void 0;
12439
+ const archive = this.archive;
12440
+ this.archive = void 0;
12441
+ await panel?.destroy();
12442
+ archive?.reader.close();
12443
+ }
12444
+ remember() {
12445
+ localStorage.setItem(this.prefix + this.identity.id, JSON.stringify(this.identity));
12446
+ localStorage.setItem(this.lastKey, JSON.stringify(this.identity));
12447
+ }
12448
+ readIdentity(value) {
12449
+ if (!value) return null;
12450
+ const identity = JSON.parse(value);
12451
+ if (typeof identity?.id !== "string" || !identity.id || typeof identity?.name !== "string") return null;
12452
+ return { id: identity.id, name: identity.name };
12453
+ }
12454
+ updateRecent() {
12455
+ this.recent.replaceChildren(new Option("Choose a saved document\u2026", ""));
12456
+ for (let i = 0; i < localStorage.length; i++) {
12457
+ const key = localStorage.key(i);
12458
+ if (!key.startsWith(this.prefix)) continue;
12459
+ try {
12460
+ const identity = this.readIdentity(localStorage.getItem(key));
12461
+ if (identity) this.recent.append(new Option(identity.name, identity.id));
12462
+ } catch {
12463
+ }
12464
+ }
12465
+ this.recent.value = this.identity.id;
12466
+ this.recent.parentElement.hidden = this.recent.options.length <= 1;
12467
+ }
12468
+ confirm(message) {
12469
+ return this.dialog.ownerDocument.defaultView?.confirm(message) ?? false;
12470
+ }
12471
+ captureState(bytes) {
12472
+ this.captured = this.generation;
12473
+ this.capturedVersion = this.ribbon.editor?.version ?? null;
12474
+ this.capturedBytes = bytes.slice();
12475
+ }
12476
+ confirmReplace() {
12477
+ return !this.hasUnsavedChanges || this.confirm("Replace your open document? Save a version or download it first to keep your unsaved changes.");
12478
+ }
12479
+ explain(error) {
12480
+ return historyControlError(error, this.draft?.checkpoints.hasPending);
12481
+ }
12482
+ command(action) {
12483
+ void this.run(action).catch(() => {
12484
+ });
12485
+ }
12486
+ syncBusy() {
12487
+ const busy = this.busy;
12488
+ this.ribbon.surface.inert = busy;
12489
+ const chrome = this.ribbon.element.querySelector(".dxr-chrome");
12490
+ if (chrome) chrome.inert = busy;
12491
+ this.recent.disabled = this.resumeButton.disabled = this.back.disabled = busy;
12492
+ const file = this.ribbon.control("file");
12493
+ if (file) file.disabled = busy;
12494
+ const create = this.ribbon.control("new");
12495
+ if (create) create.disabled = busy;
12496
+ }
12497
+ async run(action) {
12498
+ if (this.destroyed || this.busy) return;
12499
+ const work = Promise.resolve().then(action);
12500
+ this.active = work;
12501
+ this.dialog.setAttribute("aria-busy", "true");
12502
+ this.body.inert = true;
12503
+ this.syncBusy();
12504
+ this.ribbon.element.querySelector(".dxr-chrome").inert = true;
12505
+ this.ribbon.surface.inert = true;
12506
+ try {
12507
+ await work;
12508
+ } catch (error) {
12509
+ this.status.textContent = this.explain(error);
12510
+ this.ribbon.setStatus(this.explain(error));
12511
+ throw error;
12512
+ } finally {
12513
+ this.active = void 0;
12514
+ this.body.inert = false;
12515
+ this.dialog.setAttribute("aria-busy", "false");
12516
+ this.syncBusy();
12517
+ this.ribbon.element.querySelector(".dxr-chrome")?.removeAttribute("inert");
12518
+ this.ribbon.surface.inert = false;
12519
+ }
12520
+ }
12521
+ button(label, action, parent) {
12522
+ const button = parent.ownerDocument.createElement("button");
12523
+ button.type = "button";
12524
+ button.textContent = label;
12525
+ button.setAttribute("aria-label", label);
12526
+ button.addEventListener("click", action, { signal: this.events.signal });
12527
+ parent.append(button);
12528
+ return button;
12529
+ }
12530
+ };
12531
+ function download(bytes, name, doc) {
12532
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
12533
+ const link = doc.createElement("a");
12534
+ link.href = url;
12535
+ link.download = name;
12536
+ link.click();
12537
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
12538
+ }
12539
+ function sameBytes(left2, right2) {
12540
+ return left2.length === right2.length && left2.every((value, index) => value === right2[index]);
12541
+ }
12542
+ var HISTORY_DRAWER_CSS = `
12543
+ .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}
12544
+ .dxr-history-dialog{width:min(420px,calc(100vw - 24px));max-height:calc(100dvh - 24px);margin:12px 12px 12px auto;border-radius:18px}
12545
+ .dxr-history-dialog::backdrop,.dxr-version-preview::backdrop{background:#16323333;backdrop-filter:blur(2px)}
12546
+ .dxr-history-dialog [hidden],.dxr-version-preview [hidden]{display:none!important}
12547
+ .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}
12548
+ .dxr-history-dialog button:hover,.dxr-version-preview button:hover{background:#eff8f6}
12549
+ .dxr-history-dialog :focus-visible,.dxr-version-preview :focus-visible{outline:3px solid #0f766e;outline-offset:3px}
12550
+ .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}
12551
+ .dxr-history-dialog .dxr-history-close{border:0;font-size:13px;background:#fff}
12552
+ .dxr-history-source{font-weight:600;overflow-wrap:anywhere;margin:12px 0 4px}
12553
+ .dxr-history-status{color:#597273;margin:4px 0 18px}
12554
+ .dxr-history-dialog>label{display:block;font-size:12px;color:#597273;margin:12px 0}
12555
+ .dxr-history-dialog>label select{display:block;width:100%;font:inherit;padding:8px;border:1px solid #cbd9d9;border-radius:8px;background:#fff;color:#243b42}
12556
+ .dxr-history-dialog .dx-history{padding:0;border:0;border-radius:0;background:transparent}
12557
+ .dxr-history-dialog .dx-history h2{font-size:24px;letter-spacing:-.6px;margin:12px 0}
12558
+ .dxr-history-dialog .dx-history button[data-history-action=save]{background:#0f766e;border-color:#0f766e;color:#fff;font-weight:600}
12559
+ .dxr-history-dialog .dx-history select{border-radius:9px;background:#f8fbfa}
12560
+ .dxr-history-dialog .dx-history option{padding:10px;font-size:13px}
12561
+ .dxr-version-preview{width:min(1080px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border-radius:18px}
12562
+ .dxr-version-preview h2{font-size:20px;margin:0 0 12px;overflow-wrap:anywhere}
12563
+ .dxr-version-actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
12564
+ .dxr-version-paper{max-height:70dvh;overflow:auto;background:#f2f6f5;padding:20px;border-radius:10px}
12565
+ @media(max-width:520px){.dxr-history-dialog,.dxr-version-preview{padding:16px}.dxr-version-paper{padding:8px}}
12566
+ @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)}}}
12567
+ `;
12568
+
11421
12569
  // src/ribbon-chrome.ts
11422
12570
  var RIBBON_STYLE_VERSION = "11";
11423
12571
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
@@ -11868,9 +13016,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11868
13016
  Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
11869
13017
  .dxr-surface .docx-hf-band {
11870
13018
  /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
11871
- custom property the viewport publishes rather than stretching to the whole surface. */
13019
+ custom property the viewport publishes, including when the page exceeds the surface. */
11872
13020
  position: relative;
11873
- width: min(100%, var(--docx-sheet-width, 100%));
13021
+ width: var(--docx-sheet-width, 100%);
11874
13022
  margin: 0 auto;
11875
13023
  padding: 34px 72px 12px;
11876
13024
  background: var(--dxr-sheet);
@@ -11929,7 +13077,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
11929
13077
  vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
11930
13078
  has zoomed to fit stays centered at its scaled width. */
11931
13079
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
11932
- max-width: 100%;
13080
+ /* CSS zoom scales the authored page width. A percentage cap would shrink only the
13081
+ paper to the host while its fixed-width section and content keep magnifying. */
13082
+ max-width: none;
11933
13083
  margin: 0 auto;
11934
13084
  padding: 56px 0;
11935
13085
  border-radius: 3px;
@@ -12296,6 +13446,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12296
13446
  <label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
12297
13447
  <button type="button" data-dxr="save" disabled>Save</button>
12298
13448
  </div>
13449
+ <button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
12299
13450
  <div class="dxr-quick">
12300
13451
  <button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">&#8630;</button>
12301
13452
  <button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">&#8631;</button>
@@ -13130,6 +14281,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13130
14281
  this.resizeObserver = new ResizeObserver(() => this.applyChrome());
13131
14282
  this.resizeObserver.observe(root);
13132
14283
  }
14284
+ this.history = options.history ? new RibbonHistory(this, options.history) : null;
13133
14285
  }
13134
14286
  // ── element lookup ──────────────────────────────────────────────────────────
13135
14287
  control(name) {
@@ -13154,6 +14306,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13154
14306
  this.element.querySelector("[data-dxr-files]")?.remove();
13155
14307
  }
13156
14308
  if (!this.loaderOptions) this.control("loader")?.remove();
14309
+ if (!this.options.history) this.control("history")?.remove();
14310
+ else {
14311
+ const file = this.control("file");
14312
+ if (file) {
14313
+ file.accept = ".docx,.docxhistory";
14314
+ file.setAttribute("aria-label", "Open a document or history file");
14315
+ }
14316
+ }
13157
14317
  this.require("docname").textContent = this.documentName;
13158
14318
  this.require("paginated").checked = this.options.paginated ?? false;
13159
14319
  this.require("headerfooter").checked = this.headerFooter;
@@ -13335,22 +14495,13 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13335
14495
  }
13336
14496
  open(bytes, name) {
13337
14497
  if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
13338
- if (this.live) {
13339
- try {
13340
- this.live.close();
13341
- } catch {
13342
- }
13343
- this.live = null;
13344
- }
13345
- if (name) this.documentName = name;
13346
- this.require("docname").textContent = this.documentName;
14498
+ this.history?.beforeOpen();
13347
14499
  const paginated = this.require("paginated").checked;
13348
- this.surface.dataset.view = paginated ? "paginated" : "continuous";
13349
- this.surface.replaceChildren();
13350
- this.closeFindBar();
14500
+ const candidateSurface = this.surface.cloneNode(false);
14501
+ candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
13351
14502
  const started = performance.now();
13352
14503
  const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
13353
- this.live = DocxEditor.open(this.surface, bytes, this.exports, {
14504
+ const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
13354
14505
  cssPrefix: this.options.cssPrefix,
13355
14506
  fabricateClasses: this.options.fabricateClasses,
13356
14507
  editable: this.options.editable,
@@ -13358,10 +14509,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13358
14509
  columnWidth: this.options.columnWidth,
13359
14510
  fitToWidth: this.options.fitToWidth,
13360
14511
  onEdit: (info) => {
14512
+ this.history?.edited();
13361
14513
  this.options.onEdit?.(info);
13362
14514
  this.scheduleStats();
13363
14515
  },
13364
- onMove: this.options.onMove,
14516
+ onMove: (info) => {
14517
+ this.history?.edited();
14518
+ this.options.onMove?.(info);
14519
+ },
13365
14520
  onStoryChange: (which) => this.onStoryChange(which),
13366
14521
  onCommentsChange: (info) => this.onCommentsChange(info),
13367
14522
  paginated,
@@ -13372,6 +14527,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13372
14527
  comments: this.options.comments,
13373
14528
  commentAuthor: this.author
13374
14529
  });
14530
+ this.live?.close();
14531
+ candidate.adoptContainer(this.surface);
14532
+ this.surface.dataset.view = paginated ? "paginated" : "continuous";
14533
+ this.live = candidate;
14534
+ if (name) this.documentName = name;
14535
+ this.require("docname").textContent = this.documentName;
14536
+ this.closeFindBar();
14537
+ this.history?.documentOpened(this.documentName);
13375
14538
  const saveButton = this.control("save");
13376
14539
  if (saveButton) saveButton.disabled = false;
13377
14540
  this.require("ribbon").setAttribute("aria-disabled", "false");
@@ -13419,6 +14582,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13419
14582
  destroy() {
13420
14583
  if (this.destroyed) return;
13421
14584
  this.destroyed = true;
14585
+ void this.history?.destroy();
13422
14586
  const doc = this.element.ownerDocument ?? document;
13423
14587
  doc.removeEventListener("selectionchange", this.onSelectionChange);
13424
14588
  doc.removeEventListener("mousedown", this.onDocumentMouseDown);
@@ -13455,6 +14619,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13455
14619
  const el = this.control("railOp");
13456
14620
  if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
13457
14621
  this.options.onCommand?.(label, ms);
14622
+ this.history?.edited();
13458
14623
  this.refreshRailCounts();
13459
14624
  this.refreshRailAnchor();
13460
14625
  this.scheduleStats();
@@ -13654,11 +14819,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13654
14819
  file?.addEventListener("change", async () => {
13655
14820
  const chosen = file.files?.[0];
13656
14821
  if (!chosen) return;
14822
+ file.value = "";
14823
+ if (this.history) {
14824
+ await this.history.openFile(chosen);
14825
+ return;
14826
+ }
13657
14827
  this.setStatus(`Loading ${chosen.name}\u2026`);
13658
- this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
14828
+ try {
14829
+ this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
14830
+ } catch (error) {
14831
+ this.setStatus(`Could not open this document. ${String(error)}`);
14832
+ }
13659
14833
  file.value = "";
13660
14834
  });
13661
14835
  this.control("new")?.addEventListener("click", () => {
14836
+ if (this.history) {
14837
+ void this.history.newDocument();
14838
+ return;
14839
+ }
13662
14840
  if (this.exports) this.openBlank("untitled.docx");
13663
14841
  });
13664
14842
  this.control("save")?.addEventListener("click", () => this.download());
@@ -13669,6 +14847,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13669
14847
  if (!chosen || !this.live) return;
13670
14848
  const started = performance.now();
13671
14849
  const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
14850
+ if (ok) this.history?.edited();
13672
14851
  const ms = performance.now() - started;
13673
14852
  const el = this.control("railOp");
13674
14853
  if (el) el.textContent = `picture ${Math.round(ms)} ms`;