docxodus 9.3.0 → 9.5.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 (48) hide show
  1. package/dist/editor.bundle.js +588 -114
  2. package/dist/editor.d.ts +87 -11
  3. package/dist/editor.d.ts.map +1 -1
  4. package/dist/editor.js +317 -100
  5. package/dist/editor.js.map +1 -1
  6. package/dist/embed.bundle.js +621 -115
  7. package/dist/embed.iife.js +621 -115
  8. package/dist/index.d.ts +4 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/page-geometry.d.ts +74 -0
  13. package/dist/page-geometry.d.ts.map +1 -0
  14. package/dist/page-geometry.js +105 -0
  15. package/dist/page-geometry.js.map +1 -0
  16. package/dist/pagination.bundle.js +8 -6
  17. package/dist/pagination.d.ts +2 -25
  18. package/dist/pagination.d.ts.map +1 -1
  19. package/dist/pagination.js +2 -45
  20. package/dist/pagination.js.map +1 -1
  21. package/dist/ribbon-chrome.d.ts +1 -1
  22. package/dist/ribbon-chrome.d.ts.map +1 -1
  23. package/dist/ribbon-chrome.js +18 -18
  24. package/dist/ribbon.js +2 -0
  25. package/dist/ribbon.js.map +1 -1
  26. package/dist/session.bundle.js +25 -1
  27. package/dist/session.d.ts +20 -2
  28. package/dist/session.d.ts.map +1 -1
  29. package/dist/session.js +23 -1
  30. package/dist/session.js.map +1 -1
  31. package/dist/types.d.ts +8 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/dist/viewport.d.ts +80 -0
  35. package/dist/viewport.d.ts.map +1 -0
  36. package/dist/viewport.js +139 -0
  37. package/dist/viewport.js.map +1 -0
  38. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  39. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  40. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  41. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  42. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  43. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  44. package/dist/wasm/_framework/dotnet.boot.js +5 -5
  45. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  46. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  47. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  48. package/package.json +1 -1
