docxodus 12.0.1 → 12.2.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 (75) hide show
  1. package/README.md +9 -4
  2. package/dist/editor.bundle.js +197 -25
  3. package/dist/editor.d.ts +42 -3
  4. package/dist/editor.d.ts.map +1 -1
  5. package/dist/editor.js +204 -16
  6. package/dist/editor.js.map +1 -1
  7. package/dist/embed.bundle.js +635 -50
  8. package/dist/embed.d.ts +30 -1
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +635 -50
  11. package/dist/embed.js +124 -13
  12. package/dist/embed.js.map +1 -1
  13. package/dist/export-assets.json +33 -33
  14. package/dist/history.d.ts +265 -0
  15. package/dist/history.d.ts.map +1 -0
  16. package/dist/history.js +307 -0
  17. package/dist/history.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +20 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/ribbon-chrome.d.ts +2 -2
  23. package/dist/ribbon-chrome.d.ts.map +1 -1
  24. package/dist/ribbon-chrome.js +6 -4
  25. package/dist/ribbon-chrome.js.map +1 -1
  26. package/dist/ribbon.js +39 -8
  27. package/dist/ribbon.js.map +1 -1
  28. package/dist/session.js +1 -1
  29. package/dist/session.js.map +1 -1
  30. package/dist/types.d.ts +2 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js.map +1 -1
  33. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  34. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  35. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  36. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  37. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  38. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  39. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  40. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  41. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  42. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  43. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  44. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  45. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  46. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  47. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  48. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  49. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  50. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  51. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  52. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  53. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  54. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  55. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  56. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  57. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  58. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  59. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  60. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  61. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  62. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  63. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  64. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  65. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  66. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  67. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  68. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  69. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  70. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  71. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  72. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  73. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  74. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  75. package/package.json +4 -3
package/dist/editor.js CHANGED
@@ -37,6 +37,14 @@ function blockPreviewText(unit) {
37
37
  const text = (unit.textContent ?? "").trim().replace(/\s+/g, " ");
38
38
  return text.length > 48 ? `${text.slice(0, 48)}…` : text;
39
39
  }
