ngx-kit 9.1.0 → 9.2.1

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,7 +2438,10 @@ 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 };
2444
+ carry = { x: 0, y: 0 };
2254
2445
  onTick;
2255
2446
  edge;
2256
2447
  maxSpeed;
@@ -2261,6 +2452,7 @@ class AutoScroller {
2261
2452
  /** onTick اجرا می‌شود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder به‌روز شود. */
2262
2453
  start(fromEl, onTick) {
2263
2454
  this.win = fromEl.ownerDocument.defaultView ?? undefined;
2455
+ this.pointerDoc = fromEl.ownerDocument;
2264
2456
  this.container = findScrollableAncestor(fromEl);
2265
2457
  this.onTick = onTick;
2266
2458
  }
@@ -2269,10 +2461,24 @@ class AutoScroller {
2269
2461
  this.container = findScrollableAncestor(fromEl);
2270
2462
  }
2271
2463
  update(clientX, clientY) {
2464
+ // The scroll container may live in another document (a page-builder iframe). Its rect is in
2465
+ // THAT document's viewport pixels, so bring the pointer into the same space first.
2466
+ let x = clientX;
2467
+ let y = clientY;
2468
+ const containerDoc = documentOf(this.container);
2469
+ if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
2470
+ const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
2471
+ if (!p) {
2472
+ this.speed = { x: 0, y: 0 };
2473
+ return;
2474
+ }
2475
+ x = p.x;
2476
+ y = p.y;
2477
+ }
2272
2478
  const rect = getScrollRect(this.container);
2273
2479
  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),
2480
+ x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
2481
+ y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
2276
2482
  };
