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.iife.js
CHANGED
|
@@ -2485,6 +2485,10 @@ var Docxodus = (() => {
|
|
|
2485
2485
|
get hasPending() {
|
|
2486
2486
|
return this.request !== null;
|
|
2487
2487
|
}
|
|
2488
|
+
/** Snapshot of the exact request a retry will recover, including its original operation kind. */
|
|
2489
|
+
get pendingRequest() {
|
|
2490
|
+
return structuredClone(this.request);
|
|
2491
|
+
}
|
|
2488
2492
|
get needsRefresh() {
|
|
2489
2493
|
return this.stale;
|
|
2490
2494
|
}
|
|
@@ -2731,25 +2735,26 @@ var Docxodus = (() => {
|
|
|
2731
2735
|
const doc = container.ownerDocument;
|
|
2732
2736
|
this.element = doc.createElement("section");
|
|
2733
2737
|
this.element.className = "dx-history";
|
|
2734
|
-
this.element.setAttribute("aria-label", "
|
|
2738
|
+
this.element.setAttribute("aria-label", "Version history");
|
|
2735
2739
|
const style = doc.createElement("style");
|
|
2736
2740
|
style.textContent = HISTORY_CSS;
|
|
2737
2741
|
const title = doc.createElement("h2");
|
|
2738
|
-
title.textContent = "
|
|
2742
|
+
title.textContent = "Version history";
|
|
2739
2743
|
this.status = doc.createElement("p");
|
|
2740
2744
|
this.status.setAttribute("role", "status");
|
|
2741
2745
|
this.status.setAttribute("aria-live", "polite");
|
|
2742
2746
|
this.fieldset = doc.createElement("fieldset");
|
|
2743
2747
|
const legend = doc.createElement("legend");
|
|
2744
|
-
legend.textContent = "
|
|
2748
|
+
legend.textContent = "Saved versions";
|
|
2745
2749
|
this.fieldset.append(legend);
|
|
2746
2750
|
this.element.append(style, title, this.status, this.fieldset);
|
|
2747
2751
|
container.append(this.element);
|
|
2748
2752
|
const note = doc.createElement("p");
|
|
2753
|
+
note.dataset.historyExisting = "";
|
|
2749
2754
|
note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
|
|
2750
2755
|
this.fieldset.append(note);
|
|
2751
2756
|
this.button("refresh", "Refresh history", () => this.load(true));
|
|
2752
|
-
this.button("latest", "
|
|
2757
|
+
this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
|
|
2753
2758
|
this.versions = this.select("Version");
|
|
2754
2759
|
this.versions.size = 6;
|
|
2755
2760
|
this.detail = doc.createElement("p");
|
|
@@ -2757,56 +2762,80 @@ var Docxodus = (() => {
|
|
|
2757
2762
|
this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2758
2763
|
this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
|
|
2759
2764
|
this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
|
|
2760
|
-
|
|
2765
|
+
const comparison = this.disclosure("Compare versions");
|
|
2766
|
+
this.before = this.select("Compare from", comparison);
|
|
2761
2767
|
this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2762
2768
|
const compareNote = doc.createElement("p");
|
|
2763
2769
|
compareNote.textContent = "Compare from this version to the selected version above.";
|
|
2764
|
-
|
|
2770
|
+
comparison.append(compareNote);
|
|
2765
2771
|
this.button("compare", "Compare versions", async () => {
|
|
2766
2772
|
const before = this.records[Number(this.before.value)];
|
|
2767
2773
|
await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
|
|
2768
|
-
});
|
|
2774
|
+
}, comparison);
|
|
2769
2775
|
this.button("more", "Load older versions", async () => {
|
|
2770
2776
|
if (this.next) await this.page(this.next, false);
|
|
2771
2777
|
});
|
|
2772
|
-
|
|
2773
|
-
|
|
2778
|
+
const saveGroup = doc.createElement("div");
|
|
2779
|
+
saveGroup.className = "dx-history-save";
|
|
2780
|
+
this.fieldset.insertBefore(saveGroup, note);
|
|
2781
|
+
const attribution = doc.createElement("details");
|
|
2782
|
+
const attributionTitle = doc.createElement("summary");
|
|
2783
|
+
attributionTitle.textContent = "Saved by";
|
|
2784
|
+
attribution.append(attributionTitle);
|
|
2785
|
+
saveGroup.append(attribution);
|
|
2786
|
+
this.author = this.input("Your name", "text", options.author ?? "You", attribution);
|
|
2787
|
+
this.label = this.input("Version name (optional)", "text", "", saveGroup);
|
|
2788
|
+
saveGroup.hidden = !options.checkpoints;
|
|
2774
2789
|
this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
|
|
2775
|
-
this.button("save", "Save
|
|
2790
|
+
this.button("save", "Save version", async () => {
|
|
2776
2791
|
const view = await options.checkpoints.save(await options.capture(), this.metadata());
|
|
2777
2792
|
await options.onCheckpoint?.(view, "save");
|
|
2778
2793
|
await this.load(false);
|
|
2779
2794
|
this.label.value = "";
|
|
2780
|
-
this.status.textContent = "
|
|
2781
|
-
});
|
|
2795
|
+
this.status.textContent = "Version saved. Your draft remains open.";
|
|
2796
|
+
}, saveGroup);
|
|
2797
|
+
saveGroup.append(attribution);
|
|
2782
2798
|
this.button("restore", "Restore selected", async () => {
|
|
2783
2799
|
const version = this.selected();
|
|
2784
|
-
|
|
2800
|
+
const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
|
|
2785
2801
|
|
|
2786
|
-
Your current draft and all later versions will be kept.`)
|
|
2802
|
+
Your current draft and all later versions will be kept.`);
|
|
2803
|
+
if (!confirmed) {
|
|
2787
2804
|
this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
|
|
2788
2805
|
return;
|
|
2789
2806
|
}
|
|
2790
2807
|
const view = await options.checkpoints.restore(version.id, this.metadata());
|
|
2791
2808
|
await options.onCheckpoint?.(view, "restore");
|
|
2792
2809
|
await this.load(false);
|
|
2793
|
-
this.status.textContent = "Restored as a new
|
|
2810
|
+
this.status.textContent = options.restoreUpdatesDraft ? "Version restored. All saved versions are kept." : "Restored as a new saved version. Your draft and later versions are kept.";
|
|
2794
2811
|
});
|
|
2795
2812
|
const restoreNote = doc.createElement("p");
|
|
2796
2813
|
restoreNote.hidden = !options.checkpoints;
|
|
2797
|
-
restoreNote.
|
|
2814
|
+
if (options.checkpoints) restoreNote.dataset.historyExisting = "";
|
|
2815
|
+
restoreNote.textContent = options.restoreUpdatesDraft ? "Restore returns your document to this version and keeps every saved version." : "Restore creates a new saved version. Preview latest to view it; your draft stays open.";
|
|
2798
2816
|
this.fieldset.append(restoreNote);
|
|
2799
|
-
this.button("retry", "Retry
|
|
2817
|
+
this.button("retry", "Retry save", async () => {
|
|
2818
|
+
const request = options.checkpoints.pendingRequest;
|
|
2819
|
+
if (request?.kind === "restore" && options.confirmRestore) {
|
|
2820
|
+
const target = await options.reader.getVersion(request.target);
|
|
2821
|
+
if (!await options.confirmRestore(versionTitle(target))) {
|
|
2822
|
+
this.status.textContent = "Restore retry canceled. Your draft is unchanged; the saved action still needs recovery.";
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2800
2826
|
const view = await options.checkpoints.retry();
|
|
2801
|
-
await options.onCheckpoint?.(view, "retry");
|
|
2827
|
+
await options.onCheckpoint?.(view, "retry", request ?? void 0);
|
|
2802
2828
|
await this.load(false);
|
|
2803
|
-
this.status.textContent = "
|
|
2829
|
+
this.status.textContent = request?.kind === "restore" && options.restoreUpdatesDraft ? "Version restored. All saved versions are kept. Refresh history to check for newer versions." : "Saved version recovered. Your draft remains open. Refresh history to check for newer versions.";
|
|
2804
2830
|
});
|
|
2805
2831
|
const sharing = this.disclosure("Download with history");
|
|
2806
2832
|
const sharingNote = doc.createElement("p");
|
|
2807
2833
|
sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
|
|
2808
2834
|
sharing.append(sharingNote);
|
|
2809
|
-
this.button("archive", "Download
|
|
2835
|
+
this.button("archive", "Download with version history", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
|
|
2836
|
+
const portability = doc.createElement("p");
|
|
2837
|
+
portability.textContent = "Saved versions stay on this device. Download a history file to keep a portable copy; clearing browser data removes local versions.";
|
|
2838
|
+
sharing.append(portability);
|
|
2810
2839
|
const time = this.disclosure("Find a version by time");
|
|
2811
2840
|
const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
|
|
2812
2841
|
this.button("time", "Preview at time", async () => {
|
|
@@ -2856,7 +2885,7 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
2856
2885
|
this.versions.replaceChildren();
|
|
2857
2886
|
this.before.replaceChildren();
|
|
2858
2887
|
}
|
|
2859
|
-
this.status.textContent = this.options.checkpoints?.hasPending ? "A
|
|
2888
|
+
this.status.textContent = this.options.checkpoints?.hasPending ? "A save needs recovery. Retry it before saving more changes." : this.view ? "Select a version to preview, download or restore." : "No saved versions yet. Save your first version when you are ready.";
|
|
2860
2889
|
}
|
|
2861
2890
|
async page(cursor, reset) {
|
|
2862
2891
|
const page = await this.options.reader.listVersions(cursor, this.pageSize);
|
|
@@ -2893,6 +2922,11 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
2893
2922
|
this.actions.get("restore").hidden = !this.options.checkpoints;
|
|
2894
2923
|
this.actions.get("restore").disabled = !hasVersion || pending || stale;
|
|
2895
2924
|
this.actions.get("retry").hidden = !pending;
|
|
2925
|
+
this.versions.parentElement.hidden = this.detail.hidden = !hasVersion;
|
|
2926
|
+
this.versions.size = Math.max(2, Math.min(6, this.records.length));
|
|
2927
|
+
for (const key of ["latest", "preview", "download"]) this.actions.get(key).hidden = !hasVersion;
|
|
2928
|
+
this.actions.get("restore").hidden = !hasVersion || !this.options.checkpoints;
|
|
2929
|
+
for (const element of Array.from(this.element.querySelectorAll("[data-history-existing]"))) element.hidden = !hasVersion;
|
|
2896
2930
|
this.detail.textContent = hasVersion ? [
|
|
2897
2931
|
versionTitle(this.selected()),
|
|
2898
2932
|
this.selected().record.metadata.message,
|
|
@@ -2971,7 +3005,9 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
2971
3005
|
}
|
|
2972
3006
|
}
|
|
2973
3007
|
button(key, title, action, parent = this.fieldset) {
|
|
2974
|
-
|
|
3008
|
+
const button = this.commandButton(title, action, parent);
|
|
3009
|
+
button.dataset.historyAction = key;
|
|
3010
|
+
this.actions.set(key, button);
|
|
2975
3011
|
}
|
|
2976
3012
|
commandButton(title, action, parent) {
|
|
2977
3013
|
const button = parent.ownerDocument.createElement("button");
|
|
@@ -2984,13 +3020,13 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
2984
3020
|
parent.append(button);
|
|
2985
3021
|
return button;
|
|
2986
3022
|
}
|
|
2987
|
-
select(title) {
|
|
3023
|
+
select(title, parent = this.fieldset) {
|
|
2988
3024
|
const label = this.fieldset.ownerDocument.createElement("label");
|
|
2989
3025
|
label.textContent = title;
|
|
2990
3026
|
const select = label.ownerDocument.createElement("select");
|
|
2991
3027
|
select.setAttribute("aria-label", title);
|
|
2992
3028
|
label.append(select);
|
|
2993
|
-
|
|
3029
|
+
parent.append(label);
|
|
2994
3030
|
return select;
|
|
2995
3031
|
}
|
|
2996
3032
|
input(title, type, value = "", parent = this.fieldset) {
|
|
@@ -3005,6 +3041,7 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
3005
3041
|
}
|
|
3006
3042
|
disclosure(title) {
|
|
3007
3043
|
const details = this.fieldset.ownerDocument.createElement("details");
|
|
3044
|
+
details.dataset.historyExisting = "";
|
|
3008
3045
|
const summary = details.ownerDocument.createElement("summary");
|
|
3009
3046
|
summary.textContent = title;
|
|
3010
3047
|
details.append(summary);
|
|
@@ -3014,22 +3051,22 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
3014
3051
|
};
|
|
3015
3052
|
function historyControlError(error, pending = false) {
|
|
3016
3053
|
const code = error instanceof DocxHistoryError ? error.code : "";
|
|
3017
|
-
if (code === "StaleHead") return "A newer
|
|
3054
|
+
if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
|
|
3018
3055
|
if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
|
|
3019
3056
|
if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
|
|
3020
|
-
if (pending) return "The
|
|
3057
|
+
if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
|
|
3021
3058
|
if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
|
|
3022
3059
|
if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
|
|
3023
3060
|
if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
|
|
3024
3061
|
return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
|
|
3025
3062
|
}
|
|
3026
3063
|
function versionTitle(version) {
|
|
3027
|
-
const { metadata
|
|
3028
|
-
return `${metadata.label ||
|
|
3064
|
+
const { metadata } = version.record;
|
|
3065
|
+
return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
|
|
3029
3066
|
}
|
|
3030
3067
|
function formatTime(value) {
|
|
3031
3068
|
const time = new Date(value);
|
|
3032
|
-
return Number.isNaN(time.getTime()) ? value :
|
|
3069
|
+
return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
|
|
3033
3070
|
}
|
|
3034
3071
|
var HISTORY_CSS = `
|
|
3035
3072
|
.dx-history{font:14px/1.5 system-ui,sans-serif;color:#203047;background:#fff;border:1px solid #cbd5e1;border-radius:12px;padding:18px;min-width:0}
|
|
@@ -6268,6 +6305,19 @@ Your current draft and all later versions will be kept.`)) {
|
|
|
6268
6305
|
scale: options.scale ?? 1
|
|
6269
6306
|
};
|
|
6270
6307
|
}
|
|
6308
|
+
/** Retarget the mounted document to an equivalent host after its DOM is adopted. */
|
|
6309
|
+
adoptHost(host) {
|
|
6310
|
+
if (host === this.host) return;
|
|
6311
|
+
this.observer?.disconnect();
|
|
6312
|
+
this.observer = null;
|
|
6313
|
+
this.host.style.removeProperty("--docx-sheet-width");
|
|
6314
|
+
this.host = host;
|
|
6315
|
+
this.refresh();
|
|
6316
|
+
if (this.root && typeof ResizeObserver !== "undefined") {
|
|
6317
|
+
this.observer = new ResizeObserver(() => this.refresh());
|
|
6318
|
+
this.observer.observe(this.host);
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6271
6321
|
/**
|
|
6272
6322
|
* Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
|
|
6273
6323
|
* Safe to call on every remount; the previous root is released first.
|
|
@@ -11201,15 +11251,20 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
11201
11251
|
revisionAuthor: opts.revisionAuthor
|
|
11202
11252
|
}));
|
|
11203
11253
|
const editor = new _DocxEditor(container, exports, handle, opts);
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11254
|
+
try {
|
|
11255
|
+
editor.refreshAnchorMap();
|
|
11256
|
+
if (opts.headerFooter) editor.createRegion();
|
|
11257
|
+
const fullHtml = editor.renderFullHtml(bytes);
|
|
11258
|
+
if (opts.paginated) editor.mountPaginated(fullHtml);
|
|
11259
|
+
else editor.mountHtml(fullHtml);
|
|
11260
|
+
editor.syncRegionToBody();
|
|
11261
|
+
editor.setupBlockDrag();
|
|
11262
|
+
if (opts.comments) editor.createGutter();
|
|
11263
|
+
return editor;
|
|
11264
|
+
} catch (error) {
|
|
11265
|
+
editor.close();
|
|
11266
|
+
throw error;
|
|
11267
|
+
}
|
|
11213
11268
|
}
|
|
11214
11269
|
/**
|
|
11215
11270
|
* Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
|
|
@@ -11224,6 +11279,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
11224
11279
|
this.assertOpen();
|
|
11225
11280
|
return this.exports.DocxSessionBridge.Save(this.handle);
|
|
11226
11281
|
}
|
|
11282
|
+
/** Monotonic committed document version, when supported by the loaded engine. */
|
|
11283
|
+
get version() {
|
|
11284
|
+
this.assertOpen();
|
|
11285
|
+
const value = this.exports.DocxSessionBridge.GetVersion?.(this.handle);
|
|
11286
|
+
return value ? JSON.parse(value).version : null;
|
|
11287
|
+
}
|
|
11227
11288
|
/** Release the underlying WASM session. The editor is unusable afterward. */
|
|
11228
11289
|
close() {
|
|
11229
11290
|
if (this.closed) return;
|
|
@@ -11258,6 +11319,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
11258
11319
|
get root() {
|
|
11259
11320
|
return this.container;
|
|
11260
11321
|
}
|
|
11322
|
+
/**
|
|
11323
|
+
* Move a fully rendered candidate into the host's stable public surface.
|
|
11324
|
+
* Internal editor chrome that binds directly to the container is recreated there;
|
|
11325
|
+
* document blocks and their listeners move with the DOM nodes.
|
|
11326
|
+
*/
|
|
11327
|
+
adoptContainer(container) {
|
|
11328
|
+
this.assertOpen();
|
|
11329
|
+
if (container === this.container) return;
|
|
11330
|
+
this.teardownBlockDrag();
|
|
11331
|
+
this.gutter?.dispose();
|
|
11332
|
+
this.gutter = null;
|
|
11333
|
+
const previous = this.container;
|
|
11334
|
+
container.replaceChildren(...Array.from(previous.childNodes));
|
|
11335
|
+
this.container = container;
|
|
11336
|
+
this.viewport.adoptHost(container);
|
|
11337
|
+
this.setupBlockDrag();
|
|
11338
|
+
if (this.options.comments) this.createGutter();
|
|
11339
|
+
}
|
|
11261
11340
|
/**
|
|
11262
11341
|
* The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
|
|
11263
11342
|
* host and has been scaled to fit rather than reflowed — the honest thing to show a user who
|
|
@@ -14417,6 +14496,481 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14417
14496
|
(doc.head ?? doc.documentElement).appendChild(style);
|
|
14418
14497
|
}
|
|
14419
14498
|
|
|
14499
|
+
// src/ribbon-history.ts
|
|
14500
|
+
var RibbonHistory = class {
|
|
14501
|
+
constructor(ribbon, options) {
|
|
14502
|
+
this.ribbon = ribbon;
|
|
14503
|
+
this.options = options;
|
|
14504
|
+
this.events = new AbortController();
|
|
14505
|
+
this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
|
|
14506
|
+
this.destroyed = false;
|
|
14507
|
+
this.installing = false;
|
|
14508
|
+
this.dirty = false;
|
|
14509
|
+
this.generation = 0;
|
|
14510
|
+
this.captured = -1;
|
|
14511
|
+
this.savedVersion = null;
|
|
14512
|
+
this.observedVersion = null;
|
|
14513
|
+
this.capturedVersion = null;
|
|
14514
|
+
const doc = ribbon.element.ownerDocument;
|
|
14515
|
+
this.prefix = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:document:`;
|
|
14516
|
+
this.lastKey = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:workspace:${options.workspaceId ?? crypto.randomUUID()}`;
|
|
14517
|
+
const style = doc.createElement("style");
|
|
14518
|
+
style.textContent = HISTORY_DRAWER_CSS;
|
|
14519
|
+
this.dialog = doc.createElement("dialog");
|
|
14520
|
+
this.dialog.className = "dxr-history-dialog";
|
|
14521
|
+
this.dialog.setAttribute("aria-label", "Version history");
|
|
14522
|
+
const header = doc.createElement("div");
|
|
14523
|
+
header.className = "dxr-history-header";
|
|
14524
|
+
this.dialog.append(header);
|
|
14525
|
+
const close = this.button("Close version history", () => this.dialog.close(), header);
|
|
14526
|
+
close.className = "dxr-history-close";
|
|
14527
|
+
close.textContent = "Close \u2715";
|
|
14528
|
+
this.source = doc.createElement("p");
|
|
14529
|
+
this.source.className = "dxr-history-source";
|
|
14530
|
+
this.status = doc.createElement("p");
|
|
14531
|
+
this.status.setAttribute("role", "status");
|
|
14532
|
+
this.status.className = "dxr-history-status";
|
|
14533
|
+
const recentLabel = doc.createElement("label");
|
|
14534
|
+
recentLabel.textContent = "Saved documents on this device";
|
|
14535
|
+
this.recent = doc.createElement("select");
|
|
14536
|
+
this.recent.setAttribute("aria-label", recentLabel.textContent);
|
|
14537
|
+
recentLabel.append(this.recent);
|
|
14538
|
+
this.recent.addEventListener("change", () => this.command(async () => {
|
|
14539
|
+
const selectedId = this.recent.value;
|
|
14540
|
+
this.recent.value = this.identity.id;
|
|
14541
|
+
const selected = this.readIdentity(localStorage.getItem(this.prefix + selectedId));
|
|
14542
|
+
if (!selected || !this.confirmReplace()) return;
|
|
14543
|
+
await this.loadSaved(selected);
|
|
14544
|
+
await this.mountPanel();
|
|
14545
|
+
this.updateRecent();
|
|
14546
|
+
}), { signal: this.events.signal });
|
|
14547
|
+
this.dialog.append(this.source, this.status, recentLabel);
|
|
14548
|
+
this.resumeButton = this.button("Continue editing this document", () => this.command(() => this.importArchive()), this.dialog);
|
|
14549
|
+
this.back = this.button("Back to my document", () => this.command(async () => {
|
|
14550
|
+
await this.closeArchive();
|
|
14551
|
+
await this.mountPanel();
|
|
14552
|
+
}), this.dialog);
|
|
14553
|
+
this.body = doc.createElement("div");
|
|
14554
|
+
this.dialog.append(this.body);
|
|
14555
|
+
this.previewDialog = doc.createElement("dialog");
|
|
14556
|
+
this.previewDialog.className = "dxr-version-preview";
|
|
14557
|
+
this.previewDialog.setAttribute("aria-label", "Version preview");
|
|
14558
|
+
this.panelObserver = new MutationObserver(() => this.syncBusy());
|
|
14559
|
+
ribbon.element.append(style, this.dialog, this.previewDialog);
|
|
14560
|
+
for (const dialog of [this.dialog, this.previewDialog]) {
|
|
14561
|
+
dialog.addEventListener("keydown", (event) => event.stopPropagation(), { signal: this.events.signal });
|
|
14562
|
+
dialog.addEventListener("click", (event) => {
|
|
14563
|
+
if (event.target === dialog) {
|
|
14564
|
+
const rect = dialog.getBoundingClientRect();
|
|
14565
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
|
|
14566
|
+
}
|
|
14567
|
+
}, { signal: this.events.signal });
|
|
14568
|
+
}
|
|
14569
|
+
this.dialog.addEventListener("close", () => ribbon.control("history")?.focus(), { signal: this.events.signal });
|
|
14570
|
+
ribbon.control("history")?.addEventListener("click", () => {
|
|
14571
|
+
void this.show();
|
|
14572
|
+
}, { signal: this.events.signal });
|
|
14573
|
+
doc.defaultView?.addEventListener("beforeunload", (event) => {
|
|
14574
|
+
if (this.hasUnsavedChanges || this.busy) event.preventDefault();
|
|
14575
|
+
}, { signal: this.events.signal });
|
|
14576
|
+
doc.defaultView?.addEventListener("pagehide", (event) => {
|
|
14577
|
+
if (!event.persisted) void this.destroy();
|
|
14578
|
+
}, { signal: this.events.signal });
|
|
14579
|
+
}
|
|
14580
|
+
get busy() {
|
|
14581
|
+
return !!this.active || this.panel?.element.getAttribute("aria-busy") === "true";
|
|
14582
|
+
}
|
|
14583
|
+
get hasUnsavedChanges() {
|
|
14584
|
+
return this.dirty || (this.ribbon.editor?.version ?? null) !== this.savedVersion;
|
|
14585
|
+
}
|
|
14586
|
+
/** Called by the ribbon before a programmatic replacement. */
|
|
14587
|
+
beforeOpen() {
|
|
14588
|
+
if (!this.installing && this.busy) throw new DocxHistoryError("Busy", "Finish the version action before opening another document.");
|
|
14589
|
+
}
|
|
14590
|
+
/** A new DOCX is a new document identity, even if its filename matches another document. */
|
|
14591
|
+
documentOpened(name) {
|
|
14592
|
+
if (this.installing) return;
|
|
14593
|
+
this.identity = { id: crypto.randomUUID(), name };
|
|
14594
|
+
this.draft = void 0;
|
|
14595
|
+
this.dirty = false;
|
|
14596
|
+
this.generation++;
|
|
14597
|
+
this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
|
|
14598
|
+
this.dialog.close();
|
|
14599
|
+
this.previewDialog.close();
|
|
14600
|
+
void this.closeArchive();
|
|
14601
|
+
}
|
|
14602
|
+
edited() {
|
|
14603
|
+
const version = this.ribbon.editor?.version ?? null;
|
|
14604
|
+
if (version !== null && version === this.observedVersion) return;
|
|
14605
|
+
this.observedVersion = version;
|
|
14606
|
+
this.dirty = true;
|
|
14607
|
+
this.generation++;
|
|
14608
|
+
}
|
|
14609
|
+
/** Restores a saved workspace only when requested by the host, before exposing the editor. */
|
|
14610
|
+
async resume() {
|
|
14611
|
+
if (!this.options.workspaceId) return;
|
|
14612
|
+
try {
|
|
14613
|
+
const previous = this.readIdentity(localStorage.getItem(this.lastKey));
|
|
14614
|
+
if (previous) await this.run(() => this.loadSaved(previous));
|
|
14615
|
+
} catch (error) {
|
|
14616
|
+
this.ribbon.setStatus("Your document is open. Version history is unavailable on this device.");
|
|
14617
|
+
this.status.textContent = this.explain(error);
|
|
14618
|
+
}
|
|
14619
|
+
}
|
|
14620
|
+
async show() {
|
|
14621
|
+
if (this.destroyed) return;
|
|
14622
|
+
if (!this.dialog.open) this.dialog.showModal();
|
|
14623
|
+
if (this.busy) return;
|
|
14624
|
+
await this.run(async () => {
|
|
14625
|
+
await this.mountPanel();
|
|
14626
|
+
this.updateRecent();
|
|
14627
|
+
}).catch(() => {
|
|
14628
|
+
});
|
|
14629
|
+
}
|
|
14630
|
+
async openFile(file) {
|
|
14631
|
+
await this.run(async () => {
|
|
14632
|
+
if (/\.docxhistory$/i.test(file.name)) {
|
|
14633
|
+
if (file.size > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History file is too large.");
|
|
14634
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
14635
|
+
const reader = await this.options.openArchive(bytes);
|
|
14636
|
+
try {
|
|
14637
|
+
await reader.read();
|
|
14638
|
+
} catch (error) {
|
|
14639
|
+
reader.close();
|
|
14640
|
+
throw error;
|
|
14641
|
+
}
|
|
14642
|
+
await this.closeArchive();
|
|
14643
|
+
this.archive = { reader, bytes, name: file.name };
|
|
14644
|
+
if (!this.dialog.open) this.dialog.showModal();
|
|
14645
|
+
await this.mountPanel();
|
|
14646
|
+
this.updateRecent();
|
|
14647
|
+
} else {
|
|
14648
|
+
if (!/\.docx$/i.test(file.name)) throw new Error("Choose a Word document or a document with version history.");
|
|
14649
|
+
if (!this.confirmReplace()) return;
|
|
14650
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
14651
|
+
this.install(bytes, { id: crypto.randomUUID(), name: file.name });
|
|
14652
|
+
this.draft = void 0;
|
|
14653
|
+
this.dirty = true;
|
|
14654
|
+
await this.closeArchive();
|
|
14655
|
+
this.dialog.close();
|
|
14656
|
+
}
|
|
14657
|
+
}).catch(() => {
|
|
14658
|
+
});
|
|
14659
|
+
}
|
|
14660
|
+
async newDocument() {
|
|
14661
|
+
await this.run(async () => {
|
|
14662
|
+
if (!this.confirmReplace()) return;
|
|
14663
|
+
this.installing = true;
|
|
14664
|
+
try {
|
|
14665
|
+
this.ribbon.openBlank("Untitled.docx");
|
|
14666
|
+
} finally {
|
|
14667
|
+
this.installing = false;
|
|
14668
|
+
}
|
|
14669
|
+
this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
|
|
14670
|
+
this.draft = void 0;
|
|
14671
|
+
this.dirty = false;
|
|
14672
|
+
this.generation++;
|
|
14673
|
+
this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
|
|
14674
|
+
try {
|
|
14675
|
+
localStorage.removeItem(this.lastKey);
|
|
14676
|
+
} catch {
|
|
14677
|
+
}
|
|
14678
|
+
await this.closeArchive();
|
|
14679
|
+
this.dialog.close();
|
|
14680
|
+
}).catch(() => {
|
|
14681
|
+
});
|
|
14682
|
+
}
|
|
14683
|
+
async destroy() {
|
|
14684
|
+
if (this.destroyed) return;
|
|
14685
|
+
this.destroyed = true;
|
|
14686
|
+
this.events.abort();
|
|
14687
|
+
this.dialog.remove();
|
|
14688
|
+
this.previewDialog.remove();
|
|
14689
|
+
this.panelObserver.disconnect();
|
|
14690
|
+
await this.active?.catch(() => {
|
|
14691
|
+
});
|
|
14692
|
+
await this.closeArchive();
|
|
14693
|
+
this.preview?.destroy();
|
|
14694
|
+
this.client?.close();
|
|
14695
|
+
this.store?.close();
|
|
14696
|
+
}
|
|
14697
|
+
async ensureStorage() {
|
|
14698
|
+
if (this.client) return;
|
|
14699
|
+
const store = await openIndexedDbHistoryStore(this.options.storageName ?? "docxodus-editor");
|
|
14700
|
+
try {
|
|
14701
|
+
this.client = this.options.openHistory(store.storage);
|
|
14702
|
+
this.store = store;
|
|
14703
|
+
} catch (error) {
|
|
14704
|
+
store.close();
|
|
14705
|
+
throw error;
|
|
14706
|
+
}
|
|
14707
|
+
}
|
|
14708
|
+
async ensureDraft() {
|
|
14709
|
+
await this.ensureStorage();
|
|
14710
|
+
if (!this.draft) {
|
|
14711
|
+
const document2 = this.client.document(this.identity.id);
|
|
14712
|
+
this.draft = { ...this.identity, checkpoints: await HistoryCheckpoints.open(document2, this.store.journal(this.identity.id)) };
|
|
14713
|
+
}
|
|
14714
|
+
return this.draft;
|
|
14715
|
+
}
|
|
14716
|
+
async loadSaved(identity) {
|
|
14717
|
+
await this.ensureStorage();
|
|
14718
|
+
const document2 = this.client.document(identity.id);
|
|
14719
|
+
const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id));
|
|
14720
|
+
const pending = checkpoints.pendingRequest;
|
|
14721
|
+
const view = checkpoints.view;
|
|
14722
|
+
if (!view && pending?.kind !== "save") throw new Error("This saved document is no longer on this device.");
|
|
14723
|
+
const bytes = pending?.kind === "save" ? pending.bytes : await document2.exportDocx(view.version.id);
|
|
14724
|
+
this.install(bytes, identity);
|
|
14725
|
+
this.draft = { ...identity, checkpoints };
|
|
14726
|
+
this.dirty = pending?.kind === "save";
|
|
14727
|
+
if (pending?.kind === "save") this.captureState(pending.bytes);
|
|
14728
|
+
await this.closeArchive();
|
|
14729
|
+
this.remember();
|
|
14730
|
+
}
|
|
14731
|
+
install(bytes, identity) {
|
|
14732
|
+
if (this.destroyed) throw new DocxHistoryError("Closed", "The editor is closed.");
|
|
14733
|
+
this.installing = true;
|
|
14734
|
+
try {
|
|
14735
|
+
this.ribbon.open(bytes, identity.name);
|
|
14736
|
+
} finally {
|
|
14737
|
+
this.installing = false;
|
|
14738
|
+
}
|
|
14739
|
+
this.identity = identity;
|
|
14740
|
+
this.dirty = false;
|
|
14741
|
+
this.generation++;
|
|
14742
|
+
this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
|
|
14743
|
+
}
|
|
14744
|
+
async mountPanel() {
|
|
14745
|
+
const current = this.archive ?? await this.ensureDraft();
|
|
14746
|
+
const writable = "checkpoints" in current;
|
|
14747
|
+
const holder = this.body.ownerDocument.createElement("div");
|
|
14748
|
+
const panel = mountHistoryControls(holder, {
|
|
14749
|
+
reader: writable ? current.checkpoints.document : current.reader,
|
|
14750
|
+
checkpoints: writable ? current.checkpoints : void 0,
|
|
14751
|
+
author: this.options.author,
|
|
14752
|
+
documentName: current.name,
|
|
14753
|
+
capture: writable ? () => {
|
|
14754
|
+
const bytes = this.ribbon.save();
|
|
14755
|
+
if (!bytes) throw new Error("Open a document first.");
|
|
14756
|
+
this.remember();
|
|
14757
|
+
this.captureState(bytes);
|
|
14758
|
+
return bytes;
|
|
14759
|
+
} : void 0,
|
|
14760
|
+
confirmRestore: writable ? (title) => this.confirm(`Restore ${title}?${this.hasUnsavedChanges ? "\n\nYour unsaved changes will be replaced." : ""}
|
|
14761
|
+
|
|
14762
|
+
All saved versions will be kept.`) : void 0,
|
|
14763
|
+
onCheckpoint: async (view, action, request) => {
|
|
14764
|
+
if (this.destroyed) return;
|
|
14765
|
+
const savedCapture = action === "save" || action === "retry" && request?.kind === "save" && this.capturedBytes && sameBytes(this.capturedBytes, request.bytes);
|
|
14766
|
+
if (savedCapture && this.captured === this.generation && this.capturedVersion === (this.ribbon.editor?.version ?? null)) {
|
|
14767
|
+
this.dirty = false;
|
|
14768
|
+
this.savedVersion = this.capturedVersion;
|
|
14769
|
+
}
|
|
14770
|
+
this.captured = -1;
|
|
14771
|
+
this.capturedBytes = void 0;
|
|
14772
|
+
if ((action === "restore" || action === "retry" && request?.kind === "restore") && writable)
|
|
14773
|
+
this.install(await current.checkpoints.document.exportDocx(view.version.id), this.identity);
|
|
14774
|
+
this.remember();
|
|
14775
|
+
this.updateRecent();
|
|
14776
|
+
},
|
|
14777
|
+
restoreUpdatesDraft: true,
|
|
14778
|
+
preview: (bytes, title) => this.showPreview(bytes, title)
|
|
14779
|
+
});
|
|
14780
|
+
try {
|
|
14781
|
+
await panel.ready;
|
|
14782
|
+
} catch (error) {
|
|
14783
|
+
await panel.destroy();
|
|
14784
|
+
throw error;
|
|
14785
|
+
}
|
|
14786
|
+
if (this.destroyed) {
|
|
14787
|
+
await panel.destroy();
|
|
14788
|
+
return;
|
|
14789
|
+
}
|
|
14790
|
+
await this.panel?.destroy();
|
|
14791
|
+
this.panel = panel;
|
|
14792
|
+
this.body.replaceChildren(holder);
|
|
14793
|
+
this.panelObserver.disconnect();
|
|
14794
|
+
this.panelObserver.observe(panel.element, { attributes: true, attributeFilter: ["aria-busy"] });
|
|
14795
|
+
this.source.textContent = current.name;
|
|
14796
|
+
this.resumeButton.hidden = this.back.hidden = writable;
|
|
14797
|
+
this.status.textContent = writable ? "Your saved versions stay on this device." : "You\u2019re browsing a history file. Your open document is safe.";
|
|
14798
|
+
}
|
|
14799
|
+
async showPreview(bytes, title) {
|
|
14800
|
+
const doc = this.previewDialog.ownerDocument;
|
|
14801
|
+
const holder = doc.createElement("div");
|
|
14802
|
+
holder.className = "dxr-version-paper";
|
|
14803
|
+
const preview = await this.options.preview(holder, bytes);
|
|
14804
|
+
if (this.destroyed) {
|
|
14805
|
+
preview.destroy();
|
|
14806
|
+
return;
|
|
14807
|
+
}
|
|
14808
|
+
const heading = doc.createElement("h2");
|
|
14809
|
+
heading.textContent = title;
|
|
14810
|
+
const actions = doc.createElement("div");
|
|
14811
|
+
actions.className = "dxr-version-actions";
|
|
14812
|
+
this.button("Back to version history", () => this.previewDialog.close(), actions);
|
|
14813
|
+
if (!this.archive) this.button("Use as draft", () => this.command(async () => {
|
|
14814
|
+
if (!this.confirmReplace()) return;
|
|
14815
|
+
this.install(bytes, this.identity);
|
|
14816
|
+
this.dirty = true;
|
|
14817
|
+
this.previewDialog.close();
|
|
14818
|
+
this.dialog.close();
|
|
14819
|
+
}), actions);
|
|
14820
|
+
this.button("Download this version", () => download(bytes, "version.docx", doc), actions);
|
|
14821
|
+
this.preview?.destroy();
|
|
14822
|
+
this.preview = preview;
|
|
14823
|
+
this.previewDialog.replaceChildren(heading, actions, holder);
|
|
14824
|
+
if (!this.previewDialog.open) this.previewDialog.showModal();
|
|
14825
|
+
}
|
|
14826
|
+
async importArchive() {
|
|
14827
|
+
if (!this.archive || !this.confirmReplace()) return;
|
|
14828
|
+
await this.ensureStorage();
|
|
14829
|
+
const imported = await this.client.importHistoryArchive(this.archive.bytes);
|
|
14830
|
+
const identity = { id: imported.archive.documentId, name: this.archive.name.replace(/\.docxhistory$/i, ".docx") };
|
|
14831
|
+
const document2 = this.client.document(identity.id);
|
|
14832
|
+
const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id), imported.view);
|
|
14833
|
+
this.install(await document2.exportDocx(imported.view.version.id), identity);
|
|
14834
|
+
this.draft = { ...identity, checkpoints };
|
|
14835
|
+
this.remember();
|
|
14836
|
+
await this.closeArchive();
|
|
14837
|
+
await this.mountPanel();
|
|
14838
|
+
this.updateRecent();
|
|
14839
|
+
}
|
|
14840
|
+
async closeArchive() {
|
|
14841
|
+
this.panelObserver.disconnect();
|
|
14842
|
+
const panel = this.panel;
|
|
14843
|
+
this.panel = void 0;
|
|
14844
|
+
const archive = this.archive;
|
|
14845
|
+
this.archive = void 0;
|
|
14846
|
+
await panel?.destroy();
|
|
14847
|
+
archive?.reader.close();
|
|
14848
|
+
}
|
|
14849
|
+
remember() {
|
|
14850
|
+
localStorage.setItem(this.prefix + this.identity.id, JSON.stringify(this.identity));
|
|
14851
|
+
localStorage.setItem(this.lastKey, JSON.stringify(this.identity));
|
|
14852
|
+
}
|
|
14853
|
+
readIdentity(value) {
|
|
14854
|
+
if (!value) return null;
|
|
14855
|
+
const identity = JSON.parse(value);
|
|
14856
|
+
if (typeof identity?.id !== "string" || !identity.id || typeof identity?.name !== "string") return null;
|
|
14857
|
+
return { id: identity.id, name: identity.name };
|
|
14858
|
+
}
|
|
14859
|
+
updateRecent() {
|
|
14860
|
+
this.recent.replaceChildren(new Option("Choose a saved document\u2026", ""));
|
|
14861
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
14862
|
+
const key = localStorage.key(i);
|
|
14863
|
+
if (!key.startsWith(this.prefix)) continue;
|
|
14864
|
+
try {
|
|
14865
|
+
const identity = this.readIdentity(localStorage.getItem(key));
|
|
14866
|
+
if (identity) this.recent.append(new Option(identity.name, identity.id));
|
|
14867
|
+
} catch {
|
|
14868
|
+
}
|
|
14869
|
+
}
|
|
14870
|
+
this.recent.value = this.identity.id;
|
|
14871
|
+
this.recent.parentElement.hidden = this.recent.options.length <= 1;
|
|
14872
|
+
}
|
|
14873
|
+
confirm(message) {
|
|
14874
|
+
return this.dialog.ownerDocument.defaultView?.confirm(message) ?? false;
|
|
14875
|
+
}
|
|
14876
|
+
captureState(bytes) {
|
|
14877
|
+
this.captured = this.generation;
|
|
14878
|
+
this.capturedVersion = this.ribbon.editor?.version ?? null;
|
|
14879
|
+
this.capturedBytes = bytes.slice();
|
|
14880
|
+
}
|
|
14881
|
+
confirmReplace() {
|
|
14882
|
+
return !this.hasUnsavedChanges || this.confirm("Replace your open document? Save a version or download it first to keep your unsaved changes.");
|
|
14883
|
+
}
|
|
14884
|
+
explain(error) {
|
|
14885
|
+
return historyControlError(error, this.draft?.checkpoints.hasPending);
|
|
14886
|
+
}
|
|
14887
|
+
command(action) {
|
|
14888
|
+
void this.run(action).catch(() => {
|
|
14889
|
+
});
|
|
14890
|
+
}
|
|
14891
|
+
syncBusy() {
|
|
14892
|
+
const busy = this.busy;
|
|
14893
|
+
this.ribbon.surface.inert = busy;
|
|
14894
|
+
const chrome = this.ribbon.element.querySelector(".dxr-chrome");
|
|
14895
|
+
if (chrome) chrome.inert = busy;
|
|
14896
|
+
this.recent.disabled = this.resumeButton.disabled = this.back.disabled = busy;
|
|
14897
|
+
const file = this.ribbon.control("file");
|
|
14898
|
+
if (file) file.disabled = busy;
|
|
14899
|
+
const create = this.ribbon.control("new");
|
|
14900
|
+
if (create) create.disabled = busy;
|
|
14901
|
+
}
|
|
14902
|
+
async run(action) {
|
|
14903
|
+
if (this.destroyed || this.busy) return;
|
|
14904
|
+
const work = Promise.resolve().then(action);
|
|
14905
|
+
this.active = work;
|
|
14906
|
+
this.dialog.setAttribute("aria-busy", "true");
|
|
14907
|
+
this.body.inert = true;
|
|
14908
|
+
this.syncBusy();
|
|
14909
|
+
this.ribbon.element.querySelector(".dxr-chrome").inert = true;
|
|
14910
|
+
this.ribbon.surface.inert = true;
|
|
14911
|
+
try {
|
|
14912
|
+
await work;
|
|
14913
|
+
} catch (error) {
|
|
14914
|
+
this.status.textContent = this.explain(error);
|
|
14915
|
+
this.ribbon.setStatus(this.explain(error));
|
|
14916
|
+
throw error;
|
|
14917
|
+
} finally {
|
|
14918
|
+
this.active = void 0;
|
|
14919
|
+
this.body.inert = false;
|
|
14920
|
+
this.dialog.setAttribute("aria-busy", "false");
|
|
14921
|
+
this.syncBusy();
|
|
14922
|
+
this.ribbon.element.querySelector(".dxr-chrome")?.removeAttribute("inert");
|
|
14923
|
+
this.ribbon.surface.inert = false;
|
|
14924
|
+
}
|
|
14925
|
+
}
|
|
14926
|
+
button(label, action, parent) {
|
|
14927
|
+
const button = parent.ownerDocument.createElement("button");
|
|
14928
|
+
button.type = "button";
|
|
14929
|
+
button.textContent = label;
|
|
14930
|
+
button.setAttribute("aria-label", label);
|
|
14931
|
+
button.addEventListener("click", action, { signal: this.events.signal });
|
|
14932
|
+
parent.append(button);
|
|
14933
|
+
return button;
|
|
14934
|
+
}
|
|
14935
|
+
};
|
|
14936
|
+
function download(bytes, name, doc) {
|
|
14937
|
+
const url = URL.createObjectURL(new Blob([bytes.slice()], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
|
|
14938
|
+
const link = doc.createElement("a");
|
|
14939
|
+
link.href = url;
|
|
14940
|
+
link.download = name;
|
|
14941
|
+
link.click();
|
|
14942
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
14943
|
+
}
|
|
14944
|
+
function sameBytes(left2, right2) {
|
|
14945
|
+
return left2.length === right2.length && left2.every((value, index) => value === right2[index]);
|
|
14946
|
+
}
|
|
14947
|
+
var HISTORY_DRAWER_CSS = `
|
|
14948
|
+
.dxr-history-dialog,.dxr-version-preview{box-sizing:border-box;color:#243b42;background:#fff;border:1px solid #dce5e5;box-shadow:0 24px 80px #163f4033;font:14px/1.5 system-ui,sans-serif;padding:24px;overscroll-behavior:contain}
|
|
14949
|
+
.dxr-history-dialog{width:min(420px,calc(100vw - 24px));max-height:calc(100dvh - 24px);margin:12px 12px 12px auto;border-radius:18px}
|
|
14950
|
+
.dxr-history-dialog::backdrop,.dxr-version-preview::backdrop{background:#16323333;backdrop-filter:blur(2px)}
|
|
14951
|
+
.dxr-history-dialog [hidden],.dxr-version-preview [hidden]{display:none!important}
|
|
14952
|
+
.dxr-history-dialog button,.dxr-version-preview button{font:inherit;color:inherit;border:1px solid #cbd9d9;background:#fff;border-radius:9px;min-height:40px;padding:8px 12px;cursor:pointer}
|
|
14953
|
+
.dxr-history-dialog button:hover,.dxr-version-preview button:hover{background:#eff8f6}
|
|
14954
|
+
.dxr-history-dialog :focus-visible,.dxr-version-preview :focus-visible{outline:3px solid #0f766e;outline-offset:3px}
|
|
14955
|
+
.dxr-history-header{display:flex;justify-content:flex-end;position:sticky;top:0;z-index:2;background:#fff;box-shadow:0 0 0 6px #fff}
|
|
14956
|
+
.dxr-history-dialog .dxr-history-close{border:0;font-size:13px;background:#fff}
|
|
14957
|
+
.dxr-history-source{font-weight:600;overflow-wrap:anywhere;margin:12px 0 4px}
|
|
14958
|
+
.dxr-history-status{color:#597273;margin:4px 0 18px}
|
|
14959
|
+
.dxr-history-dialog>label{display:block;font-size:12px;color:#597273;margin:12px 0}
|
|
14960
|
+
.dxr-history-dialog>label select{display:block;width:100%;font:inherit;padding:8px;border:1px solid #cbd9d9;border-radius:8px;background:#fff;color:#243b42}
|
|
14961
|
+
.dxr-history-dialog .dx-history{padding:0;border:0;border-radius:0;background:transparent}
|
|
14962
|
+
.dxr-history-dialog .dx-history h2{font-size:24px;letter-spacing:-.6px;margin:12px 0}
|
|
14963
|
+
.dxr-history-dialog .dx-history button[data-history-action=save]{background:#0f766e;border-color:#0f766e;color:#fff;font-weight:600}
|
|
14964
|
+
.dxr-history-dialog .dx-history select{border-radius:9px;background:#f8fbfa}
|
|
14965
|
+
.dxr-history-dialog .dx-history option{padding:10px;font-size:13px}
|
|
14966
|
+
.dxr-version-preview{width:min(1080px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border-radius:18px}
|
|
14967
|
+
.dxr-version-preview h2{font-size:20px;margin:0 0 12px;overflow-wrap:anywhere}
|
|
14968
|
+
.dxr-version-actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
|
|
14969
|
+
.dxr-version-paper{max-height:70dvh;overflow:auto;background:#f2f6f5;padding:20px;border-radius:10px}
|
|
14970
|
+
@media(max-width:520px){.dxr-history-dialog,.dxr-version-preview{padding:16px}.dxr-version-paper{padding:8px}}
|
|
14971
|
+
@media(prefers-reduced-motion:no-preference){.dxr-history-dialog[open]{animation:dxr-history-in .16s ease-out}@keyframes dxr-history-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:translateX(0)}}}
|
|
14972
|
+
`;
|
|
14973
|
+
|
|
14420
14974
|
// src/ribbon-chrome.ts
|
|
14421
14975
|
var RIBBON_STYLE_VERSION = "11";
|
|
14422
14976
|
var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
|
|
@@ -14867,9 +15421,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14867
15421
|
Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
|
|
14868
15422
|
.dxr-surface .docx-hf-band {
|
|
14869
15423
|
/* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
|
|
14870
|
-
custom property the viewport publishes
|
|
15424
|
+
custom property the viewport publishes, including when the page exceeds the surface. */
|
|
14871
15425
|
position: relative;
|
|
14872
|
-
width:
|
|
15426
|
+
width: var(--docx-sheet-width, 100%);
|
|
14873
15427
|
margin: 0 auto;
|
|
14874
15428
|
padding: 34px 72px 12px;
|
|
14875
15429
|
background: var(--dxr-sheet);
|
|
@@ -14928,7 +15482,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14928
15482
|
vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
|
|
14929
15483
|
has zoomed to fit stays centered at its scaled width. */
|
|
14930
15484
|
.dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
|
|
14931
|
-
|
|
15485
|
+
/* CSS zoom scales the authored page width. A percentage cap would shrink only the
|
|
15486
|
+
paper to the host while its fixed-width section and content keep magnifying. */
|
|
15487
|
+
max-width: none;
|
|
14932
15488
|
margin: 0 auto;
|
|
14933
15489
|
padding: 56px 0;
|
|
14934
15490
|
border-radius: 3px;
|
|
@@ -15295,6 +15851,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15295
15851
|
<label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
|
|
15296
15852
|
<button type="button" data-dxr="save" disabled>Save</button>
|
|
15297
15853
|
</div>
|
|
15854
|
+
<button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
|
|
15298
15855
|
<div class="dxr-quick">
|
|
15299
15856
|
<button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">↶</button>
|
|
15300
15857
|
<button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">↷</button>
|
|
@@ -16129,6 +16686,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16129
16686
|
this.resizeObserver = new ResizeObserver(() => this.applyChrome());
|
|
16130
16687
|
this.resizeObserver.observe(root);
|
|
16131
16688
|
}
|
|
16689
|
+
this.history = options.history ? new RibbonHistory(this, options.history) : null;
|
|
16132
16690
|
}
|
|
16133
16691
|
// ── element lookup ──────────────────────────────────────────────────────────
|
|
16134
16692
|
control(name) {
|
|
@@ -16153,6 +16711,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16153
16711
|
this.element.querySelector("[data-dxr-files]")?.remove();
|
|
16154
16712
|
}
|
|
16155
16713
|
if (!this.loaderOptions) this.control("loader")?.remove();
|
|
16714
|
+
if (!this.options.history) this.control("history")?.remove();
|
|
16715
|
+
else {
|
|
16716
|
+
const file = this.control("file");
|
|
16717
|
+
if (file) {
|
|
16718
|
+
file.accept = ".docx,.docxhistory";
|
|
16719
|
+
file.setAttribute("aria-label", "Open a document or history file");
|
|
16720
|
+
}
|
|
16721
|
+
}
|
|
16156
16722
|
this.require("docname").textContent = this.documentName;
|
|
16157
16723
|
this.require("paginated").checked = this.options.paginated ?? false;
|
|
16158
16724
|
this.require("headerfooter").checked = this.headerFooter;
|
|
@@ -16334,22 +16900,13 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16334
16900
|
}
|
|
16335
16901
|
open(bytes, name) {
|
|
16336
16902
|
if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
|
|
16337
|
-
|
|
16338
|
-
try {
|
|
16339
|
-
this.live.close();
|
|
16340
|
-
} catch {
|
|
16341
|
-
}
|
|
16342
|
-
this.live = null;
|
|
16343
|
-
}
|
|
16344
|
-
if (name) this.documentName = name;
|
|
16345
|
-
this.require("docname").textContent = this.documentName;
|
|
16903
|
+
this.history?.beforeOpen();
|
|
16346
16904
|
const paginated = this.require("paginated").checked;
|
|
16347
|
-
this.surface.
|
|
16348
|
-
|
|
16349
|
-
this.closeFindBar();
|
|
16905
|
+
const candidateSurface = this.surface.cloneNode(false);
|
|
16906
|
+
candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
|
|
16350
16907
|
const started = performance.now();
|
|
16351
16908
|
const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
|
|
16352
|
-
|
|
16909
|
+
const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
|
|
16353
16910
|
cssPrefix: this.options.cssPrefix,
|
|
16354
16911
|
fabricateClasses: this.options.fabricateClasses,
|
|
16355
16912
|
editable: this.options.editable,
|
|
@@ -16357,10 +16914,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16357
16914
|
columnWidth: this.options.columnWidth,
|
|
16358
16915
|
fitToWidth: this.options.fitToWidth,
|
|
16359
16916
|
onEdit: (info) => {
|
|
16917
|
+
this.history?.edited();
|
|
16360
16918
|
this.options.onEdit?.(info);
|
|
16361
16919
|
this.scheduleStats();
|
|
16362
16920
|
},
|
|
16363
|
-
onMove:
|
|
16921
|
+
onMove: (info) => {
|
|
16922
|
+
this.history?.edited();
|
|
16923
|
+
this.options.onMove?.(info);
|
|
16924
|
+
},
|
|
16364
16925
|
onStoryChange: (which) => this.onStoryChange(which),
|
|
16365
16926
|
onCommentsChange: (info) => this.onCommentsChange(info),
|
|
16366
16927
|
paginated,
|
|
@@ -16371,6 +16932,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16371
16932
|
comments: this.options.comments,
|
|
16372
16933
|
commentAuthor: this.author
|
|
16373
16934
|
});
|
|
16935
|
+
this.live?.close();
|
|
16936
|
+
candidate.adoptContainer(this.surface);
|
|
16937
|
+
this.surface.dataset.view = paginated ? "paginated" : "continuous";
|
|
16938
|
+
this.live = candidate;
|
|
16939
|
+
if (name) this.documentName = name;
|
|
16940
|
+
this.require("docname").textContent = this.documentName;
|
|
16941
|
+
this.closeFindBar();
|
|
16942
|
+
this.history?.documentOpened(this.documentName);
|
|
16374
16943
|
const saveButton = this.control("save");
|
|
16375
16944
|
if (saveButton) saveButton.disabled = false;
|
|
16376
16945
|
this.require("ribbon").setAttribute("aria-disabled", "false");
|
|
@@ -16418,6 +16987,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16418
16987
|
destroy() {
|
|
16419
16988
|
if (this.destroyed) return;
|
|
16420
16989
|
this.destroyed = true;
|
|
16990
|
+
void this.history?.destroy();
|
|
16421
16991
|
const doc = this.element.ownerDocument ?? document;
|
|
16422
16992
|
doc.removeEventListener("selectionchange", this.onSelectionChange);
|
|
16423
16993
|
doc.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
@@ -16454,6 +17024,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16454
17024
|
const el = this.control("railOp");
|
|
16455
17025
|
if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
|
|
16456
17026
|
this.options.onCommand?.(label, ms);
|
|
17027
|
+
this.history?.edited();
|
|
16457
17028
|
this.refreshRailCounts();
|
|
16458
17029
|
this.refreshRailAnchor();
|
|
16459
17030
|
this.scheduleStats();
|
|
@@ -16653,11 +17224,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16653
17224
|
file?.addEventListener("change", async () => {
|
|
16654
17225
|
const chosen = file.files?.[0];
|
|
16655
17226
|
if (!chosen) return;
|
|
17227
|
+
file.value = "";
|
|
17228
|
+
if (this.history) {
|
|
17229
|
+
await this.history.openFile(chosen);
|
|
17230
|
+
return;
|
|
17231
|
+
}
|
|
16656
17232
|
this.setStatus(`Loading ${chosen.name}\u2026`);
|
|
16657
|
-
|
|
17233
|
+
try {
|
|
17234
|
+
this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
|
|
17235
|
+
} catch (error) {
|
|
17236
|
+
this.setStatus(`Could not open this document. ${String(error)}`);
|
|
17237
|
+
}
|
|
16658
17238
|
file.value = "";
|
|
16659
17239
|
});
|
|
16660
17240
|
this.control("new")?.addEventListener("click", () => {
|
|
17241
|
+
if (this.history) {
|
|
17242
|
+
void this.history.newDocument();
|
|
17243
|
+
return;
|
|
17244
|
+
}
|
|
16661
17245
|
if (this.exports) this.openBlank("untitled.docx");
|
|
16662
17246
|
});
|
|
16663
17247
|
this.control("save")?.addEventListener("click", () => this.download());
|
|
@@ -16668,6 +17252,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16668
17252
|
if (!chosen || !this.live) return;
|
|
16669
17253
|
const started = performance.now();
|
|
16670
17254
|
const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
|
|
17255
|
+
if (ok) this.history?.edited();
|
|
16671
17256
|
const ms = performance.now() - started;
|
|
16672
17257
|
const el = this.control("railOp");
|
|
16673
17258
|
if (el) el.textContent = `picture ${Math.round(ms)} ms`;
|
|
@@ -19132,7 +19717,7 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
19132
19717
|
}
|
|
19133
19718
|
async function createRibbonEditor(container, source, options = {}) {
|
|
19134
19719
|
const el = resolveContainer2(container);
|
|
19135
|
-
const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
|
|
19720
|
+
const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
|
|
19136
19721
|
const mount2 = createScopedMount(el);
|
|
19137
19722
|
mount2.root.style.height = "100%";
|
|
19138
19723
|
mount2.root.style.minHeight = "0";
|
|
@@ -19142,6 +19727,12 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
19142
19727
|
// boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
|
|
19143
19728
|
frame: "card",
|
|
19144
19729
|
...ribbonOptions,
|
|
19730
|
+
history: history ? {
|
|
19731
|
+
...typeof history === "object" ? history : {},
|
|
19732
|
+
openHistory: openDocxHistory,
|
|
19733
|
+
openArchive: openDocxHistoryArchive,
|
|
19734
|
+
preview: (container2, bytes) => createViewer(container2, bytes, { wasmBasePath: wasmBasePath2, renderTrackedChanges: true })
|
|
19735
|
+
} : void 0,
|
|
19145
19736
|
// Exports arrive after the runtime boots; the loader covers that gap.
|
|
19146
19737
|
exports: void 0
|
|
19147
19738
|
});
|
|
@@ -19159,6 +19750,7 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
19159
19750
|
ribbon.loader.stage(2);
|
|
19160
19751
|
if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
|
|
19161
19752
|
else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
|
|
19753
|
+
await ribbon.history?.resume();
|
|
19162
19754
|
ribbon.loader.stage(3);
|
|
19163
19755
|
ribbon.loader.done();
|
|
19164
19756
|
return ribbon;
|