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
@@ -161,20 +161,18 @@ var DocxodusEditor = (() => {
161
161
  return (renderer ?? RENDERERS.decimal)(value);
162
162
  }
163
163
 
164
- // src/pagination.ts
164
+ // src/page-geometry.ts
165
165
  var DEFAULT_PAGE_WIDTH = 612;
166
166
  var DEFAULT_PAGE_HEIGHT = 792;
167
167
  var DEFAULT_MARGIN = 72;
168
- var MAX_FOOTNOTE_AREA_RATIO = 0.6;
169
- var MIN_BODY_CONTENT_HEIGHT = 72;
168
+ var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
170
169
  function pxToPt(px) {
171
170
  return px * 0.75;
172
171
  }
173
172
  function ptToPx(pt) {
174
173
  return pt / 0.75;
175
174
  }
176
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
177
- function parseDimensions(section) {
175
+ function parseSectionDimensions(section) {
178
176
  const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
179
177
  const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
180
178
  const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
@@ -198,6 +196,42 @@ var DocxodusEditor = (() => {
198
196
  footerHeight
199
197
  };
200
198
  }
199
+ function sectionWrappers(root) {
200
+ const sections = Array.from(root.querySelectorAll("[data-section-index]"));
201
+ return sections.length > 0 ? sections : [root];
202
+ }
203
+ var MIN_FIT_SCALE = 0.25;
204
+ function fitScale(availablePx, naturalPt, max = 1) {
205
+ if (!(availablePx > 0) || !(naturalPt > 0)) return max;
206
+ const naturalPx = ptToPx(naturalPt);
207
+ if (naturalPx <= availablePx) return max;
208
+ return Math.max(MIN_FIT_SCALE, Math.min(max, availablePx / naturalPx));
209
+ }
210
+ function applyZoom(el, scale, naturalPt) {
211
+ if (scale === 1) {
212
+ el.style.removeProperty("zoom");
213
+ el.style.removeProperty("transform");
214
+ el.style.removeProperty("transform-origin");
215
+ el.style.removeProperty("margin-right");
216
+ el.style.removeProperty("margin-bottom");
217
+ return;
218
+ }
219
+ const zoomSupported = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("zoom", "0.5");
220
+ if (zoomSupported) {
221
+ el.style.zoom = String(scale);
222
+ return;
223
+ }
224
+ el.style.transform = `scale(${scale})`;
225
+ el.style.transformOrigin = "top left";
226
+ if (naturalPt) {
227
+ el.style.marginRight = `-${ptToPx(naturalPt.width * (1 - scale))}px`;
228
+ el.style.marginBottom = `-${ptToPx(naturalPt.height * (1 - scale))}px`;
229
+ }
230
+ }
231
+
232
+ // src/pagination.ts
233
+ var MAX_FOOTNOTE_AREA_RATIO = 0.6;
234
+ var MIN_BODY_CONTENT_HEIGHT = 72;
201
235
  var PaginationEngine = class {
202
236
  /**
203
237
  * Creates a new pagination engine.
@@ -243,7 +277,7 @@ var DocxodusEditor = (() => {
243
277
  const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
244
278
  for (const section of sectionsToProcess) {
245
279
  const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
246
- const dims = parseDimensions(section);
280
+ const dims = parseSectionDimensions(section);
247
281
  this.stagingElement.style.visibility = "hidden";
248
282
  this.stagingElement.style.position = "absolute";
249
283
  this.stagingElement.style.left = "-9999px";
@@ -1541,6 +1575,114 @@ var DocxodusEditor = (() => {
1541
1575
  return engine.paginate();
1542
1576
  }
1543
1577
 
1578
+ // src/viewport.ts
1579
+ var DocumentViewport = class {
1580
+ constructor(host, options = {}) {
1581
+ this.root = null;
1582
+ /** Natural page size in points — the widest section's PAGE box, which is what must fit. */
1583
+ this.natural = { width: 0, height: 0 };
1584
+ this.observer = null;
1585
+ this.host = host;
1586
+ this.options = {
1587
+ columnWidth: options.columnWidth ?? "section",
1588
+ fitToWidth: options.fitToWidth ?? true,
1589
+ scale: options.scale ?? 1
1590
+ };
1591
+ }
1592
+ /**
1593
+ * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
1594
+ * Safe to call on every remount; the previous root is released first.
1595
+ *
1596
+ * `applySectionGeometry` is false for the paginated view, which already builds real page
1597
+ * boxes at the section's dimensions — there the viewport contributes only the fit zoom.
1598
+ */
1599
+ attach(root, applySectionGeometry) {
1600
+ this.release();
1601
+ this.root = root;
1602
+ this.natural = applySectionGeometry ? this.stampSections(root) : this.measurePages(root);
1603
+ this.refresh();
1604
+ if (typeof ResizeObserver !== "undefined") {
1605
+ this.observer = new ResizeObserver(() => this.refresh());
1606
+ this.observer.observe(this.host);
1607
+ }
1608
+ }
1609
+ /** Recompute the fit zoom against the host's current width. */
1610
+ refresh() {
1611
+ if (!this.root) return;
1612
+ const scale = this.scale;
1613
+ applyZoom(this.root, scale, this.natural);
1614
+ this.host.style.setProperty(
1615
+ "--docx-sheet-width",
1616
+ this.natural.width > 0 ? `${ptToPx(this.natural.width) * scale}px` : "100%"
1617
+ );
1618
+ }
1619
+ /** The zoom currently applied (1 = 100%). Reported by the ribbon's anchor rail. */
1620
+ get scale() {
1621
+ if (!this.root) return this.options.scale;
1622
+ return this.options.fitToWidth ? fitScale(this.availableWidthPx(), this.natural.width, this.options.scale) : this.options.scale;
1623
+ }
1624
+ dispose() {
1625
+ this.release();
1626
+ this.root = null;
1627
+ }
1628
+ release() {
1629
+ this.observer?.disconnect();
1630
+ this.observer = null;
1631
+ if (this.root) applyZoom(this.root, 1);
1632
+ this.host.style.removeProperty("--docx-sheet-width");
1633
+ }
1634
+ /** The host's content box, which is the space a page has to fit into. */
1635
+ availableWidthPx() {
1636
+ const style = typeof getComputedStyle === "function" ? getComputedStyle(this.host) : null;
1637
+ const padding = style ? (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0) : 0;
1638
+ return Math.max(0, this.host.clientWidth - padding);
1639
+ }
1640
+ /**
1641
+ * Give each section wrapper its `w:sectPr` geometry: the authored text column, guttered by
1642
+ * the authored margins. The wrapper then measures exactly one page wide, which is what the
1643
+ * sheet chrome paints and what the fit zoom scales.
1644
+ */
1645
+ stampSections(root) {
1646
+ const sections = sectionWrappers(root);
1647
+ if (this.options.columnWidth === "fluid") {
1648
+ root.style.removeProperty("width");
1649
+ for (const section of sections) {
1650
+ section.style.removeProperty("width");
1651
+ section.style.removeProperty("padding-left");
1652
+ section.style.removeProperty("padding-right");
1653
+ }
1654
+ return { width: 0, height: 0 };
1655
+ }
1656
+ let widest = 0;
1657
+ for (const section of sections) {
1658
+ const dims = parseSectionDimensions(section);
1659
+ section.style.width = `${dims.contentWidth}pt`;
1660
+ section.style.paddingLeft = `${dims.marginLeft}pt`;
1661
+ section.style.paddingRight = `${dims.marginRight}pt`;
1662
+ section.style.boxSizing = "content-box";
1663
+ section.style.marginLeft = "auto";
1664
+ section.style.marginRight = "auto";
1665
+ widest = Math.max(widest, dims.pageWidth);
1666
+ }
1667
+ if (widest > 0 && sections[0] !== root) root.style.width = `${widest}pt`;
1668
+ return { width: widest, height: 0 };
1669
+ }
1670
+ /**
1671
+ * The paginated view's page boxes are already page-sized — `pagination.ts` writes each
1672
+ * box's `width` in points — so the widest of those is the natural width. Reading the
1673
+ * inline width rather than the laid-out box keeps this independent of the per-box zoom
1674
+ * pagination may itself have applied.
1675
+ */
1676
+ measurePages(root) {
1677
+ let widest = 0;
1678
+ for (const box of Array.from(root.children)) {
1679
+ const declared = /^([\d.]+)pt$/.exec(box.style.width || "");
1680
+ widest = Math.max(widest, declared ? parseFloat(declared[1]) : pxToPt(box.offsetWidth));
1681
+ }
1682
+ return { width: widest, height: 0 };
1683
+ }
1684
+ };
1685
+
1544
1686
  // src/editor-headerfooter.ts
1545
1687
  var PAGE_FORMAT_LABELS = [
1546
1688
  { value: "", label: "Format\u2026" },
@@ -3629,6 +3771,146 @@ var DocxodusEditor = (() => {
3629
3771
  return once(cleanup);
3630
3772
  }
3631
3773
 
3774
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js
3775
+ function ownKeys4(e, r) {
3776
+ var t = Object.keys(e);
3777
+ if (Object.getOwnPropertySymbols) {
3778
+ var o = Object.getOwnPropertySymbols(e);
3779
+ r && (o = o.filter(function(r2) {
3780
+ return Object.getOwnPropertyDescriptor(e, r2).enumerable;
3781
+ })), t.push.apply(t, o);
3782
+ }
3783
+ return t;
3784
+ }
3785
+ function _objectSpread4(e) {
3786
+ for (var r = 1; r < arguments.length; r++) {
3787
+ var t = null != arguments[r] ? arguments[r] : {};
3788
+ r % 2 ? ownKeys4(Object(t), true).forEach(function(r2) {
3789
+ _defineProperty(e, r2, t[r2]);
3790
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys4(Object(t)).forEach(function(r2) {
3791
+ Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
3792
+ });
3793
+ }
3794
+ return e;
3795
+ }
3796
+ function defaultOffset() {
3797
+ return {
3798
+ x: 0,
3799
+ y: 0
3800
+ };
3801
+ }
3802
+ function setCustomNativeDragPreview(_ref) {
3803
+ var render = _ref.render, nativeSetDragImage = _ref.nativeSetDragImage, _ref$getOffset = _ref.getOffset, getOffset = _ref$getOffset === void 0 ? defaultOffset : _ref$getOffset;
3804
+ var container = document.createElement("div");
3805
+ if (supportsPopover()) {
3806
+ container.setAttribute("popover", "manual");
3807
+ }
3808
+ Object.assign(container.style, _objectSpread4(_objectSpread4({
3809
+ // Ensuring we don't cause reflow when adding the element to the page
3810
+ // Using `position:fixed` rather than `position:absolute` so we are
3811
+ // positioned on the current viewport.
3812
+ // `position:fixed` also creates a new stacking context, so we don't need to do that here
3813
+ position: "fixed"
3814
+ }, supportsPopover() ? (
3815
+ // needs to come first as it has 'inset: unset' which
3816
+ // needs to be overridden by our top / left values
3817
+ popoverResetUserAgentStyles
3818
+ ) : {
3819
+ // Fallback: using maximum possible z-index so that this element
3820
+ // will always be on top of other positioned content.
3821
+ zIndex: maxZIndex
3822
+ }), {}, {
3823
+ // According to `mdn`, the element can be offscreen:
3824
+ // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage#imgelement
3825
+ //
3826
+ // However, that information does not appear in the specs:
3827
+ // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdragimage-dev
3828
+ //
3829
+ // If the element is _completely_ offscreen, Safari@17.1 will cancel the drag
3830
+ top: 0,
3831
+ left: 0,
3832
+ // Avoiding any additional events caused by the new element (being super safe)
3833
+ pointerEvents: "none"
3834
+ }));
3835
+ document.body.append(container);
3836
+ if (supportsPopover()) {
3837
+ container.showPopover();
3838
+ }
3839
+ var unmount = render({
3840
+ container
3841
+ });
3842
+ queueMicrotask(function() {
3843
+ var previewOffset = getOffset({
3844
+ container
3845
+ });
3846
+ if (isSafari()) {
3847
+ var rect = container.getBoundingClientRect();
3848
+ if (rect.width === 0) {
3849
+ return;
3850
+ }
3851
+ container.style.left = "-".concat(rect.width - 1e-4, "px");
3852
+ }
3853
+ nativeSetDragImage === null || nativeSetDragImage === void 0 || nativeSetDragImage(container, previewOffset.x, previewOffset.y);
3854
+ });
3855
+ function cleanup() {
3856
+ unbindMonitor();
3857
+ unmount === null || unmount === void 0 || unmount();
3858
+ document.body.removeChild(container);
3859
+ }
3860
+ var unbindMonitor = monitorForElements({
3861
+ // Remove portal in the dragstart event so that the user will never see it
3862
+ onDragStart: cleanup,
3863
+ // Backup: remove portal when the drop finishes (this would be an error case)
3864
+ onDrop: cleanup
3865
+ });
3866
+ }
3867
+
3868
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/util/is-safari-on-ios.js
3869
+ var isSafariOnIOS = once(function isSafariOnIOS2() {
3870
+ if (false) {
3871
+ return false;
3872
+ }
3873
+ return isSafari() && "ontouchend" in document;
3874
+ });
3875
+
3876
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/center-under-pointer.js
3877
+ var centerUnderPointer = function centerUnderPointer2(_ref) {
3878
+ var container = _ref.container;
3879
+ var rect = container.getBoundingClientRect();
3880
+ return {
3881
+ x: rect.width / 2,
3882
+ y: rect.height / 2
3883
+ };
3884
+ };
3885
+
3886
+ // node_modules/@atlaskit/pragmatic-drag-and-drop/dist/esm/public-utils/element/custom-native-drag-preview/pointer-outside-of-preview.js
3887
+ function pointerOutsideOfPreview(point) {
3888
+ return function getOffset(_ref) {
3889
+ var container = _ref.container;
3890
+ if (isSafariOnIOS() || isAndroid()) {
3891
+ return centerUnderPointer({
3892
+ container
3893
+ });
3894
+ }
3895
+ Object.assign(container.style, {
3896
+ borderInlineStart: "".concat(point.x, " solid transparent"),
3897
+ borderTop: "".concat(point.y, " solid transparent")
3898
+ });
3899
+ var computed = window.getComputedStyle(container);
3900
+ if (computed.direction === "rtl") {
3901
+ var box = container.getBoundingClientRect();
3902
+ return {
3903
+ x: box.width,
3904
+ y: 0
3905
+ };
3906
+ }
3907
+ return {
3908
+ x: 0,
3909
+ y: 0
3910
+ };
3911
+ };
3912
+ }
3913
+
3632
3914
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/shared/engagement-history.js
3633
3915
  var ledger2 = /* @__PURE__ */ new Map();
3634
3916
  var requested = /* @__PURE__ */ new Set();
@@ -3762,7 +4044,7 @@ var DocxodusEditor = (() => {
3762
4044
  }
3763
4045
 
3764
4046
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/shared/configuration.js
3765
- function ownKeys4(e, r) {
4047
+ function ownKeys5(e, r) {
3766
4048
  var t = Object.keys(e);
3767
4049
  if (Object.getOwnPropertySymbols) {
3768
4050
  var o = Object.getOwnPropertySymbols(e);
@@ -3772,12 +4054,12 @@ var DocxodusEditor = (() => {
3772
4054
  }
3773
4055
  return t;
3774
4056
  }
3775
- function _objectSpread4(e) {
4057
+ function _objectSpread5(e) {
3776
4058
  for (var r = 1; r < arguments.length; r++) {
3777
4059
  var t = null != arguments[r] ? arguments[r] : {};
3778
- r % 2 ? ownKeys4(Object(t), true).forEach(function(r2) {
4060
+ r % 2 ? ownKeys5(Object(t), true).forEach(function(r2) {
3779
4061
  _defineProperty(e, r2, t[r2]);
3780
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys4(Object(t)).forEach(function(r2) {
4062
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys5(Object(t)).forEach(function(r2) {
3781
4063
  Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
3782
4064
  });
3783
4065
  }
@@ -3812,7 +4094,7 @@ var DocxodusEditor = (() => {
3812
4094
  };
3813
4095
  function getInternalConfig(provided) {
3814
4096
  var _provided$maxScrollSp;
3815
- return _objectSpread4(_objectSpread4({}, baseConfig), {}, {
4097
+ return _objectSpread5(_objectSpread5({}, baseConfig), {}, {
3816
4098
  // only allowing limited control over the config at this stage
3817
4099
  maxPixelScrollPerSecond: maxPixelScrollPerSecond[(_provided$maxScrollSp = provided === null || provided === void 0 ? void 0 : provided.maxScrollSpeed) !== null && _provided$maxScrollSp !== void 0 ? _provided$maxScrollSp : "standard"]
3818
4100
  });
@@ -4296,7 +4578,7 @@ var DocxodusEditor = (() => {
4296
4578
  }
4297
4579
 
4298
4580
  // node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll/dist/esm/over-element/make-api.js
4299
- function ownKeys5(e, r) {
4581
+ function ownKeys6(e, r) {
4300
4582
  var t = Object.keys(e);
4301
4583
  if (Object.getOwnPropertySymbols) {
4302
4584
  var o = Object.getOwnPropertySymbols(e);
@@ -4306,12 +4588,12 @@ var DocxodusEditor = (() => {
4306
4588
  }
4307
4589
  return t;
4308
4590
  }
4309
- function _objectSpread5(e) {
4591
+ function _objectSpread6(e) {
4310
4592
  for (var r = 1; r < arguments.length; r++) {
4311
4593
  var t = null != arguments[r] ? arguments[r] : {};
4312
- r % 2 ? ownKeys5(Object(t), true).forEach(function(r2) {
4594
+ r % 2 ? ownKeys6(Object(t), true).forEach(function(r2) {
4313
4595
  _defineProperty(e, r2, t[r2]);
4314
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys5(Object(t)).forEach(function(r2) {
4596
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys6(Object(t)).forEach(function(r2) {
4315
4597
  Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
4316
4598
  });
4317
4599
  }
@@ -4350,7 +4632,7 @@ var DocxodusEditor = (() => {
4350
4632
  }
4351
4633
  function autoScrollWindow() {
4352
4634
  var args = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
4353
- var unique = _objectSpread5({}, args);
4635
+ var unique = _objectSpread6({}, args);
4354
4636
  windowRegistry.add(unique);
4355
4637
  function cleanup() {
4356
4638
  windowRegistry.delete(unique);
@@ -4503,6 +4785,11 @@ var DocxodusEditor = (() => {
4503
4785
  var EDITABLE_TAGS = /* @__PURE__ */ new Set(["P", "H1", "H2", "H3", "H4", "H5", "H6"]);
4504
4786
  var BLOCK_DRAG_TYPE = "docxodus-block";
4505
4787
  var blockDragStyledDocuments = /* @__PURE__ */ new WeakSet();
4788
+ function blockPreviewText(unit) {
4789
+ if (unit.tagName === "TABLE") return "Table";
4790
+ const text = (unit.textContent ?? "").trim().replace(/\s+/g, " ");
4791
+ return text.length > 48 ? `${text.slice(0, 48)}\u2026` : text;
4792
+ }
4506
4793
  function ensureBlockDragStyles(doc) {
4507
4794
  if (blockDragStyledDocuments.has(doc)) return;
4508
4795
  blockDragStyledDocuments.add(doc);
@@ -4517,10 +4804,28 @@ var DocxodusEditor = (() => {
4517
4804
  }
4518
4805
  .docx-block-handle:hover, .docx-block-handle:focus-visible { color: #344054; border-color: #98a2b3; outline: none; }
4519
4806
  .docx-block-handle[aria-pressed="true"] { color: #175cd3; border-color: #84adff; background: #eff8ff; }
4520
- .docx-block-handle.docx-block-dragging { cursor: grabbing; opacity: .78; }
4807
+ .docx-block-handle.docx-block-dragging { cursor: grabbing; opacity: .35; }
4808
+ /* The block being carried. Dimming it is the "a drag is happening" signal that survives the
4809
+ pointer being anywhere on screen \u2014 the drop line only says where, not what. */
4810
+ .docx-block-drag-source { opacity: .38; transition: opacity 120ms ease-out; }
4811
+ /* Positioned by transform so tracking the pointer costs no layout. Flipping display none\u2192block
4812
+ restarts the fade \u2014 one cheap entry animation per appearance, none while it tracks. */
4521
4813
  .docx-block-drop-indicator {
4522
- position: fixed; z-index: 2147482999; display: none; height: 3px; pointer-events: none;
4523
- border-radius: 999px; background: #2e90fa; box-shadow: 0 0 0 1px rgba(255,255,255,.85);
4814
+ position: fixed; top: 0; left: 0; z-index: 2147482999; display: none; height: 0;
4815
+ pointer-events: none; border-top: 2px solid #2e90fa;
4816
+ filter: drop-shadow(0 1px 2px rgba(46,144,250,.5));
4817
+ animation: docx-block-drop-in 110ms ease-out;
4818
+ }
4819
+ .docx-block-drop-indicator::before {
4820
+ content: ""; position: absolute; top: -5px; left: -2px; width: 8px; height: 8px;
4821
+ border-radius: 50%; background: #2e90fa;
4822
+ }
4823
+ @keyframes docx-block-drop-in { from { opacity: 0; } to { opacity: 1; } }
4824
+ .docx-block-drag-preview {
4825
+ max-width: 320px; padding: 6px 10px; border: 1px solid #b2ddff; border-radius: 6px;
4826
+ background: #eff8ff; color: #175cd3; box-shadow: 0 6px 16px rgba(16,24,40,.18);
4827
+ font: 500 13px/1.35 system-ui, sans-serif; white-space: nowrap; overflow: hidden;
4828
+ text-overflow: ellipsis;
4524
4829
  }
4525
4830
  .docx-block-move-menu {
4526
4831
  position: fixed; z-index: 2147483001; display: none; min-width: 150px; padding: 5px;
@@ -4908,12 +5213,22 @@ var DocxodusEditor = (() => {
4908
5213
  this.blockMoveLive = null;
4909
5214
  this.blockDragSource = null;
4910
5215
  this.blockDragCleanup = [];
4911
- this.blockDragTargetCleanup = [];
5216
+ /** Block boxes measured at drag start — see `BlockDropZone`. Empty when no drag is in flight. */
5217
+ this.dropZones = [];
5218
+ /** Combined scroll offset when `dropZones` was measured, and the scroller measured against. */
5219
+ this.dropZoneOrigin = 0;
5220
+ this.dropZoneScroller = null;
4912
5221
  this.blockDragging = false;
4913
5222
  this.blockDragPointerDown = false;
4914
5223
  /** Anchors the current drag source may legally move next to, per the engine's own rules.
4915
5224
  * Null when the bridge predates ValidMoveTargets — then every block is offered, as before. */
4916
5225
  this.blockMoveTargets = null;
5226
+ /** Memoized `ValidMoveTargets` answers, keyed by source anchor and dropped whenever an edit
5227
+ * lands. The legal-target set is a property of the document, so hovering back and forth over
5228
+ * the same blocks between edits must not re-ask the engine. */
5229
+ this.blockMoveTargetCache = /* @__PURE__ */ new Map();
5230
+ /** Cancels the pending idle prefetch of the hovered block's targets — see `showBlockHandle`. */
5231
+ this.blockMoveTargetPrefetch = null;
4917
5232
  /** Why the last move was refused, verbatim from the engine — diagnostics, not announcement copy. */
4918
5233
  this.lastMoveError = null;
4919
5234
  /**
@@ -5031,6 +5346,11 @@ var DocxodusEditor = (() => {
5031
5346
  this.handle = handle;
5032
5347
  this.options = options;
5033
5348
  this.editRoot = container;
5349
+ this.viewport = new DocumentViewport(container, {
5350
+ columnWidth: options.columnWidth,
5351
+ fitToWidth: options.fitToWidth,
5352
+ scale: options.scale
5353
+ });
5034
5354
  if (typeof document !== "undefined") {
5035
5355
  document.addEventListener("selectionchange", this.onSelectionChange);
5036
5356
  document.addEventListener("mousedown", this.onMouseDown, true);
@@ -5113,6 +5433,8 @@ var DocxodusEditor = (() => {
5113
5433
  editable: options.editable ?? true,
5114
5434
  paginated: options.paginated ?? false,
5115
5435
  scale: options.scale ?? 1,
5436
+ columnWidth: options.columnWidth ?? "section",
5437
+ fitToWidth: options.fitToWidth ?? true,
5116
5438
  headerFooter: options.headerFooter ?? false,
5117
5439
  blockDrag: options.blockDrag ?? false,
5118
5440
  trackedChanges: options.trackedChanges ?? 0 /* Accept */,
@@ -5169,6 +5491,7 @@ var DocxodusEditor = (() => {
5169
5491
  }
5170
5492
  this.clearDragSelection();
5171
5493
  this.teardownBlockDrag();
5494
+ this.viewport.dispose();
5172
5495
  this.exports.DocxSessionBridge.CloseSession(this.handle);
5173
5496
  }
5174
5497
  /**
@@ -5186,6 +5509,14 @@ var DocxodusEditor = (() => {
5186
5509
  get root() {
5187
5510
  return this.container;
5188
5511
  }
5512
+ /**
5513
+ * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
5514
+ * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
5515
+ * is wondering why a phone shows the whole page.
5516
+ */
5517
+ get zoom() {
5518
+ return this.viewport.scale;
5519
+ }
5189
5520
  /**
5190
5521
  * The live `DocxSession` handle backing this editor — the model of record.
5191
5522
  *
@@ -5214,8 +5545,7 @@ var DocxodusEditor = (() => {
5214
5545
  return true;
5215
5546
  }
5216
5547
  const destination = res.created?.[0] ?? res.modified?.[0];
5217
- if (this.renderTrackedChanges) this.remount();
5218
- else this.reconcile();
5548
+ this.reconcile();
5219
5549
  const moved = destination ? this.bodyUnitNodes().find((el) => el.getAttribute("data-anchor") === destination.unid) : null;
5220
5550
  if (moved) {
5221
5551
  moved.classList.add("docx-block-move-flash");
@@ -5240,7 +5570,12 @@ var DocxodusEditor = (() => {
5240
5570
  const node = target instanceof Node ? target : null;
5241
5571
  const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
5242
5572
  if (!el || !this.editRoot.contains(el)) return null;
5243
- return this.bodyUnitNodes().find((unit) => unit === el || unit.contains(el)) ?? null;
5573
+ let unit = null;
5574
+ for (let candidate = el.closest("[data-anchor]"); candidate && this.editRoot.contains(candidate); candidate = candidate.parentElement?.closest("[data-anchor]") ?? null) {
5575
+ unit = candidate;
5576
+ }
5577
+ if (!unit || unit.closest("section.footnotes, section.endnotes")) return null;
5578
+ return unit;
5244
5579
  }
5245
5580
  isMovableBlockUnit(unit) {
5246
5581
  if (!unit || !this.anchorIdOf(unit)) return false;
@@ -5262,18 +5597,18 @@ var DocxodusEditor = (() => {
5262
5597
  if (!handle || !this.isMovableBlockUnit(unit)) return;
5263
5598
  const changed = unit !== this.blockDragSource;
5264
5599
  this.blockDragSource = unit;
5265
- if (changed) this.refreshBlockMoveTargets(unit);
5266
- if (this.blockMoveTargets?.size === 0) {
5600
+ const known = this.blockMoveTargetsFor(unit, { cachedOnly: true });
5601
+ if (known?.size === 0) {
5267
5602
  handle.style.display = "none";
5268
5603
  return;
5269
5604
  }
5605
+ if (changed && known === void 0) this.prefetchBlockMoveTargets(unit);
5270
5606
  const rect = unit.getBoundingClientRect();
5271
5607
  handle.style.display = "flex";
5272
5608
  handle.style.left = `${Math.max(4, rect.left - 32)}px`;
5273
5609
  handle.style.top = `${Math.max(4, rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - 28) / 2)))}px`;
5274
- const preview = (unit.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 48);
5610
+ const preview = blockPreviewText(unit);
5275
5611
  handle.setAttribute("aria-label", preview ? `Move block: ${preview}` : "Move block");
5276
- if (changed) this.refreshBlockDropTargets();
5277
5612
  }
5278
5613
  hideBlockHandle() {
5279
5614
  if (this.blockDragging || this.blockMoveMenu?.style.display === "block") return;
@@ -5283,14 +5618,33 @@ var DocxodusEditor = (() => {
5283
5618
  const source = this.currentBlockDragSource();
5284
5619
  if (source && this.blockDragHandle?.style.display !== "none") this.showBlockHandle(source);
5285
5620
  }
5286
- showDropIndicator(target, position) {
5621
+ /** Draw the drop line on `zone`'s requested edge, or take it away when there is no target. */
5622
+ paintDropIndicator(data) {
5287
5623
  const indicator = this.blockDropIndicator;
5624
+ const zone = data.zone;
5288
5625
  if (!indicator) return;
5289
- const rect = this.unitWrapperOf(target).getBoundingClientRect();
5626
+ if (!zone) {
5627
+ this.hideDropIndicator();
5628
+ return;
5629
+ }
5630
+ const y = Math.round(this.dropEdgeY(zone, data.position === "after" ? "after" : "before") + this.dropZoneShift()) - 1;
5631
+ indicator.style.transform = `translate3d(${Math.round(zone.left)}px, ${y}px, 0)`;
5632
+ indicator.style.width = `${Math.max(24, Math.round(zone.width))}px`;
5290
5633
  indicator.style.display = "block";
5291
- indicator.style.left = `${rect.left}px`;
5292
- indicator.style.top = `${position === "before" ? rect.top - 1 : rect.bottom - 1}px`;
5293
- indicator.style.width = `${Math.max(24, rect.width)}px`;
5634
+ }
5635
+ /**
5636
+ * Where to draw the line for an insertion on `position` of `zone` — the MIDDLE of the gap to
5637
+ * the neighbour on that side, not the zone's own border-box edge. A paragraph's `w:spacing`
5638
+ * becomes a CSS margin, which sits outside the box, so drawing on the edge underlines the
5639
+ * block's last line instead of reading as a gap between two blocks. Falls back to the raw edge
5640
+ * at the ends of the flow, and degrades to the same value when blocks are contiguous.
5641
+ */
5642
+ dropEdgeY(zone, position) {
5643
+ const neighbour = this.dropZones[zone.index + (position === "after" ? 1 : -1)];
5644
+ if (!neighbour) {
5645
+ return position === "after" ? zone.bottom + zone.marginAfter / 2 : zone.top - zone.marginBefore / 2;
5646
+ }
5647
+ return position === "after" ? (zone.bottom + neighbour.top) / 2 : (neighbour.bottom + zone.top) / 2;
5294
5648
  }
5295
5649
  hideDropIndicator() {
5296
5650
  if (this.blockDropIndicator) this.blockDropIndicator.style.display = "none";
@@ -5310,19 +5664,52 @@ var DocxodusEditor = (() => {
5310
5664
  * behaviour this replaces. Null (no bridge support) keeps the previous offer-everything path.
5311
5665
  */
5312
5666
  refreshBlockMoveTargets(source) {
5667
+ this.blockMoveTargets = source ? this.blockMoveTargetsFor(source) ?? null : null;
5668
+ }
5669
+ /**
5670
+ * This block's legal destinations, from the memo when it is there. Returns `undefined` — not
5671
+ * `null` — for "not asked yet", so a caller can tell an unknown answer from the engine's
5672
+ * "no bridge support, offer everything" one.
5673
+ */
5674
+ blockMoveTargetsFor(source, options = {}) {
5313
5675
  const bridge = this.exports.DocxSessionBridge;
5314
- const sourceId = source ? this.anchorIdOf(source) : null;
5315
- if (!sourceId || typeof bridge.ValidMoveTargets !== "function") {
5316
- this.blockMoveTargets = null;
5317
- return;
5318
- }
5676
+ const sourceId = this.anchorIdOf(source);
5677
+ if (!sourceId || typeof bridge.ValidMoveTargets !== "function") return null;
5678
+ if (this.blockMoveTargetCache.has(sourceId)) return this.blockMoveTargetCache.get(sourceId);
5679
+ if (options.cachedOnly) return void 0;
5680
+ let targets;
5319
5681
  try {
5320
- const targets = JSON.parse(bridge.ValidMoveTargets(this.handle, sourceId));
5321
- this.blockMoveTargets = new Map(
5322
- targets.map((t) => [t.anchorId, { before: t.before, after: t.after }])
5323
- );
5682
+ const parsed = JSON.parse(bridge.ValidMoveTargets(this.handle, sourceId));
5683
+ targets = new Map(parsed.map((t) => [t.anchorId, { before: t.before, after: t.after }]));
5324
5684
  } catch {
5325
- this.blockMoveTargets = null;
5685
+ targets = null;
5686
+ }
5687
+ this.blockMoveTargetCache.set(sourceId, targets);
5688
+ return targets;
5689
+ }
5690
+ /**
5691
+ * Ask for the hovered block's destinations off the interaction path, and hide the handle if
5692
+ * the answer comes back empty and that block is still the one under the pointer. The handle
5693
+ * therefore appears immediately on hover and withdraws a beat later on the rare immovable
5694
+ * block, instead of every hover paying for the query up front.
5695
+ */
5696
+ prefetchBlockMoveTargets(source) {
5697
+ const view = this.container.ownerDocument.defaultView;
5698
+ if (!view) return;
5699
+ this.blockMoveTargetPrefetch?.();
5700
+ const run = () => {
5701
+ this.blockMoveTargetPrefetch = null;
5702
+ if (this.closed || !source.isConnected || this.blockDragSource !== source) return;
5703
+ if (this.blockMoveTargetsFor(source)?.size === 0 && this.blockDragHandle)
5704
+ this.blockDragHandle.style.display = "none";
5705
+ };
5706
+ const idle = view;
5707
+ if (idle.requestIdleCallback && idle.cancelIdleCallback) {
5708
+ const id = idle.requestIdleCallback(run, { timeout: 500 });
5709
+ this.blockMoveTargetPrefetch = () => idle.cancelIdleCallback(id);
5710
+ } else {
5711
+ const id = view.setTimeout(run, 0);
5712
+ this.blockMoveTargetPrefetch = () => view.clearTimeout(id);
5326
5713
  }
5327
5714
  }
5328
5715
  /**
@@ -5337,45 +5724,75 @@ var DocxodusEditor = (() => {
5337
5724
  if (!sides) return false;
5338
5725
  return position ? sides[position] : sides.before || sides.after;
5339
5726
  }
5727
+ /** Measure every movable block once, at drag start. See `BlockDropZone`. */
5728
+ captureDropZones() {
5729
+ const view = this.container.ownerDocument.defaultView;
5730
+ this.dropZoneScroller = this.scrollContainer();
5731
+ this.dropZoneOrigin = this.scrollOffsetSum();
5732
+ this.dropZones = [];
5733
+ const boxes = [];
5734
+ for (const unit of this.bodyUnitNodes()) {
5735
+ const anchorId = this.isMovableBlockUnit(unit) ? this.anchorIdOf(unit) : null;
5736
+ if (!anchorId) continue;
5737
+ const box = this.unitWrapperOf(unit);
5738
+ const rect = box.getBoundingClientRect();
5739
+ boxes.push(box);
5740
+ this.dropZones.push({
5741
+ unit,
5742
+ anchorId,
5743
+ index: this.dropZones.length,
5744
+ top: rect.top,
5745
+ bottom: rect.bottom,
5746
+ left: rect.left,
5747
+ width: rect.width,
5748
+ marginBefore: 0,
5749
+ marginAfter: 0
5750
+ });
5751
+ }
5752
+ const ends = new Set([0, this.dropZones.length - 1].filter((i) => i >= 0 && i < boxes.length));
5753
+ for (const i of ends) {
5754
+ const style = view?.getComputedStyle(boxes[i]);
5755
+ this.dropZones[i].marginBefore = parseFloat(style?.marginTop ?? "0") || 0;
5756
+ this.dropZones[i].marginAfter = parseFloat(style?.marginBottom ?? "0") || 0;
5757
+ }
5758
+ }
5759
+ scrollOffsetSum() {
5760
+ const view = this.container.ownerDocument.defaultView;
5761
+ return (view?.scrollY ?? 0) + (this.dropZoneScroller?.scrollTop ?? 0);
5762
+ }
5763
+ /** How far the measured boxes have travelled since capture, from scrolling (drag autoscroll). */
5764
+ dropZoneShift() {
5765
+ return this.dropZoneOrigin - this.scrollOffsetSum();
5766
+ }
5340
5767
  /**
5341
- * The side of `unit` a drop at `clientY` should land on: the half the pointer is in, snapped to
5342
- * the other side when only that one is legal. Snapping rather than refusing keeps a reachable
5343
- * target usable the illegal side is usually illegal only because a section break or a
5344
- * cross-block range sits between the two blocks on that side.
5768
+ * Where a drop at `clientY` lands, or null when nothing there is legal.
5769
+ *
5770
+ * Resolution is by VERTICAL GEOMETRY over the measured blocks, not by which element the pointer
5771
+ * is over: the drag handle floats in the page margin, so a drag straight down the gutter — the
5772
+ * natural gesture — never crosses a paragraph box, and element hit testing gave those drags no
5773
+ * indicator and no drop at all. The nearest block by vertical distance is the target; the half
5774
+ * the pointer is in picks the side, snapped to the other side when only that one is legal
5775
+ * (a section break or a cross-block range usually makes exactly one side illegal). When neither
5776
+ * side is legal — the pointer is in a region this block cannot reach — there is no drop, and
5777
+ * nothing is drawn.
5345
5778
  */
5346
- dropPositionFor(unit, clientY) {
5347
- const rect = unit.getBoundingClientRect();
5348
- const preferred = clientY < rect.top + rect.height / 2 ? "before" : "after";
5349
- if (this.isValidMoveTarget(unit, preferred)) return preferred;
5350
- const other = preferred === "before" ? "after" : "before";
5351
- return this.isValidMoveTarget(unit, other) ? other : preferred;
5352
- }
5353
- refreshBlockDropTargets() {
5354
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
5355
- if (!this.blockDragHandle || this.options.paginated) return;
5356
- for (const unit of this.bodyUnitNodes().filter((el) => this.isMovableBlockUnit(el))) {
5357
- this.blockDragTargetCleanup.push(dropTargetForElements({
5358
- element: unit,
5359
- // A target the engine would refuse is not a drop target at all, so Pragmatic never
5360
- // fires onDragEnter for it and no indicator is drawn over it.
5361
- canDrop: ({ source }) => source.data.type === BLOCK_DRAG_TYPE && source.data.sourceAnchorId !== this.anchorIdOf(unit) && this.isValidMoveTarget(unit),
5362
- getData: ({ input }) => ({
5363
- type: BLOCK_DRAG_TYPE,
5364
- targetAnchorId: this.anchorIdOf(unit),
5365
- position: this.dropPositionFor(unit, input.clientY),
5366
- targetElement: unit
5367
- }),
5368
- onDragEnter: ({ self }) => {
5369
- const pos = self.data.position === "after" ? "after" : "before";
5370
- this.showDropIndicator(unit, pos);
5371
- },
5372
- onDrag: ({ self }) => {
5373
- const pos = self.data.position === "after" ? "after" : "before";
5374
- this.showDropIndicator(unit, pos);
5375
- },
5376
- onDragLeave: () => this.hideDropIndicator()
5377
- }));
5779
+ resolveDropAt(clientY) {
5780
+ const y = clientY - this.dropZoneShift();
5781
+ let best = null;
5782
+ let bestGap = Infinity;
5783
+ for (const zone of this.dropZones) {
5784
+ const gap = y < zone.top ? zone.top - y : y > zone.bottom ? y - zone.bottom : 0;
5785
+ if (gap < bestGap) {
5786
+ best = zone;
5787
+ bestGap = gap;
5788
+ }
5789
+ if (gap === 0) break;
5378
5790
  }
5791
+ if (!best || best.unit === this.blockDragSource) return null;
5792
+ const preferred = y < (best.top + best.bottom) / 2 ? "before" : "after";
5793
+ if (this.isValidMoveTarget(best.unit, preferred)) return { zone: best, position: preferred };
5794
+ const other = preferred === "before" ? "after" : "before";
5795
+ return this.isValidMoveTarget(best.unit, other) ? { zone: best, position: other } : null;
5379
5796
  }
5380
5797
  closeBlockMoveMenu(restoreFocus = false) {
5381
5798
  if (!this.blockMoveMenu || !this.blockDragHandle) return;
@@ -5412,7 +5829,8 @@ var DocxodusEditor = (() => {
5412
5829
  const index = units.indexOf(source);
5413
5830
  if (index < 0) return null;
5414
5831
  const position = action === "up" || action === "top" ? "before" : "after";
5415
- const candidates = units.filter((el) => el !== source && this.isValidMoveTarget(el, position)).filter((el) => position === "before" ? units.indexOf(el) < index : units.indexOf(el) > index);
5832
+ const side = position === "before" ? units.slice(0, index) : units.slice(index + 1);
5833
+ const candidates = side.filter((el) => this.isValidMoveTarget(el, position));
5416
5834
  if (candidates.length === 0) return null;
5417
5835
  if (action === "up") return { target: candidates[candidates.length - 1], position };
5418
5836
  if (action === "down") return { target: candidates[0], position };
@@ -5549,20 +5967,50 @@ var DocxodusEditor = (() => {
5549
5967
  const source = this.currentBlockDragSource();
5550
5968
  return { type: BLOCK_DRAG_TYPE, sourceAnchorId: source ? this.anchorIdOf(source) : void 0 };
5551
5969
  },
5970
+ // The browser would otherwise ghost the 26px grip, which says nothing about what is moving.
5971
+ onGenerateDragPreview: ({ nativeSetDragImage }) => {
5972
+ const source = this.currentBlockDragSource();
5973
+ setCustomNativeDragPreview({
5974
+ nativeSetDragImage,
5975
+ getOffset: pointerOutsideOfPreview({ x: "14px", y: "10px" }),
5976
+ render: ({ container }) => {
5977
+ const chip = doc.createElement("div");
5978
+ chip.className = "docx-block-drag-preview";
5979
+ chip.textContent = source && blockPreviewText(source) || "Move block";
5980
+ container.appendChild(chip);
5981
+ return () => chip.remove();
5982
+ }
5983
+ });
5984
+ },
5552
5985
  onDragStart: () => {
5986
+ const source = this.currentBlockDragSource();
5553
5987
  this.blockDragging = true;
5554
5988
  handle.classList.add("docx-block-dragging");
5989
+ source?.classList.add("docx-block-drag-source");
5555
5990
  this.closeBlockMoveMenu();
5556
- this.refreshBlockMoveTargets(this.currentBlockDragSource());
5557
- this.refreshBlockDropTargets();
5991
+ this.refreshBlockMoveTargets(source);
5992
+ this.captureDropZones();
5558
5993
  },
5559
5994
  onDrop: () => {
5560
5995
  this.blockDragging = false;
5561
5996
  this.blockDragPointerDown = false;
5997
+ this.dropZones = [];
5562
5998
  handle.classList.remove("docx-block-dragging");
5999
+ doc.querySelectorAll(".docx-block-drag-source").forEach((el) => el.classList.remove("docx-block-drag-source"));
5563
6000
  this.hideDropIndicator();
5564
6001
  }
5565
6002
  }));
6003
+ this.blockDragCleanup.push(dropTargetForElements({
6004
+ element: this.editRoot,
6005
+ canDrop: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
6006
+ getData: ({ input }) => {
6007
+ const hit = this.resolveDropAt(input.clientY);
6008
+ return hit ? { type: BLOCK_DRAG_TYPE, targetAnchorId: hit.zone.anchorId, position: hit.position, zone: hit.zone } : { type: BLOCK_DRAG_TYPE };
6009
+ },
6010
+ onDragEnter: ({ self }) => this.paintDropIndicator(self.data),
6011
+ onDrag: ({ self }) => this.paintDropIndicator(self.data),
6012
+ onDragLeave: () => this.hideDropIndicator()
6013
+ }));
5566
6014
  this.blockDragCleanup.push(monitorForElements({
5567
6015
  canMonitor: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
5568
6016
  onDrop: ({ source, location: location2 }) => {
@@ -5589,10 +6037,13 @@ var DocxodusEditor = (() => {
5589
6037
  canScroll: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
5590
6038
  getAllowedAxis: () => "vertical"
5591
6039
  }));
5592
- this.refreshBlockDropTargets();
5593
6040
  }
5594
6041
  teardownBlockDrag() {
5595
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
6042
+ this.blockMoveTargetPrefetch?.();
6043
+ this.blockMoveTargetPrefetch = null;
6044
+ this.blockMoveTargetCache.clear();
6045
+ this.dropZones = [];
6046
+ this.dropZoneScroller = null;
5596
6047
  for (const cleanup of this.blockDragCleanup.splice(0)) cleanup();
5597
6048
  this.blockDragHandle?.remove();
5598
6049
  this.blockDropIndicator?.remove();
@@ -5681,17 +6132,17 @@ var DocxodusEditor = (() => {
5681
6132
  bodyRoot.after(this.region.footerBand);
5682
6133
  this.region.refreshAll();
5683
6134
  }
5684
- /** Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks. */
6135
+ /**
6136
+ * Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks.
6137
+ *
6138
+ * The body always gets its own `.docx-body-flow` wrapper — not only when bands are docked.
6139
+ * It is the sheet: the element the viewport gives page geometry to and zooms, and the one
6140
+ * the bands dock around. Without it the container would have to be both the scrolling host
6141
+ * and the scaled page, which are different boxes.
6142
+ */
5685
6143
  mountHtml(fullHtml) {
5686
6144
  const parsed = new DOMParser().parseFromString(fullHtml, "text/html");
5687
6145
  const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
5688
- if (!this.region) {
5689
- this.container.innerHTML = styles + parsed.body.innerHTML;
5690
- this.editRoot = this.container;
5691
- if (this.options.editable) this.wireBlocks(this.container);
5692
- this.stampPlanState();
5693
- return;
5694
- }
5695
6146
  this.container.innerHTML = styles;
5696
6147
  const flow = document.createElement("div");
5697
6148
  flow.className = "docx-body-flow";
@@ -5700,7 +6151,8 @@ var DocxodusEditor = (() => {
5700
6151
  this.editRoot = flow;
5701
6152
  if (this.options.editable) this.wireBlocks(flow);
5702
6153
  this.stampPlanState();
5703
- this.dockBands(flow);
6154
+ if (this.region) this.dockBands(flow);
6155
+ this.viewport.attach(flow, true);
5704
6156
  }
5705
6157
  /** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
5706
6158
  mountPaginated(fullHtml) {
@@ -5721,6 +6173,7 @@ var DocxodusEditor = (() => {
5721
6173
  this.editRoot = pageRoot;
5722
6174
  if (this.options.editable) this.wireBlocks(pageRoot);
5723
6175
  if (this.region) this.dockBands(target);
6176
+ this.viewport.attach(pageRoot, false);
5724
6177
  }
5725
6178
  wireBlocks(root) {
5726
6179
  root.querySelectorAll("[data-anchor]").forEach((el) => this.wireBlock(el));
@@ -6109,11 +6562,22 @@ var DocxodusEditor = (() => {
6109
6562
  }
6110
6563
  parseEdit(json) {
6111
6564
  try {
6112
- return JSON.parse(json);
6565
+ const result = JSON.parse(json);
6566
+ if (result.success) this.invalidateBlockMoveTargets();
6567
+ return result;
6113
6568
  } catch {
6114
6569
  return { success: false };
6115
6570
  }
6116
6571
  }
6572
+ /**
6573
+ * Drop the memoized `ValidMoveTargets` answers. Which blocks a block may move next to is a
6574
+ * fact about the DOCUMENT, so it survives hovering but not editing — and the two places a
6575
+ * document changes are `parseEdit` (every mutation that returns an `EditResult`) and
6576
+ * undo/redo, which return a bare boolean and so cannot go through it.
6577
+ */
6578
+ invalidateBlockMoveTargets() {
6579
+ this.blockMoveTargetCache.clear();
6580
+ }
6117
6581
  // ─── M5: formatting commands (ribbon) ────────────────────────────────
6118
6582
  // ─── Multi-block selection helpers (format a whole stack of paragraphs at once) ──────
6119
6583
  /**
@@ -6667,12 +7131,16 @@ var DocxodusEditor = (() => {
6667
7131
  /** Undo the last edit (incremental repaint; falls back to a full re-render). */
6668
7132
  undo() {
6669
7133
  if (this.closed) return;
6670
- if (this.exports.DocxSessionBridge.Undo(this.handle)) this.reconcile();
7134
+ if (!this.exports.DocxSessionBridge.Undo(this.handle)) return;
7135
+ this.invalidateBlockMoveTargets();
7136
+ this.reconcile();
6671
7137
  }
6672
7138
  /** Redo the last undone edit (incremental repaint; falls back to a full re-render). */
6673
7139
  redo() {
6674
7140
  if (this.closed) return;
6675
- if (this.exports.DocxSessionBridge.Redo(this.handle)) this.reconcile();
7141
+ if (!this.exports.DocxSessionBridge.Redo(this.handle)) return;
7142
+ this.invalidateBlockMoveTargets();
7143
+ this.reconcile();
6676
7144
  }
6677
7145
  // ─── Header/footer region commands (no-ops unless `headerFooter` is on) ───────────────
6678
7146
  /**
@@ -6901,7 +7369,11 @@ var DocxodusEditor = (() => {
6901
7369
  const oldTokens = oldNodes.map(_DocxEditor.domTokenOf);
6902
7370
  const oldKinds = oldNodes.map(_DocxEditor.domKindOf);
6903
7371
  const bodyDiff = diffUnits(oldTokens, plan.body);
6904
- if (needsRemount(bodyDiff, plan.body, oldKinds)) return this.bail("needsRemount (li change or churn)");
7372
+ if (needsRemount(bodyDiff, plan.body, oldKinds)) {
7373
+ return this.bail(
7374
+ `needsRemount (li change or churn): +${bodyDiff.added.length} -${bodyDiff.removed.length} ~${bodyDiff.substituted.length} moved=${bodyDiff.moved.length} of ${plan.body.length}`
7375
+ );
7376
+ }
6905
7377
  const fnState = this.notesDiff("footnotes", plan.footnotes);
6906
7378
  const enState = this.notesDiff("endnotes", plan.endnotes);
6907
7379
  if (fnState === null || enState === null) return this.bail("notes container unstampable/missing");
@@ -7510,14 +7982,7 @@ var DocxodusEditor = (() => {
7510
7982
  SHOULD win.
7511
7983
  (No backticks in this file's comments: the stylesheet is a template literal.) */
7512
7984
  .dxr-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
7513
- .dxr[data-chrome] .dxr-surface { max-width: 920px; margin: 26px auto; padding: 0 16px 96px; }
7514
- .dxr-surface[data-view="continuous"] > * { background: var(--dxr-sheet); }
7515
- .dxr[data-chrome] .dxr-surface[data-view="continuous"] {
7516
- padding: 56px 72px;
7517
- border-radius: 3px;
7518
- background: var(--dxr-sheet);
7519
- box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
7520
- }
7985
+ .dxr[data-chrome] .dxr-surface { margin: 26px auto; padding: 0 16px 96px; }
7521
7986
  .dxr-surface [contenteditable="true"]:focus {
7522
7987
  outline: 2px solid var(--dxr-accent);
7523
7988
  outline-offset: 2px;
@@ -7529,7 +7994,10 @@ var DocxodusEditor = (() => {
7529
7994
  so they dock as their own regions. Styled from the same tokens as the ribbon
7530
7995
  so the surface reads as one instrument rather than two apps. */
7531
7996
  .dxr-surface .docx-hf-band {
7532
- margin: 0 0 14px;
7997
+ /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
7998
+ custom property the viewport publishes rather than stretching to the whole surface. */
7999
+ width: min(100%, var(--docx-sheet-width, 100%));
8000
+ margin: 0 auto 14px;
7533
8001
  padding: 9px 72px 13px;
7534
8002
  border: 1px solid var(--dxr-rule);
7535
8003
  border-left: 2px solid #c2ccd9;
@@ -7537,7 +8005,7 @@ var DocxodusEditor = (() => {
7537
8005
  background: var(--dxr-sheet);
7538
8006
  }
7539
8007
  .dxr-surface .docx-hf-band + .docx-body-flow { margin-top: 0; }
7540
- .dxr-surface .docx-hf-band[data-hf-band="footer"] { margin: 14px 0 0; }
8008
+ .dxr-surface .docx-hf-band[data-hf-band="footer"] { margin: 14px auto 0; }
7541
8009
  .dxr-surface .docx-hf-chrome {
7542
8010
  display: flex;
7543
8011
  gap: 8px;
@@ -7592,13 +8060,15 @@ var DocxodusEditor = (() => {
7592
8060
  text-transform: none;
7593
8061
  }
7594
8062
  .dxr-surface .docx-hf-band[data-hf-inherited] { border-style: dashed; }
7595
- .dxr[data-chrome] .dxr-surface[data-view="continuous"]:has(.docx-body-flow) {
7596
- padding: 0;
7597
- background: transparent;
7598
- box-shadow: none;
7599
- }
8063
+ /* The sheet IS the page. The editor's viewport sizes .docx-body-flow to the section's page
8064
+ width and its section wrappers to the authored text column, so the horizontal gutters here
8065
+ are the document's own w:sectPr margins, not a padding this chrome invents; only the
8066
+ vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
8067
+ has zoomed to fit stays centered at its scaled width. */
7600
8068
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
7601
- padding: 56px 72px;
8069
+ max-width: 100%;
8070
+ margin: 0 auto;
8071
+ padding: 56px 0;
7602
8072
  border-radius: 3px;
7603
8073
  background: var(--dxr-sheet);
7604
8074
  box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
@@ -7677,9 +8147,11 @@ var DocxodusEditor = (() => {
7677
8147
  .dxr[data-chrome="compact"] .dxr-note { display: none; }
7678
8148
  .dxr[data-chrome="compact"] .dxr-rail { display: none; }
7679
8149
  .dxr[data-chrome="compact"] .dxr-hint { display: none; }
8150
+ /* Compact trims the chrome around the page, never the page: the document's own column width
8151
+ is what the viewport's fit-to-width zoom scales, so a phone shows a whole smaller page
8152
+ instead of a narrower one that breaks its lines somewhere Word never would. */
7680
8153
  .dxr[data-chrome="compact"] .dxr-surface { margin: 12px auto; padding: 0 10px 64px; }
7681
- .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] { padding: 22px 18px; }
7682
- .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] .docx-body-flow { padding: 22px 18px; }
8154
+ .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] .docx-body-flow { padding: 22px 0; }
7683
8155
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-band { padding: 9px 18px 13px; }
7684
8156
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-chrome,
7685
8157
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-warning { margin-left: 0; }
@@ -8429,6 +8901,8 @@ var DocxodusEditor = (() => {
8429
8901
  fabricateClasses: this.options.fabricateClasses,
8430
8902
  editable: this.options.editable,
8431
8903
  scale: this.options.scale,
8904
+ columnWidth: this.options.columnWidth,
8905
+ fitToWidth: this.options.fitToWidth,
8432
8906
  onEdit: this.options.onEdit,
8433
8907
  onMove: this.options.onMove,
8434
8908
  paginated,