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