2277
2483
  if ((this.speed.x || this.speed.y) && !this.raf) {
2278
2484
  this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
@@ -2283,8 +2489,10 @@ class AutoScroller {
2283
2489
  this.win?.cancelAnimationFrame(this.raf);
2284
2490
  this.raf = 0;
2285
2491
  this.speed = { x: 0, y: 0 };
2492
+ this.carry = { x: 0, y: 0 };
2286
2493
  this.onTick = undefined;
2287
2494
  this.win = undefined;
2495
+ this.pointerDoc = undefined;
2288
2496
  }
2289
2497
  tick = () => {
2290
2498
  if (!this.speed.x && !this.speed.y) {
@@ -2292,14 +2500,21 @@ class AutoScroller {
2292
2500
  return;
2293
2501
  }
2294
2502
  const container = this.container;
2295
- if (isWindow(container)) {
2296
- container.scrollBy(this.speed.x, this.speed.y);
2297
- }
2298
- else {
2299
- // Edge speed is measured in viewport pixels; scrollLeft/Top are local to the scroller.
2300
- const local = viewportDeltaToLocal(container, this.speed);
2301
- container.scrollLeft += local.x;
2302
- container.scrollTop += local.y;
2503
+ // Edge speed is measured in viewport pixels; an element's scrollLeft/Top are local to it.
2504
+ const local = isWindow(container) ? this.speed : viewportDeltaToLocal(container, this.speed);
2505
+ // Browsers snap scroll offsets to whole pixels, so a slow speed (< 1px/frame, i.e. when the
2506
+ // pointer has just entered the edge zone) would never move anything. Carry the remainder.
2507
+ this.carry.x += local.x;
2508
+ this.carry.y += local.y;
2509
+ const dx = Math.trunc(this.carry.x);
2510
+ const dy = Math.trunc(this.carry.y);
2511
+ this.carry.x -= dx;
2512
+ this.carry.y -= dy;
2513
+ if (dx || dy) {
2514
+ // `behavior: 'instant'` matters: with `scroll-behavior: smooth` on <html> or on the scroller
2515
+ // (very common in apps) every per-frame scroll would start a new animation that cancels the
2516
+ // previous one, and the list would not move at all.
2517
+ container.scrollBy({ left: dx, top: dy, behavior: 'instant' });
2303
2518
  }
2304
2519
  // Pointer ثابت مانده ولی محتوا اسکرول شده؛
2305
2520
  // بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
@@ -2318,7 +2533,21 @@ function getScrollRect(container) {
2318
2533
  if (isWindow(container)) {
2319
2534
  return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
2320
2535
  }
2321
- return container.getBoundingClientRect();
2536
+ // A scroller taller than the screen (or partly outside it) is only "edge-sensitive" at the
2537
+ // edges the user can actually see: clip its rect to the document viewport.
2538
+ const r = container.getBoundingClientRect();
2539
+ const win = container.ownerDocument.defaultView;
2540
+ if (!win)
2541
+ return r;
2542
+ return {
2543
+ left: Math.max(r.left, 0),
2544
+ top: Math.max(r.top, 0),
2545
+ right: Math.min(r.right, win.innerWidth),
2546
+ bottom: Math.min(r.bottom, win.innerHeight),
2547
+ };
2548
+ }
2549
+ function documentOf(container) {
2550
+ return isWindow(container) ? container.document : container.ownerDocument;
2322
2551
  }
2323
2552
  function findScrollableAncestor(el) {
2324
2553
  let node = el;
@@ -2328,7 +2557,12 @@ function findScrollableAncestor(el) {
2328
2557
  break;
2329
2558
  const style = win.getComputedStyle(node);
2330
2559
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2331
- if (scrollable && node.scrollHeight > node.clientHeight) {
2560
+ if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2561
+ // <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
2562
+ // edge detection must use the window's viewport instead.
2563
+ const doc = node.ownerDocument;
2564
+ if (node === doc.documentElement || node === doc.body)
2565
+ return win;
2332
2566
  return node;
2333
2567
  }
2334
2568
  node = node.parentElement;
@@ -2403,6 +2637,19 @@ class NgxDraggable {
2403
2637
  /** Auto-scroll the nearest scrollable ancestor while dragging near its edge. */
2404
2638
  autoScroll = input(true, /* @ts-ignore */
2405
2639
  ...(ngDevMode ? [{ debugName: "autoScroll" }] : /* istanbul ignore next */ []));
2640
+ /**
2641
+ * Presses that start inside an element matching this selector never start a drag (the element
2642
+ * stays fully usable: text caret, clicks, selection). Handy for editable regions of a block.
2643
+ */
2644
+ noDragSelector = input('[data-no-drag]', /* @ts-ignore */
2645
+ ...(ngDevMode ? [{ debugName: "noDragSelector" }] : /* istanbul ignore next */ []));
2646
+ /**
2647
+ * When `true`, presses on native interactive controls (`button, a, input, textarea, select`,
2648
+ * `[contenteditable]`) do not start a drag either. Off by default so that a page-builder canvas
2649
+ * block can be grabbed by its own button/link, which is exactly what you want while editing.
2650
+ */
2651
+ ignoreInteractive = input(false, /* @ts-ignore */
2652
+ ...(ngDevMode ? [{ debugName: "ignoreInteractive" }] : /* istanbul ignore next */ []));
2406
2653
  set data(v) {
2407
2654
  this._ref.data = v;
2408
2655
  }
@@ -2431,6 +2678,8 @@ class NgxDraggable {
2431
2678
  lastWindowScroll = { left: 0, top: 0 };
2432
2679
  removeContainerScroll;
2433
2680
  removeWindowScroll;
2681
+ removeDocKeydown;
2682
+ keyboardDrag = false;
2434
2683
  renderer = inject(Renderer2);
2435
2684
  constructor(host) {
2436
2685
  this.host = host;
@@ -2474,6 +2723,7 @@ class NgxDraggable {
2474
2723
  this.removeEscape?.();
2475
2724
  this.removeCancel?.();
2476
2725
  this.removeLostCapture?.();
2726
+ this.removeDocKeydown?.();
2477
2727
  this.scroller.stop();
2478
2728
  this.stopScrollTracking();
2479
2729
  // Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
@@ -2484,7 +2734,7 @@ class NgxDraggable {
2484
2734
  pointerDown(e) {
2485
2735
  if (this.disabled() ||
2486
2736
  e.button !== 0 ||
2487
- // this.isInteractive(e.target) ||
2737
+ this.isExcluded(e.target) ||
2488
2738
  !this.isOnHandle(e.target))
2489
2739
  return;
2490
2740
  // Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
@@ -2494,15 +2744,27 @@ class NgxDraggable {
2494
2744
  claimedPointerEvents.add(e);
2495
2745
  e.preventDefault();
2496
2746
  this.down = true;
2747
+ this.keyboardDrag = false;
2497
2748
  this.pointerId = e.pointerId;
2498
- this._ref.el.setPointerCapture(this.pointerId);
2749
+ // Pointer capture keeps move/up events flowing to this document even while the pointer is
2750
+ // over an <iframe> (the page-builder canvas). It can throw (pointer already gone, synthetic
2751
+ // events from automation tools) — in that case we simply fall back to plain document events.
2752
+ try {
2753
+ this._ref.el.setPointerCapture(this.pointerId);
2754
+ }
2755
+ catch {
2756
+ /* not capturable: document level listeners below still work inside this document */
2757
+ }
2499
2758
  this.start = { x: e.clientX, y: e.clientY };
2500
2759
  this._ref.boundary = this.boundary();
2501
2760
  this._ref.lockAxis = this.lockAxis();
2502
2761
  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
2762
  const doc = this._ref.el.ownerDocument;
2763
+ // Listen on the DOCUMENT (captured events bubble up to it): this keeps working when the
2764
+ // dragged element is re-rendered / detached by the framework mid-drag, and it is the same
2765
+ // for real captured events and for events dispatched by test tooling.
2766
+ this.removeMove = this.renderer.listen(doc, 'pointermove', (ev) => this.pointerMove(ev));
2767
+ this.removeUp = this.renderer.listen(doc, 'pointerup', (ev) => this.pointerUp(ev));
2506
2768
  this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
2507
2769
  if (ev.key === 'Escape' && this.dragging())
2508
2770
  this.cancel();
@@ -2587,9 +2849,7 @@ class NgxDraggable {
2587
2849
  pointerUp(e) {
2588
2850
  if (!this.down || e.pointerId !== this.pointerId)
2589
2851
  return;
2590
- if (this._ref.el.hasPointerCapture(this.pointerId)) {
2591
- this._ref.el.releasePointerCapture(this.pointerId);
2592
- }
2852
+ this.releaseCapture();
2593
2853
  if (this.dragging()) {
2594
2854
  // Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
2595
2855
  // The release position is authoritative for list sorting (a fast flick may end with
@@ -2601,6 +2861,9 @@ class NgxDraggable {
2601
2861
  // endDrag must run for free drags too, otherwise their lifecycle never completes.
2602
2862
  this._ref.endDrag();
2603
2863
  this.service.end(this._ref);
2864
+ // The browser still fires a `click` on the captured element after the button is released.
2865
+ // After a real drag that click must not select the block / follow its link.
2866
+ this.swallowNextClick();
2604
2867
  }
2605
2868
  this.finish();
2606
2869
  this.dragEnd.emit(this._ref.pointer);
@@ -2615,15 +2878,27 @@ class NgxDraggable {
2615
2878
  }
2616
2879
  this.pointerUp(e);
2617
2880
  }
2881
+ releaseCapture() {
2882
+ if (this.pointerId < 0)
2883
+ return;
2884
+ try {
2885
+ if (this._ref.el.hasPointerCapture?.(this.pointerId)) {
2886
+ this._ref.el.releasePointerCapture(this.pointerId);
2887
+ }
2888
+ }
2889
+ catch {
2890
+ /* element detached or pointer already released */
2891
+ }
2892
+ }
2618
2893
  cancel() {
2619
2894
  this._ref.cancelDrag();
2620
2895
  this.service.end(this._ref);
2621
2896
  this.finish();
2622
2897
  }
2623
2898
  finish() {
2624
- if (this.pointerId >= 0 && this._ref.el.hasPointerCapture?.(this.pointerId)) {
2625
- this._ref.el.releasePointerCapture(this.pointerId);
2626
- }
2899
+ this.releaseCapture();
2900
+ this.removeDocKeydown?.();
2901
+ this.removeDocKeydown = undefined;
2627
2902
  this.dragging.set(false);
2628
2903
  this.down = false;
2629
2904
  this.scroller.stop();
@@ -2636,10 +2911,20 @@ class NgxDraggable {
2636
2911
  this.removeMove = this.removeUp = this.removeEscape = undefined;
2637
2912
  this.removeCancel = this.removeLostCapture = undefined;
2638
2913
  }
2914
+ /** Keys already processed (the host listener and the document listener may both see one). */
2915
+ handledKeys = new WeakSet();
2639
2916
  /** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
2640
2917
  keyDown(e) {
2641
- if (this.disabled())
2918
+ if (this.disabled() || this.handledKeys.has(e))
2919
+ return;
2920
+ // Only the draggable ITSELF being focused means "move me". Arrow keys typed into an input,
2921
+ // a textarea or a contenteditable region inside the block belong to that control (caret
2922
+ // movement), and must never start a drag or be swallowed.
2923
+ if (e.target !== this.host.nativeElement && e.target !== this._ref.el && !this.dragging())
2642
2924
  return;
2925
+ if (this.dragging() && isEditableTarget(e.target))
2926
+ return;
2927
+ this.handledKeys.add(e);
2643
2928
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2644
2929
  const deltas = {
2645
2930
  ArrowLeft: { x: -step, y: 0 },
@@ -2665,6 +2950,7 @@ class NgxDraggable {
2665
2950
  this.dragging.set(true);
2666
2951
  this.service.begin(this._ref);
2667
2952
  this.startScrollTracking();
2953
+ this.beginKeyboardSession();
2668
2954
  this.dragStart.emit(origin);
2669
2955
  }
2670
2956
  this._ref.nudge(delta.x, delta.y);
@@ -2677,26 +2963,62 @@ class NgxDraggable {
2677
2963
  this.service.end(this._ref);
2678
2964
  this.dragEnd.emit(this._ref.pointer);
2679
2965
  this.finish();
2966
+ this.restoreFocus();
2680
2967
  }
2681
2968
  else if (e.key === 'Escape' && this.dragging()) {
2682
2969
  e.preventDefault();
2683
2970
  this._ref.cancelDrag();
2684
2971
  this.service.end(this._ref);
2685
2972
  this.finish();
2973
+ this.restoreFocus();
2686
2974
  }
2687
2975
  }
2976
+ /**
2977
+ * A list item is `display:none` while it is being moved, so the browser drops its focus and
2978
+ * the following arrow/Enter/Escape presses would go to <body>. Listen on the document for the
2979
+ * duration of a keyboard drag so the gesture keeps working.
2980
+ */
2981
+ beginKeyboardSession() {
2982
+ this.keyboardDrag = true;
2983
+ this.removeDocKeydown?.();
2984
+ this.removeDocKeydown = this.renderer.listen(this._ref.el.ownerDocument, 'keydown', (ev) => this.keyDown(ev));
2985
+ }
2986
+ restoreFocus() {
2987
+ this.removeDocKeydown?.();
2988
+ this.removeDocKeydown = undefined;
2989
+ if (this.keyboardDrag) {
2990
+ this.keyboardDrag = false;
2991
+ this._ref.el.focus?.({ preventScroll: true });
2992
+ }
2993
+ }
2994
+ /** Swallow the single `click` the browser dispatches right after a completed drag. */
2995
+ swallowNextClick() {
2996
+ const doc = this._ref.el.ownerDocument;
2997
+ const stop = (ev) => {
2998
+ ev.stopPropagation();
2999
+ ev.preventDefault();
3000
+ };
3001
+ doc.addEventListener('click', stop, { capture: true, once: true });
3002
+ // If no click follows (pointer released elsewhere), do not eat the user's NEXT real click.
3003
+ doc.defaultView?.setTimeout(() => doc.removeEventListener('click', stop, { capture: true }), 0);
3004
+ }
2688
3005
  isOnHandle(target) {
2689
3006
  if (!this.dragHandle())
2690
3007
  return true;
2691
3008
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2692
3009
  return isElementTarget(target) && !!target.closest(this.dragHandle());
2693
3010
  }
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]'));
3011
+ /** Should a press on `target` be left alone (no drag)? */
3012
+ isExcluded(target) {
3013
+ if (!isElementTarget(target))
3014
+ return false;
3015
+ const selector = this.noDragSelector();
3016
+ if (selector && target.closest(selector))
3017
+ return true;
3018
+ return this.ignoreInteractive() && isEditableOrInteractive(target);
2697
3019
  }
2698
3020
  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 });
3021
+ 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
3022
  }
2701
3023
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
2702
3024
  type: Directive,
@@ -2711,13 +3033,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2711
3033
  '[class.disable-drag]': 'disabled()',
2712
3034
  },
2713
3035
  }]
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: [{
3036
+ }], 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
3037
  type: Input,
2716
3038
  args: ['data']
2717
3039
  }], customPreview: [{
2718
3040
  type: ContentChild,
2719
3041
  args: [NgxDragPreview]
2720
3042
  }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
3043
+ function isEditableOrInteractive(el) {
3044
+ return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3045
+ }
3046
+ function isEditableTarget(target) {
3047
+ if (!isElementTarget(target))
3048
+ return false;
3049
+ return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3050
+ }
2721
3051
  function isElementTarget(target) {
2722
3052
  return (!!target &&
2723
3053
  typeof target === 'object' &&
@@ -2816,7 +3146,12 @@ class NgxResizable {
2816
3146
  this.beforeTransform = element.style.transform;
2817
3147
  this.resizing.set(true);
2818
3148
  element.style.transition = 'none';
2819
- element.setPointerCapture?.(e.pointerId);
3149
+ try {
3150
+ element.setPointerCapture?.(e.pointerId);
3151
+ }
3152
+ catch {
3153
+ /* synthetic / already-ended pointer: document listeners below still receive the events */
3154
+ }
2820
3155
  const doc = element.ownerDocument;
2821
3156
  this.removeDocumentListeners = [
2822
3157
  this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
@@ -2873,6 +3208,11 @@ class NgxResizable {
2873
3208
  if (this.disabled()) {
2874
3209
  return;
2875
3210
  }
3211
+ // Only when the resizable element itself is focused: arrow keys pressed inside an input /
3212
+ // contenteditable that lives in the block are caret movement, not a resize request.
3213
+ if (e.target !== this.el.nativeElement) {
3214
+ return;
3215
+ }
2876
3216
  const rtl = isRtl(this.el.nativeElement);
2877
3217
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2878
3218
  let dw = 0;
@@ -2896,8 +3236,9 @@ class NgxResizable {
2896
3236
  const rect = this.el.nativeElement.getBoundingClientRect();
2897
3237
  const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
2898
3238
  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`;
3239
+ const styled = this.toStyleSize(width, height);
3240
+ this.el.nativeElement.style.width = `${styled.width}px`;
3241
+ this.el.nativeElement.style.height = `${styled.height}px`;
2901
3242
  const out = {
2902
3243
  width,
2903
3244
  height,
@@ -2967,8 +3308,9 @@ class NgxResizable {
2967
3308
  /**
2968
3309
  * Boundary constraint.
2969
3310
  */
2970
- if (this.boundary) {
2971
- const clamped = clampResizeWithinBoundary(this.boundary()?.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
3311
+ const boundaryEl = this.boundary();
3312
+ if (boundaryEl) {
3313
+ const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
2972
3314
  width = clamped.width / scaleX;
2973
3315
  height = clamped.height / scaleY;
2974
3316
  moveLeft = clamped.left;
@@ -3001,8 +3343,9 @@ class NgxResizable {
3001
3343
  * Changing width/height may cause the browser to re-layout
3002
3344
  * the element, especially for relative/flex/grid/static layouts.
3003
3345
  */
3004
- element.style.width = `${out.width}px`;
3005
- element.style.height = `${out.height}px`;
3346
+ const style = this.toStyleSize(out.width, out.height);
3347
+ element.style.width = `${style.width}px`;
3348
+ element.style.height = `${style.height}px`;
3006
3349
  /**
3007
3350
  * Force/read layout after changing the size.
3008
3351
  */
@@ -3037,9 +3380,29 @@ class NgxResizable {
3037
3380
  element.style.transform =
3038
3381
  `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
3039
3382
  }
3383
+ /**
3384
+ * `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
3385
+ * `width/height` we write are interpreted according to `box-sizing`: for the CSS default
3386
+ * (`content-box`) writing the border-box number would make a padded/bordered block jump by
3387
+ * its padding+border the instant a resize starts. Convert so the box is exactly the size
3388
+ * reported in `IResizableOutput`.
3389
+ */
3390
+ toStyleSize(width, height) {
3391
+ const el = this.el.nativeElement;
3392
+ const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
3393
+ if (!cs || cs.boxSizing === 'border-box')
3394
+ return { width, height };
3395
+ const px = (v) => Number.parseFloat(v) || 0;
3396
+ const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
3397
+ const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
3398
+ return { width: Math.max(0, width - extraX), height: Math.max(0, height - extraY) };
3399
+ }
3040
3400
  clampGrid(value, axis) {
3041
- const grid = typeof this.grid() === 'number' ? this.grid : this.grid()?.[axis];
3042
- if (!grid || grid <= 0) {
3401
+ const g = this.grid();
3402
+ // NOTE: the previous code returned the input *signal function* for a numeric grid, so every
3403
+ // snapped size became NaN and the resize silently stopped working.
3404
+ const grid = typeof g === 'number' ? g : g?.[axis];
3405
+ if (!grid || !Number.isFinite(grid) || grid <= 0) {
3043
3406
  return value;
3044
3407
  }
3045
3408
  return Math.round(value / grid) * grid;
@@ -3052,8 +3415,13 @@ class NgxResizable {
3052
3415
  for (const remove of this.removeDocumentListeners)
3053
3416
  remove();
3054
3417
  this.removeDocumentListeners = [];
3055
- if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3056
- this.el.nativeElement.releasePointerCapture(pointerId);
3418
+ try {
3419
+ if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3420
+ this.el.nativeElement.releasePointerCapture(pointerId);
3421
+ }
3422
+ }
3423
+ catch {
3424
+ /* already released */
3057
3425
  }
3058
3426
  this.beforeTransform = '';
3059
3427
  }
@@ -3194,6 +3562,13 @@ class NgxDropList {
3194
3562
  set disableSort(value) {
3195
3563
  this._ref.disableSort = value === true;
3196
3564
  }
3565
+ /** `'stretch'`: the placeholder only copies the item's size along the list's main axis. */
3566
+ set placeholderFit(value) {
3567
+ this._ref.placeholderFit = value === 'stretch' ? 'stretch' : 'item';
3568
+ }
3569
+ set sortAnimationDuration(value) {
3570
+ this._ref.sortAnimationDuration = value == null ? 200 : Math.max(0, value);
3571
+ }
3197
3572
  drop = new EventEmitter();
3198
3573
  customPlaceholder;
3199
3574
  sub;
@@ -3225,7 +3600,7 @@ class NgxDropList {
3225
3600
  this.service.removeDropList(this._ref);
3226
3601
  }
3227
3602
  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 });
3603
+ 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
3604
  }
3230
3605
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
3231
3606
  type: Directive,
@@ -3241,6 +3616,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3241
3616
  type: Input
3242
3617
  }], disableSort: [{
3243
3618
  type: Input
3619
+ }], placeholderFit: [{
3620
+ type: Input
3621
+ }], sortAnimationDuration: [{
3622
+ type: Input
3244
3623
  }], drop: [{
3245
3624
  type: Output
3246
3625
  }], customPlaceholder: [{
@@ -3313,9 +3692,54 @@ function clamp(v, max) {
3313
3692
  return Math.max(0, Math.min(max, v));
3314
3693
  }
3315
3694
 
3695
+ /**
3696
+ * Helpers for a page builder's "export HTML" step.
3697
+ *
3698
+ * While you edit, the directives decorate the canvas DOM: `ngx-*` classes, `tabindex="0"`,
3699
+ * `aria-grabbed`, `touch-action: none`, and — if an export happens mid-drag — a placeholder or a
3700
+ * preview clone. None of that belongs in the page a visitor receives. Run the canvas through
3701
+ * `getCleanHtml()` (or `stripEditorArtifacts()` on a clone you already have) before saving.
3702
+ */
3703
+ const TRANSIENT_NODES = '.ngx-drag-placeholder, .ngx-drag-preview';
3704
+ /** Mutates `root` (and its subtree) in place. Pass a CLONE of live DOM, never the live canvas. */
3705
+ function stripEditorArtifacts(root, options = {}) {
3706
+ const prefix = options.classPrefix ?? 'ngx-';
3707
+ const dropClasses = new Set(['disable-drag', ...(options.extraClasses ?? [])]);
3708
+ const dropAttrs = ['aria-grabbed', ...(options.extraAttributes ?? [])];
3709
+ root.querySelectorAll(TRANSIENT_NODES).forEach((n) => n.remove());
3710
+ const all = [root, ...Array.from(root.querySelectorAll('*'))];
3711
+ for (const el of all) {
3712
+ // `tabindex="0"` is only ours on draggables (a real focusable of the page keeps its own).
3713
+ if (el.classList.contains('ngx-draggable') && el.getAttribute('tabindex') === '0') {
3714
+ el.removeAttribute('tabindex');
3715
+ }
3716
+ for (const attr of dropAttrs)
3717
+ el.removeAttribute(attr);
3718
+ for (const cls of Array.from(el.classList)) {
3719
+ if (cls.startsWith(prefix) || dropClasses.has(cls))
3720
+ el.classList.remove(cls);
3721
+ }
3722
+ if (!el.classList.length)
3723
+ el.removeAttribute('class');
3724
+ const style = el.style;
3725
+ if (style) {
3726
+ if (style.touchAction === 'none')
3727
+ style.removeProperty('touch-action');
3728
+ if (!el.getAttribute('style')?.trim())
3729
+ el.removeAttribute('style');
3730
+ }
3731
+ }
3732
+ }
3733
+ /** Serialised, editor-free HTML of `el`'s children (the canvas content). */
3734
+ function getCleanHtml(el, options) {
3735
+ const clone = el.cloneNode(true);
3736
+ stripEditorArtifacts(clone, options);
3737
+ return clone.innerHTML;
3738
+ }
3739
+
3316
3740
  /**
3317
3741
  * Generated bundle index. Do not edit.
3318
3742
  */
3319
3743
 
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 };
3744
+ 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
3745
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map