40
+ /** The floating handle's own box and its gap to the block, as `ensureBlockDragStyles` sizes it. */
41
+ const BLOCK_HANDLE_WIDTH = 26;
42
+ const BLOCK_HANDLE_HEIGHT = 28;
43
+ const BLOCK_HANDLE_GAP = 6;
44
+ /** `value` pulled into [`low`, `high`]; `low` wins when the range is narrower than the box. */
45
+ function clampToRange(value, low, high) {
46
+ return Math.max(low, Math.min(value, high));
47
+ }
40
48
  function ensureBlockDragStyles(doc) {
41
49
  if (blockDragStyledDocuments.has(doc))
42
50
  return;
@@ -606,16 +614,20 @@ function selectionSpanIn(block) {
606
614
  const span = trimmedSpan(block, { start: Math.min(start, end), length: Math.abs(end - start) });
607
615
  return span.length > 0 ? span : null;
608
616
  }
609
- /** Restore a content-text selection spanning [start, start+length) within `el` (skips markers). */
610
- function selectRange(el, start, length) {
611
- const sel = typeof window !== "undefined" ? window.getSelection() : null;
617
+ /**
618
+ * A DOM Range over the content text spanning [start, start+length) within `el` (skips markers),
619
+ * or null when the block is detached or the offsets do not resolve. Building the range is kept
620
+ * separate from selecting it so a caller that only wants to PAINT a span (find preview) does not
621
+ * have to touch the selection — and therefore focus — at all.
622
+ */
623
+ function contentRangeIn(el, start, length) {
612
624
  // The block may have been swapped out of the document by a re-render before this runs
613
625
  // (e.g. a focus-stealing toolbar control firing twice). addRange on a detached range
614
626
  // throws "the given range isn't in document" — skip rather than warn.
615
- if (!sel || !el.isConnected)
616
- return;
617
- el.focus();
618
- const range = document.createRange();
627
+ if (!el.isConnected)
628
+ return null;
629
+ const doc = el.ownerDocument;
630
+ const range = doc.createRange();
619
631
  const from = contentPositionIn(el, start);
620
632
  const to = contentPositionIn(el, start + length);
621
633
  try {
@@ -623,11 +635,66 @@ function selectRange(el, start, length) {
623
635
  range.setEnd(to.node, to.offset);
624
636
  }
625
637
  catch {
626
- return;
638
+ return null;
627
639
  }
640
+ return range;
641
+ }
642
+ /** Restore a content-text selection spanning [start, start+length) within `el` (skips markers). */
643
+ function selectRange(el, start, length) {
644
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
645
+ if (!sel || !el.isConnected)
646
+ return;
647
+ el.focus();
648
+ const range = contentRangeIn(el, start, length);
649
+ if (!range)
650
+ return;
628
651
  sel.removeAllRanges();
629
652
  sel.addRange(range);
630
653
  }
654
+ // ─── Find painting ──────────────────────────────────────────────────────────
655
+ //
656
+ // A find box has to show WHERE the hits are while the user is still typing into it, so it cannot
657
+ // express "the current match" as a selection: putting a caret in a contenteditable block focuses
658
+ // that block, and the next keystroke lands in the document instead of the search field. These
659
+ // highlights paint the same information with no focus and no DOM mutation — the CSS Custom
660
+ // Highlight API takes plain Ranges over the live text and styles them from the stylesheet.
661
+ /** Every match currently on screen. */
662
+ const FIND_HIGHLIGHT = "docxodus-find";
663
+ /** The one the find bar's counter is pointing at. */
664
+ const FIND_HIGHLIGHT_ACTIVE = "docxodus-find-active";
665
+ /** Ranges are cheap, but a pathological query ("e" in a book) is not worth painting in full. */
666
+ const FIND_PAINT_LIMIT = 500;
667
+ const findHighlightStyledDocuments = new WeakSet();
668
+ /**
669
+ * Which editor last painted into the shared registry. Only that instance may clear it, so a
670
+ * second surface on the page cannot wipe the first one's find painting when its own find bar
671
+ * closes. Two live searches still share one painting — the newer one wins, which is what a
672
+ * single visible "current match" should mean.
673
+ */
674
+ let findPaintOwner = null;
675
+ function ensureFindHighlightStyles(doc) {
676
+ if (findHighlightStyledDocuments.has(doc))
677
+ return;
678
+ findHighlightStyledDocuments.add(doc);
679
+ const style = doc.createElement("style");
680
+ style.dataset.docxodusFindHighlight = "true";
681
+ style.textContent = `
682
+ ::highlight(${FIND_HIGHLIGHT}) { background-color: rgba(15, 118, 110, .16); }
683
+ ::highlight(${FIND_HIGHLIGHT_ACTIVE}) { background-color: rgba(15, 118, 110, .38); }
684
+ `;
685
+ (doc.head ?? doc.documentElement).appendChild(style);
686
+ }
687
+ function highlightRegistry() {
688
+ if (typeof CSS === "undefined")
689
+ return null;
690
+ const registry = CSS.highlights;
691
+ const ctor = globalThis.Highlight;
692
+ return registry && typeof ctor === "function" ? registry : null;
693
+ }
694
+ function makeHighlight(ranges) {
695
+ const ctor = globalThis.Highlight;
696
+ return new ctor(...ranges);
697
+ }
631
698
  /**
632
699
  * True when `el`'s immediate parent is a paragraph-border `<div>` the full render wrapped it in
633
700
  * (CreateBorderDivs groups visibly-bordered paragraphs into a div). The body wrapper div has no
@@ -697,6 +764,10 @@ export class DocxEditor {
697
764
  this.blockMoveMenu = null;
698
765
  this.blockMoveLive = null;
699
766
  this.blockDragSource = null;
767
+ /** Whether hover or focus currently wants a handle for `blockDragSource`.
768
+ * Tracked apart from the handle's `display`, which the viewport clip also drives: a handle
769
+ * withdrawn because its block scrolled out of view has to come back when it scrolls in. */
770
+ this.blockHandleWanted = false;
700
771
  this.blockDragCleanup = [];
701
772
  /** Block boxes measured at drag start — see `BlockDropZone`. Empty when no drag is in flight. */
702
773
  this.dropZones = [];
@@ -1009,6 +1080,7 @@ export class DocxEditor {
1009
1080
  document.removeEventListener("mouseup", this.onMouseUp, true);
1010
1081
  }
1011
1082
  this.clearDragSelection();
1083
+ this.clearFindMatches();
1012
1084
  this.teardownBlockDrag();
1013
1085
  this.gutter?.dispose();
1014
1086
  this.gutter = null;
@@ -1151,12 +1223,49 @@ export class DocxEditor {
1151
1223
  }
1152
1224
  return null;
1153
1225
  }
1226
+ /**
1227
+ * The rectangle the floating handle may occupy: the window, narrowed by every ancestor that
1228
+ * clips its overflow.
1229
+ *
1230
+ * The handle is `position: fixed`, so it is placed in viewport coordinates and no ancestor's
1231
+ * overflow clips it for us. But the editor is routinely mounted inside a bounded scroller —
1232
+ * the ribbon puts its surface in one, inside a clipped card — and a block scrolled out of that
1233
+ * scroller has to take its handle with it rather than leave it parked over the host's chrome.
1234
+ *
1235
+ * Gated on the computed `overflow` rather than on whether an element scrolls right now, so the
1236
+ * answer does not depend on how much content happens to be loaded.
1237
+ */
1238
+ blockHandleClipRect() {
1239
+ const view = this.container.ownerDocument.defaultView;
1240
+ const clip = {
1241
+ top: 0,
1242
+ left: 0,
1243
+ right: view?.innerWidth ?? Number.MAX_SAFE_INTEGER,
1244
+ bottom: view?.innerHeight ?? Number.MAX_SAFE_INTEGER,
1245
+ };
1246
+ for (let el = this.editRoot; el; el = el.parentElement) {
1247
+ const style = view?.getComputedStyle(el);
1248
+ if (!style || (style.overflowX === "visible" && style.overflowY === "visible"))
1249
+ continue;
1250
+ const box = el.getBoundingClientRect();
1251
+ if (style.overflowY !== "visible") {
1252
+ clip.top = Math.max(clip.top, box.top);
1253
+ clip.bottom = Math.min(clip.bottom, box.bottom);
1254
+ }
1255
+ if (style.overflowX !== "visible") {
1256
+ clip.left = Math.max(clip.left, box.left);
1257
+ clip.right = Math.min(clip.right, box.right);
1258
+ }
1259
+ }
1260
+ return clip;
1261
+ }
1154
1262
  showBlockHandle(unit) {
1155
1263
  const handle = this.blockDragHandle;
1156
1264
  if (!handle || !this.isMovableBlockUnit(unit))
1157
1265
  return;
1158
1266
  const changed = unit !== this.blockDragSource;
1159
1267
  this.blockDragSource = unit;
1268
+ this.blockHandleWanted = true;
1160
1269
  // A block the engine will not move anywhere — one owning a section break, or already carrying
1161
1270
  // revision markup a tracked move would have to re-wrap — gets no handle rather than a handle
1162
1271
  // that always fails. Asking costs an engine round trip, so hovering only ever CONSUMES a
@@ -1170,21 +1279,40 @@ export class DocxEditor {
1170
1279
  if (changed && known === undefined)
1171
1280
  this.prefetchBlockMoveTargets(unit);
1172
1281
  const rect = unit.getBoundingClientRect();
1282
+ // The handle belongs to the editor's viewport, not the window's. A block scrolled out of that
1283
+ // viewport gets no handle at all: clamping one back into view would leave a grip sitting next
1284
+ // to a block that is not there — and clamping to the WINDOW, as this used to, parked it on
1285
+ // whatever chrome the host draws above the editor.
1286
+ const clip = this.blockHandleClipRect();
1287
+ if (rect.bottom <= clip.top || rect.top >= clip.bottom
1288
+ || rect.right <= clip.left || rect.left >= clip.right) {
1289
+ // A gesture in flight owns the handle: it is the drag's own source element, and an open
1290
+ // move menu is anchored to it and hands focus back to it on Escape. Withdrawing it
1291
+ // mid-gesture is the same mistake as leaving it behind — mirror `hideBlockHandle`'s guard
1292
+ // and leave it where the gesture put it.
1293
+ if (!this.blockDragging && this.blockMoveMenu?.style.display !== "block")
1294
+ handle.style.display = "none";
1295
+ return;
1296
+ }
1173
1297
  handle.style.display = "flex";
1174
- handle.style.left = `${Math.max(4, rect.left - 32)}px`;
1175
- handle.style.top = `${Math.max(4, rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - 28) / 2)))}px`;
1298
+ // A block only partly in view keeps its handle, pulled to the edge it is disappearing past.
1299
+ const top = rect.top
1300
+ + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - BLOCK_HANDLE_HEIGHT) / 2));
1301
+ handle.style.left = `${clampToRange(rect.left - (BLOCK_HANDLE_WIDTH + BLOCK_HANDLE_GAP), clip.left, clip.right - BLOCK_HANDLE_WIDTH)}px`;
1302
+ handle.style.top = `${clampToRange(top, clip.top, clip.bottom - BLOCK_HANDLE_HEIGHT)}px`;
1176
1303
  const preview = blockPreviewText(unit);
1177
1304
  handle.setAttribute("aria-label", preview ? `Move block: ${preview}` : "Move block");
1178
1305
  }
