ngx-kit 9.0.0 → 9.1.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,15 +69,67 @@ 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';
84
135
  class DragPreviewRef {
@@ -135,9 +186,10 @@ class DragPreviewRef {
135
186
  willChange: 'transform',
136
187
  zIndex: PREVIEW_Z_INDEX,
137
188
  });
189
+ const win = doc.defaultView;
138
190
  if (isRtl(doc.body)) {
139
191
  Object.assign(this.element.style, {
140
- right: `${window.innerWidth - rect.right}px`,
192
+ right: `${(win?.innerWidth ?? rect.right) - rect.right}px`,
141
193
  left: 'auto',
142
194
  });
143
195
  }
@@ -147,7 +199,7 @@ class DragPreviewRef {
147
199
  right: 'auto',
148
200
  });
149
201
  }
150
- doc.body.appendChild(this.element);
202
+ (doc.body ?? doc.documentElement).appendChild(this.element);
151
203
  return this.element;
152
204
  }
153
205
  detach() {
@@ -161,6 +213,139 @@ class DragPreviewRef {
161
213
  }
162
214
  }
163
215
 
216
+ function viewportDeltaToLocal(element, delta, includeElementTransform = true) {
217
+ const matrix = getLocalToViewportMatrix(element, includeElementTransform);
218
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
219
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-8) {
220
+ // A zero-scale ancestor has no invertible coordinate space. Avoid NaN/Infinity.
221
+ return { x: 0, y: 0 };
222
+ }
223
+ const inverseA = matrix.d / determinant;
224
+ const inverseB = -matrix.b / determinant;
225
+ const inverseC = -matrix.c / determinant;
226
+ const inverseD = matrix.a / determinant;
227
+ return {
228
+ x: inverseA * delta.x + inverseC * delta.y,
229
+ y: inverseB * delta.x + inverseD * delta.y,
230
+ };
231
+ }
232
+ function localDeltaToViewport(element, delta) {
233
+ const matrix = getLocalToViewportMatrix(element);
234
+ return {
235
+ x: matrix.a * delta.x + matrix.c * delta.y,
236
+ y: matrix.b * delta.x + matrix.d * delta.y,
237
+ };
238
+ }
239
+ /** Returns the composed 2D linear transform from element-local to viewport space. */
240
+ function getLocalToViewportMatrix(element, includeElementTransform = true) {
241
+ const win = element.ownerDocument.defaultView;
242
+ if (!win || typeof win.DOMMatrix !== 'function') {
243
+ return { a: 1, b: 0, c: 0, d: 1 };
244
+ }
245
+ let result = { a: 1, b: 0, c: 0, d: 1 };
246
+ let current = includeElementTransform ? element : element.parentElement;
247
+ while (current) {
248
+ const style = win.getComputedStyle(current);
249
+ const transform = style.transform;
250
+ if (transform && transform !== 'none') {
251
+ try {
252
+ const matrix = new win.DOMMatrix(transform);
253
+ result = multiplyLinear({ a: matrix.a, b: matrix.b, c: matrix.c, d: matrix.d }, result);
254
+ }
255
+ catch {
256
+ // Ignore unsupported/malformed transforms and continue with remaining ancestors.
257
+ }
258
+ }
259
+ // CSS `zoom` is not represented consistently in computed `transform` across browsers.
260
+ const zoom = Number.parseFloat(style.getPropertyValue('zoom'));
261
+ if (Number.isFinite(zoom) && zoom > 0 && zoom !== 1) {
262
+ result = multiplyLinear({ a: zoom, b: 0, c: 0, d: zoom }, result);
263
+ }
264
+ current = current.parentElement;
265
+ }
266
+ return result;
267
+ }
268
+ function multiplyLinear(left, right) {
269
+ return {
270
+ a: left.a * right.a + left.c * right.b,
271
+ b: left.b * right.a + left.d * right.b,
272
+ c: left.a * right.c + left.c * right.d,
273
+ d: left.b * right.c + left.d * right.d,
274
+ };
275
+ }
276
+ /**
277
+ * Converts a client-space point from one same-origin Document viewport to another.
278
+ * The conversion walks both frameElement chains through their common top-level document.
279
+ * Cross-origin frame boundaries cannot be inspected and return null.
280
+ */
281
+ function convertPointBetweenDocuments(point, fromDocument, toDocument) {
282
+ if (fromDocument === toDocument)
283
+ return { ...point };
284
+ const toTop = (doc, value) => {
285
+ let current = doc;
286
+ let result = { ...value };
287
+ const visited = new Set();
288
+ while (current.defaultView?.frameElement) {
289
+ if (visited.has(current))
290
+ return null;
291
+ visited.add(current);
292
+ let frame;
293
+ try {
294
+ frame = current.defaultView.frameElement;
295
+ }
296
+ catch {
297
+ return null;
298
+ }
299
+ const iframe = frame;
300
+ const rect = iframe.getBoundingClientRect();
301
+ const scaleX = iframe.offsetWidth ? rect.width / iframe.offsetWidth : 1;
302
+ const scaleY = iframe.offsetHeight ? rect.height / iframe.offsetHeight : 1;
303
+ result = {
304
+ x: rect.left + iframe.clientLeft * scaleX + result.x * scaleX,
305
+ y: rect.top + iframe.clientTop * scaleY + result.y * scaleY,
306
+ };
307
+ current = iframe.ownerDocument;
308
+ }
309
+ return { document: current, point: result };
310
+ };
311
+ const from = toTop(fromDocument, point);
312
+ if (!from)
313
+ return null;
314
+ const targetFrames = [];
315
+ let current = toDocument;
316
+ const visited = new Set();
317
+ while (current && current !== from.document) {
318
+ if (visited.has(current))
319
+ return null;
320
+ visited.add(current);
321
+ let frame;
322
+ try {
323
+ frame = current.defaultView?.frameElement ?? null;
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ if (!frame)
329
+ return null;
330
+ targetFrames.push(frame);
331
+ current = frame.ownerDocument;
332
+ }
333
+ if (current !== from.document)
334
+ return null;
335
+ let result = from.point;
336
+ for (let i = targetFrames.length - 1; i >= 0; i--) {
337
+ const iframe = targetFrames[i];
338
+ const rect = iframe.getBoundingClientRect();
339
+ const scaleX = iframe.offsetWidth ? rect.width / iframe.offsetWidth : 1;
340
+ const scaleY = iframe.offsetHeight ? rect.height / iframe.offsetHeight : 1;
341
+ result = {
342
+ x: (result.x - rect.left) / scaleX - iframe.clientLeft,
343
+ y: (result.y - rect.top) / scaleY - iframe.clientTop,
344
+ };
345
+ }
346
+ return result;
347
+ }
348
+
164
349
  let zIndexCounter = 1000;
165
350
  class DragRef {
166
351
  data;
@@ -197,6 +382,8 @@ class DragRef {
197
382
  sourceDisplay = '';
198
383
  sourcePointerEvents = '';
199
384
  sourceTransition = '';
385
+ sourceWillChange = '';
386
+ sourceTransform = '';
200
387
  /** True while a drop-list drag (body preview + placeholder) is in progress. Free drag: always false. */
201
388
  listDrag = false;
202
389
  cachedLists = [];
@@ -212,7 +399,9 @@ class DragRef {
212
399
  scrollListener;
213
400
  resizeListener;
214
401
  init() {
215
- this.previousTransform = getComputedStyle?.(this.el)?.getPropertyValue?.('transform');
402
+ this.previousTransform =
403
+ this.el.ownerDocument.defaultView?.getComputedStyle(this.el).transform ?? '';
404
+ this.sourceTransform = this.el.style.transform;
216
405
  }
217
406
  withDropList(list) {
218
407
  if (this.dropList === list)
@@ -242,13 +431,16 @@ class DragRef {
242
431
  const list = this.dropList && this.dropList.el === this.el.parentElement ? this.dropList : null;
243
432
  this.originDropList = list;
244
433
  this.originIndex = list?.getItemIndex(this) ?? -1;
245
- this.previousTransform = getComputedStyle(this.el).getPropertyValue('transform');
434
+ const win = this.el.ownerDocument.defaultView;
435
+ this.previousTransform = win?.getComputedStyle(this.el).transform ?? '';
436
+ this.sourceTransform = this.el.style.transform;
246
437
  this.isDragging.set(true);
247
438
  this.previousZIndex = this.el.style.zIndex;
248
439
  this.sourceVisibility = this.el.style.visibility;
249
440
  this.sourceDisplay = this.el.style.display;
250
441
  this.sourcePointerEvents = this.el.style.pointerEvents;
251
442
  this.sourceTransition = this.el.style.transition;
443
+ this.sourceWillChange = this.el.style.willChange;
252
444
  this.el.classList.add('ngx-draggable--dragging');
253
445
  this.el.style.willChange = 'transform';
254
446
  this.el.style.transition = 'none';
@@ -334,7 +526,8 @@ class DragRef {
334
526
  this.el.style.visibility = this.sourceVisibility;
335
527
  this.el.style.display = this.sourceDisplay;
336
528
  this.el.style.pointerEvents = this.sourcePointerEvents;
337
- this.el.style.willChange = '';
529
+ this.el.style.willChange = this.sourceWillChange;
530
+ this.el.style.transition = this.sourceTransition;
338
531
  this.el.style.zIndex = this.previousZIndex;
339
532
  }
340
533
  cancelDrag() {
@@ -350,7 +543,7 @@ class DragRef {
350
543
  return;
351
544
  }
352
545
  // ---- free drag: unchanged ----
353
- this.el.style.transform = this.previousTransform;
546
+ this.el.style.transform = this.sourceTransform;
354
547
  this.el.style.visibility = this.sourceVisibility;
355
548
  this.el.style.pointerEvents = this.sourcePointerEvents;
356
549
  this.scrollCompensation = { x: 0, y: 0 };
@@ -358,10 +551,24 @@ class DragRef {
358
551
  }
359
552
  /** Abort without emitting anything (the directive was destroyed mid-drag). Idempotent. */
360
553
  dispose() {
361
- if (!this.listDrag)
554
+ if (!this.isDragging())
362
555
  return;
363
556
  this.isDragging.set(false);
364
- this.finishListDrag(false);
557
+ this.position.set({ x: 0, y: 0 });
558
+ if (this.listDrag) {
559
+ this.finishListDrag(false);
560
+ return;
561
+ }
562
+ this.el.style.transform = this.sourceTransform;
563
+ this.el.style.visibility = this.sourceVisibility;
564
+ this.el.style.display = this.sourceDisplay;
565
+ this.el.style.pointerEvents = this.sourcePointerEvents;
566
+ this.el.style.transition = this.sourceTransition;
567
+ this.el.style.willChange = this.sourceWillChange;
568
+ this.el.style.zIndex = this.previousZIndex;
569
+ this.el.classList.remove('ngx-draggable--dragging');
570
+ this.scrollCompensation = { x: 0, y: 0 };
571
+ this.position.set({ x: 0, y: 0 });
365
572
  }
366
573
  /** True while a drop-list drag is running. */
367
574
  get isListDrag() {
@@ -410,14 +617,17 @@ class DragRef {
410
617
  : '';
411
618
  const tx = this.moveDx + this.scrollCompensation.x;
412
619
  const ty = this.moveDy + this.scrollCompensation.y;
413
- this.el.style.transform = `${base}translate3d(${tx}px, ${ty}px, 0)`;
620
+ const local = viewportDeltaToLocal(this.el, { x: tx, y: ty });
621
+ this.el.style.transform = `${base}translate3d(${local.x}px, ${local.y}px, 0)`;
414
622
  }
415
623
  // -------------------------------------------------------------------------------------
416
624
  // drop-list drag
417
625
  // -------------------------------------------------------------------------------------
418
626
  beginListDrag(origin, pointer) {
419
627
  const doc = this.el.ownerDocument;
420
- const win = doc.defaultView ?? window;
628
+ const win = doc.defaultView;
629
+ if (!win)
630
+ return;
421
631
  this.dropListsSnapshot = null; // force refreshDropCandidates() to do a full pass below
422
632
  this.refreshDropCandidates(origin);
423
633
  this.lastEffective = { ...pointer };
@@ -484,9 +694,11 @@ class DragRef {
484
694
  if (!origin)
485
695
  return;
486
696
  this.refreshDropCandidates(origin);
487
- const next = this.resolveDropList(pointer);
697
+ const resolved = this.resolveDropList(pointer);
698
+ const next = resolved?.list ?? null;
699
+ const targetPointer = resolved?.pointer ?? pointer;
488
700
  if (next === this.activeDropList) {
489
- next?.sortItem(this, pointer);
701
+ next?.sortItem(this, targetPointer);
490
702
  return;
491
703
  }
492
704
  const prev = this.activeDropList;
@@ -501,7 +713,7 @@ class DragRef {
501
713
  origin.exit(this); // truly outside the origin's territory now: reset it (idempotent)
502
714
  }
503
715
  this.activeDropList = next;
504
- next?.enter(this, pointer.x, pointer.y); // fresh entry, or resumes a still-alive (frozen) session
716
+ next?.enter(this, targetPointer.x, targetPointer.y); // coordinates belong to the target document
505
717
  }
506
718
  /**
507
719
  * Innermost connected list under the pointer, from the precomputed candidates: deeper always
@@ -513,15 +725,22 @@ class DragRef {
513
725
  let best = null;
514
726
  let bestDepth = -1;
515
727
  for (const { list, depth } of this.dropCandidates) {
516
- if (!list._containsPoint(pointer.x, pointer.y))
728
+ const targetDocument = list.el?.ownerDocument;
729
+ if (!targetDocument)
730
+ continue;
731
+ // Pointer coordinates originate in the draggable's document. A same-origin iframe has
732
+ // its own viewport coordinate space, so convert before testing its DropList geometry.
733
+ const targetPoint = convertPointBetweenDocuments(pointer, this.el.ownerDocument, targetDocument);
734
+ if (!targetPoint || !list._containsPoint(targetPoint.x, targetPoint.y))
517
735
  continue;
518
736
  if (!best || depth > bestDepth) {
519
- best = list;
737
+ best = { list, pointer: targetPoint };
520
738
  bestDepth = depth;
521
739
  }
522
- else if (depth === bestDepth && best !== this.activeDropList) {
523
- if (list === this.activeDropList || list._area() < best._area())
524
- best = list;
740
+ else if (depth === bestDepth && best.list !== this.activeDropList) {
741
+ if (list === this.activeDropList || list._area() < best.list._area()) {
742
+ best = { list, pointer: targetPoint };
743
+ }
525
744
  }
526
745
  }
527
746
  return best;
@@ -536,7 +755,7 @@ class DragRef {
536
755
  if (this.scrollListener)
537
756
  doc.removeEventListener('scroll', this.scrollListener, { capture: true });
538
757
  if (this.resizeListener)
539
- (doc.defaultView ?? window).removeEventListener('resize', this.resizeListener);
758
+ doc.defaultView?.removeEventListener('resize', this.resizeListener);
540
759
  this.scrollListener = this.resizeListener = undefined;
541
760
  const target = commit ? this.activeDropList : null;
542
761
  const event = target?._finish(this, true) ?? null;
@@ -556,7 +775,7 @@ class DragRef {
556
775
  this.el.style.display = this.sourceDisplay;
557
776
  this.el.style.pointerEvents = this.sourcePointerEvents;
558
777
  this.el.style.transition = this.sourceTransition;
559
- this.el.style.willChange = '';
778
+ this.el.style.willChange = this.sourceWillChange;
560
779
  this.el.style.zIndex = this.previousZIndex;
561
780
  if (event && target)
562
781
  target.onDrop.emit(event);
@@ -564,6 +783,7 @@ class DragRef {
564
783
  updatePreview() {
565
784
  if (!this.preview || !this.preview.element)
566
785
  return;
786
+ // Preview is fixed in the source document's viewport coordinate space.
567
787
  this.preview.element.style.transform = `translate3d(${this.moveDx}px, ${this.moveDy}px, 0)`;
568
788
  }
569
789
  }
@@ -586,7 +806,7 @@ class ElementHelper {
586
806
  return false;
587
807
  }
588
808
  static findParentBySelector(elm, selector) {
589
- var all = document.querySelectorAll(selector);
809
+ var all = elm.ownerDocument.querySelectorAll(selector);
590
810
  var cur = elm.parentNode;
591
811
  while (cur && !ElementHelper.collectionHas(all, cur)) {
592
812
  //keep going up until you find a match
@@ -615,15 +835,17 @@ function getFirstLevelDraggables(container) {
615
835
  return firstLevel;
616
836
  }
617
837
  function isElementNode(node) {
618
- return node?.nodeType === Node.ELEMENT_NODE;
838
+ return node?.nodeType === Node.ELEMENT_NODE; // 1
619
839
  }
620
840
  function isDocumentNode(node) {
621
- return node?.nodeType === Node.DOCUMENT_NODE;
841
+ return node?.nodeType === Node.DOCUMENT_NODE; // 9
622
842
  }
623
843
  function isShadowRootNode(node) {
624
844
  return node?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && 'host' in node;
625
845
  }
626
846
  function getOwnerWindow(node) {
847
+ if (isDocumentNode(node))
848
+ return node.defaultView;
627
849
  return node?.ownerDocument?.defaultView ?? null;
628
850
  }
629
851
  function isWindow(value) {
@@ -950,7 +1172,7 @@ class ScrollFrame {
950
1172
  * @param start first element to inspect (walks up through its ancestors)
951
1173
  * @param includeStart whether `start` itself counts (true for a list whose own scroll moves its items)
952
1174
  */
953
- constructor(start, includeStart = true, win = window) {
1175
+ constructor(start, includeStart = true, win = start?.ownerDocument.defaultView ?? null) {
954
1176
  this.win = win;
955
1177
  let node = includeStart ? start : (start?.parentElement ?? null);
956
1178
  const root = start?.ownerDocument.documentElement;
@@ -960,7 +1182,8 @@ class ScrollFrame {
960
1182
  this.targets.push(node);
961
1183
  node = node.parentElement;
962
1184
  }
963
- this.targets.push(win);
1185
+ if (win)
1186
+ this.targets.push(win);
964
1187
  this.origin = this.read();
965
1188
  }
966
1189
  /** Call when any scroll event fires. */
@@ -980,23 +1203,26 @@ class ScrollFrame {
980
1203
  let x = 0;
981
1204
  let y = 0;
982
1205
  for (const t of this.targets) {
983
- if (t === this.win) {
1206
+ if (this.win && t === this.win) {
984
1207
  x += this.win.scrollX;
985
1208
  y += this.win.scrollY;
986
1209
  }
987
1210
  else {
988
- x += t.scrollLeft;
989
- y += t.scrollTop;
1211
+ const el = t;
1212
+ // Element scroll offsets are local CSS pixels; geometry snapshots are viewport pixels.
1213
+ const delta = localDeltaToViewport(el, { x: el.scrollLeft, y: el.scrollTop });
1214
+ x += delta.x;
1215
+ y += delta.y;
990
1216
  }
991
1217
  }
992
1218
  return { x, y };
993
1219
  }
994
1220
  }
995
1221
  function scrolls(el) {
996
- const s = getComputedStyle?.(el);
1222
+ const s = el.ownerDocument.defaultView?.getComputedStyle(el);
997
1223
  // `hidden` containers still scroll programmatically; `clip` never does.
998
1224
  const scrollish = (v) => v === 'auto' || v === 'scroll' || v === 'overlay' || v === 'hidden';
999
- return scrollish(s.overflowX) || scrollish(s.overflowY);
1225
+ return !!s && (scrollish(s.overflowX) || scrollish(s.overflowY));
1000
1226
  }
1001
1227
 
1002
1228
  /**
@@ -1007,7 +1233,7 @@ function scrolls(el) {
1007
1233
  */
1008
1234
  function makeSpacer(container) {
1009
1235
  for (const n of Array.from(container.childNodes)) {
1010
- if (n.nodeType === Node.TEXT_NODE && n.nodeValue && /^\s+$/.test(n.nodeValue)) {
1236
+ if (n.nodeType === 3 && n.nodeValue && /^\s+$/.test(n.nodeValue)) {
1011
1237
  return container.ownerDocument.createTextNode(n.nodeValue);
1012
1238
  }
1013
1239
  }
@@ -1215,6 +1441,7 @@ class SortSession {
1215
1441
  const horizontal = this.model.axis === 'x';
1216
1442
  this.entries.forEach((e, i) => {
1217
1443
  const o = offsets[i];
1444
+ // `o` is derived from DOMRect measurements (viewport CSS pixels).
1218
1445
  this.setOffset(e, horizontal ? { x: o, y: 0 } : { x: 0, y: o });
1219
1446
  });
1220
1447
  }
@@ -1228,7 +1455,8 @@ class SortSession {
1228
1455
  }
1229
1456
  else {
1230
1457
  this.applied.set(e.el, off);
1231
- e.el.style.transform = `translate3d(${off.x}px, ${off.y}px, 0)${e.base}`;
1458
+ const local = viewportDeltaToLocal(e.el, off, false);
1459
+ e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1232
1460
  }
1233
1461
  this.onOffsetChange?.(e.el, off);
1234
1462
  }
@@ -1253,7 +1481,8 @@ class SortSession {
1253
1481
  const dx = first[i].left - last[i].left;
1254
1482
  const dy = first[i].top - last[i].top;
1255
1483
  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}`;
1484
+ const local = viewportDeltaToLocal(e.el, { x: dx, y: dy }, false);
1485
+ e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
1257
1486
  moved.push(e);
1258
1487
  }
1259
1488
  });
@@ -1298,63 +1527,10 @@ class SortSession {
1298
1527
  }
1299
1528
  }
1300
1529
  function initialTransform(el) {
1301
- const t = getComputedStyle?.(el)?.transform;
1530
+ const t = el.ownerDocument.defaultView?.getComputedStyle(el).transform;
1302
1531
  return t && t !== 'none' ? ` ${t}` : '';
1303
1532
  }
1304
1533
 
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
1534
  /**
1359
1535
  * A drop list.
1360
1536
  *
@@ -1853,13 +2029,14 @@ class DropListRef {
1853
2029
  }
1854
2030
  /** Orientation to assume while a list has fewer than two items to measure. */
1855
2031
  function fallbackAxis(container) {
1856
- const s = getComputedStyle(container);
1857
- if (s.display.includes('flex'))
2032
+ const win = container.ownerDocument.defaultView;
2033
+ const s = win?.getComputedStyle(container);
2034
+ if (s?.display.includes('flex'))
1858
2035
  return s.flexDirection.startsWith('column') ? 'y' : 'x';
1859
- if (s.display.includes('grid'))
2036
+ if (s?.display.includes('grid'))
1860
2037
  return 'y';
1861
2038
  const child = container.firstElementChild;
1862
- if (child && getComputedStyle(child).display.startsWith('inline'))
2039
+ if (child && win?.getComputedStyle(child).display.startsWith('inline'))
1863
2040
  return 'x';
1864
2041
  return 'y';
1865
2042
  }
@@ -1871,7 +2048,9 @@ function measureHit(el) {
1871
2048
  const clips = [];
1872
2049
  const root = el.ownerDocument.documentElement;
1873
2050
  for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
1874
- const s = getComputedStyle(node);
2051
+ const s = node.ownerDocument.defaultView?.getComputedStyle(node);
2052
+ if (!s)
2053
+ continue;
1875
2054
  const cx = s.overflowX !== 'visible';
1876
2055
  const cy = s.overflowY !== 'visible';
1877
2056
  if (!cx && !cy)
@@ -1957,6 +2136,8 @@ class DragDropService {
1957
2136
  continue;
1958
2137
  }
1959
2138
  if (isDocumentNode(node)) {
2139
+ // Same-origin iframe: continue the logical DOM ancestry through its frame element.
2140
+ // This lets a DropList inside an iframe inherit an outer NgxDropListGroup.
1960
2141
  const frame = node.defaultView?.frameElement;
1961
2142
  if (frame) {
1962
2143
  node = frame;
@@ -1985,15 +2166,13 @@ class DragDropService {
1985
2166
  node = node.host;
1986
2167
  continue;
1987
2168
  }
1988
- // iframe Document
1989
- // 9 === DOCUMENT_NODE
2169
+ // Cross a same-origin iframe boundary so groups can connect lists in both documents.
1990
2170
  if (node.nodeType === 9) {
1991
2171
  const frame = node.defaultView?.frameElement;
1992
2172
  if (frame) {
1993
2173
  node = frame;
1994
2174
  continue;
1995
2175
  }
1996
- // root document
1997
2176
  break;
1998
2177
  }
1999
2178
  node = node.parentNode;
@@ -2030,12 +2209,16 @@ class DragDropService {
2030
2209
  if (!source)
2031
2210
  return null;
2032
2211
  let best = null;
2212
+ const sourceDocument = dragEl?.ownerDocument ?? source.el?.ownerDocument;
2213
+ if (!sourceDocument)
2214
+ return null;
2033
2215
  for (const list of this.dropLists()) {
2034
2216
  if (!list.el || !source.isConnectedTo(list))
2035
2217
  continue;
2036
2218
  if (dragEl && dragEl !== list.el && dragEl.contains(list.el))
2037
2219
  continue;
2038
- if (!list._containsPoint(point.x, point.y))
2220
+ const targetPoint = convertPointBetweenDocuments(point, sourceDocument, list.el.ownerDocument);
2221
+ if (!targetPoint || !list._containsPoint(targetPoint.x, targetPoint.y))
2039
2222
  continue;
2040
2223
  if (!best)
2041
2224
  best = list;
@@ -2065,7 +2248,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2065
2248
  */
2066
2249
  class AutoScroller {
2067
2250
  raf = 0;
2068
- container = window;
2251
+ container;
2252
+ win;
2069
2253
  speed = { x: 0, y: 0 };
2070
2254
  onTick;
2071
2255
  edge;
@@ -2076,6 +2260,7 @@ class AutoScroller {
2076
2260
  }
2077
2261
  /** onTick اجرا می‌شود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder به‌روز شود. */
2078
2262
  start(fromEl, onTick) {
2263
+ this.win = fromEl.ownerDocument.defaultView ?? undefined;
2079
2264
  this.container = findScrollableAncestor(fromEl);
2080
2265
  this.onTick = onTick;
2081
2266
  }
@@ -2090,15 +2275,16 @@ class AutoScroller {
2090
2275
  y: edgeSpeed(clientY, rect.top, rect.bottom, this.edge, this.maxSpeed),
2091
2276
  };
2092
2277
  if ((this.speed.x || this.speed.y) && !this.raf) {
2093
- this.raf = requestAnimationFrame(this.tick);
2278
+ this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2094
2279
  }
2095
2280
  }
2096
2281
  stop() {
2097
2282
  if (this.raf)
2098
- cancelAnimationFrame(this.raf);
2283
+ this.win?.cancelAnimationFrame(this.raf);
2099
2284
  this.raf = 0;
2100
2285
  this.speed = { x: 0, y: 0 };
2101
2286
  this.onTick = undefined;
2287
+ this.win = undefined;
2102
2288
  }
2103
2289
  tick = () => {
2104
2290
  if (!this.speed.x && !this.speed.y) {
@@ -2110,13 +2296,15 @@ class AutoScroller {
2110
2296
  container.scrollBy(this.speed.x, this.speed.y);
2111
2297
  }
2112
2298
  else {
2113
- container.scrollLeft += this.speed.x;
2114
- container.scrollTop += this.speed.y;
2299
+ // Edge speed is measured in viewport pixels; scrollLeft/Top are local to the scroller.
2300
+ const local = viewportDeltaToLocal(container, this.speed);
2301
+ container.scrollLeft += local.x;
2302
+ container.scrollTop += local.y;
2115
2303
  }
2116
2304
  // Pointer ثابت مانده ولی محتوا اسکرول شده؛
2117
2305
  // بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
2118
2306
  this.onTick?.();
2119
- this.raf = requestAnimationFrame(this.tick);
2307
+ this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2120
2308
  };
2121
2309
  }
2122
2310
  function edgeSpeed(pos, min, max, edge, maxSpeed) {
@@ -2128,25 +2316,28 @@ function edgeSpeed(pos, min, max, edge, maxSpeed) {
2128
2316
  }
2129
2317
  function getScrollRect(container) {
2130
2318
  if (isWindow(container)) {
2131
- return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
2319
+ return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
2132
2320
  }
2133
2321
  return container.getBoundingClientRect();
2134
2322
  }
2135
2323
  function findScrollableAncestor(el) {
2136
2324
  let node = el;
2137
2325
  while (node) {
2138
- const style = getComputedStyle(node);
2326
+ const win = node.ownerDocument.defaultView;
2327
+ if (!win)
2328
+ break;
2329
+ const style = win.getComputedStyle(node);
2139
2330
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2140
2331
  if (scrollable && node.scrollHeight > node.clientHeight) {
2141
2332
  return node;
2142
2333
  }
2143
2334
  node = node.parentElement;
2144
2335
  }
2145
- return el.ownerDocument.defaultView ?? window;
2336
+ return el.ownerDocument.defaultView ?? el;
2146
2337
  }
2147
2338
  function getScrollPosition(container) {
2148
2339
  if (isWindow(container)) {
2149
- return { left: window.scrollX, top: window.scrollY };
2340
+ return { left: container.scrollX, top: container.scrollY };
2150
2341
  }
2151
2342
  return { left: container.scrollLeft, top: container.scrollTop };
2152
2343
  }
@@ -2222,7 +2413,6 @@ class NgxDraggable {
2222
2413
  _ref = new DragRef();
2223
2414
  dragging = signal(false, /* @ts-ignore */
2224
2415
  ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
2225
- doc = inject(DOCUMENT);
2226
2416
  service = inject(DragDropService);
2227
2417
  scroller = new AutoScroller();
2228
2418
  down = false;
@@ -2236,7 +2426,7 @@ class NgxDraggable {
2236
2426
  removeEscape;
2237
2427
  removeCancel;
2238
2428
  removeLostCapture;
2239
- scrollTarget = window;
2429
+ scrollTarget;
2240
2430
  lastScroll = { left: 0, top: 0 };
2241
2431
  lastWindowScroll = { left: 0, top: 0 };
2242
2432
  removeContainerScroll;
@@ -2251,6 +2441,8 @@ class NgxDraggable {
2251
2441
  });
2252
2442
  }
2253
2443
  ngOnInit() {
2444
+ this.scrollTarget =
2445
+ this.host.nativeElement.ownerDocument.defaultView ?? this.host.nativeElement;
2254
2446
  this._ref.el = this.dragRootElement()
2255
2447
  ? (this.host.nativeElement.closest(this.dragRootElement()) ?? this.host.nativeElement)
2256
2448
  : this.host.nativeElement;
@@ -2310,12 +2502,13 @@ class NgxDraggable {
2310
2502
  this._ref.pointerDown(this.start);
2311
2503
  this.removeMove = this.renderer.listen(this._ref.el, 'pointermove', (ev) => this.pointerMove(ev));
2312
2504
  this.removeUp = this.renderer.listen(this._ref.el, 'pointerup', (ev) => this.pointerUp(ev));
2313
- this.removeEscape = this.renderer.listen(this.doc, 'keydown', (ev) => {
2505
+ const doc = this._ref.el.ownerDocument;
2506
+ this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
2314
2507
  if (ev.key === 'Escape' && this.dragging())
2315
2508
  this.cancel();
2316
2509
  });
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));
2510
+ this.removeCancel = this.renderer.listen(doc, 'pointercancel', (ev) => this.pointerCancel(ev));
2511
+ this.removeLostCapture = this.renderer.listen(doc, 'lostpointercapture', (ev) => this.pointerCancel(ev));
2319
2512
  }
2320
2513
  applyDragUpdate(p) {
2321
2514
  const before = this._ref.activeDropList;
@@ -2326,32 +2519,39 @@ class NgxDraggable {
2326
2519
  const after = this._ref.activeDropList;
2327
2520
  // Follow the hovered list; outside every list, scroll the page.
2328
2521
  if (after !== before)
2329
- this.scroller.retarget(after?.el ?? this.doc.body);
2522
+ this.scroller.retarget(after?.el ?? this._ref.el.ownerDocument.body);
2330
2523
  }
2331
2524
  this.dragMove.emit(p);
2332
2525
  }
2333
2526
  /** فراخوانی می‌شود دقیقاً وقتی drag واقعاً شروع می‌شود (چه با موس، چه با کیبورد). */
2334
2527
  startScrollTracking() {
2528
+ const win = this._ref.el.ownerDocument.defaultView;
2335
2529
  this.scrollTarget = findScrollableAncestor(this._ref.el);
2336
2530
  this.lastScroll = getScrollPosition(this.scrollTarget);
2337
- this.lastWindowScroll = { left: window.scrollX, top: window.scrollY };
2338
- if (this.scrollTarget !== window) {
2531
+ this.lastWindowScroll = { left: win?.scrollX ?? 0, top: win?.scrollY ?? 0 };
2532
+ if (win && this.scrollTarget !== win) {
2339
2533
  this.removeContainerScroll = this.renderer.listen(this.scrollTarget, 'scroll', () => this.handleContainerScroll());
2340
2534
  }
2341
2535
  // همیشه window رو هم جدا گوش می‌دیم؛ چون ممکنه container محلی نداشته باشیم
2342
2536
  // ولی خودِ صفحه اسکرول بشه.
2343
- this.removeWindowScroll = this.renderer.listen('window', 'scroll', () => this.handleWindowScroll());
2537
+ if (win) {
2538
+ this.removeWindowScroll = this.renderer.listen(win, 'scroll', () => this.handleWindowScroll());
2539
+ }
2344
2540
  }
2345
2541
  handleContainerScroll() {
2346
2542
  const pos = getScrollPosition(this.scrollTarget);
2347
2543
  const dx = pos.left - this.lastScroll.left;
2348
2544
  const dy = pos.top - this.lastScroll.top;
2349
2545
  this.lastScroll = pos;
2350
- this._ref.notifyScroll(dx, dy);
2546
+ const viewportDelta = localDeltaToViewport(this.scrollTarget, { x: dx, y: dy });
2547
+ this._ref.notifyScroll(viewportDelta.x, viewportDelta.y);
2351
2548
  }
2352
2549
  handleWindowScroll() {
2353
- const left = window.scrollX;
2354
- const top = window.scrollY;
2550
+ const win = this._ref.el.ownerDocument.defaultView;
2551
+ if (!win)
2552
+ return;
2553
+ const left = win.scrollX;
2554
+ const top = win.scrollY;
2355
2555
  const dx = left - this.lastWindowScroll.left;
2356
2556
  const dy = top - this.lastWindowScroll.top;
2357
2557
  this.lastWindowScroll = { left, top };
@@ -2392,14 +2592,14 @@ class NgxDraggable {
2392
2592
  }
2393
2593
  if (this.dragging()) {
2394
2594
  // 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();
2595
+ // The release position is authoritative for list sorting (a fast flick may end with
2596
+ // a pointerup that has no preceding pointermove at that position).
2597
+ const p = { x: e.clientX, y: e.clientY };
2598
+ if (this._ref.isListDrag && (p.x !== this.lastPointer.x || p.y !== this.lastPointer.y)) {
2599
+ this._ref.dragMove(p);
2402
2600
  }
2601
+ // endDrag must run for free drags too, otherwise their lifecycle never completes.
2602
+ this._ref.endDrag();
2403
2603
  this.service.end(this._ref);
2404
2604
  }
2405
2605
  this.finish();
@@ -2409,7 +2609,7 @@ class NgxDraggable {
2409
2609
  pointerCancel(e) {
2410
2610
  if (!this.down || e.pointerId !== this.pointerId)
2411
2611
  return;
2412
- if (this.dragging() && this._ref.isListDrag) {
2612
+ if (this.dragging()) {
2413
2613
  this.cancel();
2414
2614
  return;
2415
2615
  }
@@ -2421,6 +2621,9 @@ class NgxDraggable {
2421
2621
  this.finish();
2422
2622
  }
2423
2623
  finish() {
2624
+ if (this.pointerId >= 0 && this._ref.el.hasPointerCapture?.(this.pointerId)) {
2625
+ this._ref.el.releasePointerCapture(this.pointerId);
2626
+ }
2424
2627
  this.dragging.set(false);
2425
2628
  this.down = false;
2426
2629
  this.scroller.stop();
@@ -2473,26 +2676,24 @@ class NgxDraggable {
2473
2676
  this._ref.endDrag();
2474
2677
  this.service.end(this._ref);
2475
2678
  this.dragEnd.emit(this._ref.pointer);
2476
- this.dragging.set(false);
2477
- this.stopScrollTracking();
2679
+ this.finish();
2478
2680
  }
2479
2681
  else if (e.key === 'Escape' && this.dragging()) {
2480
2682
  e.preventDefault();
2481
2683
  this._ref.cancelDrag();
2482
2684
  this.service.end(this._ref);
2483
- this.dragging.set(false);
2685
+ this.finish();
2484
2686
  }
2485
2687
  }
2486
2688
  isOnHandle(target) {
2487
2689
  if (!this.dragHandle())
2488
2690
  return true;
2489
2691
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2490
- return target instanceof HTMLElement && !!target.closest(this.dragHandle());
2692
+ return isElementTarget(target) && !!target.closest(this.dragHandle());
2491
2693
  }
2492
2694
  isInteractive(target) {
2493
2695
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
2494
- return (target instanceof HTMLElement &&
2495
- !!target.closest('button,a,input,textarea,select,[data-no-drag]'));
2696
+ return (isElementTarget(target) && !!target.closest('button,a,input,textarea,select,[data-no-drag]'));
2496
2697
  }
2497
2698
  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
2699
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxDraggable, isStandalone: true, selector: "[NgxDraggable],[ngxDraggable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, dragRootElement: { classPropertyName: "dragRootElement", publicName: "dragRootElement", isSignal: true, isRequired: false, transformFunction: null }, dragHandle: { classPropertyName: "dragHandle", publicName: "dragHandle", isSignal: true, isRequired: false, transformFunction: null }, lockAxis: { classPropertyName: "lockAxis", publicName: "lockAxis", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dragRootElement: "dragRootElementChange", dragHandle: "dragHandleChange", dragStart: "dragStart", dragMove: "dragMove", dragEnd: "dragEnd" }, host: { properties: { "style.touch-action": "\"none\"", "attr.tabindex": "disabled() ? null : 0", "attr.aria-grabbed": "dragging()", "class.disable-drag": "disabled()" }, classAttribute: "ngx-draggable" }, providers: [{ provide: NGX_DRAGGABLE, useExisting: NgxDraggable }], queries: [{ propertyName: "customPreview", first: true, predicate: NgxDragPreview, descendants: true }], ngImport: i0 });
@@ -2517,6 +2718,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2517
2718
  type: ContentChild,
2518
2719
  args: [NgxDragPreview]
2519
2720
  }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
2721
+ function isElementTarget(target) {
2722
+ return (!!target &&
2723
+ typeof target === 'object' &&
2724
+ 'nodeType' in target &&
2725
+ target.nodeType === 1 &&
2726
+ 'closest' in target);
2727
+ }
2520
2728
 
2521
2729
  const ALL_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
2522
2730
  const KEYBOARD_STEP = 8;
@@ -2558,9 +2766,19 @@ class NgxResizable {
2558
2766
  beforeHeight = '';
2559
2767
  beforeTransform = '';
2560
2768
  pointerId = -1;
2769
+ startWidth = 0;
2770
+ startHeight = 0;
2771
+ removeDocumentListeners = [];
2772
+ renderer = inject(Renderer2);
2561
2773
  constructor(el) {
2562
2774
  this.el = el;
2563
2775
  }
2776
+ ngOnDestroy() {
2777
+ if (this.resizing()) {
2778
+ this.restoreBeforeResize();
2779
+ this.finish();
2780
+ }
2781
+ }
2564
2782
  onPointerDown(e) {
2565
2783
  if (this.disabled() || e.button !== 0) {
2566
2784
  return;
@@ -2585,6 +2803,10 @@ class NgxResizable {
2585
2803
  y: e.clientY,
2586
2804
  };
2587
2805
  this.beforeTransition = element.style.transition;
2806
+ this.beforeWidth = element.style.width;
2807
+ this.beforeHeight = element.style.height;
2808
+ this.startWidth = element.offsetWidth;
2809
+ this.startHeight = element.offsetHeight;
2588
2810
  /**
2589
2811
  * Keep DragRef's transform as the immutable base.
2590
2812
  *
@@ -2595,6 +2817,14 @@ class NgxResizable {
2595
2817
  this.resizing.set(true);
2596
2818
  element.style.transition = 'none';
2597
2819
  element.setPointerCapture?.(e.pointerId);
2820
+ const doc = element.ownerDocument;
2821
+ this.removeDocumentListeners = [
2822
+ this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
2823
+ this.renderer.listen(doc, 'pointerup', (event) => this.onPointerUp(event)),
2824
+ this.renderer.listen(doc, 'pointercancel', (event) => this.onPointerCancel(event)),
2825
+ this.renderer.listen(doc, 'lostpointercapture', (event) => this.onPointerCancel(event)),
2826
+ this.renderer.listen(doc, 'keydown', (event) => this.onDocumentKeyDown(event)),
2827
+ ];
2598
2828
  const out = this.calculate(e);
2599
2829
  this.apply(out);
2600
2830
  this.resizeStart.emit(out);
@@ -2624,11 +2854,20 @@ class NgxResizable {
2624
2854
  return;
2625
2855
  }
2626
2856
  e.preventDefault();
2857
+ this.restoreBeforeResize();
2858
+ this.finish();
2859
+ }
2860
+ onPointerCancel(e) {
2861
+ if (!this.resizing() || e.pointerId !== this.pointerId)
2862
+ return;
2863
+ this.restoreBeforeResize();
2864
+ this.finish();
2865
+ }
2866
+ restoreBeforeResize() {
2627
2867
  const element = this.el.nativeElement;
2628
2868
  element.style.width = this.beforeWidth;
2629
2869
  element.style.height = this.beforeHeight;
2630
2870
  element.style.transform = this.beforeTransform;
2631
- this.finish();
2632
2871
  }
2633
2872
  onHandleKeyDown(e) {
2634
2873
  if (this.disabled()) {
@@ -2655,8 +2894,8 @@ class NgxResizable {
2655
2894
  }
2656
2895
  e.preventDefault();
2657
2896
  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');
2897
+ const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
2898
+ const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
2660
2899
  this.el.nativeElement.style.width = `${width}px`;
2661
2900
  this.el.nativeElement.style.height = `${height}px`;
2662
2901
  const out = {
@@ -2672,10 +2911,14 @@ class NgxResizable {
2672
2911
  this.resizeEnd.emit(out);
2673
2912
  }
2674
2913
  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;
2914
+ const localDelta = viewportDeltaToLocal(this.el.nativeElement, {
2915
+ x: e.clientX - this.pointer.x,
2916
+ y: e.clientY - this.pointer.y,
2917
+ });
2918
+ const dx = localDelta.x;
2919
+ const dy = localDelta.y;
2920
+ let width = this.startWidth;
2921
+ let height = this.startHeight;
2679
2922
  /**
2680
2923
  * East
2681
2924
  */
@@ -2708,13 +2951,16 @@ class NgxResizable {
2708
2951
  * This is useful for boundary calculations and
2709
2952
  * emitted resize events.
2710
2953
  */
2954
+ const matrix = getLocalToViewportMatrix(this.el.nativeElement);
2955
+ const scaleX = Math.hypot(matrix.a, matrix.b) || 1;
2956
+ const scaleY = Math.hypot(matrix.c, matrix.d) || 1;
2711
2957
  let left = this.start.left;
2712
2958
  let top = this.start.top;
2713
2959
  if (this.direction.includes('w')) {
2714
- left = this.start.right - width;
2960
+ left = this.start.right - width * scaleX;
2715
2961
  }
2716
2962
  if (this.direction.includes('n')) {
2717
- top = this.start.bottom - height;
2963
+ top = this.start.bottom - height * scaleY;
2718
2964
  }
2719
2965
  let moveLeft = left - this.start.left;
2720
2966
  let moveTop = top - this.start.top;
@@ -2722,9 +2968,9 @@ class NgxResizable {
2722
2968
  * Boundary constraint.
2723
2969
  */
2724
2970
  if (this.boundary) {
2725
- const clamped = clampResizeWithinBoundary(this.boundary()?.getBoundingClientRect(), this.start, width, height, moveLeft, moveTop);
2726
- width = clamped.width;
2727
- height = clamped.height;
2971
+ const clamped = clampResizeWithinBoundary(this.boundary()?.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
2972
+ width = clamped.width / scaleX;
2973
+ height = clamped.height / scaleY;
2728
2974
  moveLeft = clamped.left;
2729
2975
  moveTop = clamped.top;
2730
2976
  left = this.start.left + moveLeft;
@@ -2787,8 +3033,9 @@ class NgxResizable {
2787
3033
  * correction required by Resize.
2788
3034
  */
2789
3035
  const baseTransform = this.beforeTransform && this.beforeTransform !== 'none' ? `${this.beforeTransform} ` : '';
3036
+ const localCorrection = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
2790
3037
  element.style.transform =
2791
- `${baseTransform}translate3d(` + `${correctionX}px, ` + `${correctionY}px, 0)`;
3038
+ `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
2792
3039
  }
2793
3040
  clampGrid(value, axis) {
2794
3041
  const grid = typeof this.grid() === 'number' ? this.grid : this.grid()?.[axis];
@@ -2800,7 +3047,14 @@ class NgxResizable {
2800
3047
  finish() {
2801
3048
  this.resizing.set(false);
2802
3049
  this.el.nativeElement.style.transition = this.beforeTransition;
3050
+ const pointerId = this.pointerId;
2803
3051
  this.pointerId = -1;
3052
+ for (const remove of this.removeDocumentListeners)
3053
+ remove();
3054
+ this.removeDocumentListeners = [];
3055
+ if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
3056
+ this.el.nativeElement.releasePointerCapture(pointerId);
3057
+ }
2804
3058
  this.beforeTransform = '';
2805
3059
  }
2806
3060
  resolvedDirections() {
@@ -2823,7 +3077,7 @@ class NgxResizable {
2823
3077
  * Explicit resize handle has priority.
2824
3078
  */
2825
3079
  // TODO notsafe instanceof:node.nodeType === Node.ELEMENT_NODE
2826
- const handle = e.target instanceof HTMLElement
3080
+ const handle = isHTMLElementTarget(e.target)
2827
3081
  ? e.target.closest('[data-ngx-resize-handle]')
2828
3082
  : null;
2829
3083
  if (handle && element.contains(handle)) {
@@ -2866,7 +3120,7 @@ class NgxResizable {
2866
3120
  return this.resolvedDirections().includes(direction) ? direction : null;
2867
3121
  }
2868
3122
  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 });
3123
+ 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
3124
  }
2871
3125
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, decorators: [{
2872
3126
  type: Directive,
@@ -2880,19 +3134,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2880
3134
  }], 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
3135
  type: HostListener,
2882
3136
  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
3137
  }], onHandleKeyDown: [{
2893
3138
  type: HostListener,
2894
3139
  args: ['keydown', ['$event']]
2895
3140
  }] } });
3141
+ function isHTMLElementTarget(target) {
3142
+ return (!!target &&
3143
+ typeof target === 'object' &&
3144
+ 'nodeType' in target &&
3145
+ target.nodeType === 1 &&
3146
+ 'closest' in target);
3147
+ }
2896
3148
 
2897
3149
  const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
2898
3150
  /**