docxodus 12.2.0 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +9 -4
  2. package/dist/editor.bundle.js +2 -1
  3. package/dist/embed.bundle.js +592 -3
  4. package/dist/embed.d.ts +1 -1
  5. package/dist/embed.iife.js +592 -3
  6. package/dist/embed.js +1 -1
  7. package/dist/export-assets.json +9 -9
  8. package/dist/history-checkpoints.d.ts +48 -0
  9. package/dist/history-checkpoints.d.ts.map +1 -0
  10. package/dist/history-checkpoints.js +100 -0
  11. package/dist/history-checkpoints.js.map +1 -0
  12. package/dist/history-controls.d.ts +30 -0
  13. package/dist/history-controls.d.ts.map +1 -0
  14. package/dist/history-controls.js +333 -0
  15. package/dist/history-controls.js.map +1 -0
  16. package/dist/history-indexeddb.d.ts +12 -0
  17. package/dist/history-indexeddb.d.ts.map +1 -0
  18. package/dist/history-indexeddb.js +154 -0
  19. package/dist/history-indexeddb.js.map +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/ribbon.js +3 -1
  25. package/dist/ribbon.js.map +1 -1
  26. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  27. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  28. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  29. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  30. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  31. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  32. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  33. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  34. package/dist/wasm/_framework/dotnet.boot.js +6 -6
  35. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  36. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  37. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  38. package/package.json +3 -1
@@ -2278,7 +2278,7 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2278
2278
  return `${reference.digest.value}:${reference.length}`;
2279
2279
  };
2280
2280
  const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