1179
1306
  hideBlockHandle() {
1180
1307
  if (this.blockDragging || this.blockMoveMenu?.style.display === "block")
1181
1308
  return;
1309
+ this.blockHandleWanted = false;
1182
1310
  if (this.blockDragHandle)
1183
1311
  this.blockDragHandle.style.display = "none";
1184
1312
  }
1185
1313
  positionBlockHandle() {
1186
1314
  const source = this.currentBlockDragSource();
1187
- if (source && this.blockDragHandle?.style.display !== "none")
1315
+ if (source && this.blockHandleWanted)
1188
1316
  this.showBlockHandle(source);
1189
1317
  }
1190
1318
  /** Draw the drop line on `zone`'s requested edge, or take it away when there is no target. */
@@ -1663,6 +1791,7 @@ export class DocxEditor {
1663
1791
  this.blockMoveMenu = null;
1664
1792
  this.blockMoveLive = null;
1665
1793
  this.blockDragSource = null;
1794
+ this.blockHandleWanted = false;
1666
1795
  this.blockDragging = false;
1667
1796
  this.blockDragPointerDown = false;
1668
1797
  }
@@ -3642,16 +3771,75 @@ export class DocxEditor {
3642
3771
  }
3643
3772
  return out;
3644
3773
  }
3645
- /** Select a match and scroll it into view. */
3774
+ /**
3775
+ * Select a match, focus its block and scroll it into view — the caret lands on the hit, so
3776
+ * typing continues in the document. A find box that is still being typed into wants
3777
+ * `showFindMatches` instead; this is the commit step (closing the bar, jumping in to edit).
3778
+ */
3646
3779
  selectMatch(match) {
3647
3780
  if (!match.block.isConnected)
3648
3781
  return;
3782
+ this.clearFindMatches();
3649
3783
  this.activeBlock = match.block;
3650
3784
  selectRange(match.block, match.start, match.length);
3651
3785
  match.block.scrollIntoView({ block: "center", behavior: "smooth" });
3652
3786
  }
