docxodus 9.4.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.
@@ -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,7 +5213,11 @@ 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.
@@ -5037,6 +5346,11 @@ var DocxodusEditor = (() => {
5037
5346
  this.handle = handle;
5038
5347
  this.options = options;
5039
5348
  this.editRoot = container;
5349
+ this.viewport = new DocumentViewport(container, {
5350
+ columnWidth: options.columnWidth,
5351
+ fitToWidth: options.fitToWidth,
5352
+ scale: options.scale
5353
+ });
5040
5354
  if (typeof document !== "undefined") {
5041
5355
  document.addEventListener("selectionchange", this.onSelectionChange);
5042
5356
  document.addEventListener("mousedown", this.onMouseDown, true);
@@ -5119,6 +5433,8 @@ var DocxodusEditor = (() => {
5119
5433
  editable: options.editable ?? true,
5120
5434
  paginated: options.paginated ?? false,
5121
5435
  scale: options.scale ?? 1,
5436
+ columnWidth: options.columnWidth ?? "section",
5437
+ fitToWidth: options.fitToWidth ?? true,
5122
5438
  headerFooter: options.headerFooter ?? false,
5123
5439
  blockDrag: options.blockDrag ?? false,
5124
5440
  trackedChanges: options.trackedChanges ?? 0 /* Accept */,
@@ -5175,6 +5491,7 @@ var DocxodusEditor = (() => {
5175
5491
  }
5176
5492
  this.clearDragSelection();
5177
5493
  this.teardownBlockDrag();
5494
+ this.viewport.dispose();
5178
5495
  this.exports.DocxSessionBridge.CloseSession(this.handle);
5179
5496
  }
5180
5497
  /**
@@ -5192,6 +5509,14 @@ var DocxodusEditor = (() => {
5192
5509
  get root() {
5193
5510
  return this.container;
5194
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
+ }
5195
5520
  /**
5196
5521
  * The live `DocxSession` handle backing this editor — the model of record.
5197
5522
  *
@@ -5282,7 +5607,7 @@ var DocxodusEditor = (() => {
5282
5607
  handle.style.display = "flex";
5283
5608
  handle.style.left = `${Math.max(4, rect.left - 32)}px`;
5284
5609
  handle.style.top = `${Math.max(4, rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - 28) / 2)))}px`;
5285
- const preview = (unit.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 48);
5610
+ const preview = blockPreviewText(unit);
5286
5611
  handle.setAttribute("aria-label", preview ? `Move block: ${preview}` : "Move block");
5287
5612
  }
5288
5613
  hideBlockHandle() {
@@ -5293,14 +5618,33 @@ var DocxodusEditor = (() => {
5293
5618
  const source = this.currentBlockDragSource();
5294
5619
  if (source && this.blockDragHandle?.style.display !== "none") this.showBlockHandle(source);
5295
5620
  }
5296
- 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) {
5297
5623
  const indicator = this.blockDropIndicator;
5624
+ const zone = data.zone;
5298
5625
  if (!indicator) return;
5299
- 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`;
5300
5633
  indicator.style.display = "block";
5301
- indicator.style.left = `${rect.left}px`;
5302
- indicator.style.top = `${position === "before" ? rect.top - 1 : rect.bottom - 1}px`;
5303
- 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;
5304
5648
  }
5305
5649
  hideDropIndicator() {
5306
5650
  if (this.blockDropIndicator) this.blockDropIndicator.style.display = "none";
@@ -5380,45 +5724,75 @@ var DocxodusEditor = (() => {
5380
5724
  if (!sides) return false;
5381
5725
  return position ? sides[position] : sides.before || sides.after;
5382
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
+ }
5383
5767
  /**
5384
- * The side of `unit` a drop at `clientY` should land on: the half the pointer is in, snapped to
5385
- * the other side when only that one is legal. Snapping rather than refusing keeps a reachable
5386
- * target usable the illegal side is usually illegal only because a section break or a
5387
- * 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.
5388
5778
  */
5389
- dropPositionFor(unit, clientY) {
5390
- const rect = unit.getBoundingClientRect();
5391
- const preferred = clientY < rect.top + rect.height / 2 ? "before" : "after";
5392
- if (this.isValidMoveTarget(unit, preferred)) return preferred;
5393
- const other = preferred === "before" ? "after" : "before";
5394
- return this.isValidMoveTarget(unit, other) ? other : preferred;
5395
- }
5396
- refreshBlockDropTargets() {
5397
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
5398
- if (!this.blockDragHandle || this.options.paginated) return;
5399
- for (const unit of this.bodyUnitNodes().filter((el) => this.isMovableBlockUnit(el))) {
5400
- this.blockDragTargetCleanup.push(dropTargetForElements({
5401
- element: unit,
5402
- // A target the engine would refuse is not a drop target at all, so Pragmatic never
5403
- // fires onDragEnter for it and no indicator is drawn over it.
5404
- canDrop: ({ source }) => source.data.type === BLOCK_DRAG_TYPE && source.data.sourceAnchorId !== this.anchorIdOf(unit) && this.isValidMoveTarget(unit),
5405
- getData: ({ input }) => ({
5406
- type: BLOCK_DRAG_TYPE,
5407
- targetAnchorId: this.anchorIdOf(unit),
5408
- position: this.dropPositionFor(unit, input.clientY),
5409
- targetElement: unit
5410
- }),
5411
- onDragEnter: ({ self }) => {
5412
- const pos = self.data.position === "after" ? "after" : "before";
5413
- this.showDropIndicator(unit, pos);
5414
- },
5415
- onDrag: ({ self }) => {
5416
- const pos = self.data.position === "after" ? "after" : "before";
5417
- this.showDropIndicator(unit, pos);
5418
- },
5419
- onDragLeave: () => this.hideDropIndicator()
5420
- }));
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;
5421
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;
5422
5796
  }
5423
5797
  closeBlockMoveMenu(restoreFocus = false) {
5424
5798
  if (!this.blockMoveMenu || !this.blockDragHandle) return;
@@ -5593,20 +5967,50 @@ var DocxodusEditor = (() => {
5593
5967
  const source = this.currentBlockDragSource();
5594
5968
  return { type: BLOCK_DRAG_TYPE, sourceAnchorId: source ? this.anchorIdOf(source) : void 0 };
5595
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
+ },
5596
5985
  onDragStart: () => {
5986
+ const source = this.currentBlockDragSource();
5597
5987
  this.blockDragging = true;
5598
5988
  handle.classList.add("docx-block-dragging");
5989
+ source?.classList.add("docx-block-drag-source");
5599
5990
  this.closeBlockMoveMenu();
5600
- this.refreshBlockMoveTargets(this.currentBlockDragSource());
5601
- this.refreshBlockDropTargets();
5991
+ this.refreshBlockMoveTargets(source);
5992
+ this.captureDropZones();
5602
5993
  },
5603
5994
  onDrop: () => {
5604
5995
  this.blockDragging = false;
5605
5996
  this.blockDragPointerDown = false;
5997
+ this.dropZones = [];
5606
5998
  handle.classList.remove("docx-block-dragging");
5999
+ doc.querySelectorAll(".docx-block-drag-source").forEach((el) => el.classList.remove("docx-block-drag-source"));
5607
6000
  this.hideDropIndicator();
5608
6001
  }
5609
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
+ }));
5610
6014
  this.blockDragCleanup.push(monitorForElements({
5611
6015
  canMonitor: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
5612
6016
  onDrop: ({ source, location: location2 }) => {
@@ -5633,13 +6037,13 @@ var DocxodusEditor = (() => {
5633
6037
  canScroll: ({ source }) => source.data.type === BLOCK_DRAG_TYPE,
5634
6038
  getAllowedAxis: () => "vertical"
5635
6039
  }));
5636
- this.refreshBlockDropTargets();
5637
6040
  }
5638
6041
  teardownBlockDrag() {
5639
6042
  this.blockMoveTargetPrefetch?.();
5640
6043
  this.blockMoveTargetPrefetch = null;
5641
6044
  this.blockMoveTargetCache.clear();
5642
- for (const cleanup of this.blockDragTargetCleanup.splice(0)) cleanup();
6045
+ this.dropZones = [];
6046
+ this.dropZoneScroller = null;
5643
6047
  for (const cleanup of this.blockDragCleanup.splice(0)) cleanup();
5644
6048
  this.blockDragHandle?.remove();
5645
6049
  this.blockDropIndicator?.remove();
@@ -5728,17 +6132,17 @@ var DocxodusEditor = (() => {
5728
6132
  bodyRoot.after(this.region.footerBand);
5729
6133
  this.region.refreshAll();
5730
6134
  }
5731
- /** 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
+ */
5732
6143
  mountHtml(fullHtml) {
5733
6144
  const parsed = new DOMParser().parseFromString(fullHtml, "text/html");
5734
6145
  const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
5735
- if (!this.region) {
5736
- this.container.innerHTML = styles + parsed.body.innerHTML;
5737
- this.editRoot = this.container;
5738
- if (this.options.editable) this.wireBlocks(this.container);
5739
- this.stampPlanState();
5740
- return;
5741
- }
5742
6146
  this.container.innerHTML = styles;
5743
6147
  const flow = document.createElement("div");
5744
6148
  flow.className = "docx-body-flow";
@@ -5747,7 +6151,8 @@ var DocxodusEditor = (() => {
5747
6151
  this.editRoot = flow;
5748
6152
  if (this.options.editable) this.wireBlocks(flow);
5749
6153
  this.stampPlanState();
5750
- this.dockBands(flow);
6154
+ if (this.region) this.dockBands(flow);
6155
+ this.viewport.attach(flow, true);
5751
6156
  }
5752
6157
  /** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
5753
6158
  mountPaginated(fullHtml) {
@@ -5768,6 +6173,7 @@ var DocxodusEditor = (() => {
5768
6173
  this.editRoot = pageRoot;
5769
6174
  if (this.options.editable) this.wireBlocks(pageRoot);
5770
6175
  if (this.region) this.dockBands(target);
6176
+ this.viewport.attach(pageRoot, false);
5771
6177
  }
5772
6178
  wireBlocks(root) {
5773
6179
  root.querySelectorAll("[data-anchor]").forEach((el) => this.wireBlock(el));
@@ -7576,14 +7982,7 @@ var DocxodusEditor = (() => {
7576
7982
  SHOULD win.
7577
7983
  (No backticks in this file's comments: the stylesheet is a template literal.) */
7578
7984
  .dxr-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
7579
- .dxr[data-chrome] .dxr-surface { max-width: 920px; margin: 26px auto; padding: 0 16px 96px; }
7580
- .dxr-surface[data-view="continuous"] > * { background: var(--dxr-sheet); }
7581
- .dxr[data-chrome] .dxr-surface[data-view="continuous"] {
7582
- padding: 56px 72px;
7583
- border-radius: 3px;
7584
- background: var(--dxr-sheet);
7585
- box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
7586
- }
7985
+ .dxr[data-chrome] .dxr-surface { margin: 26px auto; padding: 0 16px 96px; }
7587
7986
  .dxr-surface [contenteditable="true"]:focus {
7588
7987
  outline: 2px solid var(--dxr-accent);
7589
7988
  outline-offset: 2px;
@@ -7595,7 +7994,10 @@ var DocxodusEditor = (() => {
7595
7994
  so they dock as their own regions. Styled from the same tokens as the ribbon
7596
7995
  so the surface reads as one instrument rather than two apps. */
7597
7996
  .dxr-surface .docx-hf-band {
7598
- 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;
7599
8001
  padding: 9px 72px 13px;
7600
8002
  border: 1px solid var(--dxr-rule);
7601
8003
  border-left: 2px solid #c2ccd9;
@@ -7603,7 +8005,7 @@ var DocxodusEditor = (() => {
7603
8005
  background: var(--dxr-sheet);
7604
8006
  }
7605
8007
  .dxr-surface .docx-hf-band + .docx-body-flow { margin-top: 0; }
7606
- .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; }
7607
8009
  .dxr-surface .docx-hf-chrome {
7608
8010
  display: flex;
7609
8011
  gap: 8px;
@@ -7658,13 +8060,15 @@ var DocxodusEditor = (() => {
7658
8060
  text-transform: none;
7659
8061
  }
7660
8062
  .dxr-surface .docx-hf-band[data-hf-inherited] { border-style: dashed; }
7661
- .dxr[data-chrome] .dxr-surface[data-view="continuous"]:has(.docx-body-flow) {
7662
- padding: 0;
7663
- background: transparent;
7664
- box-shadow: none;
7665
- }
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. */
7666
8068
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
7667
- padding: 56px 72px;
8069
+ max-width: 100%;
8070
+ margin: 0 auto;
8071
+ padding: 56px 0;
7668
8072
  border-radius: 3px;
7669
8073
  background: var(--dxr-sheet);
7670
8074
  box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
@@ -7743,9 +8147,11 @@ var DocxodusEditor = (() => {
7743
8147
  .dxr[data-chrome="compact"] .dxr-note { display: none; }
7744
8148
  .dxr[data-chrome="compact"] .dxr-rail { display: none; }
7745
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. */
7746
8153
  .dxr[data-chrome="compact"] .dxr-surface { margin: 12px auto; padding: 0 10px 64px; }
7747
- .dxr[data-chrome="compact"] .dxr-surface[data-view="continuous"] { padding: 22px 18px; }
7748
- .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; }
7749
8155
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-band { padding: 9px 18px 13px; }
7750
8156
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-chrome,
7751
8157
  .dxr[data-chrome="compact"] .dxr-surface .docx-hf-warning { margin-left: 0; }
@@ -8495,6 +8901,8 @@ var DocxodusEditor = (() => {
8495
8901
  fabricateClasses: this.options.fabricateClasses,
8496
8902
  editable: this.options.editable,
8497
8903
  scale: this.options.scale,
8904
+ columnWidth: this.options.columnWidth,
8905
+ fitToWidth: this.options.fitToWidth,
8498
8906
  onEdit: this.options.onEdit,
8499
8907
  onMove: this.options.onMove,
8500
8908
  paginated,