ngx-kit 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -132,6 +132,54 @@ function resolveLogicalDirection(dir, rtl) {
132
132
 
133
133
  /** Above any app chrome (modals, sticky headers) but below the browser's own UI. */
134
134
  const PREVIEW_Z_INDEX = '2147483000';
135
+ /**
136
+ * The clone lives in <body>, so rules that depend on its ORIGINAL ancestors (`.palette .tile`,
137
+ * `.board { font: ... }`, `[dir=rtl] .x`) no longer match it. These are the properties that make
138
+ * the difference between "looks like the block I grabbed" and "looks like an unstyled <div>":
139
+ * the inherited text properties plus the root box's own visual properties. Only the ROOT is
140
+ * handled (one `getComputedStyle` at drag start); use `*ngxDragPreview` for a bespoke preview.
141
+ */
142
+ const PREVIEW_STYLE_PROPS = [
143
+ // inherited
144
+ 'direction',
145
+ 'color',
146
+ 'font-family',
147
+ 'font-size',
148
+ 'font-weight',
149
+ 'font-style',
150
+ 'line-height',
151
+ 'letter-spacing',
152
+ 'text-align',
153
+ 'text-transform',
154
+ 'white-space',
155
+ // the root box
156
+ 'background-color',
157
+ 'background-image',
158
+ 'background-size',
159
+ 'background-position',
160
+ 'background-repeat',
161
+ 'border-top',
162
+ 'border-right',
163
+ 'border-bottom',
164
+ 'border-left',
165
+ 'border-radius',
166
+ 'box-shadow',
167
+ 'padding-top',
168
+ 'padding-right',
169
+ 'padding-bottom',
170
+ 'padding-left',
171
+ 'opacity',
172
+ ];
173
+ function copyVisualStyles(source, target) {
174
+ const cs = source.ownerDocument.defaultView?.getComputedStyle(source);
175
+ if (!cs)
176
+ return;
177
+ for (const prop of PREVIEW_STYLE_PROPS) {
178
+ const value = cs.getPropertyValue(prop);
179
+ if (value)
180
+ target.style.setProperty(prop, value);
181
+ }
182
+ }
135
183
  class DragPreviewRef {
136
184
  tpl;
137
185
  /** Set by `ngxDragPreview` so the embedded view can join Angular's change-detection tree. */
@@ -163,6 +211,7 @@ class DragPreviewRef {
163
211
  // Avoid duplicated ids in the document.
164
212
  this.element.removeAttribute('id');
165
213
  this.element.querySelectorAll('[id]').forEach((n) => n.removeAttribute('id'));
214
+ copyVisualStyles(dragEl, this.element);
166
215
  this.element.classList.add('ngx-drag-preview');
167
216
  this.element.setAttribute('aria-hidden', 'true');
168
217
  this.element.setAttribute('inert', '');
@@ -214,21 +263,56 @@ class DragPreviewRef {
214
263
  }
215
264
 
216
265
  function viewportDeltaToLocal(element, delta, includeElementTransform = true) {
217
- const matrix = getLocalToViewportMatrix(element, includeElementTransform);
266
+ const inverse = invertLinear(getLocalToViewportMatrix(element, includeElementTransform));
267
+ return applyLinear(inverse, delta);
268
+ }
269
+ /**
270
+ * Inverse of a 2D linear transform, or `null` when it is not invertible (zero-scale ancestor).
271
+ * Exposed so hot paths (every pointer move) can compute the matrix ONCE per drag instead of
272
+ * walking the ancestor chain with `getComputedStyle` on every event.
273
+ */
274
+ function invertLinear(matrix) {
218
275
  const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
219
- if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-8) {
220
- // A zero-scale ancestor has no invertible coordinate space. Avoid NaN/Infinity.
276
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-8)
277
+ return null;
278
+ return {
279
+ a: matrix.d / determinant,
280
+ b: -matrix.b / determinant,
281
+ c: -matrix.c / determinant,
282
+ d: matrix.a / determinant,
283
+ };
284
+ }
285
+ /** Applies a linear transform to a vector. A `null` matrix (see `invertLinear`) yields `{0,0}`. */
286
+ function applyLinear(matrix, delta) {
287
+ if (!matrix)
221
288
  return { x: 0, y: 0 };
222
- }
223
- const inverseA = matrix.d / determinant;
224
- const inverseB = -matrix.b / determinant;
225
- const inverseC = -matrix.c / determinant;
226
- const inverseD = matrix.a / determinant;
227
289
  return {
228
- x: inverseA * delta.x + inverseC * delta.y,
229
- y: inverseB * delta.x + inverseD * delta.y,
290
+ x: matrix.a * delta.x + matrix.c * delta.y,
291
+ y: matrix.b * delta.x + matrix.d * delta.y,
230
292
  };
231
293
  }
294
+ /**
295
+ * Converts a SIZE measured in the viewport pixels of `fromDocument` into the viewport pixels of
296
+ * `toDocument` (they differ when the iframe element is scaled, e.g. a zoomed-out device preview).
297
+ * Returns `null` when the documents are unrelated / cross-origin.
298
+ */
299
+ function convertSizeBetweenDocuments(size, fromDocument, toDocument) {
300
+ if (fromDocument === toDocument)
301
+ return { ...size };
302
+ const a = convertPointBetweenDocuments({ x: 0, y: 0 }, fromDocument, toDocument);
303
+ const b = convertPointBetweenDocuments({ x: size.x, y: size.y }, fromDocument, toDocument);
304
+ if (!a || !b)
305
+ return null;
306
+ return { x: b.x - a.x, y: b.y - a.y };
307
+ }
308
+ /**
309
+ * Axis scale (always positive) between `element`'s local CSS pixels and viewport pixels,
310
+ * i.e. how many screen pixels one CSS pixel inside `element` occupies. Rotation is ignored.
311
+ */
312
+ function getLocalScale(element) {
313
+ const m = getLocalToViewportMatrix(element);
314
+ return { x: Math.hypot(m.a, m.b) || 1, y: Math.hypot(m.c, m.d) || 1 };
315
+ }
232
316
  function localDeltaToViewport(element, delta) {
233
317
  const matrix = getLocalToViewportMatrix(element);
234
318
  return {
@@ -398,6 +482,14 @@ class DragRef {
398
482
  lastEffective = { x: 0, y: 0 };
399
483
  scrollListener;
400
484
  resizeListener;
485
+ /**
486
+ * Every document that hosts a candidate list (the source document, plus any same-origin iframe
487
+ * such as a page-builder canvas) needs its own scroll/resize listeners: `scroll` does not cross
488
+ * the iframe boundary, so scrolling the canvas never reaches a listener on the parent page.
489
+ */
490
+ documentListeners = new Map();
491
+ /** viewport → local matrix of the dragged element, computed once per free drag. */
492
+ freeDragInverse;
401
493
  init() {
402
494
  this.previousTransform =
403
495
  this.el.ownerDocument.defaultView?.getComputedStyle(this.el).transform ?? '';
@@ -426,6 +518,7 @@ class DragRef {
426
518
  this.moveDx = 0;
427
519
  this.moveDy = 0;
428
520
  this.scrollCompensation = { x: 0, y: 0 };
521
+ this.freeDragInverse = undefined;
429
522
  // Only draggables that are direct children of their drop list take part in sorting.
430
523
  // Everything else is a plain free drag.
431
524
  const list = this.dropList && this.dropList.el === this.el.parentElement ? this.dropList : null;
@@ -617,7 +710,11 @@ class DragRef {
617
710
  : '';
618
711
  const tx = this.moveDx + this.scrollCompensation.x;
619
712
  const ty = this.moveDy + this.scrollCompensation.y;
620
- const local = viewportDeltaToLocal(this.el, { x: tx, y: ty });
713
+ if (this.freeDragInverse === undefined) {
714
+ // Includes the element's own (pre-drag) transform: the translate is appended after it.
715
+ this.freeDragInverse = invertLinear(getLocalToViewportMatrix(this.el, true));
716
+ }
717
+ const local = applyLinear(this.freeDragInverse, { x: tx, y: ty });
621
718
  this.el.style.transform = `${base}translate3d(${local.x}px, ${local.y}px, 0)`;
622
719
  }
623
720
  // -------------------------------------------------------------------------------------
@@ -646,9 +743,40 @@ class DragRef {
646
743
  l._refresh();
647
744
  this.updateDropTarget(this.lastEffective);
648
745
  };
649
- // Capture: `scroll` does not bubble, and any ancestor (or the page) may be the one scrolling.
650
- doc.addEventListener('scroll', this.scrollListener, { capture: true, passive: true });
651
- win.addEventListener('resize', this.resizeListener, { passive: true });
746
+ this.syncDocumentListeners();
747
+ }
748
+ /**
749
+ * Keeps scroll/resize listeners attached to every document that currently hosts a candidate
750
+ * list. Capture: `scroll` does not bubble, and any ancestor (or the page) may be scrolling.
751
+ */
752
+ syncDocumentListeners() {
753
+ if (!this.scrollListener || !this.resizeListener)
754
+ return;
755
+ const scroll = this.scrollListener;
756
+ const resize = this.resizeListener;
757
+ const wanted = new Set([this.el.ownerDocument]);
758
+ for (const list of this.cachedLists) {
759
+ const doc = list.el?.ownerDocument;
760
+ if (doc)
761
+ wanted.add(doc);
762
+ }
763
+ for (const [doc, remove] of this.documentListeners) {
764
+ if (!wanted.has(doc)) {
765
+ remove();
766
+ this.documentListeners.delete(doc);
767
+ }
768
+ }
769
+ for (const doc of wanted) {
770
+ const win = doc.defaultView;
771
+ if (!win || this.documentListeners.has(doc))
772
+ continue;
773
+ doc.addEventListener('scroll', scroll, { capture: true, passive: true });
774
+ win.addEventListener('resize', resize, { passive: true });
775
+ this.documentListeners.set(doc, () => {
776
+ doc.removeEventListener('scroll', scroll, { capture: true });
777
+ win.removeEventListener('resize', resize);
778
+ });
779
+ }
652
780
  }
653
781
  /**
654
782
  * Recomputes which registered lists are valid targets for this drag — connected to the
@@ -677,6 +805,7 @@ class DragRef {
677
805
  }
678
806
  this.dropCandidates = next;
679
807
  this.cachedLists = next.map((c) => c.list);
808
+ this.syncDocumentListeners();
680
809
  }
681
810
  /**
682
811
  * Decide which list is under the pointer, switch if needed, then re-sort in the active one.
@@ -751,11 +880,9 @@ class DragRef {
751
880
  * regardless of how the drag ended. Only then is the drop event emitted.
752
881
  */
753
882
  finishListDrag(commit) {
754
- const doc = this.el.ownerDocument;
755
- if (this.scrollListener)
756
- doc.removeEventListener('scroll', this.scrollListener, { capture: true });
757
- if (this.resizeListener)
758
- doc.defaultView?.removeEventListener('resize', this.resizeListener);
883
+ for (const remove of this.documentListeners.values())
884
+ remove();
885
+ this.documentListeners.clear();
759
886
  this.scrollListener = this.resizeListener = undefined;
760
887
  const target = commit ? this.activeDropList : null;
761
888
  const event = target?._finish(this, true) ?? null;
@@ -1284,6 +1411,13 @@ class SortSession {
1284
1411
  /** undefined = not created yet, null = this layout has no whitespace separators. */
1285
1412
  spacer;
1286
1413
  onOffsetChange;
1414
+ /**
1415
+ * viewport → local-CSS-pixel matrix of the container's coordinate space. Every slot is a direct
1416
+ * child of the container, so ONE matrix serves all of them. It is computed lazily and kept for
1417
+ * the whole session: walking the ancestor chain with `getComputedStyle` on every displacement
1418
+ * would put style reads on the hot path (see the "zero style reads per move" test).
1419
+ */
1420
+ inverse;
1287
1421
  constructor(init) {
1288
1422
  this.spacer = init.spacer;
1289
1423
  this.onOffsetChange = init.onOffsetChange;
@@ -1356,6 +1490,7 @@ class SortSession {
1356
1490
  return;
1357
1491
  const target = this.targetIdx;
1358
1492
  this.clearDisplacement();
1493
+ this.inverse = undefined; // the viewport may have changed the ancestors' transforms
1359
1494
  this.snapshot();
1360
1495
  this.frame = new ScrollFrame(this.container, true);
1361
1496
  if (this.mode === 'transform') {
@@ -1455,11 +1590,17 @@ class SortSession {
1455
1590
  }
1456
1591
  else {
1457
1592
  this.applied.set(e.el, off);
1458
- const local = viewportDeltaToLocal(e.el, off, false);
1593
+ const local = this.toLocal(off);
1459
1594
  e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1460
1595
  }
1461
1596
  this.onOffsetChange?.(e.el, off);
1462
1597
  }
1598
+ toLocal(delta) {
1599
+ if (this.inverse === undefined) {
1600
+ this.inverse = invertLinear(getLocalToViewportMatrix(this.container, true));
1601
+ }
1602
+ return applyLinear(this.inverse, delta);
1603
+ }
1463
1604
  // ---------------------------------------------------------------------------------------
1464
1605
  // flow strategy (single placeholder move + FLIP transforms)
1465
1606
  // ---------------------------------------------------------------------------------------
@@ -1481,7 +1622,7 @@ class SortSession {
1481
1622
  const dx = first[i].left - last[i].left;
1482
1623
  const dy = first[i].top - last[i].top;
1483
1624
  if (this.animationMs > 0 && (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)) {
1484
- const local = viewportDeltaToLocal(e.el, { x: dx, y: dy }, false);
1625
+ const local = this.toLocal({ x: dx, y: dy });
1485
1626
  e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1486
1627
  moved.push(e);
1487
1628
  }
@@ -1551,6 +1692,14 @@ class DropListRef {
1551
1692
  dropListGroup;
1552
1693
  /** Duration (ms) of the sibling slide animation. `0` disables animation. */
1553
1694
  sortAnimationDuration = 200;
1695
+ /**
1696
+ * How the auto-generated placeholder is sized.
1697
+ * - `'item'` (default): exactly the dragged item's footprint.
1698
+ * - `'stretch'`: only the MAIN axis follows the item; the cross axis fills the list. This is
1699
+ * what a page-builder canvas wants when a small palette tile is dragged into a full-width
1700
+ * column of blocks (the tile's own width means nothing there).
1701
+ */
1702
+ placeholderFit = 'item';
1554
1703
  _draggables = new Set();
1555
1704
  onDrop = new EventEmitter();
1556
1705
  /**
@@ -2012,14 +2161,35 @@ class DropListRef {
2012
2161
  if (ph.custom)
2013
2162
  return;
2014
2163
  placeholder.style.display = drag.originalDisplay;
2164
+ // `rect` is in the VIEWPORT pixels of the dragged item's document. CSS sizes are local to the
2165
+ // list: convert across an (optionally scaled) iframe boundary first, then through the
2166
+ // list's own scaled/zoomed ancestors. Without this a placeholder inside a `scale(.5)` canvas
2167
+ // would be drawn at half the size it reserves.
2168
+ const inTarget = convertSizeBetweenDocuments({ x: rect.width, y: rect.height }, drag.el.ownerDocument, this.el.ownerDocument) ?? { x: rect.width, y: rect.height };
2169
+ const scale = getLocalScale(this.el);
2170
+ const width = Math.abs(inTarget.x) / scale.x;
2171
+ const height = Math.abs(inTarget.y) / scale.y;
2015
2172
  // Occupies exactly the dragged item's footprint, so the gap reserved in the list — and the
2016
2173
  // sibling-displacement math derived from it — matches the item's real size.
2017
- set('width', `${rect.width}px`);
2018
- set('height', `${rect.height}px`);
2019
- // set('min-width', `${rect.width}px`);
2020
- // set('min-height', `${rect.height}px`);
2021
- // set('max-width', `${rect.width}px`);
2022
- // set('max-height', `${rect.height}px`);
2174
+ const axis = this.placeholderFit === 'stretch' ? fallbackAxis(this.el) : null;
2175
+ if (axis === 'y') {
2176
+ // vertical stack: the width is whatever the list gives its children
2177
+ set('width', 'auto');
2178
+ set('max-width', 'none');
2179
+ if (placeholder.style.display === 'inline' || placeholder.style.display === 'inline-block') {
2180
+ placeholder.style.display = 'block';
2181
+ }
2182
+ }
2183
+ else {
2184
+ set('width', `${width}px`);
2185
+ }
2186
+ if (axis === 'x') {
2187
+ set('height', 'auto');
2188
+ set('max-height', 'none');
2189
+ }
2190
+ else {
2191
+ set('height', `${height}px`);
2192
+ }
2023
2193
  set('visibility', 'visible');
2024
2194
  set('opacity', '0.16');
2025
2195
  set('pointer-events', 'none');
@@ -2047,6 +2217,24 @@ function measureHit(el) {
2047
2217
  return null;
2048
2218
  const clips = [];
2049
2219
  const root = el.ownerDocument.documentElement;
2220
+ // The visible window of the document itself. Matters for a list inside an <iframe>: a tall
2221
+ // canvas extends far below the iframe's viewport, and a pointer that is already outside the
2222
+ // frame (toolbar, footer) must not count as "over the list".
2223
+ const ownerWin = el.ownerDocument.defaultView;
2224
+ if (ownerWin?.frameElement) {
2225
+ // In quirks mode (an iframe written without <!doctype>) `documentElement.clientHeight` is the
2226
+ // height of the CONTENT, and the viewport size lives on <body> instead.
2227
+ const sizer = el.ownerDocument.compatMode === 'CSS1Compat' ? root : (el.ownerDocument.body ?? root);
2228
+ clips.push({
2229
+ box: {
2230
+ left: 0,
2231
+ top: 0,
2232
+ right: sizer.clientWidth || ownerWin.innerWidth,
2233
+ bottom: sizer.clientHeight || ownerWin.innerHeight,
2234
+ },
2235
+ frame: new ScrollFrame(null, false, null), // the window clip never moves with scrolling
2236
+ });
2237
+ }
2050
2238
  for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
2051
2239
  const s = node.ownerDocument.defaultView?.getComputedStyle(node);
2052
2240
  if (!s)
@@ -2250,6 +2438,8 @@ class AutoScroller {
2250
2438
  raf = 0;
2251
2439
  container;
2252
2440
  win;
2441
+ /** Document the pointer coordinates (`update(x, y)`) are expressed in. */
2442
+ pointerDoc;
2253
2443
  speed = { x: 0, y: 0 };
2254
2444
  onTick;
2255
2445
  edge;
@@ -2261,6 +2451,7 @@ class AutoScroller {
2261
2451
  /** onTick اجرا می‌شود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder به‌روز شود. */
2262
2452
  start(fromEl, onTick) {
2263
2453
  this.win = fromEl.ownerDocument.defaultView ?? undefined;
2454
+ this.pointerDoc = fromEl.ownerDocument;
2264
2455
  this.container = findScrollableAncestor(fromEl);
2265
2456
  this.onTick = onTick;
2266
2457
  }
@@ -2269,10 +2460,24 @@ class AutoScroller {
2269
2460
  this.container = findScrollableAncestor(fromEl);
2270
2461
  }
2271
2462
  update(clientX, clientY) {
2463
+ // The scroll container may live in another document (a page-builder iframe). Its rect is in
2464
+ // THAT document's viewport pixels, so bring the pointer into the same space first.
2465
+ let x = clientX;
2466
+ let y = clientY;
2467
+ const containerDoc = documentOf(this.container);
2468
+ if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
2469
+ const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
2470
+ if (!p) {
2471
+ this.speed = { x: 0, y: 0 };
2472
+ return;
2473
+ }
2474
+ x = p.x;
2475
+ y = p.y;
2476
+ }
2272
2477
  const rect = getScrollRect(this.container);
2273
2478
  this.speed = {
2274
- x: edgeSpeed(clientX, rect.left, rect.right, this.edge, this.maxSpeed),
2275
- y: edgeSpeed(clientY, rect.top, rect.bottom, this.edge, this.maxSpeed),
2479
+ x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
2480
+ y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
2276
2481
  };
2277
2482
  if ((this.speed.x || this.speed.y) && !this.raf) {
2278
2483
  this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
@@ -2285,6 +2490,7 @@ class AutoScroller {
2285
2490
  this.speed = { x: 0, y: 0 };
2286
2491
  this.onTick = undefined;
2287
2492
  this.win = undefined;
2493
+ this.pointerDoc = undefined;
2288
2494
  }
2289
2495
  tick = () => {
2290
2496
  if (!this.speed.x && !this.speed.y) {
@@ -2318,7 +2524,21 @@ function getScrollRect(container) {
2318
2524
  if (isWindow(container)) {
2319
2525
  return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
2320
2526
  }
2321
- return container.getBoundingClientRect();
2527
+ // A scroller taller than the screen (or partly outside it) is only "edge-sensitive" at the
2528
+ // edges the user can actually see: clip its rect to the document viewport.
2529
+ const r = container.getBoundingClientRect();
2530
+ const win = container.ownerDocument.defaultView;
2531
+ if (!win)
2532
+ return r;
2533
+ return {
2534
+ left: Math.max(r.left, 0),
2535
+ top: Math.max(r.top, 0),
2536
+ right: Math.min(r.right, win.innerWidth),
2537
+ bottom: Math.min(r.bottom, win.innerHeight),
2538
+ };
2539
+ }
2540
+ function documentOf(container) {
2541
+ return isWindow(container) ? container.document : container.ownerDocument;
2322
2542
  }
2323
2543
  function findScrollableAncestor(el) {
2324
2544
  let node = el;
@@ -2328,7 +2548,12 @@ function findScrollableAncestor(el) {
2328
2548
  break;
2329
2549
  const style = win.getComputedStyle(node);
2330
2550
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2331
- if (scrollable && node.scrollHeight > node.clientHeight) {
2551
+ if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2552
+ // <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
2553
+ // edge detection must use the window's viewport instead.
2554
+ const doc = node.ownerDocument;
2555
+ if (node === doc.documentElement || node === doc.body)
2556
+ return win;
2332
2557
  return node;
2333
2558
  }
2334
2559
  node = node.parentElement;
@@ -2403,6 +2628,19 @@ class NgxDraggable {
2403
2628
  /** Auto-scroll the nearest scrollable ancestor while dragging near its edge. */
2404
2629
  autoScroll = input(true, /* @ts-ignore */
2405
2630
  ...(ngDevMode ? [{ debugName: "autoScroll" }] : /* istanbul ignore next */ []));
2631
+ /**
2632
+ * Presses that start inside an element matching this selector never start a drag (the element
2633
+ * stays fully usable: text caret, clicks, selection). Handy for editable regions of a block.
2634
+ */
2635
+ noDragSelector = input('[data-no-drag]', /* @ts-ignore */
2636
+ ...(ngDevMode ? [{ debugName: "noDragSelector" }] : /* istanbul ignore next */ []));
2637
+ /**
2638
+ * When `true`, presses on native interactive controls (`button, a, input, textarea, select`,
2639
+ * `[contenteditable]`) do not start a drag either. Off by default so that a page-builder canvas
2640
+ * block can be grabbed by its own button/link, which is exactly what you want while editing.
2641
+ */
2642
+ ignoreInteractive = input(false, /* @ts-ignore */
2643
+ ...(ngDevMode ? [{ debugName: "ignoreInteractive" }] : /* istanbul ignore next */ []));
2406
2644
  set data(v) {
2407
2645
  this._ref.data = v;
2408
2646
  }
@@ -2431,6 +2669,8 @@ class NgxDraggable {
2431
2669
  lastWindowScroll = { left: 0, top: 0 };
2432
2670
  removeContainerScroll;
2433
2671
  removeWindowScroll;
2672
+ removeDocKeydown;
2673
+ keyboardDrag = false;
2434
2674
  renderer = inject(Renderer2);
2435
2675
  constructor(host) {
2436
2676
  this.host = host;
@@ -2474,6 +2714,7 @@ class NgxDraggable {
2474
2714
  this.removeEscape?.();
2475
2715
  this.removeCancel?.();
2476
2716
  this.removeLostCapture?.();
2717
+ this.removeDocKeydown?.();
2477
2718
  this.scroller.stop();
2478
2719
  this.stopScrollTracking();
2479
2720
  // Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
@@ -2484,7 +2725,7 @@ class NgxDraggable {
2484
2725
  pointerDown(e) {
2485
2726
  if (this.disabled() ||
2486
2727
  e.button !== 0 ||
2487
- // this.isInteractive(e.target) ||
2728
+ this.isExcluded(e.target) ||
2488
2729
  !this.isOnHandle(e.target))
2489
2730
  return;
2490
2731
  // Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
@@ -2494,15 +2735,27 @@ class NgxDraggable {
2494
2735
  claimedPointerEvents.add(e);
2495
2736
  e.preventDefault();
2496
2737
  this.down = true;
2738
+ this.keyboardDrag = false;
2497
2739
  this.pointerId = e.pointerId;
2498
- this._ref.el.setPointerCapture(this.pointerId);
2740
+ // Pointer capture keeps move/up events flowing to this document even while the pointer is
2741
+ // over an <iframe> (the page-builder canvas). It can throw (pointer already gone, synthetic
2742
+ // events from automation tools) — in that case we simply fall back to plain document events.
2743
+ try {
2744
+ this._ref.el.setPointerCapture(this.pointerId);
2745
+ }
2746
+ catch {
2747
+ /* not capturable: document level listeners below still work inside this document */
2748
+ }
2499
2749
  this.start = { x: e.clientX, y: e.clientY };
2500
2750
  this._ref.boundary = this.boundary();
2501
2751
  this._ref.lockAxis = this.lockAxis();
2502
2752
  this._ref.pointerDown(this.start);
2503
- this.removeMove = this.renderer.listen(this._ref.el, 'pointermove', (ev) => this.pointerMove(ev));
2504
- this.removeUp = this.renderer.listen(this._ref.el, 'pointerup', (ev) => this.pointerUp(ev));
2505
2753
  const doc = this._ref.el.ownerDocument;
2754
+ // Listen on the DOCUMENT (captured events bubble up to it): this keeps working when the
2755
+ // dragged element is re-rendered / detached by the framework mid-drag, and it is the same
2756
+ // for real captured events and for events dispatched by test tooling.
2757
+ this.removeMove = this.renderer.listen(doc, 'pointermove', (ev) => this.pointerMove(ev));
2758
+ this.removeUp = this.renderer.listen(doc, 'pointerup', (ev) => this.pointerUp(ev));
2506
2759
  this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
2507
2760
  if (ev.key === 'Escape' && this.dragging())
2508
2761
  this.cancel();
@@ -2587,9 +2840,7 @@ class NgxDraggable {
2587
2840
  pointerUp(e) {
2588
2841
  if (!this.down || e.pointerId !== this.pointerId)
2589
2842
  return;
2590
- if (this._ref.el.hasPointerCapture(this.pointerId)) {
2591
- this._ref.el.releasePointerCapture(this.pointerId);
2592
- }
2843
+ this.releaseCapture();
2593
2844
  if (this.dragging()) {
2594
2845
  // Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
2595
2846
  // The release position is authoritative for list sorting (a fast flick may end with
@@ -2601,6 +2852,9 @@ class NgxDraggable {
2601
2852
  // endDrag must run for free drags too, otherwise their lifecycle never completes.
2602
2853
  this._ref.endDrag();
2603
2854
  this.service.end(this._ref);
2855
+ // The browser still fires a `click` on the captured element after the button is released.
2856
+ // After a real drag that click must not select the block / follow its link.
2857
+ this.swallowNextClick();
2604
2858
  }
2605
2859
  this.finish();
2606
2860
  this.dragEnd.emit(this._ref.pointer);
@@ -2615,15 +2869,27 @@ class NgxDraggable {
2615
2869
  }
2616
2870
  this.pointerUp(e);
2617
2871
  }
2872
+ releaseCapture() {
2873
+ if (this.pointerId < 0)
2874
+ return;
2875
+ try {
2876
+ if (this._ref.el.hasPointerCapture?.(this.pointerId)) {
2877
+ this._ref.el.releasePointerCapture(this.pointerId);
2878
+ }
2879
+ }
2880
+ catch {
2881
+ /* element detached or pointer already released */
2882
+ }
2883
+ }
2618
2884
  cancel() {
2619
2885
  this._ref.cancelDrag();
2620
2886
  this.service.end(this._ref);
2621
2887
  this.finish();
2622
2888
  }
2623
2889
  finish() {
2624
- if (this.pointerId >= 0 && this._ref.el.hasPointerCapture?.(this.pointerId)) {
2625
- this._ref.el.releasePointerCapture(this.pointerId);
2626
- }
2890
+ this.releaseCapture();
2891
+ this.removeDocKeydown?.();
2892
+ this.removeDocKeydown = undefined;
2627
2893
  this.dragging.set(false);
2628
2894
  this.down = false;
2629
2895
  this.scroller.stop();
@@ -2636,10 +2902,20 @@ class NgxDraggable {
2636
2902
  this.removeMove = this.removeUp = this.removeEscape = undefined;
2637
2903
  this.removeCancel = this.removeLostCapture = undefined;
2638
2904
  }
2905
+ /** Keys already processed (the host listener and the document listener may both see one). */
2906
+ handledKeys = new WeakSet();
2639
2907
  /** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
2640
2908
  keyDown(e) {
2641
- if (this.disabled())
2909
+ if (this.disabled() || this.handledKeys.has(e))
2910
+ return;
2911
+ // Only the draggable ITSELF being focused means "move me". Arrow keys typed into an input,
2912
+ // a textarea or a contenteditable region inside the block belong to that control (caret
2913
+ // movement), and must never start a drag or be swallowed.
2914
+ if (e.target !== this.host.nativeElement && e.target !== this._ref.el && !this.dragging())
2915
+ return;
2916
+ if (this.dragging() && isEditableTarget(e.target))
2642
2917
  return;
2918
+ this.handledKeys.add(e);
2643
2919
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2644
2920
  const deltas = {
2645
2921
  ArrowLeft: { x: -step, y: 0 },
@@ -2665,6 +2941,7 @@ class NgxDraggable {
2665
2941
  this.dragging.set(true);
2666
2942
  this.service.begin(this._ref);
2667
2943
  this.startScrollTracking();
2944
+ this.beginKeyboardSession();
2668
2945
  this.dragStart.emit(origin);
2669
2946
  }
2670
2947
  this._ref.nudge(delta.x, delta.y);
@@ -2677,26 +2954,62 @@ class NgxDraggable {
2677
2954
  this.service.end(this._ref);
2678
2955
  this.dragEnd.emit(this._ref.pointer);
2679
2956
  this.finish();
2957
+ this.restoreFocus();
2680
2958
  }
2681
2959
  else if (e.key === 'Escape' && this.dragging()) {
2682
2960
  e.preventDefault();
2683
2961
  this._ref.cancelDrag();
2684
2962
  this.service.end(this._ref);
2685
2963
  this.finish();
2964
+ this.restoreFocus();
2686
2965
  }
2687
2966
  }
2967
+ /**
2968
+ * A list item is `display:none` while it is being moved, so the browser drops its focus and
2969
+ * the following arrow/Enter/Escape presses would go to <body>. Listen on the document for the
2970
+ * duration of a keyboard drag so the gesture keeps working.
2971
+ */
2972
+ beginKeyboardSession() {
2973
+ this.keyboardDrag = true;
2974
+ this.removeDocKeydown?.();
2975
+ this.removeDocKeydown = this.renderer.listen(this._ref.el.ownerDocument, 'keydown', (ev) => this.keyDown(ev));
2976
+ }
2977
+ restoreFocus() {
2978
+ this.removeDocKeydown?.();
2979
+ this.removeDocKeydown = undefined;
2980
+ if (this.keyboardDrag) {
2981
+ this.keyboardDrag = false;
2982
+ this._ref.el.focus?.({ preventScroll: true });
2983
+ }
2984
+ }
2985
+ /** Swallow the single `click` the browser dispatches right after a completed drag. */
2986
+ swallowNextClick() {
2987
+ const doc = this._ref.el.ownerDocument;
2988
+ const stop = (ev) => {
2989
+ ev.stopPropagation();
2990
+ ev.preventDefault();
2991
+ };
2992
+ doc.addEventListener('click', stop, { capture: true, once: true });
2993
+ // If no click follows (pointer released elsewhere), do not eat the user's NEXT real click.
2994
+ doc.defaultView?.setTimeout(() => doc.removeEventListener('click', stop, { capture: true }), 0);
2995
+ }
2688
2996
  isOnHandle(target) {
2689
2997
  if (!this.dragHandle())
2690
2998
  return true;
2691
2999
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2692
3000
  return isElementTarget(target) && !!target.closest(this.dragHandle());
2693
3001
  }
2694
- isInteractive(target) {
2695
- // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2696
- return (isElementTarget(target) && !!target.closest('button,a,input,textarea,select,[data-no-drag]'));
3002
+ /** Should a press on `target` be left alone (no drag)? */
3003
+ isExcluded(target) {
3004
+ if (!isElementTarget(target))
3005
+ return false;
3006
+ const selector = this.noDragSelector();
3007
+ if (selector && target.closest(selector))
3008
+ return true;
3009
+ return this.ignoreInteractive() && isEditableOrInteractive(target);
2697
3010
  }
2698
3011
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
2699
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxDraggable, isStandalone: true, selector: "[NgxDraggable],[ngxDraggable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, dragRootElement: { classPropertyName: "dragRootElement", publicName: "dragRootElement", isSignal: true, isRequired: false, transformFunction: null }, dragHandle: { classPropertyName: "dragHandle", publicName: "dragHandle", isSignal: true, isRequired: false, transformFunction: null }, lockAxis: { classPropertyName: "lockAxis", publicName: "lockAxis", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dragRootElement: "dragRootElementChange", dragHandle: "dragHandleChange", dragStart: "dragStart", dragMove: "dragMove", dragEnd: "dragEnd" }, host: { properties: { "style.touch-action": "\"none\"", "attr.tabindex": "disabled() ? null : 0", "attr.aria-grabbed": "dragging()", "class.disable-drag": "disabled()" }, classAttribute: "ngx-draggable" }, providers: [{ provide: NGX_DRAGGABLE, useExisting: NgxDraggable }], queries: [{ propertyName: "customPreview", first: true, predicate: NgxDragPreview, descendants: true }], ngImport: i0 });
3012
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxDraggable, isStandalone: true, selector: "[NgxDraggable],[ngxDraggable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, dragRootElement: { classPropertyName: "dragRootElement", publicName: "dragRootElement", isSignal: true, isRequired: false, transformFunction: null }, dragHandle: { classPropertyName: "dragHandle", publicName: "dragHandle", isSignal: true, isRequired: false, transformFunction: null }, lockAxis: { classPropertyName: "lockAxis", publicName: "lockAxis", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, noDragSelector: { classPropertyName: "noDragSelector", publicName: "noDragSelector", isSignal: true, isRequired: false, transformFunction: null }, ignoreInteractive: { classPropertyName: "ignoreInteractive", publicName: "ignoreInteractive", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dragRootElement: "dragRootElementChange", dragHandle: "dragHandleChange", dragStart: "dragStart", dragMove: "dragMove", dragEnd: "dragEnd" }, host: { properties: { "style.touch-action": "\"none\"", "attr.tabindex": "disabled() ? null : 0", "attr.aria-grabbed": "dragging()", "class.disable-drag": "disabled()" }, classAttribute: "ngx-draggable" }, providers: [{ provide: NGX_DRAGGABLE, useExisting: NgxDraggable }], queries: [{ propertyName: "customPreview", first: true, predicate: NgxDragPreview, descendants: true }], ngImport: i0 });
2700
3013
  }
2701
3014
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
2702
3015
  type: Directive,
@@ -2711,13 +3024,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2711
3024
  '[class.disable-drag]': 'disabled()',
2712
3025
  },
2713
3026
  }]
2714
- }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], dragRootElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragRootElement", required: false }] }, { type: i0.Output, args: ["dragRootElementChange"] }], dragHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragHandle", required: false }] }, { type: i0.Output, args: ["dragHandleChange"] }], lockAxis: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockAxis", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], data: [{
3027
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], dragRootElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragRootElement", required: false }] }, { type: i0.Output, args: ["dragRootElementChange"] }], dragHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragHandle", required: false }] }, { type: i0.Output, args: ["dragHandleChange"] }], lockAxis: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockAxis", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], noDragSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDragSelector", required: false }] }], ignoreInteractive: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreInteractive", required: false }] }], data: [{
2715
3028
  type: Input,
2716
3029
  args: ['data']
2717
3030
  }], customPreview: [{
2718
3031
  type: ContentChild,
2719
3032
  args: [NgxDragPreview]
2720
3033
  }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
3034
+ function isEditableOrInteractive(el) {
3035
+ return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3036
+ }
3037
+ function isEditableTarget(target) {
3038
+ if (!isElementTarget(target))
3039
+ return false;
3040
+ return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3041
+ }
2721
3042
  function isElementTarget(target) {
2722
3043
  return (!!target &&
2723
3044
  typeof target === 'object' &&
@@ -2816,7 +3137,12 @@ class NgxResizable {
2816
3137
  this.beforeTransform = element.style.transform;
2817
3138
  this.resizing.set(true);
2818
3139
  element.style.transition = 'none';
2819
- element.setPointerCapture?.(e.pointerId);
3140
+ try {
3141
+ element.setPointerCapture?.(e.pointerId);
3142
+ }
3143
+ catch {
3144
+ /* synthetic / already-ended pointer: document listeners below still receive the events */
3145
+ }
2820
3146
  const doc = element.ownerDocument;
2821
3147
  this.removeDocumentListeners = [
2822
3148
  this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
@@ -2873,6 +3199,11 @@ class NgxResizable {
2873
3199
  if (this.disabled()) {
2874
3200
  return;
2875
3201
  }
3202
+ // Only when the resizable element itself is focused: arrow keys pressed inside an input /
3203
+ // contenteditable that lives in the block are caret movement, not a resize request.
3204
+ if (e.target !== this.el.nativeElement) {
3205
+ return;
3206
+ }
2876
3207
  const rtl = isRtl(this.el.nativeElement);
2877
3208
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2878
3209
  let dw = 0;
@@ -2896,8 +3227,9 @@ class NgxResizable {
2896
3227
  const rect = this.el.nativeElement.getBoundingClientRect();
2897
3228
  const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
2898
3229
  const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
2899
- this.el.nativeElement.style.width = `${width}px`;
2900
- this.el.nativeElement.style.height = `${height}px`;
3230
+ const styled = this.toStyleSize(width, height);
3231
+ this.el.nativeElement.style.width = `${styled.width}px`;
3232
+ this.el.nativeElement.style.height = `${styled.height}px`;
2901
3233
  const out = {
2902
3234
  width,
2903
3235
  height,
@@ -2967,8 +3299,9 @@ class NgxResizable {
2967
3299
  /**
2968
3300
  * Boundary constraint.
2969
3301
  */
2970
- if (this.boundary) {
2971
- const clamped = clampResizeWithinBoundary(this.boundary()?.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
3302
+ const boundaryEl = this.boundary();
3303
+ if (boundaryEl) {
3304
+ const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
2972
3305
  width = clamped.width / scaleX;
2973
3306
  height = clamped.height / scaleY;
2974
3307
  moveLeft = clamped.left;
@@ -3001,8 +3334,9 @@ class NgxResizable {
3001
3334
  * Changing width/height may cause the browser to re-layout
3002
3335
  * the element, especially for relative/flex/grid/static layouts.
3003
3336
  */
3004
- element.style.width = `${out.width}px`;
3005
- element.style.height = `${out.height}px`;
3337
+ const style = this.toStyleSize(out.width, out.height);
3338
+ element.style.width = `${style.width}px`;
3339
+ element.style.height = `${style.height}px`;
3006
3340
  /**
3007
3341
  * Force/read layout after changing the size.
3008
3342
  */
@@ -3037,9 +3371,29 @@ class NgxResizable {
3037
3371
  element.style.transform =
3038
3372
  `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
3039
3373
  }
3374
+ /**
3375
+ * `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
3376
+ * `width/height` we write are interpreted according to `box-sizing`: for the CSS default
3377
+ * (`content-box`) writing the border-box number would make a padded/bordered block jump by
3378
+ * its padding+border the instant a resize starts. Convert so the box is exactly the size
3379
+ * reported in `IResizableOutput`.
3380
+ */
3381
+ toStyleSize(width, height) {
3382
+ const el = this.el.nativeElement;
3383
+ const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
3384
+ if (!cs || cs.boxSizing === 'border-box')
3385
+ return { width, height };
3386
+ const px = (v) => Number.parseFloat(v) || 0;
3387
+ const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
3388
+ const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
3389
+ return { width: Math.max(0, width - extraX), height: Math.max(0, height - extraY) };
3390
+ }
3040
3391
  clampGrid(value, axis) {
3041
- const grid = typeof this.grid() === 'number' ? this.grid : this.grid()?.[axis];
3042
- if (!grid || grid <= 0) {
3392
+ const g = this.grid();
3393
+ // NOTE: the previous code returned the input *signal function* for a numeric grid, so every
3394
+ // snapped size became NaN and the resize silently stopped working.
3395
+ const grid = typeof g === 'number' ? g : g?.[axis];
3396
+ if (!grid || !Number.isFinite(grid) || grid <= 0) {
3043
3397
  return value;
3044
3398
  }
3045
3399
  return Math.round(value / grid) * grid;
@@ -3052,8 +3406,13 @@ class NgxResizable {
3052
3406
  for (const remove of this.removeDocumentListeners)
3053
3407
  remove();
3054
3408
  this.removeDocumentListeners = [];
3055
- if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3056
- this.el.nativeElement.releasePointerCapture(pointerId);
3409
+ try {
3410
+ if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3411
+ this.el.nativeElement.releasePointerCapture(pointerId);
3412
+ }
3413
+ }
3414
+ catch {
3415
+ /* already released */
3057
3416
  }
3058
3417
  this.beforeTransform = '';
3059
3418
  }
@@ -3194,6 +3553,13 @@ class NgxDropList {
3194
3553
  set disableSort(value) {
3195
3554
  this._ref.disableSort = value === true;
3196
3555
  }
3556
+ /** `'stretch'`: the placeholder only copies the item's size along the list's main axis. */
3557
+ set placeholderFit(value) {
3558
+ this._ref.placeholderFit = value === 'stretch' ? 'stretch' : 'item';
3559
+ }
3560
+ set sortAnimationDuration(value) {
3561
+ this._ref.sortAnimationDuration = value == null ? 200 : Math.max(0, value);
3562
+ }
3197
3563
  drop = new EventEmitter();
3198
3564
  customPlaceholder;
3199
3565
  sub;
@@ -3225,7 +3591,7 @@ class NgxDropList {
3225
3591
  this.service.removeDropList(this._ref);
3226
3592
  }
3227
3593
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
3228
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropList, isStandalone: true, selector: "[NgxDropList],[ngxDropList]", inputs: { data: "data", connectedTo: "connectedTo", disableSort: "disableSort" }, outputs: { drop: "drop" }, host: { classAttribute: "ngx-drop-list" }, providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }], queries: [{ propertyName: "customPlaceholder", first: true, predicate: NgxPlaceholder, descendants: true }], ngImport: i0 });
3594
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropList, isStandalone: true, selector: "[NgxDropList],[ngxDropList]", inputs: { data: "data", connectedTo: "connectedTo", disableSort: "disableSort", placeholderFit: "placeholderFit", sortAnimationDuration: "sortAnimationDuration" }, outputs: { drop: "drop" }, host: { classAttribute: "ngx-drop-list" }, providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }], queries: [{ propertyName: "customPlaceholder", first: true, predicate: NgxPlaceholder, descendants: true }], ngImport: i0 });
3229
3595
  }
3230
3596
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
3231
3597
  type: Directive,
@@ -3241,6 +3607,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3241
3607
  type: Input
3242
3608
  }], disableSort: [{
3243
3609
  type: Input
3610
+ }], placeholderFit: [{
3611
+ type: Input
3612
+ }], sortAnimationDuration: [{
3613
+ type: Input
3244
3614
  }], drop: [{
3245
3615
  type: Output
3246
3616
  }], customPlaceholder: [{
@@ -3313,9 +3683,54 @@ function clamp(v, max) {
3313
3683
  return Math.max(0, Math.min(max, v));
3314
3684
  }
3315
3685
 
3686
+ /**
3687
+ * Helpers for a page builder's "export HTML" step.
3688
+ *
3689
+ * While you edit, the directives decorate the canvas DOM: `ngx-*` classes, `tabindex="0"`,
3690
+ * `aria-grabbed`, `touch-action: none`, and — if an export happens mid-drag — a placeholder or a
3691
+ * preview clone. None of that belongs in the page a visitor receives. Run the canvas through
3692
+ * `getCleanHtml()` (or `stripEditorArtifacts()` on a clone you already have) before saving.
3693
+ */
3694
+ const TRANSIENT_NODES = '.ngx-drag-placeholder, .ngx-drag-preview';
3695
+ /** Mutates `root` (and its subtree) in place. Pass a CLONE of live DOM, never the live canvas. */
3696
+ function stripEditorArtifacts(root, options = {}) {
3697
+ const prefix = options.classPrefix ?? 'ngx-';
3698
+ const dropClasses = new Set(['disable-drag', ...(options.extraClasses ?? [])]);
3699
+ const dropAttrs = ['aria-grabbed', ...(options.extraAttributes ?? [])];
3700
+ root.querySelectorAll(TRANSIENT_NODES).forEach((n) => n.remove());
3701
+ const all = [root, ...Array.from(root.querySelectorAll('*'))];
3702
+ for (const el of all) {
3703
+ // `tabindex="0"` is only ours on draggables (a real focusable of the page keeps its own).
3704
+ if (el.classList.contains('ngx-draggable') && el.getAttribute('tabindex') === '0') {
3705
+ el.removeAttribute('tabindex');
3706
+ }
3707
+ for (const attr of dropAttrs)
3708
+ el.removeAttribute(attr);
3709
+ for (const cls of Array.from(el.classList)) {
3710
+ if (cls.startsWith(prefix) || dropClasses.has(cls))
3711
+ el.classList.remove(cls);
3712
+ }
3713
+ if (!el.classList.length)
3714
+ el.removeAttribute('class');
3715
+ const style = el.style;
3716
+ if (style) {
3717
+ if (style.touchAction === 'none')
3718
+ style.removeProperty('touch-action');
3719
+ if (!el.getAttribute('style')?.trim())
3720
+ el.removeAttribute('style');
3721
+ }
3722
+ }
3723
+ }
3724
+ /** Serialised, editor-free HTML of `el`'s children (the canvas content). */
3725
+ function getCleanHtml(el, options) {
3726
+ const clone = el.cloneNode(true);
3727
+ stripEditorArtifacts(clone, options);
3728
+ return clone.innerHTML;
3729
+ }
3730
+
3316
3731
  /**
3317
3732
  * Generated bundle index. Do not edit.
3318
3733
  */
3319
3734
 
3320
- export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, transferArrayItem };
3735
+ export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, getCleanHtml, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, stripEditorArtifacts, transferArrayItem };
3321
3736
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map