ngx-kit 9.0.1 → 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.
@@ -1,6 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, EventEmitter, Injectable, InjectionToken, inject, ApplicationRef, Directive, model, input, output, Renderer2, effect, ContentChild, Input, HostListener, Output, ElementRef } from '@angular/core';
3
- import { DOCUMENT } from '@angular/common';
4
3
 
5
4
  /**
6
5
  * Boundary clamping utilities.
@@ -70,17 +69,117 @@ function clampResizeWithinBoundary(boundaryRect, startRect, width, height, left,
70
69
  return { width: clampedWidth, height: clampedHeight, left: clampedLeft, top: clampedTop };
71
70
  }
72
71
  /** Current page zoom level (1 = 100%). Falls back to devicePixelRatio pre visualViewport. */
73
- function getPageZoom() {
74
- return window.visualViewport?.scale ?? window.devicePixelRatio ?? 1;
72
+ function getPageZoom(element) {
73
+ const win = element?.ownerDocument.defaultView ?? (typeof window !== 'undefined' ? window : null);
74
+ return win?.visualViewport?.scale ?? win?.devicePixelRatio ?? 1;
75
75
  }
76
76
  /** Adjusts a raw pointer-derived value for the current page zoom level. */
77
- function adjustForZoom(value) {
78
- const zoom = getPageZoom();
77
+ function adjustForZoom(value, element) {
78
+ const zoom = getPageZoom(element);
79
79
  return zoom ? value / zoom : value;
80
80
  }
81
81
 
82
+ /** Base (LTR) physical direction for every logical direction. */
83
+ const LTR_BASE = {
84
+ start: 'w',
85
+ end: 'e',
86
+ 'n-start': 'nw',
87
+ 'n-end': 'ne',
88
+ 's-start': 'sw',
89
+ 's-end': 'se',
90
+ };
91
+ /** Mirrors the horizontal component of a physical direction. 'n' / 's' pass through unchanged. */
92
+ const MIRROR = {
93
+ n: 'n',
94
+ s: 's',
95
+ e: 'w',
96
+ w: 'e',
97
+ ne: 'nw',
98
+ nw: 'ne',
99
+ se: 'sw',
100
+ sw: 'se',
101
+ };
102
+ function isLogicalDirection(dir) {
103
+ return Object.prototype.hasOwnProperty.call(LTR_BASE, dir);
104
+ }
105
+ /**
106
+ * True when `element` sits inside a right-to-left context.
107
+ *
108
+ * Reads the *computed* `direction` style instead of `element.dir`, because
109
+ * `direction` is inherited and is almost never set on the resizable element
110
+ * itself - it's usually set on `<html>`, `<body>`, or some ancestor wrapper.
111
+ */
112
+ function isRtl(element) {
113
+ const win = element.ownerDocument.defaultView;
114
+ return win ? win.getComputedStyle(element).direction === 'rtl' : false;
115
+ }
116
+ /**
117
+ * Resolves a logical direction ('start' | 'end' | 'n-start' | 'n-end' | 's-start' | 's-end')
118
+ * to a physical, viewport-space direction ('n' | 'e' | 's' | 'w' | 'ne' | 'nw' | 'se' | 'sw')
119
+ * based on `rtl`.
120
+ *
121
+ * Physical directions pass through untouched, so it's always safe to call this
122
+ * even when `dir` might already be physical (e.g. resolving a direction that was
123
+ * already resolved once before).
124
+ */
125
+ function resolveLogicalDirection(dir, rtl) {
126
+ if (!isLogicalDirection(dir)) {
127
+ return dir;
128
+ }
129
+ const base = LTR_BASE[dir];
130
+ return rtl ? MIRROR[base] : base;
131
+ }
132
+
82
133
  /** Above any app chrome (modals, sticky headers) but below the browser's own UI. */
83
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
+ }
84
183
  class DragPreviewRef {
85
184
  tpl;
86
185
  /** Set by `ngxDragPreview` so the embedded view can join Angular's change-detection tree. */
@@ -112,6 +211,7 @@ class DragPreviewRef {
112
211
  // Avoid duplicated ids in the document.
113
212
  this.element.removeAttribute('id');
114
213
  this.element.querySelectorAll('[id]').forEach((n) => n.removeAttribute('id'));
214
+ copyVisualStyles(dragEl, this.element);
115
215
  this.element.classList.add('ngx-drag-preview');
116
216
  this.element.setAttribute('aria-hidden', 'true');
117
217
  this.element.setAttribute('inert', '');
@@ -135,9 +235,10 @@ class DragPreviewRef {
135
235
  willChange: 'transform',
136
236
  zIndex: PREVIEW_Z_INDEX,
137
237
  });
238
+ const win = doc.defaultView;
138
239
  if (isRtl(doc.body)) {
139
240
  Object.assign(this.element.style, {
140
- right: `${window.innerWidth - rect.right}px`,
241
+ right: `${(win?.innerWidth ?? rect.right) - rect.right}px`,
141
242
  left: 'auto',
142
243
  });
143
244
  }
@@ -147,7 +248,7 @@ class DragPreviewRef {
147
248
  right: 'auto',
148
249
  });
149
250
  }
150
- doc.body.appendChild(this.element);
251
+ (doc.body ?? doc.documentElement).appendChild(this.element);
151
252
  return this.element;
152
253
  }
153
254
  detach() {
@@ -161,6 +262,174 @@ class DragPreviewRef {
161
262
  }
162
263
  }
163
264
 
265
+ function viewportDeltaToLocal(element, delta, includeElementTransform = true) {
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) {
275
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
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)
288
+ return { x: 0, y: 0 };
289
+ return {
290
+ x: matrix.a * delta.x + matrix.c * delta.y,
291
+ y: matrix.b * delta.x + matrix.d * delta.y,
292
+ };
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
+ }
316
+ function localDeltaToViewport(element, delta) {
317
+ const matrix = getLocalToViewportMatrix(element);
318
+ return {
319
+ x: matrix.a * delta.x + matrix.c * delta.y,
320
+ y: matrix.b * delta.x + matrix.d * delta.y,
321
+ };
322
+ }
323
+ /** Returns the composed 2D linear transform from element-local to viewport space. */
324
+ function getLocalToViewportMatrix(element, includeElementTransform = true) {
325
+ const win = element.ownerDocument.defaultView;
326
+ if (!win || typeof win.DOMMatrix !== 'function') {
327
+ return { a: 1, b: 0, c: 0, d: 1 };
328
+ }
329
+ let result = { a: 1, b: 0, c: 0, d: 1 };
330
+ let current = includeElementTransform ? element : element.parentElement;
331
+ while (current) {
332
+ const style = win.getComputedStyle(current);
333
+ const transform = style.transform;
334
+ if (transform && transform !== 'none') {
335
+ try {
336
+ const matrix = new win.DOMMatrix(transform);
337
+ result = multiplyLinear({ a: matrix.a, b: matrix.b, c: matrix.c, d: matrix.d }, result);
338
+ }
339
+ catch {
340
+ // Ignore unsupported/malformed transforms and continue with remaining ancestors.
341
+ }
342
+ }
343
+ // CSS `zoom` is not represented consistently in computed `transform` across browsers.
344
+ const zoom = Number.parseFloat(style.getPropertyValue('zoom'));
345
+ if (Number.isFinite(zoom) && zoom > 0 && zoom !== 1) {
346
+ result = multiplyLinear({ a: zoom, b: 0, c: 0, d: zoom }, result);
347
+ }
348
+ current = current.parentElement;
349
+ }
350
+ return result;
351
+ }
352
+ function multiplyLinear(left, right) {
353
+ return {
354
+ a: left.a * right.a + left.c * right.b,
355
+ b: left.b * right.a + left.d * right.b,
356
+ c: left.a * right.c + left.c * right.d,
357
+ d: left.b * right.c + left.d * right.d,
358
+ };
359
+ }
360
+ /**
361
+ * Converts a client-space point from one same-origin Document viewport to another.
362
+ * The conversion walks both frameElement chains through their common top-level document.
363
+ * Cross-origin frame boundaries cannot be inspected and return null.
364
+ */
365
+ function convertPointBetweenDocuments(point, fromDocument, toDocument) {
366
+ if (fromDocument === toDocument)
367
+ return { ...point };
368
+ const toTop = (doc, value) => {
369
+ let current = doc;
370
+ let result = { ...value };
371
+ const visited = new Set();
372
+ while (current.defaultView?.frameElement) {
373
+ if (visited.has(current))
374
+ return null;
375
+ visited.add(current);
376
+ let frame;
377
+ try {
378
+ frame = current.defaultView.frameElement;
379
+ }
380
+ catch {
381
+ return null;
382
+ }
383
+ const iframe = frame;
384
+ const rect = iframe.getBoundingClientRect();
385
+ const scaleX = iframe.offsetWidth ? rect.width / iframe.offsetWidth : 1;
386
+ const scaleY = iframe.offsetHeight ? rect.height / iframe.offsetHeight : 1;
387
+ result = {
388
+ x: rect.left + iframe.clientLeft * scaleX + result.x * scaleX,
389
+ y: rect.top + iframe.clientTop * scaleY + result.y * scaleY,
390
+ };
391
+ current = iframe.ownerDocument;
392
+ }
393
+ return { document: current, point: result };
394
+ };
395
+ const from = toTop(fromDocument, point);
396
+ if (!from)
397
+ return null;
398
+ const targetFrames = [];
399
+ let current = toDocument;
400
+ const visited = new Set();
401
+ while (current && current !== from.document) {
402
+ if (visited.has(current))
403
+ return null;
404
+ visited.add(current);
405
+ let frame;
406
+ try {
407
+ frame = current.defaultView?.frameElement ?? null;
408
+ }
409
+ catch {
410
+ return null;
411
+ }
412
+ if (!frame)
413
+ return null;
414
+ targetFrames.push(frame);
415
+ current = frame.ownerDocument;
416
+ }
417
+ if (current !== from.document)
418
+ return null;
419
+ let result = from.point;
420
+ for (let i = targetFrames.length - 1; i >= 0; i--) {
421
+ const iframe = targetFrames[i];
422
+ const rect = iframe.getBoundingClientRect();
423
+ const scaleX = iframe.offsetWidth ? rect.width / iframe.offsetWidth : 1;
424
+ const scaleY = iframe.offsetHeight ? rect.height / iframe.offsetHeight : 1;
425
+ result = {
426
+ x: (result.x - rect.left) / scaleX - iframe.clientLeft,
427
+ y: (result.y - rect.top) / scaleY - iframe.clientTop,
428
+ };
429
+ }
430
+ return result;
431
+ }
432
+
164
433
  let zIndexCounter = 1000;