@@ -132,8 +132,12 @@ var Docxodus = (() => {
132
132
  ComparisonLogLevel: () => ComparisonLogLevel,
133
133
  ConflictResolution: () => ConflictResolution,
134
134
  ContextBoundary: () => ContextBoundary,
135
+ DEFAULT_MARGIN: () => DEFAULT_MARGIN,
136
+ DEFAULT_PAGE_HEIGHT: () => DEFAULT_PAGE_HEIGHT,
137
+ DEFAULT_PAGE_WIDTH: () => DEFAULT_PAGE_WIDTH,
135
138
  DiffFormat: () => DiffFormat,
136
139
  DocumentElementType: () => DocumentElementType,
140
+ DocumentViewport: () => DocumentViewport,
137
141
  DocxDiffFormatComparison: () => DocxDiffFormatComparison,
138
142
  DocxDiffRevisionGranularity: () => DocxDiffRevisionGranularity,
139
143
  DocxEditor: () => DocxEditor,
@@ -179,6 +183,7 @@ var Docxodus = (() => {
179
183
  findElementsByType: () => findElementsByType,
180
184
  findMovePair: () => findMovePair,
181
185
  findTextOccurrences: () => findTextOccurrences,
186
+ fitScale: () => fitScale,
182
187
  generateAnnotationCss: () => generateAnnotationCss,
183
188
  generateAnnotationVisibilityCss: () => generateAnnotationVisibilityCss,
184
189
  getAnnotations: () => getAnnotations,
@@ -202,7 +207,10 @@ var Docxodus = (() => {
202
207
  mountRibbon: () => mountRibbon,
203
208
  openDocxSession: () => openDocxSession2,
204
209
  paginateHtml: () => paginateHtml,
210
+ parseSectionDimensions: () => parseSectionDimensions,
205
211
  projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
212
+ ptToPx: () => ptToPx,
213
+ pxToPt: () => pxToPt,
206
214
  removeAnnotation: () => removeAnnotation,
207
215
  removeAnnotationFromHtml: () => removeAnnotationFromHtml,
208
216
  renderBlockHtml: () => renderBlockHtml,
@@ -601,7 +609,9 @@ var Docxodus = (() => {
601
609
  * Table row/column editing, addressed by a cell-paragraph anchor (e.g. one returned from
602
610
  * {@link insertTable}'s `created`). Insert clones the reference row/column's widths and starts
603
611
  * empty (`created` lists the new cell-paragraph anchors); delete of the last row/column removes
604
- * the whole table. v1 assumes a rectangular grid (no horizontal cell merges).
612
+ * the whole table. All four are grid-aware: inserting across a merge extends it, deleting
613
+ * through one narrows it, and deleting a vertical merge's lead row promotes the next row to
614
+ * carry it — the grid is never left ragged.
605
615
  */
606
616
  insertTableRow(cellAnchorId, position) {
607
617
  return JSON.parse(this.wasm.InsertTableRow(this.handle, cellAnchorId, position));
@@ -615,6 +625,28 @@ var Docxodus = (() => {
615
625
  deleteTableColumn(cellAnchorId) {
616
626
  return JSON.parse(this.wasm.DeleteTableColumn(this.handle, cellAnchorId));
617
627
  }
628
+ /**
629
+ * Merge the rectangle of cells anchored at `cellAnchorId` running `rowSpan` rows down ×
630
+ * `colSpan` cells right (Word's *Merge Cells*): `w:gridSpan` for the horizontal extent,
631
+ * `w:vMerge` restart/continue for the vertical one. The rectangle must tile the same whole grid
632
+ * columns in every row it covers and must not clip a vertical merge entering from above or
633
+ * continuing below — a partial overlap fails with `invalid_table_merge` instead of tearing the
634
+ * grid. `content` decides what happens to the absorbed cells' content (default `"append"`).
635
+ */
636
+ mergeCells(cellAnchorId, rowSpan, colSpan, content = "append") {
637
+ return JSON.parse(
638
+ this.wasm.MergeCells(this.handle, cellAnchorId, rowSpan, colSpan, content)
639
+ );
640
+ }
641
+ /**
642
+ * Split the merged cell at `cellAnchorId` back into unit cells, dropping its `w:gridSpan` and
643
+ * `w:vMerge` markup and restoring one cell per grid column (each taking its `w:tblGrid` width).
644
+ * Addressing a vertical-merge continuation unmerges the whole run. A cell with no merge markup
645
+ * fails with `invalid_table_merge`.
646
+ */
647
+ unmergeCells(cellAnchorId) {
648
+ return JSON.parse(this.wasm.UnmergeCells(this.handle, cellAnchorId));
649
+ }
618
650
  /**
619
651
  * Table styling, addressed by a cell-paragraph anchor — the post-insert counterpart of
620
652
  * {@link insertTable}'s options (issue #315 Stage A). `setColumnWidths` retunes `w:tblGrid` +
@@ -1414,20 +1446,18 @@ var Docxodus = (() => {
1414
1446
  return (renderer ?? RENDERERS.decimal)(value);
1415
1447
  }
1416
1448
 
1417
- // src/pagination.ts
1449
+ // src/page-geometry.ts
1418
1450
  var DEFAULT_PAGE_WIDTH = 612;
1419
1451
  var DEFAULT_PAGE_HEIGHT = 792;
1420
1452
  var DEFAULT_MARGIN = 72;
1421
- var MAX_FOOTNOTE_AREA_RATIO = 0.6;
1422
- var MIN_BODY_CONTENT_HEIGHT = 72;
1453
+ var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
1423
1454
  function pxToPt(px) {
1424
1455
  return px * 0.75;
1425
1456
  }
1426
1457
  function ptToPx(pt) {
1427
1458
  return pt / 0.75;
1428
1459
  }
1429
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
1430
- function parseDimensions(section) {
1460
+ function parseSectionDimensions(section) {
1431
1461
  const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
1432
1462
  const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
1433
1463
  const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
@@ -1451,6 +1481,42 @@ var Docxodus = (() => {
1451
1481
  footerHeight
1452
1482
  };
1453
1483
  }
1484
+ function sectionWrappers(root) {
1485
+ const sections = Array.from(root.querySelectorAll("[data-section-index]"));
1486
+ return sections.length > 0 ? sections : [root];
1487
+ }
1488
+ var MIN_FIT_SCALE = 0.25;
1489
+ function fitScale(availablePx, naturalPt, max = 1) {
1490
+ if (!(availablePx > 0) || !(naturalPt > 0)) return max;
1491
+ const naturalPx = ptToPx(naturalPt);
1492
+ if (naturalPx <= availablePx) return max;
1493
+ return Math.max(MIN_FIT_SCALE, Math.min(max, availablePx / naturalPx));
1494
+ }
1495
+ function applyZoom(el, scale, naturalPt) {
1496
+ if (scale === 1) {
1497
+ el.style.removeProperty("zoom");
1498
+ el.style.removeProperty("transform");
1499
+ el.style.removeProperty("transform-origin");
1500
+ el.style.removeProperty("margin-right");
1501
+ el.style.removeProperty("margin-bottom");
1502
+ return;
1503
+ }
1504
+ const zoomSupported = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("zoom", "0.5");
1505
+ if (zoomSupported) {
1506
+ el.style.zoom = String(scale);
1507
+ return;
1508
+ }
1509
+ el.style.transform = `scale(${scale})`;
1510
+ el.style.transformOrigin = "top left";
1511
+ if (naturalPt) {
1512
+ el.style.marginRight = `-${ptToPx(naturalPt.width * (1 - scale))}px`;
1513
+ el.style.marginBottom = `-${ptToPx(naturalPt.height * (1 - scale))}px`;
1514
+ }
1515
+ }
1516
+
1517
+ // src/pagination.ts
1518
+ var MAX_FOOTNOTE_AREA_RATIO = 0.6;
1519
+ var MIN_BODY_CONTENT_HEIGHT = 72;
1454
1520
  var PaginationEngine = class {
1455
1521
  /**
1456
1522
  * Creates a new pagination engine.
@@ -1496,7 +1562,7 @@ var Docxodus = (() => {
1496
1562
  const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
1497
1563
  for (const section of sectionsToProcess) {
1498
1564
  const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
1499
- const dims = parseDimensions(section);
1565
+ const dims = parseSectionDimensions(section);
1500
1566
  this.stagingElement.style.visibility = "hidden";
1501
1567
  this.stagingElement.style.position = "absolute";
1502
1568
  this.stagingElement.style.left = "-9999px";
@@ -2794,6 +2860,114 @@ var Docxodus = (() => {
2794
2860
  return engine.paginate();
2795
2861
  }
2796
2862
 
2863
+ // src/viewport.ts
2864
+ var DocumentViewport = class {
2865
+ constructor(host, options = {}) {
2866
+ this.root = null;
2867
+ /** Natural page size in points — the widest section's PAGE box, which is what must fit. */
2868
+ this.natural = { width: 0, height: 0 };
2869
+ this.observer = null;
2870
+ this.host = host;
2871
+ this.options = {
2872
+ columnWidth: options.columnWidth ?? "section",
2873
+ fitToWidth: options.fitToWidth ?? true,
2874
+ scale: options.scale ?? 1
2875
+ };
2876
+ }
2877
+ /**
2878
+ * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
2879
+ * Safe to call on every remount; the previous root is released first.
2880
+ *
2881
+ * `applySectionGeometry` is false for the paginated view, which already builds real page
2882
+ * boxes at the section's dimensions — there the viewport contributes only the fit zoom.
2883
+ */
2884
+ attach(root, applySectionGeometry) {
2885
+ this.release();
2886
+ this.root = root;
2887
+ this.natural = applySectionGeometry ? this.stampSections(root) : this.measurePages(root);
2888
+ this.refresh();
2889
+ if (typeof ResizeObserver !== "undefined") {
2890
+ this.observer = new ResizeObserver(() => this.refresh());
2891
+ this.observer.observe(this.host);
2892
+ }
2893
+ }
2894
+ /** Recompute the fit zoom against the host's current width. */
2895
+ refresh() {
2896
+ if (!this.root) return;
2897
+ const scale = this.scale;
2898
+ applyZoom(this.root, scale, this.natural);
2899
+ this.host.style.setProperty(
2900
+ "--docx-sheet-width",
2901
+ this.natural.width > 0 ? `${ptToPx(this.natural.width) * scale}px` : "100%"
2902
+ );
2903
+ }
2904
+ /** The zoom currently applied (1 = 100%). Reported by the ribbon's anchor rail. */
2905
+ get scale() {
2906
+ if (!this.root) return this.options.scale;
2907
+ return this.options.fitToWidth ? fitScale(this.availableWidthPx(), this.natural.width, this.options.scale) : this.options.scale;
2908
+ }
2909
+ dispose() {
2910
+ this.release();
2911
+ this.root = null;
2912
+ }
2913
+ release() {
2914
+ this.observer?.disconnect();
2915
+ this.observer = null;
2916
+ if (this.root) applyZoom(this.root, 1);
2917
+ this.host.style.removeProperty("--docx-sheet-width");
2918
+ }
2919
+ /** The host's content box, which is the space a page has to fit into. */
2920
+ availableWidthPx() {
2921
+ const style = typeof getComputedStyle === "function" ? getComputedStyle(this.host) : null;
2922
+ const padding = style ? (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0) : 0;
2923
+ return Math.max(0, this.host.clientWidth - padding);
2924
+ }
2925
+ /**
2926
+ * Give each section wrapper its `w:sectPr` geometry: the authored text column, guttered by
2927
+ * the authored margins. The wrapper then measures exactly one page wide, which is what the
2928
+ * sheet chrome paints and what the fit zoom scales.
2929
+ */
2930
+ stampSections(root) {
2931
+ const sections = sectionWrappers(root);
2932
+ if (this.options.columnWidth === "fluid") {
2933
+ root.style.removeProperty("width");
2934
+ for (const section of sections) {
2935
+ section.style.removeProperty("width");
2936
+ section.style.removeProperty("padding-left");
2937
+ section.style.removeProperty("padding-right");
2938
+ }
2939
+ return { width: 0, height: 0 };
2940
+ }
2941
+ let widest = 0;
2942
+ for (const section of sections) {
2943
+ const dims = parseSectionDimensions(section);
2944
+ section.style.width = `${dims.contentWidth}pt`;
2945
+ section.style.paddingLeft = `${dims.marginLeft}pt`;
2946
+ section.style.paddingRight = `${dims.marginRight}pt`;
2947
+ section.style.boxSizing = "content-box";
2948
+ section.style.marginLeft = "auto";
2949
+ section.style.marginRight = "auto";
2950
+ widest = Math.max(widest, dims.pageWidth);
2951
+ }
2952
+ if (widest > 0 && sections[0] !== root) root.style.width = `${widest}pt`;
2953
+ return { width: widest, height: 0 };
2954
+ }
2955
+ /**
2956
+ * The paginated view's page boxes are already page-sized — `pagination.ts` writes each
2957
+ * box's `width` in points — so the widest of those is the natural width. Reading the
2958
+ * inline width rather than the laid-out box keeps this independent of the per-box zoom
2959
+ * pagination may itself have applied.
2960
+ */
2961
+ measurePages(root) {
2962
+ let widest = 0;
2963
+ for (const box of Array.from(root.children)) {
2964
+ const declared = /^([\d.]+)pt$/.exec(box.style.width || "");
2965
+ widest = Math.max(widest, declared ? parseFloat(declared[1]) : pxToPt(box.offsetWidth));
2966
+ }
2967
+ return { width: widest, height: 0 };
2968
+ }
2969
+ };
2970
+
2797
2971
  // src/editor-headerfooter.ts
2798
2972
  var PAGE_FORMAT_LABELS = [
2799
2973
  { value: "", label: "Format\u2026" },
@@ -4882,6 +5056,146 @@ var Docxodus = (() => {
4882
5056
  return once(cleanup);
4883
5057
  }
4884
5058
 
5059
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js
5060
+ function ownKeys4(e, r) {
5061
+ var t = Object.keys(e);
5062
+ if (Object.getOwnPropertySymbols) {
5063
+ var o = Object.getOwnPropertySymbols(e);
5064
+ r && (o = o.filter(function(r2) {
5065
+ return Object.getOwnPropertyDescriptor(e, r2).enumerable;
5066
+ })), t.push.apply(t, o);
5067
+ }
5068
+ return t;
5069
+ }
5070
+ function _objectSpread4(e) {
5071
+ for (var r = 1; r < arguments.length; r++) {
5072
+ var t = null != arguments[r] ? arguments[r] : {};
5073
+ r % 2 ? ownKeys4(Object(t), true).forEach(function(r2) {
5074
+ _defineProperty(e, r2, t[r2]);
5075
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys4(Object(t)).forEach(function(r2) {
5076
+ Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
5077
+ });
5078
+ }
5079
+ return e;
5080
+ }
5081
+ function defaultOffset() {
5082
+ return {
5083
+ x: 0,
5084
+ y: 0
5085
+ };
5086
+ }
5087
+ function setCustomNativeDragPreview(_ref) {
5088
+ var render = _ref.render, nativeSetDragImage = _ref.nativeSetDragImage, _ref$getOffset = _ref.getOffset, getOffset = _ref$getOffset === void 0 ? defaultOffset : _ref$getOffset;
5089
+ var container = document.createElement("div");
5090
+ if (supportsPopover()) {
5091
+ container.setAttribute("popover", "manual");
5092
+ }
5093
+ Object.assign(container.style, _objectSpread4(_objectSpread4({
5094
+ // Ensuring we don't cause reflow when adding the element to the page
5095
+ // Using `position:fixed` rather than `position:absolute` so we are
5096
+ // positioned on the current viewport.
5097
+ // `position:fixed` also creates a new stacking context, so we don't need to do that here
5098
+ position: "fixed"
5099
+ }, supportsPopover() ? (
5100
+ // needs to come first as it has 'inset: unset' which
5101
+ // needs to be overridden by our top / left values
5102
+ popoverResetUserAgentStyles
5103
+ ) : {
5104
+ // Fallback: using maximum possible z-index so that this element
5105
+ // will always be on top of other positioned content.
5106
+ zIndex: maxZIndex
5107
+ }), {}, {
5108
+ // According to `mdn`, the element can be offscreen:
5109
+ // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage#imgelement
5110
+ //
5111
+ // However, that information does not appear in the specs:
5112
+ // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdragimage-dev
5113
+ //
5114
+ // If the element is _completely_ offscreen, Safari@17.1 will cancel the drag
5115
+ top: 0,
5116
+ left: 0,
5117
+ // Avoiding any additional events caused by the new element (being super safe)
5118
+ pointerEvents: "none"
5119
+ }));
5120
+ document.body.append(container);
5121
+ if (supportsPopover()) {
5122
+ container.showPopover();
5123
+ }
5124
+ var unmount = render({
5125
+ container
5126
+ });
5127
+ queueMicrotask(function() {
5128
+ var previewOffset = getOffset({
5129
+ container
5130
+ });
5131
+ if (isSafari()) {
5132
+ var rect = container.getBoundingClientRect();
5133
+ if (rect.width === 0) {
5134
+ return;
5135
+ }
5136
+ container.style.left = "-".concat(rect.width - 1e-4, "px");
5137
+ }
5138
+ nativeSetDragImage === null || nativeSetDragImage === void 0 || nativeSetDragImage(container, previewOffset.x, previewOffset.y);
5139
+ });
5140
+ function cleanup() {
5141
+ unbindMonitor();
5142
+ unmount === null || unmount === void 0 || unmount();
5143
+ document.body.removeChild(container);
5144
+ }
5145
+ var unbindMonitor = monitorForElements({
5146
+ // Remove portal in the dragstart event so that the user will never see it
5147
+ onDragStart: cleanup,
5148
+ // Backup: remove portal when the drop finishes (this would be an error case)
5149
+ onDrop: cleanup
5150
+ });
5151
+ }
5152
+
5153
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/util/is-safari-on-ios.js
5154
+ var isSafariOnIOS = once(function isSafariOnIOS2() {
5155
+ if (false) {
5156
+ return false;
5157
+ }
5158
+ return isSafari() && "ontouchend" in document;
5159
+ });
5160
+
5161
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/center-under-pointer.js
5162
+ var centerUnderPointer = function centerUnderPointer2(_ref) {
5163
+ var container = _ref.container;
5164
+ var rect = container.getBoundingClientRect();
5165
+ return {
5166
+ x: rect.width / 2,
5167
+ y: rect.height / 2
5168
+ };
5169
+ };
5170
+
5171
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/pointer-outside-of-preview.js
5172
+ function pointerOutsideOfPreview(point) {
5173
+ return function getOffset(_ref) {
5174
+ var container = _ref.container;
5175
+ if (isSafariOnIOS() || isAndroid()) {
5176
+ return centerUnderPointer({
5177
+ container
5178
+ });
5179
+ }
5180
+ Object.assign(container.style, {
5181
+ borderInlineStart: "".concat(point.x, " solid transparent"),
5182
+ borderTop: "".concat(point.y, " solid transparent")
5183
+ });
5184
+ var computed = window.getComputedStyle(container);
5185
+ if (computed.direction === "rtl") {
5186
+ var box = container.getBoundingClientRect();
5187
+ return {
5188
+ x: box.width,
5189
+ y: 0
5190
+ };
5191
+ }
5192
+ return {
5193
+ x: 0,
5194
+ y: 0
5195
+ };
5196
+ };
5197
+ }
5198
+
4885
5199
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/shared/engagement-history.js
4886
5200
  var ledger2 = /* @__PURE__ */ new Map();
4887
5201
  var requested = /* @__PURE__ */ new Set();
@@ -5015,7 +5329,7 @@ var Docxodus = (() => {
5015
5329
  }
5016
5330
 
5017
5331
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/shared/configuration.js
5018
- function ownKeys4(e, r) {
5332
+ function ownKeys5(e, r) {
5019
5333
  var t = Object.keys(e);
5020
5334
  if (Object.getOwnPropertySymbols) {
5021
5335
  var o = Object.getOwnPropertySymbols(e);
@@ -5025,12 +5339,12 @@ var Docxodus = (() => {
5025
5339
  }
5026
5340
  return t;
5027
5341
  }
5028
- function _objectSpread4(e) {
5342
+ function _objectSpread5(e) {
5029
5343
  for (var r = 1; r < arguments.length; r++) {
5030
5344
  var t = null != arguments[r] ? arguments[r] : {};
5031
- r % 2 ? ownKeys4(Object(t), true).forEach(function(r2) {
5345
+ r % 2 ? ownKeys5(Object(t), true).forEach(function(r2) {
5032
5346
  _defineProperty(e, r2, t[r2]);
5033
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys4(Object(t)).forEach(function(r2) {
5347
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys5(Object(t)).forEach(function(r2) {
5034
5348
  Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
5035
5349
  });
5036
5350
  }
@@ -5065,7 +5379,7 @@ var Docxodus = (() => {
5065
5379
  };
5066
5380
  function getInternalConfig(provided) {
5067
5381
  var _provided$maxScrollSp;
5068
- return _objectSpread4(_objectSpread4({}, baseConfig), {}, {
5382
+ return _objectSpread5(_objectSpread5({}, baseConfig), {}, {
5069
5383
  // only allowing limited control over the config at this stage
5070
5384
  maxPixelScrollPerSecond: maxPixelScrollPerSecond[(_provided$maxScrollSp = provided === null || provided === void 0 ? void 0 : provided.maxScrollSpeed) !== null && _provided$maxScrollSp !== void 0 ? _provided$maxScrollSp : "standard"]
5071
5385
  });
@@ -5549,7 +5863,7 @@ var Docxodus = (() => {
5549
5863
  }
5550
5864
 
5551
5865
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/over-element/make-api.js
5552
- function ownKeys5(e, r) {
5866
+ function ownKeys6(e, r) {
5553
5867
  var t = Object.keys(e);
5554
5868
  if (Object.getOwnPropertySymbols) {
5555
5869
  var o = Object.getOwnPropertySymbols(e);
@@ -5559,12 +5873,12 @@ var Docxodus = (() => {
5559
5873
  }
5560
5874
  return t;
5561
5875
  }
5562
- function _objectSpread5(e) {
5876
+ function _objectSpread6(e) {
5563
5877
  for (var r = 1; r < arguments.length; r++) {
5564
5878
  var t = null != arguments[r] ? arguments[r] : {};
5565
- r % 2 ? ownKeys5(Object(t), true).forEach(function(r2) {
5879
+ r % 2 ? ownKeys6(Object(t), true).forEach(function(r2) {
5566
5880
  _defineProperty(e, r2, t[r2]);
5567
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys5(Object(t)).forEach(function(r2) {
5881
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys6(Object(t)).forEach(function(r2) {
5568
5882
  Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
5569
5883
  });
5570
5884
  }
@@ -5603,7 +5917,7 @@ var Docxodus = (() => {
5603
5917
  }
5604
5918
  function autoScrollWindow() {
5605
5919
  var args = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
5606
- var unique = _objectSpread5({}, args);
5920
+ var unique = _objectSpread6({}, args);
5607
5921
  windowRegistry.add(unique);
5608
5922
  function cleanup() {
5609
5923
  windowRegistry.delete(unique);
@@ -5756,6 +6070,11 @@ var Docxodus = (() => {
5756
6070
  var EDITABLE_TAGS = /* @__PURE__ */ new Set(["P", "H1", "H2", "H3", "H4", "H5", "H6"]);
5757
6071
  var BLOCK_DRAG_TYPE = "docxodus-block";
5758
6072
  var blockDragStyledDocuments = /* @__PURE__ */ new WeakSet();
6073
+ function blockPreviewText(unit) {
6074
+ if (unit.tagName === "TABLE") return "Table";
6075
+ const text = (unit.textContent ?? "").trim().replace(/\s+/g, " ");
6076
+ return text.length > 48 ? `${text.slice(0, 48)}\u2026` : text;
6077
+ }
5759
6078
  function ensureBlockDragStyles(doc) {
5760
6079
  if (blockDragStyledDocuments.has(doc)) return;
5761
6080
  blockDragStyledDocuments.add(doc);
@@ -5770,10 +6089,28 @@ var Docxodus = (() => {
5770
6089
  }
5771
6090
  .docx-block-handle:hover, .docx-block-handle:focus-visible { color: #344054; border-color: #98a2b3; outline: none; }
5772
6091
  .docx-block-handle[aria-pressed="true"] { color: #175cd3; border-color: #84adff; background: #eff8ff; }
5773
- .docx-block-handle.docx-block-dragging { cursor: grabbing; opacity: .78; }
6092
+ .docx-block-handle.docx-block-dragging { cursor: grabbing; opacity: .35; }
6093
+ /* The block being carried. Dimming it is the "a drag is happening" signal that survives the
6094
+ pointer being anywhere on screen \u2014 the drop line only says where, not what. */
6095
+ .docx-block-drag-source { opacity: .38; transition: opacity 120ms ease-out; }
6096
+ /* Positioned by transform so tracking the pointer costs no layout. Flipping display none\u2192block
6097
+ restarts the fade \u2014 one cheap entry animation per appearance, none while it tracks. */
5774
6098
  .docx-block-drop-indicator {
5775
- position: fixed; z-index: 2147482999; display: none; height: 3px; pointer-events: none;
5776
- border-radius: 999px; background: #2e90fa; box-shadow: 0 0 0 1px rgba(255,255,255,.85);
6099
+ position: fixed; top: 0; left: 0; z-index: 2147482999; display: none; height: 0;
6100
+ pointer-events: none; border-top: 2px solid #2e90fa;
6101
+ filter: drop-shadow(0 1px 2px rgba(46,144,250,.5));
6102
+ animation: docx-block-drop-in 110ms ease-out;
6103
+ }
6104
+ .docx-block-drop-indicator::before {
6105
+ content: ""; position: absolute; top: -5px; left: -2px; width: 8px; height: 8px;
6106
+ border-radius: 50%; background: #2e90fa;
6107
+ }
6108
+ @keyframes docx-block-drop-in { from { opacity: 0; } to { opacity: 1; } }
6109
+ .docx-block-drag-preview {
6110
+ max-width: 320px; padding: 6px 10px; border: 1px solid #b2ddff; border-radius: 6px;
6111
+ background: #eff8ff; color: #175cd3; box-shadow: 0 6px 16px rgba(16,24,40,.18);
6112
+ font: 500 13px/1.35 system-ui, sans-serif; white-space: nowrap; overflow: hidden;
6113
+ text-overflow: ellipsis;
5777
6114
  }
5778
6115
  .docx-block-move-menu {
5779
6116
  position: fixed; z-index: 2147483001; display: none; min-width: 150px; padding: 5px;
@@ -6161,12 +6498,22 @@ var Docxodus = (() => {
6161
6498
  this.blockMoveLive = null;
6162
6499
  this.blockDragSource = null;
6163
6500
  this.blockDragCleanup = [];
6164
- this.blockDragTargetCleanup = [];
6501
+ /** Block boxes measured at drag start — see `BlockDropZone`. Empty when no drag is in flight. */
6502
+ this.dropZones = [];
6503
+ /** Combined scroll offset when `dropZones` was measured, and the scroller measured against. */
6504
+ this.dropZoneOrigin = 0;
6505
+ this.dropZoneScroller = null;
6165
6506
  this.blockDragging = false;
6166
6507
  this.blockDragPointerDown = false;
6167
6508
  /** Anchors the current drag source may legally move next to, per the engine's own rules.
6168
6509
  * Null when the bridge predates ValidMoveTargets — then every block is offered, as before. */
6169
6510
  this.blockMoveTargets = null;
6511
+ /** Memoized `ValidMoveTargets` answers, keyed by source anchor and dropped whenever an edit
6512
+ * lands. The legal-target set is a property of the document, so hovering back and forth over
6513
+ * the same blocks between edits must not re-ask the engine. */
6514
+ this.blockMoveTargetCache = /* @__PURE__ */ new Map();
6515
+ /** Cancels the pending idle prefetch of the hovered block's targets — see `showBlockHandle`. */
6516
+ this.blockMoveTargetPrefetch = null;
6170
6517
  /** Why the last move was refused, verbatim from the engine — diagnostics, not announcement copy. */
6171
6518
  this.lastMoveError = null;
6172
6519
  /**
@@ -6284,6 +6631,11 @@ var Docxodus = (() => {
6284
6631
  this.handle = handle;
6285
6632
  this.options = options;
6286
6633
  this.editRoot = container;
6634
+ this.viewport = new DocumentViewport(container, {
6635
+ columnWidth: options.columnWidth,
6636
+ fitToWidth: options.fitToWidth,
6637
+ scale: options.scale
6638
+ });
6287
6639
  if (typeof document !== "undefined") {
6288
6640
  document.addEventListener("selectionchange", this.onSelectionChange);
6289
6641
  document.addEventListener("mousedown", this.onMouseDown, true);
@@ -6366,6 +6718,8 @@ var Docxodus = (() => {
6366
6718
  editable: options.editable ?? true,
6367
6719
  paginated: options.paginated ?? false,
6368
6720
  scale: options.scale ?? 1,
6721
+ columnWidth: options.columnWidth ?? "section",
6722
+ fitToWidth: options.fitToWidth ?? true,
6369
6723
  headerFooter: options.headerFooter ?? false,
6370
6724
  blockDrag: options.blockDrag ?? false,
6371
6725
  trackedChanges: options.trackedChanges ?? 0 /* Accept */,
@@ -6422,6 +6776,7 @@ var Docxodus = (() => {
6422
6776
  }
6423
6777
  this.clearDragSelection();
6424
6778
  this.teardownBlockDrag();
6779
+ this.viewport.dispose();
6425
6780
  this.exports.DocxSessionBridge.CloseSession(this.handle);
6426
6781
  }
6427
6782
  /**
@@ -6439,6 +6794,14 @@ var Docxodus = (() => {
6439
6794
  get root() {
6440
6795
  return this.container;
6441
6796
  }
6797
+ /**
6798
+ * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
6799
+ * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
6800
+ * is wondering why a phone shows the whole page.
6801
+ */
6802
+ get zoom() {
6803
+ return this.viewport.scale;
6804
+ }
6442
6805
  /**
6443
6806
  * The live `DocxSession` handle backing this editor — the model of record.
6444
6807
  *
@@ -6467,8 +6830,7 @@ var Docxodus = (() => {
6467
6830
  return true;
6468
6831
  }
6469
6832
  const destination = res.created?.[0] ?? res.modified?.[0];
6470
- if (this.renderTrackedChanges) this.remount();
6471
- else this.reconcile();
6833
+ this.reconcile();
6472
6834
  const moved = destination ? this.bodyUnitNodes().find((el) => el.getAttribute("data-anchor") === destination.unid) : null;
6473
6835
  if (moved) {
6474
6836
  moved.classList.add("docx-block-move-flash");
@@ -6493,7 +6855,12 @@ var Docxodus = (() => {
6493
6855
  const node = target instanceof Node ? target : null;
6494
6856
  const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
6495
6857
  if (!el || !this.editRoot.contains(el)) return null;
6496
- return this.bodyUnitNodes().find((unit) => unit === el || unit.contains(el)) ?? null;
6858
+ let unit = null;
6859
+ for (let candidate = el.closest("[data-anchor]"); candidate && this.editRoot.contains(candidate); candidate = candidate.parentElement?.closest("[data-anchor]") ?? null) {
6860
+ unit = candidate;
6861
+ }
6862
+ if (!unit || unit.closest("section.footnotes, section.endnotes")) return null;
6863
+ return unit;
6497
6864
  }
6498
6865
  isMovableBlockUnit(unit) {
6499
6866
  if (!unit || !this.anchorIdOf(unit)) return false;
@@ -6515,18 +6882,18 @@ var Docxodus = (() => {
6515
6882
  if (!handle || !this.isMovableBlockUnit(unit)) return;
6516
6883
  const changed = unit !== this.blockDragSource;
6517
6884
  this.blockDragSource = unit;
6518
- if (changed) this.refreshBlockMoveTargets(unit);
6519
- if (this.blockMoveTargets?.size === 0) {
6885
+ const known = this.blockMoveTargetsFor(unit, { cachedOnly: true });
6886
+ if (known?.size === 0) {
6520
6887
  handle.style.display = "none";
6521
6888
  return;
6522
6889
  }
6890
+ if (changed && known === void 0) this.prefetchBlockMoveTargets(unit);
6523
6891
  const rect = unit.getBoundingClientRect();
6524
6892
  handle.style.display = "flex";
6525
6893
  handle.style.left = `${Math.max(4, rect.left - 32)}px`;
6526
6894
  handle.style.top = `${Math.max(4, rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - 28) / 2)))}px`;
6527
- const preview = (unit.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 48);
6895
+ const preview = blockPreviewText(unit);
6528
6896
  handle.setAttribute("aria-label", preview ? `Move block: ${preview}` : "Move block");
6529
- if (changed) this.refreshBlockDropTargets();
6530
6897
  }
6531
6898
  hideBlockHandle() {
6532
6899
  if (this.blockDragging || this.blockMoveMenu?.style.display === "block") return;
@@ -6536,14 +6903,33 @@ var Docxodus = (() => {
6536
6903
  const source = this.currentBlockDragSource();
6537
6904
  if (source && this.blockDragHandle?.style.display !== "none") this.showBlockHandle(source);
6538
6905
  }
6539
- showDropIndicator(target, position) {
6906
+ /** Draw the drop line on `zone`'s requested edge, or take it away when there is no target. */
6907
+ paintDropIndicator(data) {
6540
6908
  const indicator = this.blockDropIndicator;
6909
+ const zone = data.zone;
6541
6910
  if (!indicator) return;
6542
- const rect = this.unitWrapperOf(target).getBoundingClientRect();
6911
+ if (!zone) {
6912
+ this.hideDropIndicator();
6913
+ return;
6914
+ }
6915
+ const y = Math.round(this.dropEdgeY(zone, data.position === "after" ? "after" : "before") + this.dropZoneShift()) - 1;
6916
+ indicator.style.transform = `translate3d(${Math.round(zone.left)}px, ${y}px, 0)`;
6917
+ indicator.style.width = `${Math.max(24, Math.round(zone.width))}px`;
6543
6918
  indicator.style.display = "block";
6544
- indicator.style.left = `${rect.left}px`;
6545
- indicator.style.top = `${position === "before" ? rect.top - 1 : rect.bottom - 1}px`;
6546
- indicator.style.width = `${Math.max(24, rect.width)}px`;
6919
+ }
6920
+ /**
6921
+ * Where to draw the line for an insertion on `position` of `zone` — the MIDDLE of the gap to
6922
+ * the neighbour on that side, not the zone's own border-box edge. A paragraph's `w:spacing`
6923
+ * becomes a CSS margin, which sits outside the box, so drawing on the edge underlines the
6924
+ * block's last line instead of reading as a gap between two blocks. Falls back to the raw edge
6925
+ * at the ends of the flow, and degrades to the same value when blocks are contiguous.
6926
+ */
6927
+ dropEdgeY(zone, position) {
6928
+ const neighbour = this.dropZones[zone.index + (position === "after" ? 1 : -1)];
6929
+ if (!neighbour) {
6930
+ return position === "after" ? zone.bottom + zone.marginAfter / 2 : zone.top - zone.marginBefore / 2;
6931
+ }
6932
+ return position === "after" ? (zone.bottom + neighbour.top) / 2 : (neighbour.bottom + zone.top) / 2;
6547
6933
  }
6548
6934
  hideDropIndicator() {
6549
6935
  if (this.blockDropIndicator) this.blockDropIndicator.style.display = "none";
@@ -6563,19 +6949,52 @@ var Docxodus = (() => {
6563
6949
  * behaviour this replaces. Null (no bridge support) keeps the previous offer-everything path.
6564
6950
  */
6565
6951
  refreshBlockMoveTargets(source) {
6952
+ this.blockMoveTargets = source ? this.blockMoveTargetsFor(source) ?? null : null;
6953
+ }
6954
+ /**
6955
+ * This block's legal destinations, from the memo when it is there. Returns `undefined` — not
6956
+ * `null` — for "not asked yet", so a caller can tell an unknown answer from the engine's
6957
+ * "no bridge support, offer everything" one.
6958
+ */
6959
+ blockMoveTargetsFor(source, options = {}) {
6566
6960
  const bridge = this.exports.DocxSessionBridge;
6567
- const sourceId = source ? this.anchorIdOf(source) : null;
6568
- if (!sourceId || typeof bridge.ValidMoveTargets !== "function") {
6569
- this.blockMoveTargets = null;
6570
- return;
6571
- }
6961
+ const sourceId = this.anchorIdOf(source);
6962
+ if (!sourceId || typeof bridge.ValidMoveTargets !== "function") return null;
6963
+ if (this.blockMoveTargetCache.has(sourceId)) return this.blockMoveTargetCache.get(sourceId);
6964
+ if (options.cachedOnly) return void 0;
6965
+ let targets;
6572
6966
  try {
6573
- const targets = JSON.parse(bridge.ValidMoveTargets(this.handle, sourceId));
6574
- this.blockMoveTargets = new Map(
6575
- targets.map((t) => [t.anchorId, { before: t.before, after: t.after }])
6576
- );
6967
+ const parsed = JSON.parse(bridge.ValidMoveTargets(this.handle, sourceId));
6968
+ targets = new Map(parsed.map((t) => [t.anchorId, { before: t.before, after: t.after }]));
6577
6969
  } catch {
6578
- this.blockMoveTargets = null;
6970
+ targets = null;
6971
+ }
6972
+ this.blockMoveTargetCache.set(sourceId, targets);
6973
+ return targets;
6974
+ }
6975
+ /**
6976
+ * Ask for the hovered block's destinations off the interaction path, and hide the handle if
6977
+ * the answer comes back empty and that block is still the one under the pointer. The handle
6978
+ * therefore appears immediately on hover and withdraws a beat later on the rare immovable
6979
+ * block, instead of every hover paying for the query up front.
6980
+ */
6981
+ prefetchBlockMoveTargets(source) {
6982
+ const view = this.container.ownerDocument.defaultView;
6983
+ if (!view) return;
6984
+ this.blockMoveTargetPrefetch?.();
6985
+ const run = () => {
6986
+ this.blockMoveTargetPrefetch = null;
6987
+ if (this.closed || !source.isConnected || this.blockDragSource !== source) return;
6988
+ if (this.blockMoveTargetsFor(source)?.size === 0 && this.blockDragHandle)
6989
+ this.blockDragHandle.style.display = "none";
6990
+ };
6991
+ const idle = view;
6992
+ if (idle.requestIdleCallback && idle.cancelIdleCallback) {
6993
+ const id = idle.requestIdleCallback(run, { timeout: 500 });
6994
+ this.blockMoveTargetPrefetch = () => idle.cancelIdleCallback(id);
6995
+ } else {
6996
+ const id = view.setTimeout(run, 0);
6997
+ this.blockMoveTargetPrefetch = () => view.clearTimeout(id);
6579
6998
  }
6580
6999
  }
6581
7000
  /**
@@ -6590,45 +7009,75 @@ var Docxodus = (() => {
6590
7009
  if (!sides) return false;
6591
7010
  return position ? sides[position] : sides.before || sides.after;
6592
7011
  }
7012
+ /** Measure every movable block once, at drag start. See `BlockDropZone`. */
7013
+ captureDropZones() {
7014
+ const view = this.container.ownerDocument.defaultView;
7015
+ this.dropZoneScroller = this.scrollContainer();
7016
+ this.dropZoneOrigin = this.scrollOffsetSum();
7017
+ this.dropZones = [];
7018
+ const boxes = [];
7019
+ for (const unit of this.bodyUnitNodes()) {
7020
+ const anchorId = this.isMovableBlockUnit(unit) ? this.anchorIdOf(unit) : null;
7021
+ if (!anchorId) continue;
7022
+ const box = this.unitWrapperOf(unit);
7023
+ const rect = box.getBoundingClientRect();
7024
+ boxes.push(box);
7025
+ this.dropZones.push({
7026
+ unit,
7027
+ anchorId,
7028
+ index: this.dropZones.length,
7029
+ top: rect.top,
7030
+ bottom: rect.bottom,
7031
+ left: rect.left,
7032
+ width: rect.width,
7033
+ marginBefore: 0,
7034
+ marginAfter: 0
7035
+ });
7036
+ }
7037
+ const ends = new Set([0, this.dropZones.length - 1].filter((i) => i >= 0 && i < boxes.length));
7038
+ for (const i of ends) {
7039
+ const style = view?.getComputedStyle(boxes[i]);
7040
+ this.dropZones[i].marginBefore = parseFloat(style?.marginTop ?? "0") || 0;
7041
+ this.dropZones[i].marginAfter = parseFloat(style?.marginBottom ?? "0") || 0;
7042
+ }
7043
+ }
7044
+ scrollOffsetSum() {
7045
+ const view = this.container.ownerDocument.defaultView;
7046
+ return (view?.scrollY ?? 0) + (this.dropZoneScroller?.scrollTop ?? 0);
7047
+ }
7048
+ /** How far the measured boxes have travelled since capture, from scrolling (drag autoscroll). */
7049
+ dropZoneShift() {
7050
+ return this.dropZoneOrigin - this.scrollOffsetSum();
7051
+ }
6593
7052
  /**
6594
- * The side of `unit` a drop at `clientY` should land on: the half the pointer is in, snapped to
6595
- * the other side when only that one is legal. Snapping rather than refusing keeps a reachable
6596
- * target usable — the illegal side is usually illegal only because a section break or a
6597
- * cross-block range sits between the two blocks on that side.
7053
+ * Where a drop at `clientY` lands, or null when nothing there is legal.
7054
+ *
7055
+ * Resolution is by VERTICAL GEOMETRY over the measured blocks, not by which element the pointer
7056
+ * is over: the drag handle floats in the page margin, so a drag straight down the gutter — the
7057
+ * natural gesture — never crosses a paragraph box, and element hit testing gave those drags no
7058
+ * indicator and no drop at all. The nearest block by vertical distance is the target; the half
7059
+ * the pointer is in picks the side, snapped to the other side when only that one is legal
7060
+ * (a section break or a cross-block range usually makes exactly one side illegal). When neither
7061
+ * side is legal — the pointer is in a region this block cannot reach — there is no drop, and
7062
+ * nothing is drawn.
6598
7063
  */
6599
- dropPositionFor(unit, clientY) {
6600
- const rect = unit.getBoundingClientRect();
6601
- const preferred = clientY < rect.top + rect.height / 2 ? "before" : "after";
6602
- if (this.isValidMoveTarget(unit, preferred)) return preferred;
6603
- const other = preferred === "before" ? "after" : "before";
6604
- return this.isValidMoveTarget(unit, other) ? other : preferred;
6605
- }
6606
- refreshBlockDropTargets() {
6607
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
6608
- if (!this.blockDragHandle || this.options.paginated) return;
6609
- for (const unit of this.bodyUnitNodes().filter((el) => this.isMovableBlockUnit(el))) {
6610
- this.blockDragTargetCleanup.push(dropTargetForElements({
6611
- element: unit,
6612
- // A target the engine would refuse is not a drop target at all, so Pragmatic never
6613
- // fires onDragEnter for it and no indicator is drawn over it.
6614
- canDrop: ({ source }) => source.data.type === BLOCK_DRAG_TYPE && source.data.sourceAnchorId !== this.anchorIdOf(unit) && this.isValidMoveTarget(unit),
6615
- getData: ({ input }) => ({
6616
- type: BLOCK_DRAG_TYPE,
6617
- targetAnchorId: this.anchorIdOf(unit),
6618
- position: this.dropPositionFor(unit, input.clientY),
6619
- targetElement: unit
6620
- }),
6621
- onDragEnter: ({ self }) => {
6622
- const pos = self.data.position === "after" ? "after" : "before";
6623
- this.showDropIndicator(unit, pos);
6624
- },
6625
- onDrag: ({ self }) => {
6626
- const pos = self.data.position === "after" ? "after" : "before";
6627
- this.showDropIndicator(unit, pos);
6628
- },
6629
- onDragLeave: () => this.hideDropIndicator()
6630
- }));
7064
+ resolveDropAt(clientY) {
7065
+ const y = clientY - this.dropZoneShift();
7066
+ let best = null;
7067
+ let bestGap = Infinity;
7068
+ for (const zone of this.dropZones) {
7069
+ const gap = y < zone.top ? zone.top - y : y > zone.bottom ? y - zone.bottom : 0;
7070
+ if (gap < bestGap) {
7071
+ best = zone;
7072
+ bestGap = gap;
7073
+ }
7074
+ if (gap === 0) break;
6631
7075
  }
7076
+ if (!best || best.unit === this.blockDragSource) return null;
7077
+ const preferred = y < (best.top + best.bottom) / 2 ? "before" : "after";
7078
+ if (this.isValidMoveTarget(best.unit, preferred)) return { zone: best, position: preferred };
7079
+ const other = preferred === "before" ? "after" : "before";
7080
+ return this.isValidMoveTarget(best.unit, other) ? { zone: best, position: other } : null;
6632
7081
  }
6633
7082
  closeBlockMoveMenu(restoreFocus = false) {
6634
7083
  if (!this.blockMoveMenu || !this.blockDragHandle) return;
@@ -6665,7 +7114,8 @@ var Docxodus = (() => {
6665
7114
  const index = units.indexOf(source);
6666
7115
  if (index < 0) return null;
6667
7116
  const position = action === "up" || action === "top" ? "before" : "after";
6668
- const candidates = units.filter((el) => el !== source && this.isValidMoveTarget(el, position)).filter((el) => position === "before" ? units.indexOf(el) < index : units.indexOf(el) > index);
7117
+ const side = position === "before" ? units.slice(0, index) : units.slice(index + 1);
7118
+ const candidates = side.filter((el) => this.isValidMoveTarget(el, position));
6669
7119
  if (candidates.length === 0) return null;
6670
7120
  if (action === "up") return { target: candidates[candidates.length - 1], position };
6671
7121
  if (action === "down") return { target: candidates[0], position };
@@ -6802,20 +7252,50 @@ var Docxodus = (() => {
6802
7252
  const source = this.currentBlockDragSource();
6803
7253
  return { type: BLOCK_DRAG_TYPE, sourceAnchorId: source ? this.anchorIdOf(source) : void 0 };
6804
7254
  },
7255
+ // The browser would otherwise ghost the 26px grip, which says nothing about what is moving.
7256
+ onGenerateDragPreview: ({ nativeSetDragImage }) => {
7257
+ const source = this.currentBlockDragSource();
7258
+ setCustomNativeDragPreview({
7259
+ nativeSetDragImage,
7260
+ getOffset: pointerOutsideOfPreview({ x: "14px", y: "10px" }),
7261
+ render: ({ container }) => {
7262
+ const chip = doc.createElement("div");
7263
+ chip.className = "docx-block-drag-preview";
7264
+ chip.textContent = source && blockPreviewText(source) || "Move block";
7265
+ container.appendChild(chip);
7266
+ return () => chip.remove();
7267
+ }
7268
+ });
7269
+ },
6805
7270
  onDragStart: () => {
7271
+ const source = this.currentBlockDragSource();
6806
7272
  this.blockDragging = true;
6807
7273
  handle.classList.add("docx-block-dragging");
7274
+ source?.classList.add("docx-block-drag-source");
6808
7275
  this.closeBlockMoveMenu();
6809
- this.refreshBlockMoveTargets(this.currentBlockDragSource());
6810
- this.refreshBlockDropTargets();
7276
+ this.refreshBlockMoveTargets(source);
7277
+ this.captureDropZones();
6811
7278
  },
6812
7279
  onDrop: () => {
6813
7280
  this.blockDragging = false;
6814
7281
  this.blockDragPointerDown = false;
7282
+ this.dropZones = [];
6815
7283
  handle.classList.remove("docx-block-dragging");
7284
+ doc.querySelectorAll(".docx-block-drag-source").forEach((el) => el.classList.remove("docx-block-drag-source"));
6816
7285
  this.hideDropIndicator();
6817
7286
  }
6818
7287
  }));
7288
+ this.blockDragCleanup.push(dropTargetForElements({
7289
+ element: this.editRoot,
7290
+ canDrop: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
7291
+ getData: ({ input }) => {
7292
+ const hit = this.resolveDropAt(input.clientY);
7293
+ return hit ? { type: BLOCK_DRAG_TYPE, targetAnchorId: hit.zone.anchorId, position: hit.position, zone: hit.zone } : { type: BLOCK_DRAG_TYPE };
7294
+ },
7295
+ onDragEnter: ({ self }) => this.paintDropIndicator(self.data),
7296
+ onDrag: ({ self }) => this.paintDropIndicator(self.data),
7297
+ onDragLeave: () => this.hideDropIndicator()
7298
+ }));
6819
7299
  this.blockDragCleanup.push(monitorForElements({
6820
7300
  canMonitor: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
6821
7301
  onDrop: ({ source, location: location2 }) => {
@@ -6842,10 +7322,13 @@ var Docxodus = (() => {
6842
7322
  canScroll: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
6843
7323
  getAllowedAxis: () => "vertical"
6844
7324
  }));
6845
- this.refreshBlockDropTargets();
6846
7325
  }
6847
7326
  teardownBlockDrag() {
6848
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
7327
+ this.blockMoveTargetPrefetch?.();
7328
+ this.blockMoveTargetPrefetch = null;
7329
+ this.blockMoveTargetCache.clear();
7330
+ this.dropZones = [];
7331
+ this.dropZoneScroller = null;
6849
7332
  for (const cleanup of this.blockDragCleanup.splice(0)) cleanup();
6850
7333
  this.blockDragHandle?.remove();
6851
7334
  this.blockDropIndicator?.remove();
@@ -6934,17 +7417,17 @@ var Docxodus = (() => {
6934
7417
  bodyRoot.after(this.region.footerBand);
6935
7418
  this.region.refreshAll();
6936
7419
  }
6937
- /** Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks. */
7420
+ /**
7421
+ * Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks.
7422
+ *
7423
+ * The body always gets its own `.docx-body-flow` wrapper — not only when bands are docked.
7424
+ * It is the sheet: the element the viewport gives page geometry to and zooms, and the one
7425
+ * the bands dock around. Without it the container would have to be both the scrolling host
7426
+ * and the scaled page, which are different boxes.
7427
+ */
6938
7428
  mountHtml(fullHtml) {
6939
7429
  const parsed = new DOMParser().parseFromString(fullHtml, "text/html");
6940
7430
  const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
6941
- if (!this.region) {
6942
- this.container.innerHTML = styles + parsed.body.innerHTML;
6943
- this.editRoot = this.container;
6944
- if (this.options.editable) this.wireBlocks(this.container);
6945
- this.stampPlanState();
6946
- return;
6947
- }
6948
7431
  this.container.innerHTML = styles;
6949
7432
  const flow = document.createElement("div");
6950
7433
  flow.className = "docx-body-flow";
@@ -6953,7 +7436,8 @@ var Docxodus = (() => {
6953
7436
  this.editRoot = flow;
6954
7437
  if (this.options.editable) this.wireBlocks(flow);
6955
7438
  this.stampPlanState();
6956
- this.dockBands(flow);
7439
+ if (this.region) this.dockBands(flow);
7440
+ this.viewport.attach(flow, true);
6957
7441
  }
6958
7442
  /** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
6959
7443
  mountPaginated(fullHtml) {
@@ -6974,6 +7458,7 @@ var Docxodus = (() => {
6974
7458
  this.editRoot = pageRoot;
6975
7459
  if (this.options.editable) this.wireBlocks(pageRoot);
6976
7460
  if (this.region) this.dockBands(target);
7461
+ this.viewport.attach(pageRoot, false);
6977
7462
  }
6978
7463
  wireBlocks(root) {
6979
7464
  root.querySelectorAll("[data-anchor]").forEach((el) => this.wireBlock(el));
@@ -7362,11 +7847,22 @@ var Docxodus = (() => {
7362
7847
  }
7363
7848
  parseEdit(json) {
7364
7849
  try {
7365
- return JSON.parse(json);
7850
+ const result = JSON.parse(json);
7851
+ if (result.success) this.invalidateBlockMoveTargets();
7852
+ return result;
7366
7853
  } catch {
7367
7854
  return { success: false };
7368
7855
  }
7369
7856
  }
7857
+ /**
7858
+ * Drop the memoized `ValidMoveTargets` answers. Which blocks a block may move next to is a
7859
+ * fact about the DOCUMENT, so it survives hovering but not editing — and the two places a
7860
+ * document changes are `parseEdit` (every mutation that returns an `EditResult`) and
7861
+ * undo/redo, which return a bare boolean and so cannot go through it.
7862
+ */
7863
+ invalidateBlockMoveTargets() {
7864
+ this.blockMoveTargetCache.clear();
7865
+ }
7370
7866
  // ─── M5: formatting commands (ribbon) ────────────────────────────────
7371
7867
  // ─── Multi-block selection helpers (format a whole stack of paragraphs at once) ──────
7372
7868
  /**
@@ -7920,12 +8416,16 @@ var Docxodus = (() => {
7920
8416
  /** Undo the last edit (incremental repaint; falls back to a full re-render). */
7921
8417
  undo() {
7922
8418
  if (this.closed) return;
7923
- if (this.exports.DocxSessionBridge.Undo(this.handle)) this.reconcile();
8419
+ if (!this.exports.DocxSessionBridge.Undo(this.handle)) return;
8420
+ this.invalidateBlockMoveTargets();
8421
+ this.reconcile();
7924
8422
  }
7925
8423
  /** Redo the last undone edit (incremental repaint; falls back to a full re-render). */
7926
8424
  redo() {
7927
8425
  if (this.closed) return;
7928
- if (this.exports.DocxSessionBridge.Redo(this.handle)) this.reconcile();
8426
+ if (!this.exports.DocxSessionBridge.Redo(this.handle)) return;
8427
+ this.invalidateBlockMoveTargets();
8428
+ this.reconcile();
7929
8429
  }
7930
8430
  // ─── Header/footer region commands (no-ops unless `headerFooter` is on) ───────────────
7931
8431
  /**
@@ -8154,7 +8654,11 @@ var Docxodus = (() => {
8154
8654
  const oldTokens = oldNodes.map(_DocxEditor.domTokenOf);
8155
8655
  const oldKinds = oldNodes.map(_DocxEditor.domKindOf);
8156
8656
  const bodyDiff = diffUnits(oldTokens, plan.body);
8157
- if (needsRemount(bodyDiff, plan.body, oldKinds)) return this.bail("needsRemount (li change or churn)");
8657
+ if (needsRemount(bodyDiff, plan.body, oldKinds)) {
8658
+ return this.bail(
8659
+ `needsRemount (li change or churn): +${bodyDiff.added.length} -${bodyDiff.removed.length} ~${bodyDiff.substituted.length} moved=${bodyDiff.moved.length} of ${plan.body.length}`
8660
+ );
8661
+ }
8158
8662
  const fnState = this.notesDiff("footnotes", plan.footnotes);
8159
8663
  const enState = this.notesDiff("endnotes", plan.endnotes);
8160
8664
  if (fnState === null || enState === null) return this.bail("notes container unstampable/missing");
@@ -8763,14 +9267,7 @@ var Docxodus = (() => {
8763
9267
  SHOULD win.
8764
9268
  (No backticks in this file's comments: the stylesheet is a template literal.) */
8765
9269
  .dxr-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
8766
- .dxr[data-chrome] .dxr-surface { max-width: 920px; margin: 26px auto; padding: 0 16px 96px; }
8767
- .dxr-surface[data-view="continuous"] > * { background: var(--dxr-sheet); }
8768
- .dxr[data-chrome] .dxr-surface[data-view="continuous"] {
8769
- padding: 56px 72px;
8770
- border-radius: 3px;
8771
- background: var(--dxr-sheet);
8772
- box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
8773
- }
9270
+ .dxr[data-chrome] .dxr-surface { margin: 26px auto; padding: 0 16px 96px; }
8774
9271
  .dxr-surface [contenteditable="true"]:focus {
8775
9272
  outline: 2px solid var(--dxr-accent);
8776
9273
  outline-offset: 2px;
@@ -8782,7 +9279,10 @@ var Docxodus = (() => {
8782
9279
  so they dock as their own regions. Styled from the same tokens as the ribbon
8783
9280
  so the surface reads as one instrument rather than two apps. */
8784
9281
  .dxr-surface .docx-hf-band {
8785
- margin: 0 0 14px;
9282
+ /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
9283
+ custom property the viewport publishes rather than stretching to the whole surface. */
9284
+ width: min(100%, var(--docx-sheet-width, 100%));
9285
+ margin: 0 auto 14px;
8786
9286
  padding: 9px 72px 13px;
8787
9287
  border: 1px solid var(--dxr-rule);
8788
9288
  border-left: 2px solid #c2ccd9;
@@ -8790,7 +9290,7 @@ var Docxodus = (() => {
8790
9290
  background: var(--dxr-sheet);
8791
9291
  }
8792
9292
  .dxr-surface .docx-hf-band + .docx-body-flow { margin-top: 0; }
8793
- .dxr-surface .docx-hf-band[data-hf-band="footer"] { margin: 14px 0 0; }
9293
+ .dxr-surface .docx-hf-band[data-hf-band="footer"] { margin: 14px auto 0; }
8794
9294
  .dxr-surface .docx-hf-chrome {
8795
9295
  display: flex;
8796
9296
  gap: 8px;
@@ -8845,13 +9345,15 @@ var Docxodus = (() => {
8845
9345
  text-transform: none;
8846
9346
  }
8847
9347
  .dxr-surface .docx-hf-band[data-hf-inherited] { border-style: dashed; }
8848
- .dxr[data-chrome] .dxr-surface[data-view="continuous"]:has(.docx-body-flow) {
8849
- padding: 0;
8850
- background: transparent;
8851
- box-shadow: none;
8852
- }
9348
+ /* The sheet IS the page. The editor's viewport sizes .docx-body-flow to the section's page
9349
+ width and its section wrappers to the authored text column, so the horizontal gutters here
9350
+ are the document's own w:sectPr margins, not a padding this chrome invents; only the
9351
+ vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
9352
+ has zoomed to fit stays centered at its scaled width. */
8853
9353
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
8854
- padding: 56px 72px;
9354
+ max-width: 100%;
9355
+ margin: 0 auto;
9356
+ padding: 56px 0;
8855
9357
  border-radius: 3px;
8856
9358
  background: var(--dxr-sheet);
8857
9359
  box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
@@ -8930,9 +9432,11 @@ var Docxodus = (() => {
8930
9432
  .dxr[data-chrome="compact"] .dxr-note { display: none; }
8931
9433
  .dxr[data-chrome="compact"] .dxr-rail { display: none; }
8932
9434
  .dxr[data-chrome="compact"] .dxr-hint { display: none; }
9435
+ /* Compact trims the chrome around the page, never the page: the document's own column width
9436
+ is what the viewport's fit-to-width zoom scales, so a phone shows a whole smaller page
9437
+ instead of a narrower one that breaks its lines somewhere Word never would. */
8933
9438
  .dxr[data-chrome="compact"] .dxr-surface { margin: 12px auto; padding: 0 10px 64px; }
8934
- .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] { padding: 22px 18px; }
8935
- .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] .docx-body-flow { padding: 22px 18px; }
9439
+ .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] .docx-body-flow { padding: 22px 0; }
8936
9440
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-band { padding: 9px 18px 13px; }
8937
9441
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-chrome,
8938
9442
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-warning { margin-left: 0; }
@@ -9682,6 +10186,8 @@ var Docxodus = (() => {
9682
10186
  fabricateClasses: this.options.fabricateClasses,
9683
10187
  editable: this.options.editable,
9684
10188
  scale: this.options.scale,
10189
+ columnWidth: this.options.columnWidth,
10190
+ fitToWidth: this.options.fitToWidth,
9685
10191
  onEdit: this.options.onEdit,
9686
10192
  onMove: this.options.onMove,
9687
10193
  paginated,