3653
- /** Replace one match's text (formatting of the surrounding run is inherited). */
3654
- replaceMatch(match, replacement) {
3787
+ /**
3788
+ * Paint `matches` and scroll the one at `activeIndex` into view WITHOUT moving focus or the
3789
+ * caret. This is what a find field calls on every keystroke: the document shows where the hits
3790
+ * are and rides to the current one, while the keyboard stays in the search box.
3791
+ *
3792
+ * Falls back to the document selection where the CSS Custom Highlight API is missing — still
3793
+ * without focusing the block, so the next character typed goes to the search field either way.
3794
+ */
3795
+ showFindMatches(matches, activeIndex) {
3796
+ const active = matches[activeIndex];
3797
+ this.clearFindMatches();
3798
+ if (!active || !active.block.isConnected)
3799
+ return;
3800
+ this.activeBlock = active.block;
3801
+ const activeRange = contentRangeIn(active.block, active.start, active.length);
3802
+ const registry = highlightRegistry();
3803
+ if (registry && activeRange) {
3804
+ ensureFindHighlightStyles(this.container.ownerDocument);
3805
+ const rest = [];
3806
+ for (let i = 0; i < matches.length && rest.length < FIND_PAINT_LIMIT; i++) {
3807
+ if (i === activeIndex)
3808
+ continue;
3809
+ const range = contentRangeIn(matches[i].block, matches[i].start, matches[i].length);
3810
+ if (range)
3811
+ rest.push(range);
3812
+ }
3813
+ if (rest.length > 0)
3814
+ registry.set(FIND_HIGHLIGHT, makeHighlight(rest));
3815
+ registry.set(FIND_HIGHLIGHT_ACTIVE, makeHighlight([activeRange]));
3816
+ findPaintOwner = this;
3817
+ }
3818
+ else if (activeRange) {
3819
+ // No highlight registry (older Firefox/Safari): the document selection is the only paint
3820
+ // available. It is not registry-owned, so `clearFindMatches` leaves it — the next
3821
+ // `showFindMatches` or the closing `selectMatch` overwrites it.
3822
+ const sel = this.container.ownerDocument.defaultView?.getSelection();
3823
+ sel?.removeAllRanges();
3824
+ sel?.addRange(activeRange);
3825
+ }
3826
+ active.block.scrollIntoView({ block: "center", behavior: "smooth" });
3827
+ }
3828
+ /** Drop the find painting (closing the find bar, or committing a match to the caret). */
3829
+ clearFindMatches() {
3830
+ if (findPaintOwner !== this)
3831
+ return;
3832
+ const registry = highlightRegistry();
3833
+ registry?.delete(FIND_HIGHLIGHT);
3834
+ registry?.delete(FIND_HIGHLIGHT_ACTIVE);
3835
+ findPaintOwner = null;
3836
+ }
3837
+ /**
3838
+ * Replace one match's text (formatting of the surrounding run is inherited). `focus: false`
3839
+ * leaves the keyboard where it is — a Replace button pressed from the find bar must not drag
3840
+ * the caret into the document mid-search.
3841
+ */
3842
+ replaceMatch(match, replacement, options = {}) {
3655
3843
  const block = match.block;
3656
3844
  if (this.closed || !block.isConnected)
3657
3845
  return false;
@@ -3669,7 +3857,7 @@ export class DocxEditor {
3669
3857
  if (!res.success)
3670
3858
  return false;
3671
3859
  const fresh = this.swapBlock(block, unid, res.modified?.[0]);
3672
- if (fresh)
3860
+ if (fresh && options.focus !== false)
3673
3861
  selectRange(fresh, span.start, replacement.length);
3674
3862
  return true;
3675
3863
  }
@@ -3692,7 +3880,7 @@ export class DocxEditor {
3692
3880
  if (!current.isConnected)
3693
3881
  break;
3694
3882
  const before = current;
3695
- if (this.replaceMatch({ block: current, start: m.start, length: m.length }, replacement)) {
3883
+ if (this.replaceMatch({ block: current, start: m.start, length: m.length }, replacement, { focus: options.focus })) {
3696
3884
  count++;
3697
3885
  // swapBlock replaced the node; keep following it.
3698
3886
  current = this.activeBlock && this.activeBlock !== before ? this.activeBlock : current;