165
434
  class DragRef {
166
435
  data;
@@ -197,6 +466,8 @@ class DragRef {
197
466
  sourceDisplay = '';
198
467
  sourcePointerEvents = '';
199
468
  sourceTransition = '';
469
+ sourceWillChange = '';
470
+ sourceTransform = '';
200
471
  /** True while a drop-list drag (body preview + placeholder) is in progress. Free drag: always false. */
201
472
  listDrag = false;
202
473
  cachedLists = [];
@@ -211,8 +482,18 @@ class DragRef {
211
482
  lastEffective = { x: 0, y: 0 };
212
483
  scrollListener;
213
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;
214
493
  init() {
215
- this.previousTransform = getComputedStyle?.(this.el)?.getPropertyValue?.('transform');
494
+ this.previousTransform =
495
+ this.el.ownerDocument.defaultView?.getComputedStyle(this.el).transform ?? '';
496
+ this.sourceTransform = this.el.style.transform;
216
497
  }
217
498
  withDropList(list) {
218
499
  if (this.dropList === list)
@@ -237,18 +518,22 @@ class DragRef {
237
518
  this.moveDx = 0;
238
519
  this.moveDy = 0;
239
520
  this.scrollCompensation = { x: 0, y: 0 };
521
+ this.freeDragInverse = undefined;
240
522
  // Only draggables that are direct children of their drop list take part in sorting.
241
523
  // Everything else is a plain free drag.
242
524
  const list = this.dropList && this.dropList.el === this.el.parentElement ? this.dropList : null;
243
525
  this.originDropList = list;
244
526
  this.originIndex = list?.getItemIndex(this) ?? -1;
245
- this.previousTransform = getComputedStyle(this.el).getPropertyValue('transform');
527
+ const win = this.el.ownerDocument.defaultView;
528
+ this.previousTransform = win?.getComputedStyle(this.el).transform ?? '';
529
+ this.sourceTransform = this.el.style.transform;
246
530
  this.isDragging.set(true);
247
531
  this.previousZIndex = this.el.style.zIndex;
248
532
  this.sourceVisibility = this.el.style.visibility;
249
533
  this.sourceDisplay = this.el.style.display;
250
534
  this.sourcePointerEvents = this.el.style.pointerEvents;
251
535
  this.sourceTransition = this.el.style.transition;
536
+ this.sourceWillChange = this.el.style.willChange;
252
537
  this.el.classList.add('ngx-draggable--dragging');
253
538
  this.el.style.willChange = 'transform';
254
539
  this.el.style.transition = 'none';
@@ -334,7 +619,8 @@ class DragRef {
334
619
  this.el.style.visibility = this.sourceVisibility;
335
620
  this.el.style.display = this.sourceDisplay;
336
621
  this.el.style.pointerEvents = this.sourcePointerEvents;
337
- this.el.style.willChange = '';
622
+ this.el.style.willChange = this.sourceWillChange;
623
+ this.el.style.transition = this.sourceTransition;
338
624
  this.el.style.zIndex = this.previousZIndex;
339
625
  }
340
626
  cancelDrag() {
@@ -350,7 +636,7 @@ class DragRef {
350
636
  return;
351
637
  }
352
638
  // ---- free drag: unchanged ----
353
- this.el.style.transform = this.previousTransform;
639
+ this.el.style.transform = this.sourceTransform;
354
640
  this.el.style.visibility = this.sourceVisibility;
355
641
  this.el.style.pointerEvents = this.sourcePointerEvents;
356
642
  this.scrollCompensation = { x: 0, y: 0 };
@@ -358,10 +644,24 @@ class DragRef {
358
644
  }
359
645
  /** Abort without emitting anything (the directive was destroyed mid-drag). Idempotent. */
360
646
  dispose() {
361
- if (!this.listDrag)
647
+ if (!this.isDragging())
362
648
  return;
363
649
  this.isDragging.set(false);
364
- this.finishListDrag(false);
650
+ this.position.set({ x: 0, y: 0 });
651
+ if (this.listDrag) {
652
+ this.finishListDrag(false);
653
+ return;
654
+ }
655
+ this.el.style.transform = this.sourceTransform;
656
+ this.el.style.visibility = this.sourceVisibility;
657
+ this.el.style.display = this.sourceDisplay;
658
+ this.el.style.pointerEvents = this.sourcePointerEvents;
659
+ this.el.style.transition = this.sourceTransition;
660
+ this.el.style.willChange = this.sourceWillChange;
661
+ this.el.style.zIndex = this.previousZIndex;
662
+ this.el.classList.remove('ngx-draggable--dragging');
663
+ this.scrollCompensation = { x: 0, y: 0 };
664
+ this.position.set({ x: 0, y: 0 });
365
665
  }
366
666
  /** True while a drop-list drag is running. */
367
667
  get isListDrag() {
@@ -410,14 +710,21 @@ class DragRef {
410
710
  : '';
411
711
  const tx = this.moveDx + this.scrollCompensation.x;
412
712
  const ty = this.moveDy + this.scrollCompensation.y;
413
- this.el.style.transform = `${base}translate3d(${tx}px, ${ty}px, 0)`;
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 });
718
+ this.el.style.transform = `${base}translate3d(${local.x}px, ${local.y}px, 0)`;
414
719
  }
415
720
  // -------------------------------------------------------------------------------------
416
721
  // drop-list drag
417
722
  // -------------------------------------------------------------------------------------
418
723
  beginListDrag(origin, pointer) {
419
724
  const doc = this.el.ownerDocument;
420
- const win = doc.defaultView ?? window;
725
+ const win = doc.defaultView;
726
+ if (!win)
727
+ return;
421
728
  this.dropListsSnapshot = null; // force refreshDropCandidates() to do a full pass below
422
729
  this.refreshDropCandidates(origin);
423
730
  this.lastEffective = { ...pointer };
@@ -436,9 +743,40 @@ class DragRef {
436
743
  l._refresh();
437
744
  this.updateDropTarget(this.lastEffective);
438
745
  };
439
- // Capture: `scroll` does not bubble, and any ancestor (or the page) may be the one scrolling.
440
- doc.addEventListener('scroll', this.scrollListener, { capture: true, passive: true });
441
- 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
+ }
442
780
  }
443
781
  /**
444
782
  * Recomputes which registered lists are valid targets for this drag — connected to the
@@ -467,6 +805,7 @@ class DragRef {
467
805
  }
468
806
  this.dropCandidates = next;
469
807
  this.cachedLists = next.map((c) => c.list);
808
+ this.syncDocumentListeners();
470
809
  }
471
810
  /**
472
811
  * Decide which list is under the pointer, switch if needed, then re-sort in the active one.
@@ -484,9 +823,11 @@ class DragRef {
484
823
  if (!origin)
485
824
  return;
486
825
  this.refreshDropCandidates(origin);
487
- const next = this.resolveDropList(pointer);
826
+ const resolved = this.resolveDropList(pointer);
827
+ const next = resolved?.list ?? null;
828
+ const targetPointer = resolved?.pointer ?? pointer;
488
829
  if (next === this.activeDropList) {
489
- next?.sortItem(this, pointer);
830
+ next?.sortItem(this, targetPointer);
490
831
  return;
491
832
  }
492
833
  const prev = this.activeDropList;
@@ -501,7 +842,7 @@ class DragRef {
501
842
  origin.exit(this); // truly outside the origin's territory now: reset it (idempotent)
502
843
  }
503
844
  this.activeDropList = next;
504
- next?.enter(this, pointer.x, pointer.y); // fresh entry, or resumes a still-alive (frozen) session
845
+ next?.enter(this, targetPointer.x, targetPointer.y); // coordinates belong to the target document
505
846
  }
506
847
  /**
507
848
  * Innermost connected list under the pointer, from the precomputed candidates: deeper always
@@ -513,15 +854,22 @@ class DragRef {
513
854
  let best = null;
514
855
  let bestDepth = -1;
515
856
  for (const { list, depth } of this.dropCandidates) {
516
- if (!list._containsPoint(pointer.x, pointer.y))
857
+ const targetDocument = list.el?.ownerDocument;
858
+ if (!targetDocument)
859
+ continue;
860
+ // Pointer coordinates originate in the draggable's document. A same-origin iframe has
861
+ // its own viewport coordinate space, so convert before testing its DropList geometry.
862
+ const targetPoint = convertPointBetweenDocuments(pointer, this.el.ownerDocument, targetDocument);
863
+ if (!targetPoint || !list._containsPoint(targetPoint.x, targetPoint.y))
517
864
  continue;
518
865
  if (!best || depth > bestDepth) {
519
- best = list;
866
+ best = { list, pointer: targetPoint };
520
867
  bestDepth = depth;
521
868
  }
522
- else if (depth === bestDepth && best !== this.activeDropList) {
523
- if (list === this.activeDropList || list._area() < best._area())
524
- best = list;
869
+ else if (depth === bestDepth && best.list !== this.activeDropList) {
870
+ if (list === this.activeDropList || list._area() < best.list._area()) {
871
+ best = { list, pointer: targetPoint };
872
+ }
525
873
  }
526
874
  }
527
875
  return best;
@@ -532,11 +880,9 @@ class DragRef {
532
880
  * regardless of how the drag ended. Only then is the drop event emitted.
533
881
  */
534
882
  finishListDrag(commit) {
535
- const doc = this.el.ownerDocument;
536
- if (this.scrollListener)
537
- doc.removeEventListener('scroll', this.scrollListener, { capture: true });
538
- if (this.resizeListener)
539
- (doc.defaultView ?? window).removeEventListener('resize', this.resizeListener);
883
+ for (const remove of this.documentListeners.values())
884
+ remove();
885
+ this.documentListeners.clear();
540
886
  this.scrollListener = this.resizeListener = undefined;
541
887
  const target = commit ? this.activeDropList : null;
542
888
  const event = target?._finish(this, true) ?? null;
@@ -556,7 +902,7 @@ class DragRef {
556
902
  this.el.style.display = this.sourceDisplay;
557
903
  this.el.style.pointerEvents = this.sourcePointerEvents;
558
904
  this.el.style.transition = this.sourceTransition;
559
- this.el.style.willChange = '';
905
+ this.el.style.willChange = this.sourceWillChange;
560
906
  this.el.style.zIndex = this.previousZIndex;
561
907
  if (event && target)
562
908
  target.onDrop.emit(event);
@@ -564,6 +910,7 @@ class DragRef {
564
910
  updatePreview() {
565
911
  if (!this.preview || !this.preview.element)
566
912
  return;
913
+ // Preview is fixed in the source document's viewport coordinate space.
567
914
  this.preview.element.style.transform = `translate3d(${this.moveDx}px, ${this.moveDy}px, 0)`;
568
915
  }
569
916
  }
@@ -586,7 +933,7 @@ class ElementHelper {
586
933
  return false;
587
934
  }
588
935
  static findParentBySelector(elm, selector) {
589
- var all = document.querySelectorAll(selector);
936
+ var all = elm.ownerDocument.querySelectorAll(selector);
590
937
  var cur = elm.parentNode;
591
938
  while (cur && !ElementHelper.collectionHas(all, cur)) {
592
939
  //keep going up until you find a match
@@ -615,15 +962,17 @@ function getFirstLevelDraggables(container) {
615
962
  return firstLevel;
616
963
  }
617
964
  function isElementNode(node) {
618
- return node?.nodeType === Node.ELEMENT_NODE;
965
+ return node?.nodeType === Node.ELEMENT_NODE; // 1
619
966
  }
620
967
  function isDocumentNode(node) {
621
- return node?.nodeType === Node.DOCUMENT_NODE;
968
+ return node?.nodeType === Node.DOCUMENT_NODE; // 9
622
969
  }
623
970
  function isShadowRootNode(node) {
624
971
  return node?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && 'host' in node;
625
972
  }
626
973
  function getOwnerWindow(node) {
974
+ if (isDocumentNode(node))
975
+ return node.defaultView;
627
976
  return node?.ownerDocument?.defaultView ?? null;
628
977
  }
629
978
  function isWindow(value) {
@@ -950,7 +1299,7 @@ class ScrollFrame {
950
1299
  * @param start first element to inspect (walks up through its ancestors)
951
1300
  * @param includeStart whether `start` itself counts (true for a list whose own scroll moves its items)
952
1301
  */
953
- constructor(start, includeStart = true, win = window) {
1302
+ constructor(start, includeStart = true, win = start?.ownerDocument.defaultView ?? null) {
954
1303
  this.win = win;
955
1304
  let node = includeStart ? start : (start?.parentElement ?? null);
956
1305
  const root = start?.ownerDocument.documentElement;
@@ -960,7 +1309,8 @@ class ScrollFrame {
960
1309
  this.targets.push(node);
961
1310
  node = node.parentElement;
962
1311
  }
963
- this.targets.push(win);
1312
+ if (win)
1313
+ this.targets.push(win);
964
1314
  this.origin = this.read();
965
1315
  }
966
1316
  /** Call when any scroll event fires. */
@@ -980,23 +1330,26 @@ class ScrollFrame {
980
1330
  let x = 0;
981
1331
  let y = 0;
982
1332
  for (const t of this.targets) {
983
- if (t === this.win) {
1333
+ if (this.win && t === this.win) {
984
1334
  x += this.win.scrollX;
985
1335
  y += this.win.scrollY;
986
1336
  }
987
1337
  else {
988
- x += t.scrollLeft;
989
- y += t.scrollTop;
1338
+ const el = t;
1339
+ // Element scroll offsets are local CSS pixels; geometry snapshots are viewport pixels.
1340
+ const delta = localDeltaToViewport(el, { x: el.scrollLeft, y: el.scrollTop });
1341
+ x += delta.x;
1342
+ y += delta.y;
990
1343
  }
991
1344
  }
992
1345
  return { x, y };
993
1346
  }
994
1347
  }
995
1348
  function scrolls(el) {
996
- const s = getComputedStyle?.(el);
1349
+ const s = el.ownerDocument.defaultView?.getComputedStyle(el);
997
1350
  // `hidden` containers still scroll programmatically; `clip` never does.
998
1351
  const scrollish = (v) => v === 'auto' || v === 'scroll' || v === 'overlay' || v === 'hidden';
999
- return scrollish(s.overflowX) || scrollish(s.overflowY);
1352
+ return !!s && (scrollish(s.overflowX) || scrollish(s.overflowY));
1000
1353
  }
1001
1354
 
1002
1355
  /**
@@ -1007,7 +1360,7 @@ function scrolls(el) {
1007
1360
  */
1008
1361
  function makeSpacer(container) {
1009
1362
  for (const n of Array.from(container.childNodes)) {
1010
- if (n.nodeType === Node.TEXT_NODE && n.nodeValue && /^\s+$/.test(n.nodeValue)) {
1363
+ if (n.nodeType === 3 && n.nodeValue && /^\s+$/.test(n.nodeValue)) {
1011
1364
  return container.ownerDocument.createTextNode(n.nodeValue);
1012
1365
  }
1013
1366
  }
@@ -1058,6 +1411,13 @@ class SortSession {
1058
1411
  /** undefined = not created yet, null = this layout has no whitespace separators. */
1059
1412
  spacer;
1060
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;
1061
1421
  constructor(init) {
1062
1422
  this.spacer = init.spacer;
1063
1423
  this.onOffsetChange = init.onOffsetChange;
@@ -1130,6 +1490,7 @@ class SortSession {
1130
1490
  return;
1131
1491
  const target = this.targetIdx;
1132
1492
  this.clearDisplacement();
1493
+ this.inverse = undefined; // the viewport may have changed the ancestors' transforms
1133
1494
  this.snapshot();
1134
1495
  this.frame = new ScrollFrame(this.container, true);
1135
1496
  if (this.mode === 'transform') {
@@ -1215,6 +1576,7 @@ class SortSession {
1215
1576
  const horizontal = this.model.axis === 'x';
1216
1577
  this.entries.forEach((e, i) => {
1217
1578
  const o = offsets[i];
1579
+ // `o` is derived from DOMRect measurements (viewport CSS pixels).
1218
1580
  this.setOffset(e, horizontal ? { x: o, y: 0 } : { x: 0, y: o });
1219
1581
  });
1220
1582
  }
@@ -1228,10 +1590,17 @@ class SortSession {
1228
1590
  }
1229
1591
  else {
1230
1592
  this.applied.set(e.el, off);
1231
- e.el.style.transform = `translate3d(${off.x}px, ${off.y}px, 0)${e.base}`;
1593
+ const local = this.toLocal(off);
1594
+ e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1232
1595
  }
1233
1596
  this.onOffsetChange?.(e.el, off);
1234
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
+ }
1235
1604
  // ---------------------------------------------------------------------------------------
1236
1605
  // flow strategy (single placeholder move + FLIP transforms)
1237
1606
  // ---------------------------------------------------------------------------------------
@@ -1253,7 +1622,8 @@ class SortSession {
1253
1622
  const dx = first[i].left - last[i].left;
1254
1623
  const dy = first[i].top - last[i].top;
1255
1624
  if (this.animationMs > 0 && (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)) {
1256
- e.el.style.transform = `translate3d(${dx}px, ${dy}px, 0)${e.base}`;
1625
+ const local = this.toLocal({ x: dx, y: dy });
1626
+ e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1257
1627
  moved.push(e);
1258
1628
  }
1259
1629
  });
@@ -1298,63 +1668,10 @@ class SortSession {
1298
1668
  }
1299
1669
  }
1300
1670
  function initialTransform(el) {
1301
- const t = getComputedStyle?.(el)?.transform;
1671
+ const t = el.ownerDocument.defaultView?.getComputedStyle(el).transform;
1302
1672
  return t && t !== 'none' ? ` ${t}` : '';
1303
1673
  }
1304
1674
 
1305
- /** Base (LTR) physical direction for every logical direction. */
1306
- const LTR_BASE = {
1307
- start: 'w',
1308
- end: 'e',
1309
- 'n-start': 'nw',
1310
- 'n-end': 'ne',
1311
- 's-start': 'sw',
1312
- 's-end': 'se',
1313
- };
1314
- /** Mirrors the horizontal component of a physical direction. 'n' / 's' pass through unchanged. */
1315
- const MIRROR = {
1316
- n: 'n',
1317
- s: 's',
1318
- e: 'w',
1319
- w: 'e',
1320
- ne: 'nw',
1321
- nw: 'ne',
1322
- se: 'sw',
1323
- sw: 'se',
1324
- };
1325
- function isLogicalDirection(dir) {
1326
- return Object.prototype.hasOwnProperty.call(LTR_BASE, dir);
1327
- }
1328
- /**
1329
- * True when `element` sits inside a right-to-left context.
1330
- *
1331
- * Reads the *computed* `direction` style instead of `element.dir`, because
1332
- * `direction` is inherited and is almost never set on the resizable element
1333
- * itself - it's usually set on `<html>`, `<body>`, or some ancestor wrapper.
1334
- */
1335
- function isRtl(element) {
1336
- if (typeof window === 'undefined') {
1337
- return false;
1338
- }
1339
- return window.getComputedStyle(element).direction === 'rtl';
1340
- }
1341
- /**
1342
- * Resolves a logical direction ('start' | 'end' | 'n-start' | 'n-end' | 's-start' | 's-end')
1343
- * to a physical, viewport-space direction ('n' | 'e' | 's' | 'w' | 'ne' | 'nw' | 'se' | 'sw')
1344
- * based on `rtl`.
1345
- *
1346
- * Physical directions pass through untouched, so it's always safe to call this
1347
- * even when `dir` might already be physical (e.g. resolving a direction that was
1348
- * already resolved once before).
1349
- */
1350
- function resolveLogicalDirection(dir, rtl) {
1351
- if (!isLogicalDirection(dir)) {
1352
- return dir;
1353
- }
1354
- const base = LTR_BASE[dir];
1355
- return rtl ? MIRROR[base] : base;
1356
- }
1357
-
1358
1675
  /**
1359
1676
  * A drop list.
1360
1677
  *
@@ -1375,6 +1692,14 @@ class DropListRef {
1375
1692
  dropListGroup;
1376
1693
  /** Duration (ms) of the sibling slide animation. `0` disables animation. */
1377
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';
1378
1703
  _draggables = new Set();
1379
1704
  onDrop = new EventEmitter();
1380
1705
  /**
@@ -1836,14 +2161,35 @@ class DropListRef {
1836
2161
  if (ph.custom)
1837
2162
  return;
1838
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;
1839
2172
  // Occupies exactly the dragged item's footprint, so the gap reserved in the list — and the
1840
2173
  // sibling-displacement math derived from it — matches the item's real size.
1841
- set('width', `${rect.width}px`);
1842
- set('height', `${rect.height}px`);
1843
- // set('min-width', `${rect.width}px`);
1844
- // set('min-height', `${rect.height}px`);
1845
- // set('max-width', `${rect.width}px`);
1846
- // 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
+ }
1847
2193
  set('visibility', 'visible');
1848
2194
  set('opacity', '0.16');
1849
2195
  set('pointer-events', 'none');
@@ -1853,13 +2199,14 @@ class DropListRef {
1853
2199
  }
1854
2200
  /** Orientation to assume while a list has fewer than two items to measure. */
1855
2201
  function fallbackAxis(container) {
1856
- const s = getComputedStyle(container);
1857
- if (s.display.includes('flex'))
2202
+ const win = container.ownerDocument.defaultView;
2203
+ const s = win?.getComputedStyle(container);
2204
+ if (s?.display.includes('flex'))
1858
2205
  return s.flexDirection.startsWith('column') ? 'y' : 'x';
1859
- if (s.display.includes('grid'))
2206
+ if (s?.display.includes('grid'))
1860
2207
  return 'y';
1861
2208
  const child = container.firstElementChild;
1862
- if (child && getComputedStyle(child).display.startsWith('inline'))
2209
+ if (child && win?.getComputedStyle(child).display.startsWith('inline'))
1863
2210
  return 'x';
1864
2211
  return 'y';
1865
2212
  }
@@ -1870,8 +2217,28 @@ function measureHit(el) {
1870
2217
  return null;
1871
2218
  const clips = [];
1872
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
+ }
1873
2238
  for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
1874
- const s = getComputedStyle(node);
2239
+ const s = node.ownerDocument.defaultView?.getComputedStyle(node);
2240
+ if (!s)
2241
+ continue;
1875
2242
  const cx = s.overflowX !== 'visible';
1876
2243
  const cy = s.overflowY !== 'visible';
1877
2244
  if (!cx && !cy)
@@ -1957,6 +2324,8 @@ class DragDropService {
1957
2324
  continue;
1958
2325
  }
1959
2326
  if (isDocumentNode(node)) {
2327
+ // Same-origin iframe: continue the logical DOM ancestry through its frame element.
2328
+ // This lets a DropList inside an iframe inherit an outer NgxDropListGroup.
1960
2329
  const frame = node.defaultView?.frameElement;
1961
2330
  if (frame) {
1962
2331
  node = frame;
@@ -1985,15 +2354,13 @@ class DragDropService {
1985
2354
  node = node.host;
1986
2355
  continue;
1987
2356
  }
1988
- // iframe Document
1989
- // 9 === DOCUMENT_NODE
2357
+ // Cross a same-origin iframe boundary so groups can connect lists in both documents.
1990
2358
  if (node.nodeType === 9) {
1991
2359
  const frame = node.defaultView?.frameElement;
1992
2360
  if (frame) {
1993
2361
  node = frame;
1994
2362
  continue;
1995
2363
  }
1996
- // root document
1997
2364
  break;
1998
2365
  }
1999
2366
  node = node.parentNode;
@@ -2030,12 +2397,16 @@ class DragDropService {
2030
2397
  if (!source)
2031
2398
  return null;
2032
2399
  let best = null;
2400
+ const sourceDocument = dragEl?.ownerDocument ?? source.el?.ownerDocument;
2401
+ if (!sourceDocument)
2402
+ return null;
2033
2403
  for (const list of this.dropLists()) {
2034
2404
  if (!list.el || !source.isConnectedTo(list))
2035
2405
  continue;
2036
2406
  if (dragEl && dragEl !== list.el && dragEl.contains(list.el))
2037
2407
  continue;
2038
- if (!list._containsPoint(point.x, point.y))
2408
+ const targetPoint = convertPointBetweenDocuments(point, sourceDocument, list.el.ownerDocument);
2409
+ if (!targetPoint || !list._containsPoint(targetPoint.x, targetPoint.y))
2039
2410
  continue;
2040
2411
  if (!best)
2041
2412
  best = list;
@@ -2065,7 +2436,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2065
2436
  */
2066
2437
  class AutoScroller {
2067
2438
  raf = 0;
2068
- container = window;
2439
+ container;
2440
+ win;
2441
+ /** Document the pointer coordinates (`update(x, y)`) are expressed in. */
2442
+ pointerDoc;
2069
2443
  speed = { x: 0, y: 0 };
2070
2444
  onTick;
2071
2445
  edge;
@@ -2076,6 +2450,8 @@ class AutoScroller {
2076
2450
  }
2077
2451
  /** onTick اجرا می‌شود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder به‌روز شود. */
2078
2452
  start(fromEl, onTick) {
2453
+ this.win = fromEl.ownerDocument.defaultView ?? undefined;
2454
+ this.pointerDoc = fromEl.ownerDocument;
2079
2455
  this.container = findScrollableAncestor(fromEl);
2080
2456
  this.onTick = onTick;
2081
2457
  }
@@ -2084,21 +2460,37 @@ class AutoScroller {
2084
2460
  this.container = findScrollableAncestor(fromEl);
2085
2461
  }
2086
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
+ }
2087
2477
  const rect = getScrollRect(this.container);
2088
2478
  this.speed = {
2089
- x: edgeSpeed(clientX, rect.left, rect.right, this.edge, this.maxSpeed),
2090
- 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),
2091
2481
  };
2092
2482
  if ((this.speed.x || this.speed.y) && !this.raf) {
2093
- this.raf = requestAnimationFrame(this.tick);
2483
+ this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2094
2484
  }
2095
2485
  }
2096
2486
  stop() {
2097
2487
  if (this.raf)
2098
- cancelAnimationFrame(this.raf);
2488
+ this.win?.cancelAnimationFrame(this.raf);
2099
2489
  this.raf = 0;
2100
2490
  this.speed = { x: 0, y: 0 };
2101
2491
  this.onTick = undefined;
2492
+ this.win = undefined;
2493
+ this.pointerDoc = undefined;
2102
2494
  }
2103
2495
  tick = () => {
2104
2496
  if (!this.speed.x && !this.speed.y) {
@@ -2110,13 +2502,15 @@ class AutoScroller {
2110
2502
  container.scrollBy(this.speed.x, this.speed.y);
2111
2503
  }
2112
2504
  else {
2113
- container.scrollLeft += this.speed.x;
2114
- container.scrollTop += this.speed.y;
2505
+ // Edge speed is measured in viewport pixels; scrollLeft/Top are local to the scroller.
2506
+ const local = viewportDeltaToLocal(container, this.speed);
2507
+ container.scrollLeft += local.x;
2508
+ container.scrollTop += local.y;
2115
2509
  }
2116
2510
  // Pointer ثابت مانده ولی محتوا اسکرول شده؛
2117
2511
  // بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
2118
2512
  this.onTick?.();
2119
- this.raf = requestAnimationFrame(this.tick);
2513
+ this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2120
2514
  };
2121
2515
  }
2122
2516
  function edgeSpeed(pos, min, max, edge, maxSpeed) {
@@ -2128,25 +2522,47 @@ function edgeSpeed(pos, min, max, edge, maxSpeed) {
2128
2522
  }
2129
2523
  function getScrollRect(container) {
2130
2524
  if (isWindow(container)) {
2131
- return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
2132
- }
2133
- return container.getBoundingClientRect();
2525
+ return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
2526
+ }
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;
2134
2542
  }
2135
2543
  function findScrollableAncestor(el) {
2136
2544
  let node = el;
2137
2545
  while (node) {
2138
- const style = getComputedStyle(node);
2546
+ const win = node.ownerDocument.defaultView;
2547
+ if (!win)
2548
+ break;
2549
+ const style = win.getComputedStyle(node);
2139
2550
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2140
- 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;
2141
2557
  return node;
2142
2558
  }
2143
2559
  node = node.parentElement;
2144
2560
  }
2145
- return el.ownerDocument.defaultView ?? window;
2561
+ return el.ownerDocument.defaultView ?? el;
2146
2562
  }
2147
2563
  function getScrollPosition(container) {
2148
2564
  if (isWindow(container)) {
2149
- return { left: window.scrollX, top: window.scrollY };
2565
+ return { left: container.scrollX, top: container.scrollY };
2150
2566
  }
2151
2567
  return { left: container.scrollLeft, top: container.scrollTop };
2152
2568
  }
@@ -2212,6 +2628,19 @@ class NgxDraggable {
2212
2628
  /** Auto-scroll the nearest scrollable ancestor while dragging near its edge. */
2213
2629
  autoScroll = input(true, /* @ts-ignore */
2214
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 */ []));
2215
2644
  set data(v) {
2216
2645
  this._ref.data = v;
2217
2646
  }
@@ -2222,7 +2651,6 @@ class NgxDraggable {
2222
2651
  _ref = new DragRef();
2223
2652
  dragging = signal(false, /* @ts-ignore */
2224
2653
  ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
2225
- doc = inject(DOCUMENT);
2226
2654
  service = inject(DragDropService);
2227
2655
  scroller = new AutoScroller();
2228
2656
  down = false;
@@ -2236,11 +2664,13 @@ class NgxDraggable {
2236
2664
  removeEscape;
2237
2665
  removeCancel;
2238
2666
  removeLostCapture;
2239
- scrollTarget = window;
2667
+ scrollTarget;
2240
2668
  lastScroll = { left: 0, top: 0 };
2241
2669
  lastWindowScroll = { left: 0, top: 0 };
2242
2670
  removeContainerScroll;
2243
2671
  removeWindowScroll;
2672
+ removeDocKeydown;
2673
+ keyboardDrag = false;
2244
2674
  renderer = inject(Renderer2);
2245
2675
  constructor(host) {
2246
2676
  this.host = host;
@@ -2251,6 +2681,8 @@ class NgxDraggable {
2251
2681
  });
2252
2682
  }
2253
2683
  ngOnInit() {
2684
+ this.scrollTarget =
2685
+ this.host.nativeElement.ownerDocument.defaultView ?? this.host.nativeElement;
2254
2686
  this._ref.el = this.dragRootElement()
2255
2687
  ? (this.host.nativeElement.closest(this.dragRootElement()) ?? this.host.nativeElement)
2256
2688
  : this.host.nativeElement;
@@ -2282,6 +2714,7 @@ class NgxDraggable {
2282
2714
  this.removeEscape?.();
2283
2715
  this.removeCancel?.();
2284
2716
  this.removeLostCapture?.();
2717
+ this.removeDocKeydown?.();
2285
2718
  this.scroller.stop();
2286
2719
  this.stopScrollTracking();
2287
2720
  // Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
@@ -2292,7 +2725,7 @@ class NgxDraggable {
2292
2725
  pointerDown(e) {
2293
2726
  if (this.disabled() ||
2294
2727
  e.button !== 0 ||
2295
- // this.isInteractive(e.target) ||
2728
+ this.isExcluded(e.target) ||
2296
2729
  !this.isOnHandle(e.target))
2297
2730
  return;
2298
2731
  // Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
@@ -2302,20 +2735,33 @@ class NgxDraggable {
2302
2735
  claimedPointerEvents.add(e);
2303
2736
  e.preventDefault();
2304
2737
  this.down = true;
2738
+ this.keyboardDrag = false;
2305
2739
  this.pointerId = e.pointerId;
2306
- 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
+ }
2307
2749
  this.start = { x: e.clientX, y: e.clientY };
2308
2750
  this._ref.boundary = this.boundary();
2309
2751
  this._ref.lockAxis = this.lockAxis();
2310
2752
  this._ref.pointerDown(this.start);
2311
- this.removeMove = this.renderer.listen(this._ref.el, 'pointermove', (ev) => this.pointerMove(ev));
2312
- this.removeUp = this.renderer.listen(this._ref.el, 'pointerup', (ev) => this.pointerUp(ev));
2313
- this.removeEscape = this.renderer.listen(this.doc, 'keydown', (ev) => {
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));
2759
+ this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
2314
2760
  if (ev.key === 'Escape' && this.dragging())
2315
2761
  this.cancel();
2316
2762
  });
2317
- this.removeCancel = this.renderer.listen(this.doc, 'pointercancel', (ev) => this.pointerCancel(ev));
2318
- this.removeLostCapture = this.renderer.listen(this.doc, 'lostpointercapture', (ev) => this.pointerCancel(ev));
2763
+ this.removeCancel = this.renderer.listen(doc, 'pointercancel', (ev) => this.pointerCancel(ev));
2764
+ this.removeLostCapture = this.renderer.listen(doc, 'lostpointercapture', (ev) => this.pointerCancel(ev));
2319
2765
  }
2320
2766
  applyDragUpdate(p) {
2321
2767
  const before = this._ref.activeDropList;
@@ -2326,32 +2772,39 @@ class NgxDraggable {
2326
2772
  const after = this._ref.activeDropList;
2327
2773
  // Follow the hovered list; outside every list, scroll the page.
2328
2774
  if (after !== before)
2329
- this.scroller.retarget(after?.el ?? this.doc.body);
2775
+ this.scroller.retarget(after?.el ?? this._ref.el.ownerDocument.body);
2330
2776
  }
2331
2777
  this.dragMove.emit(p);
2332
2778
  }
2333
2779
  /** فراخوانی می‌شود دقیقاً وقتی drag واقعاً شروع می‌شود (چه با موس، چه با کیبورد). */
2334
2780
  startScrollTracking() {
2781
+ const win = this._ref.el.ownerDocument.defaultView;
2335
2782
  this.scrollTarget = findScrollableAncestor(this._ref.el);
2336
2783
  this.lastScroll = getScrollPosition(this.scrollTarget);
2337
- this.lastWindowScroll = { left: window.scrollX, top: window.scrollY };
2338
- if (this.scrollTarget !== window) {
2784
+ this.lastWindowScroll = { left: win?.scrollX ?? 0, top: win?.scrollY ?? 0 };
2785
+ if (win && this.scrollTarget !== win) {
2339
2786
  this.removeContainerScroll = this.renderer.listen(this.scrollTarget, 'scroll', () => this.handleContainerScroll());
2340
2787
  }
2341
2788
  // همیشه window رو هم جدا گوش می‌دیم؛ چون ممکنه container محلی نداشته باشیم
2342
2789
  // ولی خودِ صفحه اسکرول بشه.
2343
- this.removeWindowScroll = this.renderer.listen('window', 'scroll', () => this.handleWindowScroll());
2790
+ if (win) {
2791
+ this.removeWindowScroll = this.renderer.listen(win, 'scroll', () => this.handleWindowScroll());
2792
+ }
2344
2793
  }
2345
2794
  handleContainerScroll() {
2346
2795
  const pos = getScrollPosition(this.scrollTarget);
2347
2796
  const dx = pos.left - this.lastScroll.left;
2348
2797
  const dy = pos.top - this.lastScroll.top;
2349
2798
  this.lastScroll = pos;
2350
- this._ref.notifyScroll(dx, dy);
2799
+ const viewportDelta = localDeltaToViewport(this.scrollTarget, { x: dx, y: dy });
2800
+ this._ref.notifyScroll(viewportDelta.x, viewportDelta.y);
2351
2801
  }
2352
2802
  handleWindowScroll() {
2353
- const left = window.scrollX;
2354
- const top = window.scrollY;
2803
+ const win = this._ref.el.ownerDocument.defaultView;
2804
+ if (!win)
2805
+ return;
2806
+ const left = win.scrollX;
2807
+ const top = win.scrollY;
2355
2808
  const dx = left - this.lastWindowScroll.left;
2356
2809
  const dy = top - this.lastWindowScroll.top;
2357
2810
  this.lastWindowScroll = { left, top };
@@ -2387,20 +2840,21 @@ class NgxDraggable {
2387
2840
  pointerUp(e) {
2388
2841
  if (!this.down || e.pointerId !== this.pointerId)
2389
2842
  return;
2390
- if (this._ref.el.hasPointerCapture(this.pointerId)) {
2391
- this._ref.el.releasePointerCapture(this.pointerId);
2392
- }
2843
+ this.releaseCapture();
2393
2844
  if (this.dragging()) {
2394
2845
  // Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
2395
- if (this._ref.isListDrag) {
2396
- // The release position is authoritative for the drop target (a fast flick may end with
2397
- // a pointerup that has no preceding pointermove at that position).
2398
- const p = { x: e.clientX, y: e.clientY };
2399
- if (p.x !== this.lastPointer.x || p.y !== this.lastPointer.y)
2400
- this._ref.dragMove(p);
2401
- this._ref.endDrag();
2846
+ // The release position is authoritative for list sorting (a fast flick may end with
2847
+ // a pointerup that has no preceding pointermove at that position).
2848
+ const p = { x: e.clientX, y: e.clientY };
2849
+ if (this._ref.isListDrag && (p.x !== this.lastPointer.x || p.y !== this.lastPointer.y)) {
2850
+ this._ref.dragMove(p);
2402
2851
  }
2852
+ // endDrag must run for free drags too, otherwise their lifecycle never completes.
2853
+ this._ref.endDrag();
2403
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();
2404
2858
  }
2405
2859
  this.finish();
2406
2860
  this.dragEnd.emit(this._ref.pointer);
@@ -2409,18 +2863,33 @@ class NgxDraggable {
2409
2863
  pointerCancel(e) {
2410
2864
  if (!this.down || e.pointerId !== this.pointerId)
2411
2865
  return;
2412
- if (this.dragging() && this._ref.isListDrag) {
2866
+ if (this.dragging()) {
2413
2867
  this.cancel();
2414
2868
  return;
2415
2869
  }
2416
2870
  this.pointerUp(e);
2417
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
+ }
2418
2884
  cancel() {
2419
2885
  this._ref.cancelDrag();
2420
2886
  this.service.end(this._ref);
2421
2887
  this.finish();
2422
2888
  }
2423
2889
  finish() {
2890
+ this.releaseCapture();
2891
+ this.removeDocKeydown?.();
2892
+ this.removeDocKeydown = undefined;
2424
2893
  this.dragging.set(false);
2425
2894
  this.down = false;
2426
2895
  this.scroller.stop();
@@ -2433,10 +2902,20 @@ class NgxDraggable {
2433
2902
  this.removeMove = this.removeUp = this.removeEscape = undefined;
2434
2903
  this.removeCancel = this.removeLostCapture = undefined;
2435
2904
  }
2905
+ /** Keys already processed (the host listener and the document listener may both see one). */
2906
+ handledKeys = new WeakSet();
2436
2907
  /** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
2437
2908
  keyDown(e) {
2438
- 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))
2439
2917
  return;
2918
+ this.handledKeys.add(e);
2440
2919
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2441
2920
  const deltas = {
2442
2921
  ArrowLeft: { x: -step, y: 0 },
@@ -2462,6 +2941,7 @@ class NgxDraggable {
2462
2941
  this.dragging.set(true);
2463
2942
  this.service.begin(this._ref);
2464
2943
  this.startScrollTracking();
2944
+ this.beginKeyboardSession();
2465
2945
  this.dragStart.emit(origin);
2466
2946
  }
2467
2947
  this._ref.nudge(delta.x, delta.y);
@@ -2473,29 +2953,63 @@ class NgxDraggable {
2473
2953
  this._ref.endDrag();
2474
2954
  this.service.end(this._ref);
2475
2955
  this.dragEnd.emit(this._ref.pointer);
2476
- this.dragging.set(false);
2477
- this.stopScrollTracking();
2956
+ this.finish();
2957
+ this.restoreFocus();
2478
2958
  }
2479
2959
  else if (e.key === 'Escape' && this.dragging()) {
2480
2960
  e.preventDefault();
2481
2961
  this._ref.cancelDrag();
2482
2962
  this.service.end(this._ref);
2483
- this.dragging.set(false);
2963
+ this.finish();
2964
+ this.restoreFocus();
2484
2965
  }
2485
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
+ }
2486
2996
  isOnHandle(target) {
2487
2997
  if (!this.dragHandle())
2488
2998
  return true;
2489
2999
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2490
- return target instanceof HTMLElement && !!target.closest(this.dragHandle());
3000
+ return isElementTarget(target) && !!target.closest(this.dragHandle());
2491
3001
  }
2492
- isInteractive(target) {
2493
- // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2494
- return (target instanceof HTMLElement &&
2495
- !!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);
2496
3010
  }
2497
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 });
2498
- 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 });
2499
3013
  }
2500
3014
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
2501
3015
  type: Directive,
@@ -2510,13 +3024,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2510
3024
  '[class.disable-drag]': 'disabled()',
2511
3025
  },
2512
3026
  }]
2513
- }], 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: [{
2514
3028
  type: Input,
2515
3029
  args: ['data']
2516
3030
  }], customPreview: [{
2517
3031
  type: ContentChild,
2518
3032
  args: [NgxDragPreview]
2519
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
+ }
3042
+ function isElementTarget(target) {
3043
+ return (!!target &&
3044
+ typeof target === 'object' &&
3045
+ 'nodeType' in target &&
3046
+ target.nodeType === 1 &&
3047
+ 'closest' in target);
3048
+ }
2520
3049
 
2521
3050
  const ALL_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
2522
3051
  const KEYBOARD_STEP = 8;
@@ -2558,9 +3087,19 @@ class NgxResizable {
2558
3087
  beforeHeight = '';
2559
3088
  beforeTransform = '';
2560
3089
  pointerId = -1;
3090
+ startWidth = 0;
3091
+ startHeight = 0;
3092
+ removeDocumentListeners = [];
3093
+ renderer = inject(Renderer2);
2561
3094
  constructor(el) {
2562
3095
  this.el = el;
2563
3096
  }
3097
+ ngOnDestroy() {
3098
+ if (this.resizing()) {
3099
+ this.restoreBeforeResize();
3100
+ this.finish();
3101
+ }
3102
+ }
2564
3103
  onPointerDown(e) {
2565
3104
  if (this.disabled() || e.button !== 0) {
2566
3105
  return;
@@ -2585,6 +3124,10 @@ class NgxResizable {
2585
3124
  y: e.clientY,
2586
3125
  };
2587
3126
  this.beforeTransition = element.style.transition;
3127
+ this.beforeWidth = element.style.width;
3128
+ this.beforeHeight = element.style.height;
3129
+ this.startWidth = element.offsetWidth;
3130
+ this.startHeight = element.offsetHeight;
2588
3131
  /**
2589
3132
  * Keep DragRef's transform as the immutable base.
2590
3133
  *
@@ -2594,7 +3137,20 @@ class NgxResizable {
2594
3137
  this.beforeTransform = element.style.transform;
2595
3138
  this.resizing.set(true);
2596
3139
  element.style.transition = 'none';
2597
- 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
+ }
3146
+ const doc = element.ownerDocument;
3147
+ this.removeDocumentListeners = [
3148
+ this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
3149
+ this.renderer.listen(doc, 'pointerup', (event) => this.onPointerUp(event)),
3150
+ this.renderer.listen(doc, 'pointercancel', (event) => this.onPointerCancel(event)),
3151
+ this.renderer.listen(doc, 'lostpointercapture', (event) => this.onPointerCancel(event)),
3152
+ this.renderer.listen(doc, 'keydown', (event) => this.onDocumentKeyDown(event)),
3153
+ ];
2598
3154
  const out = this.calculate(e);
2599
3155
  this.apply(out);
2600
3156
  this.resizeStart.emit(out);
@@ -2624,16 +3180,30 @@ class NgxResizable {
2624
3180
  return;
2625
3181
  }
2626
3182
  e.preventDefault();
3183
+ this.restoreBeforeResize();
3184
+ this.finish();
3185
+ }
3186
+ onPointerCancel(e) {
3187
+ if (!this.resizing() || e.pointerId !== this.pointerId)
3188
+ return;
3189
+ this.restoreBeforeResize();
3190
+ this.finish();
3191
+ }
3192
+ restoreBeforeResize() {
2627
3193
  const element = this.el.nativeElement;
2628
3194
  element.style.width = this.beforeWidth;
2629
3195
  element.style.height = this.beforeHeight;
2630
3196
  element.style.transform = this.beforeTransform;
2631
- this.finish();
2632
3197
  }
2633
3198
  onHandleKeyDown(e) {
2634
3199
  if (this.disabled()) {
2635
3200
  return;
2636
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
+ }
2637
3207
  const rtl = isRtl(this.el.nativeElement);
2638
3208
  const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
2639
3209
  let dw = 0;
@@ -2655,10 +3225,11 @@ class NgxResizable {
2655
3225
  }
2656
3226
  e.preventDefault();
2657
3227
  const rect = this.el.nativeElement.getBoundingClientRect();
2658
- const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), rect.width + dw)), 'x');
2659
- const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), rect.height + dh)), 'y');
2660
- this.el.nativeElement.style.width = `${width}px`;
2661
- this.el.nativeElement.style.height = `${height}px`;
3228
+ const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
3229
+ const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
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`;
2662
3233
  const out = {
2663
3234
  width,
2664
3235
  height,
@@ -2672,10 +3243,14 @@ class NgxResizable {
2672
3243
  this.resizeEnd.emit(out);
2673
3244
  }
2674
3245
  calculate(e) {
2675
- const dx = e.clientX - this.pointer.x;
2676
- const dy = e.clientY - this.pointer.y;
2677
- let width = this.start.width;
2678
- let height = this.start.height;
3246
+ const localDelta = viewportDeltaToLocal(this.el.nativeElement, {
3247
+ x: e.clientX - this.pointer.x,
3248
+ y: e.clientY - this.pointer.y,
3249
+ });
3250
+ const dx = localDelta.x;
3251
+ const dy = localDelta.y;
3252
+ let width = this.startWidth;
3253
+ let height = this.startHeight;
2679
3254
  /**
2680
3255
  * East
2681
3256
  */
@@ -2708,23 +3283,27 @@ class NgxResizable {
2708
3283
  * This is useful for boundary calculations and
2709
3284
  * emitted resize events.
2710
3285
  */
3286
+ const matrix = getLocalToViewportMatrix(this.el.nativeElement);
3287
+ const scaleX = Math.hypot(matrix.a, matrix.b) || 1;
3288
+ const scaleY = Math.hypot(matrix.c, matrix.d) || 1;
2711
3289
  let left = this.start.left;
2712
3290
  let top = this.start.top;
2713
3291
  if (this.direction.includes('w')) {
2714
- left = this.start.right - width;
3292
+ left = this.start.right - width * scaleX;
2715
3293
  }
2716
3294
  if (this.direction.includes('n')) {
2717
- top = this.start.bottom - height;
3295
+ top = this.start.bottom - height * scaleY;
2718
3296
  }
2719
3297
  let moveLeft = left - this.start.left;
2720
3298
  let moveTop = top - this.start.top;
2721
3299
  /**
2722
3300
  * Boundary constraint.
2723
3301
  */
2724
- if (this.boundary) {
2725
- const clamped = clampResizeWithinBoundary(this.boundary()?.getBoundingClientRect(), this.start, width, height, moveLeft, moveTop);
2726
- width = clamped.width;
2727
- height = clamped.height;
3302
+ const boundaryEl = this.boundary();
3303
+ if (boundaryEl) {
3304
+ const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
3305
+ width = clamped.width / scaleX;
3306
+ height = clamped.height / scaleY;
2728
3307
  moveLeft = clamped.left;
2729
3308
  moveTop = clamped.top;
2730
3309
  left = this.start.left + moveLeft;
@@ -2755,8 +3334,9 @@ class NgxResizable {
2755
3334
  * Changing width/height may cause the browser to re-layout
2756
3335
  * the element, especially for relative/flex/grid/static layouts.
2757
3336
  */
2758
- element.style.width = `${out.width}px`;
2759
- 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`;
2760
3340
  /**
2761
3341
  * Force/read layout after changing the size.
2762
3342
  */
@@ -2787,12 +3367,33 @@ class NgxResizable {
2787
3367
  * correction required by Resize.
2788
3368
  */
2789
3369
  const baseTransform = this.beforeTransform && this.beforeTransform !== 'none' ? `${this.beforeTransform} ` : '';
3370
+ const localCorrection = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
2790
3371
  element.style.transform =
2791
- `${baseTransform}translate3d(` + `${correctionX}px, ` + `${correctionY}px, 0)`;
3372
+ `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
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) };
2792
3390
  }
2793
3391
  clampGrid(value, axis) {
2794
- const grid = typeof this.grid() === 'number' ? this.grid : this.grid()?.[axis];
2795
- 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) {
2796
3397
  return value;
2797
3398
  }
2798
3399
  return Math.round(value / grid) * grid;
@@ -2800,7 +3401,19 @@ class NgxResizable {
2800
3401
  finish() {
2801
3402
  this.resizing.set(false);
2802
3403
  this.el.nativeElement.style.transition = this.beforeTransition;
3404
+ const pointerId = this.pointerId;
2803
3405
  this.pointerId = -1;
3406
+ for (const remove of this.removeDocumentListeners)
3407
+ remove();
3408
+ this.removeDocumentListeners = [];
3409
+ try {
3410
+ if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3411
+ this.el.nativeElement.releasePointerCapture(pointerId);
3412
+ }
3413
+ }
3414
+ catch {
3415
+ /* already released */
3416
+ }
2804
3417
  this.beforeTransform = '';
2805
3418
  }
2806
3419
  resolvedDirections() {
@@ -2823,7 +3436,7 @@ class NgxResizable {
2823
3436
  * Explicit resize handle has priority.
2824
3437
  */
2825
3438
  // TODO notsafe instanceof:node.nodeType === Node.ELEMENT_NODE
2826
- const handle = e.target instanceof HTMLElement
3439
+ const handle = isHTMLElementTarget(e.target)
2827
3440
  ? e.target.closest('[data-ngx-resize-handle]')
2828
3441
  : null;
2829
3442
  if (handle && element.contains(handle)) {
@@ -2866,7 +3479,7 @@ class NgxResizable {
2866
3479
  return this.resolvedDirections().includes(direction) ? direction : null;
2867
3480
  }
2868
3481
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
2869
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxResizable, isStandalone: true, selector: "[NgxResizable],[ngxResizable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, directions: { classPropertyName: "directions", publicName: "directions", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", minWidth: "minWidthChange", minHeight: "minHeightChange", maxWidth: "maxWidthChange", maxHeight: "maxHeightChange", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "document:pointermove": "onPointerMove($event)", "document:pointerup": "onPointerUp($event)", "document:keydown": "onDocumentKeyDown($event)", "keydown": "onHandleKeyDown($event)" }, properties: { "style.touch-action": "\"none\"" }, classAttribute: "ngx-resizable" }, ngImport: i0 });
3482
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxResizable, isStandalone: true, selector: "[NgxResizable],[ngxResizable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, directions: { classPropertyName: "directions", publicName: "directions", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", minWidth: "minWidthChange", minHeight: "minHeightChange", maxWidth: "maxWidthChange", maxHeight: "maxHeightChange", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onHandleKeyDown($event)" }, properties: { "style.touch-action": "\"none\"" }, classAttribute: "ngx-resizable" }, ngImport: i0 });
2870
3483
  }
2871
3484
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, decorators: [{
2872
3485
  type: Directive,
@@ -2880,19 +3493,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2880
3493
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }, { type: i0.Output, args: ["minWidthChange"] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }, { type: i0.Output, args: ["minHeightChange"] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }, { type: i0.Output, args: ["maxWidthChange"] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }, { type: i0.Output, args: ["maxHeightChange"] }], directions: [{ type: i0.Input, args: [{ isSignal: true, alias: "directions", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], resizeStart: [{ type: i0.Output, args: ["resizeStart"] }], resizeMove: [{ type: i0.Output, args: ["resizeMove"] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }], onPointerDown: [{
2881
3494
  type: HostListener,
2882
3495
  args: ['pointerdown', ['$event']]
2883
- }], onPointerMove: [{
2884
- type: HostListener,
2885
- args: ['document:pointermove', ['$event']]
2886
- }], onPointerUp: [{
2887
- type: HostListener,
2888
- args: ['document:pointerup', ['$event']]
2889
- }], onDocumentKeyDown: [{
2890
- type: HostListener,
2891
- args: ['document:keydown', ['$event']]
2892
3496
  }], onHandleKeyDown: [{
2893
3497
  type: HostListener,
2894
3498
  args: ['keydown', ['$event']]
2895
3499
  }] } });
3500
+ function isHTMLElementTarget(target) {
3501
+ return (!!target &&
3502
+ typeof target === 'object' &&
3503
+ 'nodeType' in target &&
3504
+ target.nodeType === 1 &&
3505
+ 'closest' in target);
3506
+ }
2896
3507
 
2897
3508
  const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
2898
3509
  /**
@@ -2942,6 +3553,13 @@ class NgxDropList {
2942
3553
  set disableSort(value) {
2943
3554
  this._ref.disableSort = value === true;
2944
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
+ }
2945
3563
  drop = new EventEmitter();
2946
3564
  customPlaceholder;
2947
3565
  sub;
@@ -2973,7 +3591,7 @@ class NgxDropList {
2973
3591
  this.service.removeDropList(this._ref);
2974
3592
  }
2975
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 });
2976
- 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 });
2977
3595
  }
2978
3596
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
2979
3597
  type: Directive,
@@ -2989,6 +3607,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2989
3607
  type: Input
2990
3608
  }], disableSort: [{
2991
3609
  type: Input
3610
+ }], placeholderFit: [{
3611
+ type: Input
3612
+ }], sortAnimationDuration: [{
3613
+ type: Input
2992
3614
  }], drop: [{
2993
3615
  type: Output
2994
3616
  }], customPlaceholder: [{
@@ -3061,9 +3683,54 @@ function clamp(v, max) {
3061
3683
  return Math.max(0, Math.min(max, v));
3062
3684
  }
3063
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
+
3064
3731
  /**
3065
3732
  * Generated bundle index. Do not edit.
3066
3733
  */
3067
3734
 
3068
- 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 };
3069
3736
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map