2281
- const equalHead = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2281
+ const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2282
2282
  return {
2283
2283
  async readBlob(reference) {
2284
2284
  return blobs.get(key(reference))?.slice() ?? null;
@@ -2300,7 +2300,7 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2300
2300
  async advanceHead(documentId, expected, state) {
2301
2301
  key(state);
2302
2302
  const current = heads.get(documentId) ?? null;
2303
- if (!equalHead(current, expected)) return null;
2303
+ if (!equalHead2(current, expected)) return null;
2304
2304
  const revision = BigInt(current?.revision ?? "0") + 1n;
2305
2305
  if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2306
2306
  const head = { revision: String(revision), state: copy(state) };
@@ -2320,6 +2320,590 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2320
2320
  };
2321
2321
  }
2322
2322
 
2323
+ // src/history-checkpoints.ts
2324
+ var HistoryCheckpoints = class _HistoryCheckpoints {
2325
+ constructor(document2, journal) {
2326
+ this.document = document2;
2327
+ this.journal = journal;
2328
+ this.current = null;
2329
+ this.request = null;
2330
+ this.running = false;
2331
+ this.stale = false;
2332
+ }
2333
+ /** Pass an already captured view when opening its exact version in an editor (for example, after import). */
2334
+ static async open(document2, journal, view) {
2335
+ const controls = new _HistoryCheckpoints(document2, journal);
2336
+ controls.request = structuredClone(await journal.read());
2337
+ if (controls.request && controls.request.documentId !== document2.documentId)
2338
+ throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
2339
+ controls.current = view === void 0 ? await document2.read() : structuredClone(view);
2340
+ if (controls.current && controls.current.state.documentId !== document2.documentId)
2341
+ throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
2342
+ return controls;
2343
+ }
2344
+ get view() {
2345
+ return structuredClone(this.current);
2346
+ }
2347
+ get hasPending() {
2348
+ return this.request !== null;
2349
+ }
2350
+ get needsRefresh() {
2351
+ return this.stale;
2352
+ }
2353
+ async refresh() {
2354
+ return this.exclusive(async () => {
2355
+ this.current = await this.document.read();
2356
+ this.stale = false;
2357
+ return this.view;
2358
+ });
2359
+ }
2360
+ async save(bytes, metadata) {
2361
+ return this.start({
2362
+ kind: "save",
2363
+ head: this.current?.head ?? null,
2364
+ bytes,
2365
+ id: crypto.randomUUID(),
2366
+ documentId: this.document.documentId,
2367
+ metadata
2368
+ });
2369
+ }
2370
+ async restore(target, metadata) {
2371
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
2372
+ return this.start({
2373
+ kind: "restore",
2374
+ head: this.current.head,
2375
+ target,
2376
+ id: crypto.randomUUID(),
2377
+ documentId: this.document.documentId,
2378
+ metadata
2379
+ });
2380
+ }
2381
+ async retry() {
2382
+ return this.exclusive(async () => {
2383
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
2384
+ return this.publish();
2385
+ });
2386
+ }
2387
+ async start(request) {
2388
+ return this.exclusive(async () => {
2389
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
2390
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
2391
+ this.request = structuredClone(request);
2392
+ return this.publish();
2393
+ });
2394
+ }
2395
+ async publish() {
2396
+ const intended = this.request;
2397
+ const request = await this.journal.put(structuredClone(intended));
2398
+ this.request = structuredClone(request);
2399
+ if (request.id !== intended.id)
2400
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
2401
+ let view;
2402
+ try {
2403
+ 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);
2404
+ } catch (error) {
2405
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
2406
+ this.stale = true;
2407
+ await this.journal.remove(request.id);
2408
+ this.request = null;
2409
+ }
2410
+ throw error;
2411
+ }
2412
+ this.current = view;
2413
+ await this.journal.remove(request.id);
2414
+ this.request = null;
2415
+ return this.view;
2416
+ }
2417
+ async exclusive(action) {
2418
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
2419
+ this.running = true;
2420
+ try {
2421
+ return await action();
2422
+ } finally {
2423
+ this.running = false;
2424
+ }
2425
+ }
2426
+ };
2427
+
2428
+ // src/history-indexeddb.ts
2429
+ async function openIndexedDbHistoryStore(name) {
2430
+ const db = await new Promise((resolve, reject) => {
2431
+ let blocked = false;
2432
+ const request = indexedDB.open(name, 1);
2433
+ request.onupgradeneeded = () => {
2434
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
2435
+ };
2436
+ request.onsuccess = () => {
2437
+ if (blocked) request.result.close();
2438
+ else resolve(request.result);
2439
+ };
2440
+ request.onerror = () => reject(request.error);
2441
+ request.onblocked = () => {
2442
+ blocked = true;
2443
+ reject(new Error("Close other tabs using this history store, then try again."));
2444
+ };
2445
+ });
2446
+ db.onversionchange = () => db.close();
2447
+ function transaction(name2, mode, action) {
2448
+ return new Promise((resolve, reject) => {
2449
+ const tx = db.transaction(name2, mode);
2450
+ let value;
2451
+ tx.oncomplete = () => resolve(value);
2452
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
2453
+ try {
2454
+ action(tx.objectStore(name2), (result) => {
2455
+ value = result;
2456
+ });
2457
+ } catch (error) {
2458
+ tx.abort();
2459
+ reject(error);
2460
+ }
2461
+ });
2462
+ }
2463
+ const storage = {
2464
+ readBlob(reference) {
2465
+ const key = referenceKey(reference);
2466
+ return transaction("blobs", "readonly", (store, result) => {
2467
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
2468
+ });
2469
+ },
2470
+ async putBlob(reference, bytes) {
2471
+ const key = referenceKey(reference);
2472
+ const captured = bytes.slice();
2473
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
2474
+ const hash = Array.from(
2475
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2476
+ (byte) => byte.toString(16).padStart(2, "0")
2477
+ ).join("");
2478
+ if (hash !== key.split(":")[0])
2479
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
2480
+ await transaction("blobs", "readwrite", (store, result) => {
2481
+ store.put(captured, key);
2482
+ result();
2483
+ });
2484
+ },
2485
+ readHead(documentId) {
2486
+ return transaction("heads", "readonly", (store, result) => {
2487
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2488
+ });
2489
+ },
2490
+ advanceHead(documentId, expected, state) {
2491
+ referenceKey(state);
2492
+ const captured = structuredClone({ expected, state });
2493
+ if (expected) validateHead(expected);
2494
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
2495
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2496
+ return transaction("heads", "readwrite", (store, result) => {
2497
+ store.get(documentId).onsuccess = (event) => {
2498
+ const current = event.target.result ?? null;
2499
+ if (!equalHead(current, captured.expected)) {
2500
+ result(null);
2501
+ return;
2502
+ }
2503
+ const head = { revision: String(revision), state: captured.state };
2504
+ store.put(head, documentId);
2505
+ result(head);
2506
+ };
2507
+ });
2508
+ },
2509
+ initializeHead(documentId, head) {
2510
+ validateHead(head);
2511
+ const captured = structuredClone(head);
2512
+ return transaction("heads", "readwrite", (store, result) => {
2513
+ store.get(documentId).onsuccess = (event) => {
2514
+ const existing = event.target.result;
2515
+ if (existing) {
2516
+ result({ initialized: false, head: existing });
2517
+ return;
2518
+ }
2519
+ store.put(captured, documentId);
2520
+ result({ initialized: true, head: captured });
2521
+ };
2522
+ });
2523
+ }
2524
+ };
2525
+ return {
2526
+ storage,
2527
+ journal(documentId) {
2528
+ return {
2529
+ read: () => transaction("requests", "readonly", (store, result) => {
2530
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2531
+ }),
2532
+ put(request) {
2533
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
2534
+ const captured = structuredClone(request);
2535
+ return transaction("requests", "readwrite", (store, result) => {
2536
+ store.get(documentId).onsuccess = (event) => {
2537
+ const existing = event.target.result;
2538
+ if (existing) {
2539
+ result(existing);
2540
+ return;
2541
+ }
2542
+ store.put(captured, documentId);
2543
+ result(captured);
2544
+ };
2545
+ });
2546
+ },
2547
+ remove(requestId) {
2548
+ return transaction("requests", "readwrite", (store, result) => {
2549
+ store.get(documentId).onsuccess = (event) => {
2550
+ if (event.target.result?.id === requestId) store.delete(documentId);
2551
+ result();
2552
+ };
2553
+ });
2554
+ }
2555
+ };
2556
+ },
2557
+ close: () => db.close()
2558
+ };
2559
+ }
2560
+ function referenceKey(reference) {
2561
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
2562
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
2563
+ return `${reference.digest.value}:${reference.length}`;
2564
+ }
2565
+ function validateHead(head) {
2566
+ referenceKey(head.state);
2567
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2568
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
2569
+ }
2570
+ function equalHead(a, b) {
2571
+ 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;
2572
+ }
2573
+
2574
+ // src/history-controls.ts
2575
+ function mountHistoryControls(container, options) {
2576
+ const pageSize = options.pageSize ?? 25;
2577
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
2578
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
2579
+ throw new Error("History controls and checkpoints must use the same document.");
2580
+ return new HistoryPanel(container, options, pageSize);
2581
+ }
2582
+ var HistoryPanel = class {
2583
+ constructor(container, options, pageSize) {
2584
+ this.options = options;
2585
+ this.pageSize = pageSize;
2586
+ this.actions = /* @__PURE__ */ new Map();
2587
+ this.events = new AbortController();
2588
+ this.records = [];
2589
+ this.next = null;
2590
+ this.view = null;
2591
+ this.active = null;
2592
+ this.destroyed = false;
2593
+ const doc = container.ownerDocument;
2594
+ this.element = doc.createElement("section");
2595
+ this.element.className = "dx-history";
2596
+ this.element.setAttribute("aria-label", "Document history");
2597
+ const style = doc.createElement("style");
2598
+ style.textContent = HISTORY_CSS;
2599
+ const title = doc.createElement("h2");
2600
+ title.textContent = "Document history";
2601
+ this.status = doc.createElement("p");
2602
+ this.status.setAttribute("role", "status");
2603
+ this.status.setAttribute("aria-live", "polite");
2604
+ this.fieldset = doc.createElement("fieldset");
2605
+ const legend = doc.createElement("legend");
2606
+ legend.textContent = "Versions and checkpoints";
2607
+ this.fieldset.append(legend);
2608
+ this.element.append(style, title, this.status, this.fieldset);
2609
+ container.append(this.element);
2610
+ const note = doc.createElement("p");
2611
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2612
+ this.fieldset.append(note);
2613
+ this.button("refresh", "Refresh history", () => this.load(true));
2614
+ this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2615
+ this.versions = this.select("Version");
2616
+ this.versions.size = 6;
2617
+ this.detail = doc.createElement("p");
2618
+ this.fieldset.append(this.detail);
2619
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2620
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2621
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2622
+ this.before = this.select("Compare from");
2623
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2624
+ const compareNote = doc.createElement("p");
2625
+ compareNote.textContent = "Compare from this version to the selected version above.";
2626
+ this.fieldset.append(compareNote);
2627
+ this.button("compare", "Compare versions", async () => {
2628
+ const before = this.records[Number(this.before.value)];
2629
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2630
+ });
2631
+ this.button("more", "Load older versions", async () => {
2632
+ if (this.next) await this.page(this.next, false);
2633
+ });
2634
+ this.author = this.input("Your name", "text", options.author ?? "You");
2635
+ this.label = this.input("Checkpoint name (optional)", "text");
2636
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2637
+ this.button("save", "Save checkpoint", async () => {
2638
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
2639
+ await options.onCheckpoint?.(view, "save");
2640
+ await this.load(false);
2641
+ this.label.value = "";
2642
+ this.status.textContent = "Checkpoint saved. Your draft remains open.";
2643
+ });
2644
+ this.button("restore", "Restore selected", async () => {
2645
+ const version = this.selected();
2646
+ if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
2647
+
2648
+ Your current draft and all later versions will be kept.`)) {
2649
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2650
+ return;
2651
+ }
2652
+ const view = await options.checkpoints.restore(version.id, this.metadata());
2653
+ await options.onCheckpoint?.(view, "restore");
2654
+ await this.load(false);
2655
+ this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
2656
+ });
2657
+ const restoreNote = doc.createElement("p");
2658
+ restoreNote.hidden = !options.checkpoints;
2659
+ restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
2660
+ this.fieldset.append(restoreNote);
2661
+ this.button("retry", "Retry checkpoint", async () => {
2662
+ const view = await options.checkpoints.retry();
2663
+ await options.onCheckpoint?.(view, "retry");
2664
+ await this.load(false);
2665
+ this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
2666
+ });
2667
+ const sharing = this.disclosure("Download with history");
2668
+ const sharingNote = doc.createElement("p");
2669
+ sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
2670
+ sharing.append(sharingNote);
2671
+ this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2672
+ const time = this.disclosure("Find a version by time");
2673
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2674
+ this.button("time", "Preview at time", async () => {
2675
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
2676
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
2677
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
2678
+ }, time);
2679
+ const activity = this.disclosure("Recorded collaboration");
2680
+ const decisions = doc.createElement("ol");
2681
+ let showOlderActivity = () => {
2682
+ };
2683
+ this.button("activity", "Load activity", async () => {
2684
+ const { operations } = await options.reader.readOperationsSince(null);
2685
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
2686
+ let shown = 0;
2687
+ showOlderActivity = () => {
2688
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
2689
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
2690
+ shown += page.length;
2691
+ this.actions.get("activity-more").hidden = shown >= operations.length;
2692
+ };
2693
+ decisions.replaceChildren();
2694
+ showOlderActivity();
2695
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
2696
+ }, activity);
2697
+ activity.append(decisions);
2698
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
2699
+ this.actions.get("activity-more").hidden = true;
2700
+ this.ready = this.run("Loading history", () => this.load(false));
2701
+ }
2702
+ refresh() {
2703
+ return this.run("Loading history", () => this.load(true));
2704
+ }
2705
+ async destroy() {
2706
+ this.destroyed = true;
2707
+ this.events.abort();
2708
+ this.element.remove();
2709
+ await this.active?.catch(() => {
2710
+ });
2711
+ }
2712
+ async load(refresh) {
2713
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
2714
+ if (this.view) await this.page(this.view.version.id, true);
2715
+ else {
2716
+ this.records = [];
2717
+ this.next = null;
2718
+ this.versions.replaceChildren();
2719
+ this.before.replaceChildren();
2720
+ }
2721
+ this.status.textContent = this.options.checkpoints?.hasPending ? "A checkpoint needs recovery. Retry it before saving more changes." : this.view ? "History loaded. Select a version to preview, download or restore." : "No checkpoints yet. Save your first checkpoint when you are ready.";
2722
+ }
2723
+ async page(cursor, reset) {
2724
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
2725
+ if (reset) {
2726
+ this.records = [];
2727
+ this.versions.replaceChildren();
2728
+ this.before.replaceChildren();
2729
+ }
2730
+ const start2 = this.records.length;
2731
+ this.records.push(...page.versions);
2732
+ this.next = page.next;
2733
+ for (let index = start2; index < this.records.length; index++) {
2734
+ for (const select of [this.versions, this.before]) {
2735
+ const option = select.ownerDocument.createElement("option");
2736
+ option.value = String(index);
2737
+ option.textContent = option.title = versionTitle(this.records[index]);
2738
+ select.append(option);
2739
+ }
2740
+ }
2741
+ if (reset) {
2742
+ this.versions.value = "0";
2743
+ this.before.value = this.records.length > 1 ? "1" : "0";
2744
+ }
2745
+ }
2746
+ update() {
2747
+ const hasVersion = this.records.length > 0;
2748
+ const pending = this.options.checkpoints?.hasPending ?? false;
2749
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
2750
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
2751
+ this.actions.get("more").hidden = !this.next;
2752
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
2753
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
2754
+ this.actions.get("save").disabled = pending || stale;
2755
+ this.actions.get("restore").hidden = !this.options.checkpoints;
2756
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
2757
+ this.actions.get("retry").hidden = !pending;
2758
+ this.detail.textContent = hasVersion ? [
2759
+ versionTitle(this.selected()),
2760
+ this.selected().record.metadata.message,
2761
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
2762
+ ].filter(Boolean).join(" \u2014 ") : "";
2763
+ }
2764
+ selected() {
2765
+ return this.records[Number(this.versions.value)];
2766
+ }
2767
+ activityItem(op, resolved) {
2768
+ const doc = this.element.ownerDocument;
2769
+ const item = doc.createElement("li");
2770
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
2771
+ const label = doc.createElement("p");
2772
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
2773
+ item.append(label);
2774
+ const description = doc.createElement("p");
2775
+ this.commandButton("View decision", async () => {
2776
+ const decision = await this.options.reader.getOperation(op.id);
2777
+ const { kind, metadata } = decision.input.request;
2778
+ description.textContent = [
2779
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
2780
+ metadata.label,
2781
+ metadata.message,
2782
+ decision.record.conflict
2783
+ ].filter(Boolean).join(" \u2014 ");
2784
+ }, item);
2785
+ item.append(description);
2786
+ this.commandButton("Download proposal", async () => {
2787
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
2788
+ }, item);
2789
+ return item;
2790
+ }
2791
+ metadata() {
2792
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
2793
+ }
2794
+ async preview(bytes, title) {
2795
+ if (!this.destroyed) await this.options.preview(bytes, title);
2796
+ }
2797
+ async download(bytes, suffix) {
2798
+ if (this.destroyed) return;
2799
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
2800
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
2801
+ if (this.options.download) {
2802
+ await this.options.download(bytes, name);
2803
+ return;
2804
+ }
2805
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
2806
+ const link = this.element.ownerDocument.createElement("a");
2807
+ link.href = url;
2808
+ link.download = name;
2809
+ link.click();
2810
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2811
+ }
2812
+ async run(label, action) {
2813
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
2814
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
2815
+ this.fieldset.disabled = true;
2816
+ this.element.setAttribute("aria-busy", "true");
2817
+ this.status.textContent = `${label}\u2026`;
2818
+ const work = Promise.resolve().then(action);
2819
+ this.active = work;
2820
+ try {
2821
+ await work;
2822
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
2823
+ } catch (error) {
2824
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
2825
+ throw error;
2826
+ } finally {
2827
+ this.active = null;
2828
+ if (!this.destroyed) {
2829
+ this.fieldset.disabled = false;
2830
+ this.element.setAttribute("aria-busy", "false");
2831
+ this.update();
2832
+ }
2833
+ }
2834
+ }
2835
+ button(key, title, action, parent = this.fieldset) {
2836
+ this.actions.set(key, this.commandButton(title, action, parent));
2837
+ }
2838
+ commandButton(title, action, parent) {
2839
+ const button = parent.ownerDocument.createElement("button");
2840
+ button.type = "button";
2841
+ button.textContent = title;
2842
+ button.addEventListener("click", () => {
2843
+ void this.run(title, action).catch(() => {
2844
+ });
2845
+ }, { signal: this.events.signal });
2846
+ parent.append(button);
2847
+ return button;
2848
+ }
2849
+ select(title) {
2850
+ const label = this.fieldset.ownerDocument.createElement("label");
2851
+ label.textContent = title;
2852
+ const select = label.ownerDocument.createElement("select");
2853
+ select.setAttribute("aria-label", title);
2854
+ label.append(select);
2855
+ this.fieldset.append(label);
2856
+ return select;
2857
+ }
2858
+ input(title, type, value = "", parent = this.fieldset) {
2859
+ const label = parent.ownerDocument.createElement("label");
2860
+ label.textContent = title;
2861
+ const input = label.ownerDocument.createElement("input");
2862
+ input.type = type;
2863
+ input.value = value;
2864
+ label.append(input);
2865
+ parent.append(label);
2866
+ return input;
2867
+ }
2868
+ disclosure(title) {
2869
+ const details = this.fieldset.ownerDocument.createElement("details");
2870
+ const summary = details.ownerDocument.createElement("summary");
2871
+ summary.textContent = title;
2872
+ details.append(summary);
2873
+ this.fieldset.append(details);
2874
+ return details;
2875
+ }
2876
+ };
2877
+ function historyControlError(error, pending = false) {
2878
+ const code = error instanceof DocxHistoryError ? error.code : "";
2879
+ if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
2880
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
2881
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
2882
+ if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
2883
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
2884
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
2885
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
2886
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
2887
+ }
2888
+ function versionTitle(version) {
2889
+ const { metadata, sequence } = version.record;
2890
+ return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
2891
+ }
2892
+ function formatTime(value) {
2893
+ const time = new Date(value);
2894
+ return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
2895
+ }
2896
+ var HISTORY_CSS = `
2897
+ .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}
2898
+ .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}
2899
+ .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%)}
2900
+ .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}
2901
+ .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}
2902
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
2903
+ .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}
2904
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
2905
+ `;
2906
+
2323
2907
  // src/page-number-format.ts
2324
2908
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2325
2909
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -15649,7 +16233,8 @@ var RibbonSurface = class {
15649
16233
  comments: this.options.comments,
15650
16234
  commentAuthor: this.author
15651
16235
  });
15652
- this.require("save").disabled = false;
16236
+ const saveButton = this.control("save");
16237
+ if (saveButton) saveButton.disabled = false;
15653
16238
  this.require("ribbon").setAttribute("aria-disabled", "false");
15654
16239
  this.setState("ready");
15655
16240
  this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
@@ -18468,6 +19053,7 @@ export {
18468
19053
  DocxHistoryReader,
18469
19054
  DocxSession,
18470
19055
  EmptyParagraphMode,
19056
+ HistoryCheckpoints,
18471
19057
  MAX_HISTORY_ARCHIVE_BYTES,
18472
19058
  PaginationEngine,
18473
19059
  PaginationMode,
@@ -18527,6 +19113,7 @@ export {
18527
19113
  getVersion,
18528
19114
  getWasmExports,
18529
19115
  hasAnnotations,
19116
+ historyControlError,
18530
19117
  initialize,
18531
19118
  installHistoryStorageImports,
18532
19119
  isDeletion,
@@ -18534,11 +19121,13 @@ export {
18534
19121
  isInitialized,
18535
19122
  isInsertion,
18536
19123
  isMove,
19124
+ mountHistoryControls,
18537
19125
  mountRibbon,
18538
19126
  navigateToPageCitation,
18539
19127
  openDocxHistory,
18540
19128
  openDocxHistoryArchive,
18541
19129
  openDocxSession2 as openDocxSession,
19130
+ openIndexedDbHistoryStore,
18542
19131
  paginateHtml,
18543
19132
  parseSectionDimensions,
18544
19133
  projectAnnotationsOntoHtml,
package/dist/embed.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * ```html
8
8
  * <div id="doc"></div>
9
9
  * <script type="module">
10
- * import { createViewer } from "https://cdn.jsdelivr.net/npm/docxodus@12.1.0/dist/embed.bundle.js";
10
+ * import { createViewer } from "https://cdn.jsdelivr.net/npm/docxodus@12.2.0/dist/embed.bundle.js";
11
11
  * await createViewer("#doc", "./contract.docx");
12
12
  * </script>
13
13
  * ```