docxodus 12.2.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 +11 -4
- package/dist/editor.bundle.js +1208 -28
- 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 +1212 -31
- package/dist/embed.d.ts +5 -2
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.iife.js +1212 -31
- package/dist/embed.js +10 -3
- package/dist/embed.js.map +1 -1
- package/dist/export-assets.json +9 -9
- package/dist/history-checkpoints.d.ts +50 -0
- package/dist/history-checkpoints.d.ts.map +1 -0
- package/dist/history-checkpoints.js +102 -0
- package/dist/history-checkpoints.js.map +1 -0
- package/dist/history-controls.d.ts +33 -0
- package/dist/history-controls.d.ts.map +1 -0
- package/dist/history-controls.js +375 -0
- package/dist/history-controls.js.map +1 -0
- package/dist/history-indexeddb.d.ts +12 -0
- package/dist/history-indexeddb.d.ts.map +1 -0
- package/dist/history-indexeddb.js +154 -0
- package/dist/history-indexeddb.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +50 -19
- 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/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +6 -6
- 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 +8 -7
package/dist/embed.iife.js
CHANGED
|
@@ -148,6 +148,7 @@ var Docxodus = (() => {
|
|
|
148
148
|
DocxHistoryReader: () => DocxHistoryReader,
|
|
149
149
|
DocxSession: () => DocxSession,
|
|
150
150
|
EmptyParagraphMode: () => EmptyParagraphMode,
|
|
151
|
+
HistoryCheckpoints: () => HistoryCheckpoints,
|
|
151
152
|
MAX_HISTORY_ARCHIVE_BYTES: () => MAX_HISTORY_ARCHIVE_BYTES,
|
|
152
153
|
PaginationEngine: () => PaginationEngine,
|
|
153
154
|
PaginationMode: () => PaginationMode,
|
|
@@ -207,6 +208,7 @@ var Docxodus = (() => {
|
|
|
207
208
|
getVersion: () => getVersion,
|
|
208
209
|
getWasmExports: () => getWasmExports,
|
|
209
210
|
hasAnnotations: () => hasAnnotations,
|
|
211
|
+
historyControlError: () => historyControlError,
|
|
210
212
|
initialize: () => initialize,
|
|
211
213
|
installHistoryStorageImports: () => installHistoryStorageImports,
|
|
212
214
|
isDeletion: () => isDeletion,
|
|
@@ -214,11 +216,13 @@ var Docxodus = (() => {
|
|
|
214
216
|
isInitialized: () => isInitialized,
|
|
215
217
|
isInsertion: () => isInsertion,
|
|
216
218
|
isMove: () => isMove,
|
|
219
|
+
mountHistoryControls: () => mountHistoryControls,
|
|
217
220
|
mountRibbon: () => mountRibbon,
|
|
218
221
|
navigateToPageCitation: () => navigateToPageCitation,
|
|
219
222
|
openDocxHistory: () => openDocxHistory,
|
|
220
223
|
openDocxHistoryArchive: () => openDocxHistoryArchive,
|
|
221
224
|
openDocxSession: () => openDocxSession2,
|
|
225
|
+
openIndexedDbHistoryStore: () => openIndexedDbHistoryStore,
|
|
222
226
|
paginateHtml: () => paginateHtml,
|
|
223
227
|
parseSectionDimensions: () => parseSectionDimensions,
|
|
224
228
|
projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
|
|
@@ -2412,7 +2416,7 @@ var Docxodus = (() => {
|
|
|
2412
2416
|
return `${reference.digest.value}:${reference.length}`;
|
|
2413
2417
|
};
|
|
2414
2418
|
const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
|
|
2415
|
-
const
|
|
2419
|
+
const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
|
|
2416
2420
|
return {
|
|
2417
2421
|
async readBlob(reference) {
|
|
2418
2422
|
return blobs.get(key(reference))?.slice() ?? null;
|
|
@@ -2434,7 +2438,7 @@ var Docxodus = (() => {
|
|
|
2434
2438
|
async advanceHead(documentId, expected, state) {
|
|
2435
2439
|
key(state);
|
|
2436
2440
|
const current = heads.get(documentId) ?? null;
|
|
2437
|
-
if (!
|
|
2441
|
+
if (!equalHead2(current, expected)) return null;
|
|
2438
2442
|
const revision = BigInt(current?.revision ?? "0") + 1n;
|
|
2439
2443
|
if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
|
|
2440
2444
|
const head = { revision: String(revision), state: copy(state) };
|
|
@@ -2454,6 +2458,627 @@ var Docxodus = (() => {
|
|
|
2454
2458
|
};
|
|
2455
2459
|
}
|
|
2456
2460
|
|
|
2461
|
+
// src/history-checkpoints.ts
|
|
2462
|
+
var HistoryCheckpoints = class _HistoryCheckpoints {
|
|
2463
|
+
constructor(document2, journal) {
|
|
2464
|
+
this.document = document2;
|
|
2465
|
+
this.journal = journal;
|
|
2466
|
+
this.current = null;
|
|
2467
|
+
this.request = null;
|
|
2468
|
+
this.running = false;
|
|
2469
|
+
this.stale = false;
|
|
2470
|
+
}
|
|
2471
|
+
/** Pass an already captured view when opening its exact version in an editor (for example, after import). */
|
|
2472
|
+
static async open(document2, journal, view) {
|
|
2473
|
+
const controls = new _HistoryCheckpoints(document2, journal);
|
|
2474
|
+
controls.request = structuredClone(await journal.read());
|
|
2475
|
+
if (controls.request && controls.request.documentId !== document2.documentId)
|
|
2476
|
+
throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
|
|
2477
|
+
controls.current = view === void 0 ? await document2.read() : structuredClone(view);
|
|
2478
|
+
if (controls.current && controls.current.state.documentId !== document2.documentId)
|
|
2479
|
+
throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
|
|
2480
|
+
return controls;
|
|
2481
|
+
}
|
|
2482
|
+
get view() {
|
|
2483
|
+
return structuredClone(this.current);
|
|
2484
|
+
}
|
|
2485
|
+
get hasPending() {
|
|
2486
|
+
return this.request !== null;
|
|
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
|
+
}
|
|
2492
|
+
get needsRefresh() {
|
|
2493
|
+
return this.stale;
|
|
2494
|
+
}
|
|
2495
|
+
async refresh() {
|
|
2496
|
+
return this.exclusive(async () => {
|
|
2497
|
+
this.current = await this.document.read();
|
|
2498
|
+
this.stale = false;
|
|
2499
|
+
return this.view;
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
async save(bytes, metadata) {
|
|
2503
|
+
return this.start({
|
|
2504
|
+
kind: "save",
|
|
2505
|
+
head: this.current?.head ?? null,
|
|
2506
|
+
bytes,
|
|
2507
|
+
id: crypto.randomUUID(),
|
|
2508
|
+
documentId: this.document.documentId,
|
|
2509
|
+
metadata
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
async restore(target, metadata) {
|
|
2513
|
+
if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
|
|
2514
|
+
return this.start({
|
|
2515
|
+
kind: "restore",
|
|
2516
|
+
head: this.current.head,
|
|
2517
|
+
target,
|
|
2518
|
+
id: crypto.randomUUID(),
|
|
2519
|
+
documentId: this.document.documentId,
|
|
2520
|
+
metadata
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
async retry() {
|
|
2524
|
+
return this.exclusive(async () => {
|
|
2525
|
+
if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
|
|
2526
|
+
return this.publish();
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
async start(request) {
|
|
2530
|
+
return this.exclusive(async () => {
|
|
2531
|
+
if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
|
|
2532
|
+
if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
|
|
2533
|
+
this.request = structuredClone(request);
|
|
2534
|
+
return this.publish();
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
async publish() {
|
|
2538
|
+
const intended = this.request;
|
|
2539
|
+
const request = await this.journal.put(structuredClone(intended));
|
|
2540
|
+
this.request = structuredClone(request);
|
|
2541
|
+
if (request.id !== intended.id)
|
|
2542
|
+
throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
|
|
2543
|
+
let view;
|
|
2544
|
+
try {
|
|
2545
|
+
view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
if (error instanceof DocxHistoryError && error.code === "StaleHead") {
|
|
2548
|
+
this.stale = true;
|
|
2549
|
+
await this.journal.remove(request.id);
|
|
2550
|
+
this.request = null;
|
|
2551
|
+
}
|
|
2552
|
+
throw error;
|
|
2553
|
+
}
|
|
2554
|
+
this.current = view;
|
|
2555
|
+
await this.journal.remove(request.id);
|
|
2556
|
+
this.request = null;
|
|
2557
|
+
return this.view;
|
|
2558
|
+
}
|
|
2559
|
+
async exclusive(action) {
|
|
2560
|
+
if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
|
|
2561
|
+
this.running = true;
|
|
2562
|
+
try {
|
|
2563
|
+
return await action();
|
|
2564
|
+
} finally {
|
|
2565
|
+
this.running = false;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
|
|
2570
|
+
// src/history-indexeddb.ts
|
|
2571
|
+
async function openIndexedDbHistoryStore(name) {
|
|
2572
|
+
const db = await new Promise((resolve, reject) => {
|
|
2573
|
+
let blocked = false;
|
|
2574
|
+
const request = indexedDB.open(name, 1);
|
|
2575
|
+
request.onupgradeneeded = () => {
|
|
2576
|
+
for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
|
|
2577
|
+
};
|
|
2578
|
+
request.onsuccess = () => {
|
|
2579
|
+
if (blocked) request.result.close();
|
|
2580
|
+
else resolve(request.result);
|
|
2581
|
+
};
|
|
2582
|
+
request.onerror = () => reject(request.error);
|
|
2583
|
+
request.onblocked = () => {
|
|
2584
|
+
blocked = true;
|
|
2585
|
+
reject(new Error("Close other tabs using this history store, then try again."));
|
|
2586
|
+
};
|
|
2587
|
+
});
|
|
2588
|
+
db.onversionchange = () => db.close();
|
|
2589
|
+
function transaction(name2, mode, action) {
|
|
2590
|
+
return new Promise((resolve, reject) => {
|
|
2591
|
+
const tx = db.transaction(name2, mode);
|
|
2592
|
+
let value;
|
|
2593
|
+
tx.oncomplete = () => resolve(value);
|
|
2594
|
+
tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
|
|
2595
|
+
try {
|
|
2596
|
+
action(tx.objectStore(name2), (result) => {
|
|
2597
|
+
value = result;
|
|
2598
|
+
});
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
tx.abort();
|
|
2601
|
+
reject(error);
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
const storage = {
|
|
2606
|
+
readBlob(reference) {
|
|
2607
|
+
const key = referenceKey(reference);
|
|
2608
|
+
return transaction("blobs", "readonly", (store, result) => {
|
|
2609
|
+
store.get(key).onsuccess = (event) => result(event.target.result ?? null);
|
|
2610
|
+
});
|
|
2611
|
+
},
|
|
2612
|
+
async putBlob(reference, bytes) {
|
|
2613
|
+
const key = referenceKey(reference);
|
|
2614
|
+
const captured = bytes.slice();
|
|
2615
|
+
if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
|
|
2616
|
+
const hash = Array.from(
|
|
2617
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
|
|
2618
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
2619
|
+
).join("");
|
|
2620
|
+
if (hash !== key.split(":")[0])
|
|
2621
|
+
throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
|
|
2622
|
+
await transaction("blobs", "readwrite", (store, result) => {
|
|
2623
|
+
store.put(captured, key);
|
|
2624
|
+
result();
|
|
2625
|
+
});
|
|
2626
|
+
},
|
|
2627
|
+
readHead(documentId) {
|
|
2628
|
+
return transaction("heads", "readonly", (store, result) => {
|
|
2629
|
+
store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
|
|
2630
|
+
});
|
|
2631
|
+
},
|
|
2632
|
+
advanceHead(documentId, expected, state) {
|
|
2633
|
+
referenceKey(state);
|
|
2634
|
+
const captured = structuredClone({ expected, state });
|
|
2635
|
+
if (expected) validateHead(expected);
|
|
2636
|
+
const revision = BigInt(expected?.revision ?? "0") + 1n;
|
|
2637
|
+
if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
|
|
2638
|
+
return transaction("heads", "readwrite", (store, result) => {
|
|
2639
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2640
|
+
const current = event.target.result ?? null;
|
|
2641
|
+
if (!equalHead(current, captured.expected)) {
|
|
2642
|
+
result(null);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const head = { revision: String(revision), state: captured.state };
|
|
2646
|
+
store.put(head, documentId);
|
|
2647
|
+
result(head);
|
|
2648
|
+
};
|
|
2649
|
+
});
|
|
2650
|
+
},
|
|
2651
|
+
initializeHead(documentId, head) {
|
|
2652
|
+
validateHead(head);
|
|
2653
|
+
const captured = structuredClone(head);
|
|
2654
|
+
return transaction("heads", "readwrite", (store, result) => {
|
|
2655
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2656
|
+
const existing = event.target.result;
|
|
2657
|
+
if (existing) {
|
|
2658
|
+
result({ initialized: false, head: existing });
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
store.put(captured, documentId);
|
|
2662
|
+
result({ initialized: true, head: captured });
|
|
2663
|
+
};
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
return {
|
|
2668
|
+
storage,
|
|
2669
|
+
journal(documentId) {
|
|
2670
|
+
return {
|
|
2671
|
+
read: () => transaction("requests", "readonly", (store, result) => {
|
|
2672
|
+
store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
|
|
2673
|
+
}),
|
|
2674
|
+
put(request) {
|
|
2675
|
+
if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
|
|
2676
|
+
const captured = structuredClone(request);
|
|
2677
|
+
return transaction("requests", "readwrite", (store, result) => {
|
|
2678
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2679
|
+
const existing = event.target.result;
|
|
2680
|
+
if (existing) {
|
|
2681
|
+
result(existing);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
store.put(captured, documentId);
|
|
2685
|
+
result(captured);
|
|
2686
|
+
};
|
|
2687
|
+
});
|
|
2688
|
+
},
|
|
2689
|
+
remove(requestId) {
|
|
2690
|
+
return transaction("requests", "readwrite", (store, result) => {
|
|
2691
|
+
store.get(documentId).onsuccess = (event) => {
|
|
2692
|
+
if (event.target.result?.id === requestId) store.delete(documentId);
|
|
2693
|
+
result();
|
|
2694
|
+
};
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
},
|
|
2699
|
+
close: () => db.close()
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function referenceKey(reference) {
|
|
2703
|
+
if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
|
|
2704
|
+
throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
|
|
2705
|
+
return `${reference.digest.value}:${reference.length}`;
|
|
2706
|
+
}
|
|
2707
|
+
function validateHead(head) {
|
|
2708
|
+
referenceKey(head.state);
|
|
2709
|
+
if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
|
|
2710
|
+
throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
|
|
2711
|
+
}
|
|
2712
|
+
function equalHead(a, b) {
|
|
2713
|
+
return a === null || b === null ? a === b : a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/history-controls.ts
|
|
2717
|
+
function mountHistoryControls(container, options) {
|
|
2718
|
+
const pageSize = options.pageSize ?? 25;
|
|
2719
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
|
|
2720
|
+
if (options.checkpoints && options.checkpoints.document !== options.reader)
|
|
2721
|
+
throw new Error("History controls and checkpoints must use the same document.");
|
|
2722
|
+
return new HistoryPanel(container, options, pageSize);
|
|
2723
|
+
}
|
|
2724
|
+
var HistoryPanel = class {
|
|
2725
|
+
constructor(container, options, pageSize) {
|
|
2726
|
+
this.options = options;
|
|
2727
|
+
this.pageSize = pageSize;
|
|
2728
|
+
this.actions = /* @__PURE__ */ new Map();
|
|
2729
|
+
this.events = new AbortController();
|
|
2730
|
+
this.records = [];
|
|
2731
|
+
this.next = null;
|
|
2732
|
+
this.view = null;
|
|
2733
|
+
this.active = null;
|
|
2734
|
+
this.destroyed = false;
|
|
2735
|
+
const doc = container.ownerDocument;
|
|
2736
|
+
this.element = doc.createElement("section");
|
|
2737
|
+
this.element.className = "dx-history";
|
|
2738
|
+
this.element.setAttribute("aria-label", "Version history");
|
|
2739
|
+
const style = doc.createElement("style");
|
|
2740
|
+
style.textContent = HISTORY_CSS;
|
|
2741
|
+
const title = doc.createElement("h2");
|
|
2742
|
+
title.textContent = "Version history";
|
|
2743
|
+
this.status = doc.createElement("p");
|
|
2744
|
+
this.status.setAttribute("role", "status");
|
|
2745
|
+
this.status.setAttribute("aria-live", "polite");
|
|
2746
|
+
this.fieldset = doc.createElement("fieldset");
|
|
2747
|
+
const legend = doc.createElement("legend");
|
|
2748
|
+
legend.textContent = "Saved versions";
|
|
2749
|
+
this.fieldset.append(legend);
|
|
2750
|
+
this.element.append(style, title, this.status, this.fieldset);
|
|
2751
|
+
container.append(this.element);
|
|
2752
|
+
const note = doc.createElement("p");
|
|
2753
|
+
note.dataset.historyExisting = "";
|
|
2754
|
+
note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
|
|
2755
|
+
this.fieldset.append(note);
|
|
2756
|
+
this.button("refresh", "Refresh history", () => this.load(true));
|
|
2757
|
+
this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
|
|
2758
|
+
this.versions = this.select("Version");
|
|
2759
|
+
this.versions.size = 6;
|
|
2760
|
+
this.detail = doc.createElement("p");
|
|
2761
|
+
this.fieldset.append(this.detail);
|
|
2762
|
+
this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2763
|
+
this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
|
|
2764
|
+
this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
|
|
2765
|
+
const comparison = this.disclosure("Compare versions");
|
|
2766
|
+
this.before = this.select("Compare from", comparison);
|
|
2767
|
+
this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
|
|
2768
|
+
const compareNote = doc.createElement("p");
|
|
2769
|
+
compareNote.textContent = "Compare from this version to the selected version above.";
|
|
2770
|
+
comparison.append(compareNote);
|
|
2771
|
+
this.button("compare", "Compare versions", async () => {
|
|
2772
|
+
const before = this.records[Number(this.before.value)];
|
|
2773
|
+
await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
|
|
2774
|
+
}, comparison);
|
|
2775
|
+
this.button("more", "Load older versions", async () => {
|
|
2776
|
+
if (this.next) await this.page(this.next, false);
|
|
2777
|
+
});
|
|
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;
|
|
2789
|
+
this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
|
|
2790
|
+
this.button("save", "Save version", async () => {
|
|
2791
|
+
const view = await options.checkpoints.save(await options.capture(), this.metadata());
|
|
2792
|
+
await options.onCheckpoint?.(view, "save");
|
|
2793
|
+
await this.load(false);
|
|
2794
|
+
this.label.value = "";
|
|
2795
|
+
this.status.textContent = "Version saved. Your draft remains open.";
|
|
2796
|
+
}, saveGroup);
|
|
2797
|
+
saveGroup.append(attribution);
|
|
2798
|
+
this.button("restore", "Restore selected", async () => {
|
|
2799
|
+
const version = this.selected();
|
|
2800
|
+
const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
|
|
2801
|
+
|
|
2802
|
+
Your current draft and all later versions will be kept.`);
|
|
2803
|
+
if (!confirmed) {
|
|
2804
|
+
this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
const view = await options.checkpoints.restore(version.id, this.metadata());
|
|
2808
|
+
await options.onCheckpoint?.(view, "restore");
|
|
2809
|
+
await this.load(false);
|
|
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.";
|
|
2811
|
+
});
|
|
2812
|
+
const restoreNote = doc.createElement("p");
|
|
2813
|
+
restoreNote.hidden = !options.checkpoints;
|
|
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.";
|
|
2816
|
+
this.fieldset.append(restoreNote);
|
|
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
|
+
}
|
|
2826
|
+
const view = await options.checkpoints.retry();
|
|
2827
|
+
await options.onCheckpoint?.(view, "retry", request ?? void 0);
|
|
2828
|
+
await this.load(false);
|
|
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.";
|
|
2830
|
+
});
|
|
2831
|
+
const sharing = this.disclosure("Download with history");
|
|
2832
|
+
const sharingNote = doc.createElement("p");
|
|
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.";
|
|
2834
|
+
sharing.append(sharingNote);
|
|
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);
|
|
2839
|
+
const time = this.disclosure("Find a version by time");
|
|
2840
|
+
const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
|
|
2841
|
+
this.button("time", "Preview at time", async () => {
|
|
2842
|
+
if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
|
|
2843
|
+
const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
|
|
2844
|
+
await this.preview(await options.reader.materialize(sequence), "Version at selected time");
|
|
2845
|
+
}, time);
|
|
2846
|
+
const activity = this.disclosure("Recorded collaboration");
|
|
2847
|
+
const decisions = doc.createElement("ol");
|
|
2848
|
+
let showOlderActivity = () => {
|
|
2849
|
+
};
|
|
2850
|
+
this.button("activity", "Load activity", async () => {
|
|
2851
|
+
const { operations } = await options.reader.readOperationsSince(null);
|
|
2852
|
+
const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
|
|
2853
|
+
let shown = 0;
|
|
2854
|
+
showOlderActivity = () => {
|
|
2855
|
+
const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
|
|
2856
|
+
decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
|
|
2857
|
+
shown += page.length;
|
|
2858
|
+
this.actions.get("activity-more").hidden = shown >= operations.length;
|
|
2859
|
+
};
|
|
2860
|
+
decisions.replaceChildren();
|
|
2861
|
+
showOlderActivity();
|
|
2862
|
+
this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
|
|
2863
|
+
}, activity);
|
|
2864
|
+
activity.append(decisions);
|
|
2865
|
+
this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
|
|
2866
|
+
this.actions.get("activity-more").hidden = true;
|
|
2867
|
+
this.ready = this.run("Loading history", () => this.load(false));
|
|
2868
|
+
}
|
|
2869
|
+
refresh() {
|
|
2870
|
+
return this.run("Loading history", () => this.load(true));
|
|
2871
|
+
}
|
|
2872
|
+
async destroy() {
|
|
2873
|
+
this.destroyed = true;
|
|
2874
|
+
this.events.abort();
|
|
2875
|
+
this.element.remove();
|
|
2876
|
+
await this.active?.catch(() => {
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
async load(refresh) {
|
|
2880
|
+
this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
|
|
2881
|
+
if (this.view) await this.page(this.view.version.id, true);
|
|
2882
|
+
else {
|
|
2883
|
+
this.records = [];
|
|
2884
|
+
this.next = null;
|
|
2885
|
+
this.versions.replaceChildren();
|
|
2886
|
+
this.before.replaceChildren();
|
|
2887
|
+
}
|
|
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.";
|
|
2889
|
+
}
|
|
2890
|
+
async page(cursor, reset) {
|
|
2891
|
+
const page = await this.options.reader.listVersions(cursor, this.pageSize);
|
|
2892
|
+
if (reset) {
|
|
2893
|
+
this.records = [];
|
|
2894
|
+
this.versions.replaceChildren();
|
|
2895
|
+
this.before.replaceChildren();
|
|
2896
|
+
}
|
|
2897
|
+
const start2 = this.records.length;
|
|
2898
|
+
this.records.push(...page.versions);
|
|
2899
|
+
this.next = page.next;
|
|
2900
|
+
for (let index = start2; index < this.records.length; index++) {
|
|
2901
|
+
for (const select of [this.versions, this.before]) {
|
|
2902
|
+
const option = select.ownerDocument.createElement("option");
|
|
2903
|
+
option.value = String(index);
|
|
2904
|
+
option.textContent = option.title = versionTitle(this.records[index]);
|
|
2905
|
+
select.append(option);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
if (reset) {
|
|
2909
|
+
this.versions.value = "0";
|
|
2910
|
+
this.before.value = this.records.length > 1 ? "1" : "0";
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
update() {
|
|
2914
|
+
const hasVersion = this.records.length > 0;
|
|
2915
|
+
const pending = this.options.checkpoints?.hasPending ?? false;
|
|
2916
|
+
const stale = this.options.checkpoints?.needsRefresh ?? false;
|
|
2917
|
+
for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
|
|
2918
|
+
this.actions.get("more").hidden = !this.next;
|
|
2919
|
+
this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
|
|
2920
|
+
this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
|
|
2921
|
+
this.actions.get("save").disabled = pending || stale;
|
|
2922
|
+
this.actions.get("restore").hidden = !this.options.checkpoints;
|
|
2923
|
+
this.actions.get("restore").disabled = !hasVersion || pending || stale;
|
|
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;
|
|
2930
|
+
this.detail.textContent = hasVersion ? [
|
|
2931
|
+
versionTitle(this.selected()),
|
|
2932
|
+
this.selected().record.metadata.message,
|
|
2933
|
+
this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
|
|
2934
|
+
].filter(Boolean).join(" \u2014 ") : "";
|
|
2935
|
+
}
|
|
2936
|
+
selected() {
|
|
2937
|
+
return this.records[Number(this.versions.value)];
|
|
2938
|
+
}
|
|
2939
|
+
activityItem(op, resolved) {
|
|
2940
|
+
const doc = this.element.ownerDocument;
|
|
2941
|
+
const item = doc.createElement("li");
|
|
2942
|
+
const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
|
|
2943
|
+
const label = doc.createElement("p");
|
|
2944
|
+
label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
|
|
2945
|
+
item.append(label);
|
|
2946
|
+
const description = doc.createElement("p");
|
|
2947
|
+
this.commandButton("View decision", async () => {
|
|
2948
|
+
const decision = await this.options.reader.getOperation(op.id);
|
|
2949
|
+
const { kind, metadata } = decision.input.request;
|
|
2950
|
+
description.textContent = [
|
|
2951
|
+
{ text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
|
|
2952
|
+
metadata.label,
|
|
2953
|
+
metadata.message,
|
|
2954
|
+
decision.record.conflict
|
|
2955
|
+
].filter(Boolean).join(" \u2014 ");
|
|
2956
|
+
}, item);
|
|
2957
|
+
item.append(description);
|
|
2958
|
+
this.commandButton("Download proposal", async () => {
|
|
2959
|
+
await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
|
|
2960
|
+
}, item);
|
|
2961
|
+
return item;
|
|
2962
|
+
}
|
|
2963
|
+
metadata() {
|
|
2964
|
+
return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
|
|
2965
|
+
}
|
|
2966
|
+
async preview(bytes, title) {
|
|
2967
|
+
if (!this.destroyed) await this.options.preview(bytes, title);
|
|
2968
|
+
}
|
|
2969
|
+
async download(bytes, suffix) {
|
|
2970
|
+
if (this.destroyed) return;
|
|
2971
|
+
const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
|
|
2972
|
+
const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
|
|
2973
|
+
if (this.options.download) {
|
|
2974
|
+
await this.options.download(bytes, name);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
|
|
2978
|
+
const link = this.element.ownerDocument.createElement("a");
|
|
2979
|
+
link.href = url;
|
|
2980
|
+
link.download = name;
|
|
2981
|
+
link.click();
|
|
2982
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
2983
|
+
}
|
|
2984
|
+
async run(label, action) {
|
|
2985
|
+
if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
|
|
2986
|
+
if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
|
|
2987
|
+
this.fieldset.disabled = true;
|
|
2988
|
+
this.element.setAttribute("aria-busy", "true");
|
|
2989
|
+
this.status.textContent = `${label}\u2026`;
|
|
2990
|
+
const work = Promise.resolve().then(action);
|
|
2991
|
+
this.active = work;
|
|
2992
|
+
try {
|
|
2993
|
+
await work;
|
|
2994
|
+
if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
|
|
2997
|
+
throw error;
|
|
2998
|
+
} finally {
|
|
2999
|
+
this.active = null;
|
|
3000
|
+
if (!this.destroyed) {
|
|
3001
|
+
this.fieldset.disabled = false;
|
|
3002
|
+
this.element.setAttribute("aria-busy", "false");
|
|
3003
|
+
this.update();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
button(key, title, action, parent = this.fieldset) {
|
|
3008
|
+
const button = this.commandButton(title, action, parent);
|
|
3009
|
+
button.dataset.historyAction = key;
|
|
3010
|
+
this.actions.set(key, button);
|
|
3011
|
+
}
|
|
3012
|
+
commandButton(title, action, parent) {
|
|
3013
|
+
const button = parent.ownerDocument.createElement("button");
|
|
3014
|
+
button.type = "button";
|
|
3015
|
+
button.textContent = title;
|
|
3016
|
+
button.addEventListener("click", () => {
|
|
3017
|
+
void this.run(title, action).catch(() => {
|
|
3018
|
+
});
|
|
3019
|
+
}, { signal: this.events.signal });
|
|
3020
|
+
parent.append(button);
|
|
3021
|
+
return button;
|
|
3022
|
+
}
|
|
3023
|
+
select(title, parent = this.fieldset) {
|
|
3024
|
+
const label = this.fieldset.ownerDocument.createElement("label");
|
|
3025
|
+
label.textContent = title;
|
|
3026
|
+
const select = label.ownerDocument.createElement("select");
|
|
3027
|
+
select.setAttribute("aria-label", title);
|
|
3028
|
+
label.append(select);
|
|
3029
|
+
parent.append(label);
|
|
3030
|
+
return select;
|
|
3031
|
+
}
|
|
3032
|
+
input(title, type, value = "", parent = this.fieldset) {
|
|
3033
|
+
const label = parent.ownerDocument.createElement("label");
|
|
3034
|
+
label.textContent = title;
|
|
3035
|
+
const input = label.ownerDocument.createElement("input");
|
|
3036
|
+
input.type = type;
|
|
3037
|
+
input.value = value;
|
|
3038
|
+
label.append(input);
|
|
3039
|
+
parent.append(label);
|
|
3040
|
+
return input;
|
|
3041
|
+
}
|
|
3042
|
+
disclosure(title) {
|
|
3043
|
+
const details = this.fieldset.ownerDocument.createElement("details");
|
|
3044
|
+
details.dataset.historyExisting = "";
|
|
3045
|
+
const summary = details.ownerDocument.createElement("summary");
|
|
3046
|
+
summary.textContent = title;
|
|
3047
|
+
details.append(summary);
|
|
3048
|
+
this.fieldset.append(details);
|
|
3049
|
+
return details;
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
function historyControlError(error, pending = false) {
|
|
3053
|
+
const code = error instanceof DocxHistoryError ? error.code : "";
|
|
3054
|
+
if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
|
|
3055
|
+
if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
|
|
3056
|
+
if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
|
|
3057
|
+
if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
|
|
3058
|
+
if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
|
|
3059
|
+
if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
|
|
3060
|
+
if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
|
|
3061
|
+
return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
|
|
3062
|
+
}
|
|
3063
|
+
function versionTitle(version) {
|
|
3064
|
+
const { metadata } = version.record;
|
|
3065
|
+
return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
|
|
3066
|
+
}
|
|
3067
|
+
function formatTime(value) {
|
|
3068
|
+
const time = new Date(value);
|
|
3069
|
+
return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
|
|
3070
|
+
}
|
|
3071
|
+
var HISTORY_CSS = `
|
|
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}
|
|
3073
|
+
.dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
|
|
3074
|
+
.dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
3075
|
+
.dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
|
|
3076
|
+
.dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
|
|
3077
|
+
.dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
|
|
3078
|
+
.dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
|
|
3079
|
+
.dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
|
|
3080
|
+
`;
|
|
3081
|
+
|
|
2457
3082
|
// src/page-number-format.ts
|
|
2458
3083
|
var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
|
|
2459
3084
|
var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
|
|
@@ -5680,6 +6305,19 @@ var Docxodus = (() => {
|
|
|
5680
6305
|
scale: options.scale ?? 1
|
|
5681
6306
|
};
|
|
5682
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
|
+
}
|
|
5683
6321
|
/**
|
|
5684
6322
|
* Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
|
|
5685
6323
|
* Safe to call on every remount; the previous root is released first.
|
|
@@ -10613,15 +11251,20 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
10613
11251
|
revisionAuthor: opts.revisionAuthor
|
|
10614
11252
|
}));
|
|
10615
11253
|
const editor = new _DocxEditor(container, exports, handle, opts);
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
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
|
+
}
|
|
10625
11268
|
}
|
|
10626
11269
|
/**
|
|
10627
11270
|
* Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
|
|
@@ -10636,6 +11279,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
10636
11279
|
this.assertOpen();
|
|
10637
11280
|
return this.exports.DocxSessionBridge.Save(this.handle);
|
|
10638
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
|
+
}
|
|
10639
11288
|
/** Release the underlying WASM session. The editor is unusable afterward. */
|
|
10640
11289
|
close() {
|
|
10641
11290
|
if (this.closed) return;
|
|
@@ -10670,6 +11319,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
10670
11319
|
get root() {
|
|
10671
11320
|
return this.container;
|
|
10672
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
|
+
}
|
|
10673
11340
|
/**
|
|
10674
11341
|
* The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
|
|
10675
11342
|
* host and has been scaled to fit rather than reflowed — the honest thing to show a user who
|
|
@@ -13829,6 +14496,481 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
13829
14496
|
(doc.head ?? doc.documentElement).appendChild(style);
|
|
13830
14497
|
}
|
|
13831
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
|
+
|
|
13832
14974
|
// src/ribbon-chrome.ts
|
|
13833
14975
|
var RIBBON_STYLE_VERSION = "11";
|
|
13834
14976
|
var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
|
|
@@ -14279,9 +15421,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14279
15421
|
Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
|
|
14280
15422
|
.dxr-surface .docx-hf-band {
|
|
14281
15423
|
/* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
|
|
14282
|
-
custom property the viewport publishes
|
|
15424
|
+
custom property the viewport publishes, including when the page exceeds the surface. */
|
|
14283
15425
|
position: relative;
|
|
14284
|
-
width:
|
|
15426
|
+
width: var(--docx-sheet-width, 100%);
|
|
14285
15427
|
margin: 0 auto;
|
|
14286
15428
|
padding: 34px 72px 12px;
|
|
14287
15429
|
background: var(--dxr-sheet);
|
|
@@ -14340,7 +15482,9 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14340
15482
|
vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
|
|
14341
15483
|
has zoomed to fit stays centered at its scaled width. */
|
|
14342
15484
|
.dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
|
|
14343
|
-
|
|
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;
|
|
14344
15488
|
margin: 0 auto;
|
|
14345
15489
|
padding: 56px 0;
|
|
14346
15490
|
border-radius: 3px;
|
|
@@ -14707,6 +15851,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
14707
15851
|
<label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
|
|
14708
15852
|
<button type="button" data-dxr="save" disabled>Save</button>
|
|
14709
15853
|
</div>
|
|
15854
|
+
<button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
|
|
14710
15855
|
<div class="dxr-quick">
|
|
14711
15856
|
<button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">↶</button>
|
|
14712
15857
|
<button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">↷</button>
|
|
@@ -15541,6 +16686,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15541
16686
|
this.resizeObserver = new ResizeObserver(() => this.applyChrome());
|
|
15542
16687
|
this.resizeObserver.observe(root);
|
|
15543
16688
|
}
|
|
16689
|
+
this.history = options.history ? new RibbonHistory(this, options.history) : null;
|
|
15544
16690
|
}
|
|
15545
16691
|
// ── element lookup ──────────────────────────────────────────────────────────
|
|
15546
16692
|
control(name) {
|
|
@@ -15565,6 +16711,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15565
16711
|
this.element.querySelector("[data-dxr-files]")?.remove();
|
|
15566
16712
|
}
|
|
15567
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
|
+
}
|
|
15568
16722
|
this.require("docname").textContent = this.documentName;
|
|
15569
16723
|
this.require("paginated").checked = this.options.paginated ?? false;
|
|
15570
16724
|
this.require("headerfooter").checked = this.headerFooter;
|
|
@@ -15746,22 +16900,13 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15746
16900
|
}
|
|
15747
16901
|
open(bytes, name) {
|
|
15748
16902
|
if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
|
|
15749
|
-
|
|
15750
|
-
try {
|
|
15751
|
-
this.live.close();
|
|
15752
|
-
} catch {
|
|
15753
|
-
}
|
|
15754
|
-
this.live = null;
|
|
15755
|
-
}
|
|
15756
|
-
if (name) this.documentName = name;
|
|
15757
|
-
this.require("docname").textContent = this.documentName;
|
|
16903
|
+
this.history?.beforeOpen();
|
|
15758
16904
|
const paginated = this.require("paginated").checked;
|
|
15759
|
-
this.surface.
|
|
15760
|
-
|
|
15761
|
-
this.closeFindBar();
|
|
16905
|
+
const candidateSurface = this.surface.cloneNode(false);
|
|
16906
|
+
candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
|
|
15762
16907
|
const started = performance.now();
|
|
15763
16908
|
const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
|
|
15764
|
-
|
|
16909
|
+
const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
|
|
15765
16910
|
cssPrefix: this.options.cssPrefix,
|
|
15766
16911
|
fabricateClasses: this.options.fabricateClasses,
|
|
15767
16912
|
editable: this.options.editable,
|
|
@@ -15769,10 +16914,14 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15769
16914
|
columnWidth: this.options.columnWidth,
|
|
15770
16915
|
fitToWidth: this.options.fitToWidth,
|
|
15771
16916
|
onEdit: (info) => {
|
|
16917
|
+
this.history?.edited();
|
|
15772
16918
|
this.options.onEdit?.(info);
|
|
15773
16919
|
this.scheduleStats();
|
|
15774
16920
|
},
|
|
15775
|
-
onMove:
|
|
16921
|
+
onMove: (info) => {
|
|
16922
|
+
this.history?.edited();
|
|
16923
|
+
this.options.onMove?.(info);
|
|
16924
|
+
},
|
|
15776
16925
|
onStoryChange: (which) => this.onStoryChange(which),
|
|
15777
16926
|
onCommentsChange: (info) => this.onCommentsChange(info),
|
|
15778
16927
|
paginated,
|
|
@@ -15783,7 +16932,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15783
16932
|
comments: this.options.comments,
|
|
15784
16933
|
commentAuthor: this.author
|
|
15785
16934
|
});
|
|
15786
|
-
this.
|
|
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);
|
|
16943
|
+
const saveButton = this.control("save");
|
|
16944
|
+
if (saveButton) saveButton.disabled = false;
|
|
15787
16945
|
this.require("ribbon").setAttribute("aria-disabled", "false");
|
|
15788
16946
|
this.setState("ready");
|
|
15789
16947
|
this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
|
|
@@ -15829,6 +16987,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15829
16987
|
destroy() {
|
|
15830
16988
|
if (this.destroyed) return;
|
|
15831
16989
|
this.destroyed = true;
|
|
16990
|
+
void this.history?.destroy();
|
|
15832
16991
|
const doc = this.element.ownerDocument ?? document;
|
|
15833
16992
|
doc.removeEventListener("selectionchange", this.onSelectionChange);
|
|
15834
16993
|
doc.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
@@ -15865,6 +17024,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
15865
17024
|
const el = this.control("railOp");
|
|
15866
17025
|
if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
|
|
15867
17026
|
this.options.onCommand?.(label, ms);
|
|
17027
|
+
this.history?.edited();
|
|
15868
17028
|
this.refreshRailCounts();
|
|
15869
17029
|
this.refreshRailAnchor();
|
|
15870
17030
|
this.scheduleStats();
|
|
@@ -16064,11 +17224,24 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16064
17224
|
file?.addEventListener("change", async () => {
|
|
16065
17225
|
const chosen = file.files?.[0];
|
|
16066
17226
|
if (!chosen) return;
|
|
17227
|
+
file.value = "";
|
|
17228
|
+
if (this.history) {
|
|
17229
|
+
await this.history.openFile(chosen);
|
|
17230
|
+
return;
|
|
17231
|
+
}
|
|
16067
17232
|
this.setStatus(`Loading ${chosen.name}\u2026`);
|
|
16068
|
-
|
|
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
|
+
}
|
|
16069
17238
|
file.value = "";
|
|
16070
17239
|
});
|
|
16071
17240
|
this.control("new")?.addEventListener("click", () => {
|
|
17241
|
+
if (this.history) {
|
|
17242
|
+
void this.history.newDocument();
|
|
17243
|
+
return;
|
|
17244
|
+
}
|
|
16072
17245
|
if (this.exports) this.openBlank("untitled.docx");
|
|
16073
17246
|
});
|
|
16074
17247
|
this.control("save")?.addEventListener("click", () => this.download());
|
|
@@ -16079,6 +17252,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
|
|
|
16079
17252
|
if (!chosen || !this.live) return;
|
|
16080
17253
|
const started = performance.now();
|
|
16081
17254
|
const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
|
|
17255
|
+
if (ok) this.history?.edited();
|
|
16082
17256
|
const ms = performance.now() - started;
|
|
16083
17257
|
const el = this.control("railOp");
|
|
16084
17258
|
if (el) el.textContent = `picture ${Math.round(ms)} ms`;
|
|
@@ -18543,7 +19717,7 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
18543
19717
|
}
|
|
18544
19718
|
async function createRibbonEditor(container, source, options = {}) {
|
|
18545
19719
|
const el = resolveContainer2(container);
|
|
18546
|
-
const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
|
|
19720
|
+
const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
|
|
18547
19721
|
const mount2 = createScopedMount(el);
|
|
18548
19722
|
mount2.root.style.height = "100%";
|
|
18549
19723
|
mount2.root.style.minHeight = "0";
|
|
@@ -18553,6 +19727,12 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
18553
19727
|
// boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
|
|
18554
19728
|
frame: "card",
|
|
18555
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,
|
|
18556
19736
|
// Exports arrive after the runtime boots; the loader covers that gap.
|
|
18557
19737
|
exports: void 0
|
|
18558
19738
|
});
|
|
@@ -18570,6 +19750,7 @@ ${parsed.documentElement.outerHTML}`;
|
|
|
18570
19750
|
ribbon.loader.stage(2);
|
|
18571
19751
|
if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
|
|
18572
19752
|
else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
|
|
19753
|
+
await ribbon.history?.resume();
|
|
18573
19754
|
ribbon.loader.stage(3);
|
|
18574
19755
|
ribbon.loader.done();
|
|
18575
19756
|
return ribbon;
|