ngx-kit 8.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, EventEmitter, Injectable, InjectionToken, inject, ElementRef, Injector, Directive, ContentChild, Output, Input, model, input, output, effect, HostListener } from '@angular/core';
2
+ import { signal, EventEmitter, Injectable, InjectionToken, model, input, output, inject, Renderer2, effect, Input, Directive, HostListener, ApplicationRef, ContentChild, Output, ElementRef } from '@angular/core';
3
3
  import { DOCUMENT } from '@angular/common';
4
4
 
5
5
  /**
@@ -218,6 +218,14 @@ class DragRef {
218
218
  /** True while a drop-list drag (body preview + placeholder) is in progress. Free drag: always false. */
219
219
  listDrag = false;
220
220
  cachedLists = [];
221
+ /**
222
+ * Candidate lists for hit-testing, filtered (connected to the origin; not inside the dragged
223
+ * element) and sorted deepest-first — both computed ONCE, not on every pointer move, since
224
+ * neither ever changes mid-drag. Kept in sync with the live registry (see `dropListsSnapshot`)
225
+ * so a list that appears or disappears mid-drag (e.g. behind an `@if`) is still picked up.
226
+ */
227
+ dropCandidates = [];
228
+ dropListsSnapshot = null;
221
229
  lastEffective = { x: 0, y: 0 };
222
230
  scrollListener;
223
231
  resizeListener;
@@ -432,19 +440,13 @@ class DragRef {
432
440
  beginListDrag(origin, pointer) {
433
441
  const doc = this.el.ownerDocument;
434
442
  const win = doc.defaultView ?? window;
435
- const lists = new Set([origin]);
436
- for (const l of this.service?.dropLists() ?? [])
437
- lists.add(l);
438
- this.cachedLists = Array.from(lists);
439
- // Foreign lists are measured as they are now (no placeholder in them yet)...
440
- for (const l of this.cachedLists)
441
- if (l !== origin)
442
- l._cacheGeometry();
443
+ this.dropListsSnapshot = null; // force refreshDropCandidates() to do a full pass below
444
+ this.refreshDropCandidates(origin);
443
445
  this.lastEffective = { ...pointer };
444
446
  this.activeDropList = origin;
445
447
  origin.enter(this, pointer.x, pointer.y);
446
- // ...the origin list AFTER its placeholder took over the source's slot, so its box still
447
- // covers the whole list (the hidden source no longer occupies space).
448
+ // The origin is (re-)measured AFTER its placeholder took over the source's slot, so its box
449
+ // still covers the whole list (the hidden source no longer occupies space).
448
450
  origin._cacheGeometry();
449
451
  this.scrollListener = () => {
450
452
  for (const l of this.cachedLists)
@@ -460,24 +462,91 @@ class DragRef {
460
462
  doc.addEventListener('scroll', this.scrollListener, { capture: true, passive: true });
461
463
  win.addEventListener('resize', this.resizeListener, { passive: true });
462
464
  }
463
- /** Decide which list is under the pointer, switch if needed, then re-sort in the active one. */
465
+ /**
466
+ * Recomputes which registered lists are valid targets for this drag — connected to the
467
+ * origin, and not nested inside the element being dragged — plus each one's nesting depth.
468
+ *
469
+ * `DragDropService.dropLists()` is an Angular signal: its array is a NEW reference only when a
470
+ * list actually registers or unregisters, so the check below is a single reference comparison
471
+ * on every pointer move and the (heavier) connectivity/containment/depth work below it only
472
+ * runs on the rare event a list appears or disappears mid-drag — never on every pointer move.
473
+ */
474
+ refreshDropCandidates(origin) {
475
+ const all = this.service?.dropLists() ?? [];
476
+ if (all === this.dropListsSnapshot)
477
+ return;
478
+ this.dropListsSnapshot = all;
479
+ const already = new Set(this.dropCandidates.map((c) => c.list));
480
+ const next = [{ list: origin, depth: origin.depth }];
481
+ for (const l of all) {
482
+ if (l === origin || !l.el || !origin.isConnectedTo(l))
483
+ continue;
484
+ if (this.el !== l.el && this.el.contains(l.el))
485
+ continue;
486
+ if (!already.has(l))
487
+ l._cacheGeometry(); // newly eligible mid-drag: measure it now
488
+ next.push({ list: l, depth: l.depth });
489
+ }
490
+ this.dropCandidates = next;
491
+ this.cachedLists = next.map((c) => c.list);
492
+ }
493
+ /**
494
+ * Decide which list is under the pointer, switch if needed, then re-sort in the active one.
495
+ *
496
+ * The origin list is the one exception to "exiting a list resets/releases it": while the
497
+ * pointer dives into one of the origin's OWN nested lists (at any depth), the origin's
498
+ * placeholder and sibling displacement are left exactly as they are — frozen, not reset —
499
+ * so returning to the origin's own level doesn't snap items back and then forward again.
500
+ * Every OTHER (foreign) list still fully releases the moment the pointer leaves it, exactly
501
+ * as before; nesting multiple foreign sessions alive at once would let their own internal
502
+ * sibling displacement compound in ways a single flat `ancestorOffset` cannot represent.
503
+ */
464
504
  updateDropTarget(pointer) {
465
505
  const origin = this.originDropList;
466
506
  if (!origin)
467
507
  return;
468
- const next = this.service
469
- ? this.service.findDropList(pointer, this.activeDropList, origin, this.el)
470
- : origin._containsPoint(pointer.x, pointer.y)
471
- ? origin
472
- : null;
473
- if (next !== this.activeDropList) {
474
- this.activeDropList?.exit(this);
475
- this.activeDropList = next;
476
- next?.enter(this, pointer.x, pointer.y);
508
+ this.refreshDropCandidates(origin);
509
+ const next = this.resolveDropList(pointer);
510
+ if (next === this.activeDropList) {
511
+ next?.sortItem(this, pointer);
512
+ return;
513
+ }
514
+ const prev = this.activeDropList;
515
+ if (prev && prev !== origin)
516
+ prev.exit(this);
517
+ const nextWithinOrigin = !!next && (next === origin || isDescendantOfList(next, origin));
518
+ if (nextWithinOrigin) {
519
+ if (prev === origin)
520
+ origin._freeze(); // no longer the innermost, but stays engaged
477
521
  }
478
522
  else {
479
- next?.sortItem(this, pointer);
523
+ origin.exit(this); // truly outside the origin's territory now: reset it (idempotent)
480
524
  }
525
+ this.activeDropList = next;
526
+ next?.enter(this, pointer.x, pointer.y); // fresh entry, or resumes a still-alive (frozen) session
527
+ }
528
+ /**
529
+ * Innermost connected list under the pointer, from the precomputed candidates: deeper always
530
+ * beats shallower; among lists at the same depth (unrelated, visually overlapping lists) the
531
+ * currently active one is kept to avoid flicker, otherwise the smaller one wins. No DOM
532
+ * reads and no `.contains()` calls happen here — everything needed was precomputed above.
533
+ */
534
+ resolveDropList(pointer) {
535
+ let best = null;
536
+ let bestDepth = -1;
537
+ for (const { list, depth } of this.dropCandidates) {
538
+ if (!list._containsPoint(pointer.x, pointer.y))
539
+ continue;
540
+ if (!best || depth > bestDepth) {
541
+ best = list;
542
+ bestDepth = depth;
543
+ }
544
+ else if (depth === bestDepth && best !== this.activeDropList) {
545
+ if (list === this.activeDropList || list._area() < best._area())
546
+ best = list;
547
+ }
548
+ }
549
+ return best;
481
550
  }
482
551
  /**
483
552
  * Single teardown path for every way a drop-list drag can end (drop on a list, drop outside,
@@ -500,6 +569,8 @@ class DragRef {
500
569
  this.placeholder = undefined;
501
570
  this.activeDropList = null;
502
571
  this.cachedLists = [];
572
+ this.dropCandidates = [];
573
+ this.dropListsSnapshot = null;
503
574
  this.listDrag = false;
504
575
  // The source element is put back exactly as it was (its transform was never touched).
505
576
  this.el.classList.remove('ngx-draggable--dragging');
@@ -518,43 +589,56 @@ class DragRef {
518
589
  this.preview.style.transform = `translate3d(${this.moveDx}px, ${this.moveDy}px, 0)`;
519
590
  }
520
591
  }
592
+ /** Is `list` nested (at any depth) inside `ancestor`, per the parent/child list tree? */
593
+ function isDescendantOfList(list, ancestor) {
594
+ for (let p = list.parentList; p; p = p.parentList) {
595
+ if (p === ancestor)
596
+ return true;
597
+ }
598
+ return false;
599
+ }
521
600
 
522
601
  class PlaceHolderRef {
602
+ /**
603
+ * Custom placeholder content, from `*ngxPlaceholder` inside the `ngxDropList`. When unset,
604
+ * `attach()` falls back to a shallow clone of the dragged element (previous behaviour).
605
+ *
606
+ * Example:
607
+ *
608
+ * <div ngxDropList>
609
+ * <span class="my-placeholder" *ngxPlaceholder></span>
610
+ * ...
611
+ * </div>
612
+ */
523
613
  tpl;
614
+ /** Set by `NgxPlaceholder` so the embedded view can join Angular's change-detection tree. */
615
+ appRef;
524
616
  dropList;
525
617
  element;
526
618
  _visible = false;
527
- /**
528
- * Angular embedded view created from `tpl`.
529
- */
530
619
  _view;
620
+ /** True once `this.element` was built from `tpl` (as opposed to the fallback clone). */
621
+ isCustom = false;
531
622
  attach(container, source, reference) {
532
- /**
533
- * If the placeholder has a template, render the template instead of
534
- * cloning the dragged element.
535
- *
536
- * Example:
537
- *
538
- * <span class="placeholder" *ngxPlaceholder></span>
539
- */
540
623
  if (!this.element) {
541
624
  if (this.tpl) {
542
625
  this._view = this.tpl.createEmbeddedView({});
626
+ this.appRef?.attachView(this._view);
543
627
  // Render bindings inside the embedded view before we move its DOM nodes.
544
628
  this._view.detectChanges();
545
629
  const element = this._view.rootNodes.find((node) => node instanceof HTMLElement);
546
630
  if (!element) {
631
+ this.appRef?.detachView(this._view);
547
632
  this._view.destroy();
548
633
  this._view = undefined;
549
- throw new Error('ngxPlaceholder template must contain at least one HTMLElement root node.');
634
+ throw new Error('ngxPlaceholder template must contain exactly one HTMLElement root node ' +
635
+ '(other root nodes, if any, are ignored).');
550
636
  }
551
637
  this.element = element;
638
+ this.isCustom = true;
552
639
  }
553
640
  else {
554
- /**
555
- * Backward-compatible fallback:
556
- * if no ngxPlaceholder template was supplied, keep the old behavior.
557
- */
641
+ // Fallback: keep the same tag/attributes as the dragged item so selectors such as
558
642
  // ".list > article" and grid/flex item rules continue to apply.
559
643
  this.element = source.cloneNode(false);
560
644
  this.element.removeAttribute('id');
@@ -562,20 +646,26 @@ class PlaceHolderRef {
562
646
  this.element.setAttribute('aria-hidden', 'true');
563
647
  this.element.setAttribute('inert', '');
564
648
  this.element.removeAttribute('tabindex');
649
+ this.isCustom = false;
565
650
  }
566
651
  }
567
- // The placeholder must never inherit a live drag transform/animation. `transform` and
568
- // `transition` are deliberately NOT forced with !important: the sort session drives them
569
- // to slide the placeholder (translate3d) to the current insertion slot.
652
+ // Safety overrides that apply regardless of who authored the element: it must never
653
+ // inherit a live drag transform/animation (transform/transition are left free for the
654
+ // sort session to drive, translate3d-ing the placeholder to the current insertion slot),
655
+ // never intercept pointer events, and never be display:none.
570
656
  this.element.style.removeProperty('transform');
571
657
  this.element.style.removeProperty('transition');
572
658
  this.element.style.removeProperty('will-change');
573
659
  this.element.style.setProperty('animation', 'none', 'important');
574
660
  this.element.style.setProperty('pointer-events', 'none', 'important');
575
661
  this.element.style.setProperty('visibility', 'visible', 'important');
576
- this.element.style.setProperty('opacity', '0.16', 'important');
577
- this.element.style.setProperty('outline', '2px dashed currentColor', 'important');
578
- this.element.style.setProperty('outline-offset', '-2px', 'important');
662
+ if (!this.isCustom) {
663
+ // Only the auto-generated clone gets the default "phantom box" look. A custom
664
+ // *ngxPlaceholder template is styled entirely by the caller.
665
+ this.element.style.setProperty('opacity', '0.16', 'important');
666
+ this.element.style.setProperty('outline', '2px dashed currentColor', 'important');
667
+ this.element.style.setProperty('outline-offset', '-2px', 'important');
668
+ }
579
669
  this._visible = true;
580
670
  if (reference && reference.parentNode === container) {
581
671
  if (this.element !== reference)
@@ -586,10 +676,17 @@ class PlaceHolderRef {
586
676
  }
587
677
  return this.element;
588
678
  }
679
+ /** True while the currently attached element came from a custom `*ngxPlaceholder` template. */
680
+ get custom() {
681
+ return this.isCustom;
682
+ }
589
683
  detach() {
590
684
  this.element?.remove();
591
- this._view?.destroy();
592
- this._view = undefined;
685
+ if (this._view) {
686
+ this.appRef?.detachView(this._view);
687
+ this._view.destroy();
688
+ this._view = undefined;
689
+ }
593
690
  this.element = undefined;
594
691
  this._visible = false;
595
692
  }
@@ -936,8 +1033,10 @@ class SortSession {
936
1033
  disposed = false;
937
1034
  /** undefined = not created yet, null = this layout has no whitespace separators. */
938
1035
  spacer;
1036
+ onOffsetChange;
939
1037
  constructor(init) {
940
1038
  this.spacer = init.spacer;
1039
+ this.onOffsetChange = init.onOffsetChange;
941
1040
  this.container = init.container;
942
1041
  this.placeholder = init.placeholder;
943
1042
  this.itemElements = init.itemElements;
@@ -1026,6 +1125,8 @@ class SortSession {
1026
1125
  e.el.style.transform = e.savedTransform;
1027
1126
  e.el.style.transition = e.savedTransition;
1028
1127
  e.el.style.willChange = e.savedWillChange;
1128
+ if (this.applied.has(e.el))
1129
+ this.onOffsetChange?.(e.el, { x: 0, y: 0 });
1029
1130
  }
1030
1131
  this.applied.clear();
1031
1132
  this.spacer?.remove();
@@ -1104,6 +1205,7 @@ class SortSession {
1104
1205
  this.applied.set(e.el, off);
1105
1206
  e.el.style.transform = `translate3d(${off.x}px, ${off.y}px, 0)${e.base}`;
1106
1207
  }
1208
+ this.onOffsetChange?.(e.el, off);
1107
1209
  }
1108
1210
  // ---------------------------------------------------------------------------------------
1109
1211
  // flow strategy (single placeholder move + FLIP transforms)
@@ -1164,6 +1266,8 @@ class SortSession {
1164
1266
  for (const e of this.entries) {
1165
1267
  e.el.style.transition = 'none';
1166
1268
  e.el.style.transform = e.savedTransform;
1269
+ if (this.applied.has(e.el))
1270
+ this.onOffsetChange?.(e.el, { x: 0, y: 0 });
1167
1271
  }
1168
1272
  this.applied.clear();
1169
1273
  }
@@ -1248,6 +1352,16 @@ class DropListRef {
1248
1352
  sortAnimationDuration = 200;
1249
1353
  _draggables = new Set();
1250
1354
  onDrop = new EventEmitter();
1355
+ /**
1356
+ * Nesting: the nearest ENCLOSING drop list (null for a top-level list), and this list's own
1357
+ * direct nested lists — i.e. lists whose nearest enclosing list is this one, not a list at any
1358
+ * depth. Set by `NgxDropList` at init from a `skipSelf` DI lookup. Used for two things: (1)
1359
+ * `depth` for "innermost wins" in `DragRef`'s target resolution, without any DOM `.contains()`
1360
+ * calls on the hot path, and (2) cascading `_setAncestorOffset` (below) to every list nested
1361
+ * inside an item that gets displaced by sorting, at any depth.
1362
+ */
1363
+ parentList = null;
1364
+ childLists = new Set();
1251
1365
  session = null;
1252
1366
  placeholder;
1253
1367
  customPlaceholder;
@@ -1258,6 +1372,96 @@ class DropListRef {
1258
1372
  /** Pre-insertion geometry of a foreign list, restored when the drag leaves it. */
1259
1373
  naturalHit = null;
1260
1374
  hitCached = false;
1375
+ /**
1376
+ * Screen-space offset contributed by displaced ANCESTOR item(s) (see `parentList` above).
1377
+ * `{0,0}` outside of that (the overwhelmingly common case). Kept as a SUM of independent
1378
+ * per-source contributions, not a single overwritten value: while the pointer hovers a
1379
+ * FOREIGN (non-origin) list nested inside the origin's own displaced item, up to two ancestor
1380
+ * sessions can be displacing this list at once — the origin's (frozen, still active further
1381
+ * up) and that foreign list's own — and one clearing its contribution on release must not
1382
+ * wipe out the other's, still-active, contribution.
1383
+ */
1384
+ ancestorContributions = new Map();
1385
+ ancestorOffset = { x: 0, y: 0 };
1386
+ /** Entries this list itself pushed via a one-time placeholder-insertion reflow (see below). */
1387
+ reflowContributions = new Map();
1388
+ /** How many enclosing drop lists this one is nested inside. Root lists are depth 0. */
1389
+ get depth() {
1390
+ let d = 0;
1391
+ for (let p = this.parentList; p; p = p.parentList)
1392
+ d++;
1393
+ return d;
1394
+ }
1395
+ _registerChild(child) {
1396
+ child.parentList = this;
1397
+ this.childLists.add(child);
1398
+ }
1399
+ _unregisterChild(child) {
1400
+ if (child.parentList === this)
1401
+ child.parentList = null;
1402
+ this.childLists.delete(child);
1403
+ }
1404
+ /**
1405
+ * One ancestor SESSION's contribution to this list's total displacement just changed (or
1406
+ * cleared, `{0,0}`). `source` identifies WHICH session — the list whose own item is doing the
1407
+ * displacing — so that this list's own recomputed total is a SUM across every currently-active
1408
+ * source, and cascades the SAME (source, offset) pair on to every nested list at any depth
1409
+ * unchanged, so a list several levels down stays hit-testable at its true on-screen position
1410
+ * regardless of which ancestor(s) are currently sorting.
1411
+ */
1412
+ _setAncestorOffset(source, offset) {
1413
+ const prev = this.ancestorContributions.get(source);
1414
+ if (prev && prev.x === offset.x && prev.y === offset.y)
1415
+ return;
1416
+ if (offset.x === 0 && offset.y === 0)
1417
+ this.ancestorContributions.delete(source);
1418
+ else
1419
+ this.ancestorContributions.set(source, offset);
1420
+ let x = 0;
1421
+ let y = 0;
1422
+ for (const o of this.ancestorContributions.values()) {
1423
+ x += o.x;
1424
+ y += o.y;
1425
+ }
1426
+ if (x === this.ancestorOffset.x && y === this.ancestorOffset.y)
1427
+ return; // no net change
1428
+ this.ancestorOffset = { x, y };
1429
+ for (const child of this.childLists)
1430
+ child._setAncestorOffset(source, offset);
1431
+ }
1432
+ /** Which of this list's DIRECT nested lists live inside `el`, notified of its new offset. */
1433
+ propagateOffset(el, offset) {
1434
+ for (const child of this.childLists) {
1435
+ if (el.contains(child.el))
1436
+ child._setAncestorOffset(this, offset);
1437
+ }
1438
+ }
1439
+ /** Rects, right now, of every `other` entry that contains one of this list's nested lists. */
1440
+ trackedEntryRects(others) {
1441
+ const map = new Map();
1442
+ for (const child of this.childLists) {
1443
+ for (const el of others) {
1444
+ if (el.contains(child.el)) {
1445
+ if (!map.has(el))
1446
+ map.set(el, el.getBoundingClientRect());
1447
+ break;
1448
+ }
1449
+ }
1450
+ }
1451
+ return map;
1452
+ }
1453
+ /** Turns the real reflow a fresh placeholder insertion just caused into ordinary offsets. */
1454
+ propagateReflow(before) {
1455
+ for (const [el, prev] of before) {
1456
+ const now = el.getBoundingClientRect();
1457
+ const dx = now.left - prev.left;
1458
+ const dy = now.top - prev.top;
1459
+ if (dx || dy) {
1460
+ this.propagateOffset(el, { x: dx, y: dy });
1461
+ this.reflowContributions.set(el, { x: dx, y: dy });
1462
+ }
1463
+ }
1464
+ }
1261
1465
  addItem(item) {
1262
1466
  this._draggables.add(item);
1263
1467
  }
@@ -1274,9 +1478,24 @@ class DropListRef {
1274
1478
  // ---------------------------------------------------------------------------------------
1275
1479
  // hit testing (cached for the duration of a drag)
1276
1480
  // ---------------------------------------------------------------------------------------
1277
- /** Measure the list and its clipping ancestors once, at drag start. */
1481
+ /**
1482
+ * Measure the list and its clipping ancestors once, at drag start (or the moment a nested
1483
+ * list becomes a candidate mid-drag). `measureHit` reads live `getBoundingClientRect()`s,
1484
+ * which already include any `translate3d` an ancestor item's displacement has applied — so if
1485
+ * this list is currently offset (nested inside a sibling that sorting is displacing right
1486
+ * now), the measurement is normalized back to the zero-offset frame `_containsPoint` expects,
1487
+ * by subtracting that offset straight back out. Without this, entering a nested list while
1488
+ * its ancestor is mid-displacement would double-count the offset on every check afterwards.
1489
+ */
1278
1490
  _cacheGeometry() {
1279
- this.hit = measureHit(this.el);
1491
+ const hit = measureHit(this.el);
1492
+ if (hit && (this.ancestorOffset.x || this.ancestorOffset.y)) {
1493
+ hit.box.left -= this.ancestorOffset.x;
1494
+ hit.box.right -= this.ancestorOffset.x;
1495
+ hit.box.top -= this.ancestorOffset.y;
1496
+ hit.box.bottom -= this.ancestorOffset.y;
1497
+ }
1498
+ this.hit = hit;
1280
1499
  this.hitCached = true;
1281
1500
  }
1282
1501
  /**
@@ -1289,11 +1508,14 @@ class DropListRef {
1289
1508
  const hit = this.hitCached ? this.hit : measureHit(this.el);
1290
1509
  if (!hit)
1291
1510
  return false;
1292
- let d = hit.frame.delta();
1293
- if (!boxContains(hit.box, x + d.x, y + d.y))
1511
+ // The list's own box shifts with any ancestor displacement; a clip window (a scroll/overflow
1512
+ // ancestor) normally does not, since it is not itself an item being sorted, so it is tested
1513
+ // against the raw pointer.
1514
+ const d0 = hit.frame.delta();
1515
+ if (!boxContains(hit.box, x - this.ancestorOffset.x + d0.x, y - this.ancestorOffset.y + d0.y))
1294
1516
  return false;
1295
1517
  for (const clip of hit.clips) {
1296
- d = clip.frame.delta();
1518
+ const d = clip.frame.delta();
1297
1519
  if (!boxContains(clip.box, x + d.x, y + d.y))
1298
1520
  return false;
1299
1521
  }
@@ -1326,7 +1548,9 @@ class DropListRef {
1326
1548
  /** The drag pointer entered this list (or the drag started inside it). */
1327
1549
  enter(drag, x = 0, y = 0) {
1328
1550
  if (this.session && this.activeDrag === drag) {
1329
- // Re-entering the list the drag started in: its session (and slot) are still alive.
1551
+ // Resuming a session that's still alive: either this is the origin list (its slot is
1552
+ // always kept), or the pointer dove into one of its own nested lists and came back up
1553
+ // (frozen rather than released in `exit()`, see there).
1330
1554
  this.hovered = true;
1331
1555
  this.el.classList.add('ngx-drop-list--active');
1332
1556
  drag.placeholder = this.placeholder;
@@ -1350,6 +1574,11 @@ class DropListRef {
1350
1574
  return s.modelIndex;
1351
1575
  }
1352
1576
  /** The pointer left this list. */
1577
+ /**
1578
+ * The pointer truly left this list (as opposed to moving deeper into one of its own nested
1579
+ * lists — see `DragRef`'s chain tracking, which never calls `exit()` for a list that remains
1580
+ * an ancestor of the new target, so this always means "left this list's territory").
1581
+ */
1353
1582
  exit(drag) {
1354
1583
  if (!this.session || this.activeDrag !== drag)
1355
1584
  return;
@@ -1364,6 +1593,17 @@ class DropListRef {
1364
1593
  this.releaseSession(drag);
1365
1594
  }
1366
1595
  }
1596
+ /**
1597
+ * The pointer dove into one of this list's own nested lists: this list is no longer the
1598
+ * innermost target, but it isn't being left either — its placeholder and sibling displacement
1599
+ * stay exactly as they are (avoids a snap-back-then-forward flicker), only the "currently
1600
+ * hovered" bookkeeping (and its visual highlight) is cleared. `DragRef`'s chain tracking calls
1601
+ * this instead of `exit()` for every list that remains an ancestor of the new target.
1602
+ */
1603
+ _freeze() {
1604
+ this.hovered = false;
1605
+ this.el.classList.remove('ngx-drop-list--active');
1606
+ }
1367
1607
  /**
1368
1608
  * Ends the drag for this list. Returns the drop event when the pointer was released over this
1369
1609
  * list, `null` otherwise (nothing to emit). All transient state is disposed either way.
@@ -1407,6 +1647,12 @@ class DropListRef {
1407
1647
  this.releaseSession(drag);
1408
1648
  this.hit = null;
1409
1649
  this.hitCached = false;
1650
+ // `ancestorOffset` is deliberately NOT reset here: `enter()` calls this unconditionally as a
1651
+ // safety-clear on every (re-)entry, including while a currently-active ancestor transform
1652
+ // still legitimately applies to this list. It self-corrects when that transform actually
1653
+ // reverts to zero, via the `onOffsetChange` cascade (see `_setAncestorOffset`), which also
1654
+ // covers the true end of a drag (the displaced list's own session disposes and reports
1655
+ // every entry back to `{0,0}`).
1410
1656
  }
1411
1657
  /** Dispose the sort session, placeholder and drag ownership. */
1412
1658
  releaseSession(drag) {
@@ -1414,6 +1660,14 @@ class DropListRef {
1414
1660
  return;
1415
1661
  this.session?.dispose();
1416
1662
  this.session = null;
1663
+ if (this.reflowContributions.size) {
1664
+ // The one-time reflow from this list's own placeholder insertion (see `propagateReflow`)
1665
+ // never went through the session's own `applied` map, so its `dispose()` above cannot
1666
+ // have cleared it — do that here instead, or it would linger as a stale contribution.
1667
+ for (const [el] of this.reflowContributions)
1668
+ this.propagateOffset(el, { x: 0, y: 0 });
1669
+ this.reflowContributions.clear();
1670
+ }
1417
1671
  this.placeholder?.detach();
1418
1672
  if (this.activeDrag && this.activeDrag.placeholder === this.placeholder) {
1419
1673
  this.activeDrag.placeholder = undefined;
@@ -1442,16 +1696,30 @@ class DropListRef {
1442
1696
  if (origin) {
1443
1697
  // The placeholder takes over the exact slot of the (now hidden) source element.
1444
1698
  ph.attach(this.el, drag.el, drag.el);
1699
+ this.lockPlaceholderGeometry(drag, ph, drag.start);
1445
1700
  }
1446
1701
  else {
1447
1702
  ph.detach();
1703
+ // Only the "other" entries that contain a nested list are worth measuring (usually none,
1704
+ // or very few, even in a large tree): inserting the placeholder is a REAL DOM node, so it
1705
+ // shifts later siblings via ordinary CSS reflow, not `translate3d` — a one-time event this
1706
+ // list's own SortSession never reports through `onOffsetChange`, since (being sized and
1707
+ // positioned by `foreignInsertionReference` to already match where it belongs) it never
1708
+ // needs to actually transform anything afterwards. Measuring the small tracked set just
1709
+ // before and after the insertion turns that one-time reflow into an ordinary ancestor
1710
+ // contribution, exactly like any other displaced entry.
1711
+ const tracked = this.childLists.size ? this.trackedEntryRects(others) : null;
1448
1712
  const reference = this.foreignInsertionReference(others, pointer, axis, rtl);
1449
1713
  ph.attach(this.el, drag.el, reference);
1714
+ // Sized to its final footprint BEFORE measuring the reflow it causes: an unsized clone
1715
+ // (still near-zero height) wouldn't have pushed `others` at all yet.
1716
+ this.lockPlaceholderGeometry(drag, ph, drag.start);
1717
+ if (tracked)
1718
+ this.propagateReflow(tracked);
1450
1719
  spacer = makeSpacer(this.el);
1451
1720
  if (spacer)
1452
1721
  this.el.insertBefore(spacer, reference ?? ph.element);
1453
1722
  }
1454
- this.lockPlaceholderGeometry(drag, ph.element, drag.start);
1455
1723
  this.placeholder = ph;
1456
1724
  this.activeDrag = drag;
1457
1725
  this.isOrigin = origin;
@@ -1464,13 +1732,16 @@ class DropListRef {
1464
1732
  fallbackRtl: rtl,
1465
1733
  animationMs: this.sortAnimationDuration,
1466
1734
  spacer,
1735
+ onOffsetChange: this.childLists.size
1736
+ ? (el, offset) => this.propagateOffset(el, offset)
1737
+ : undefined,
1467
1738
  });
1468
1739
  if (!origin && this.hitCached) {
1469
1740
  // Inserting the placeholder made this list bigger; the grown box is what the user sees
1470
1741
  // (and must be able to reach, e.g. to append below the last item). The natural box comes
1471
1742
  // back on exit, so entering/leaving at the old edge cannot flicker.
1472
1743
  this.naturalHit = this.hit;
1473
- this.hit = measureHit(this.el);
1744
+ this._cacheGeometry();
1474
1745
  }
1475
1746
  }
1476
1747
  /**
@@ -1528,12 +1799,16 @@ class DropListRef {
1528
1799
  }
1529
1800
  return -1;
1530
1801
  }
1531
- lockPlaceholderGeometry(drag, placeholder, rect) {
1532
- const set = (k, v) => placeholder.style.setProperty(k, v);
1533
- // The clone was taken from the source, which may already carry drag-time inline styles.
1534
- placeholder.style.display = drag.originalDisplay;
1802
+ lockPlaceholderGeometry(drag, ph, rect) {
1803
+ const placeholder = ph.element;
1804
+ const set = (k, v) => placeholder.style.setProperty(k, v, 'important');
1535
1805
  placeholder.style.zIndex = '';
1536
1806
  placeholder.style.removeProperty('will-change');
1807
+ if (ph.custom)
1808
+ return;
1809
+ placeholder.style.display = drag.originalDisplay;
1810
+ // Occupies exactly the dragged item's footprint, so the gap reserved in the list — and the
1811
+ // sibling-displacement math derived from it — matches the item's real size.
1537
1812
  set('width', `${rect.width}px`);
1538
1813
  set('height', `${rect.height}px`);
1539
1814
  // set('min-width', `${rect.width}px`);
@@ -1593,7 +1868,6 @@ function measureHit(el) {
1593
1868
  * Lists register themselves automatically when `NgxDropListGroup` wraps them.
1594
1869
  */
1595
1870
  class DropListGroupRef {
1596
- el;
1597
1871
  lists = new Set();
1598
1872
  add(list) {
1599
1873
  this.lists.add(list);
@@ -1613,6 +1887,21 @@ class DragDropService {
1613
1887
  ...(ngDevMode ? [{ debugName: "dropLists" }] : /* istanbul ignore next */ []));
1614
1888
  activeDrag = signal(null, /* @ts-ignore */
1615
1889
  ...(ngDevMode ? [{ debugName: "activeDrag" }] : /* istanbul ignore next */ []));
1890
+ /**
1891
+ * DOM-position registries, used to find the nearest enclosing list/group by walking real
1892
+ * ancestor elements instead of Angular's element-injector tree.
1893
+ *
1894
+ * This matters for `ngTemplateOutlet`-based recursive trees (a list of items whose own
1895
+ * children are rendered by re-invoking the SAME `<ng-template>`, as in a tree/outliner UI):
1896
+ * each invocation gets a FRESH embedded view whose injector context is wherever the outlet
1897
+ * was written in the template, not wherever its content ends up in the DOM. So `inject(TOKEN,
1898
+ * {skipSelf})` from one recursion level can never see a provider from another level — the
1899
+ * only fix within Angular's own DI is to manually thread `ngTemplateOutletInjector` through
1900
+ * every recursive call, which isn't reasonable to require of every consumer of this library.
1901
+ * A plain DOM walk has no such limitation: it only cares where elements actually ended up.
1902
+ */
1903
+ listsByEl = new Map();
1904
+ groupsByEl = new Map();
1616
1905
  registerDragItem(ref) {
1617
1906
  this.drags.update((items) => (items.includes(ref) ? items : [...items, ref]));
1618
1907
  }
@@ -1623,9 +1912,37 @@ class DragDropService {
1623
1912
  }
1624
1913
  registerDropList(ref) {
1625
1914
  this.dropLists.update((items) => (items.includes(ref) ? items : [...items, ref]));
1915
+ if (ref.el)
1916
+ this.listsByEl.set(ref.el, ref);
1626
1917
  }
1627
1918
  removeDropList(ref) {
1628
1919
  this.dropLists.update((items) => items.filter((x) => x !== ref));
1920
+ if (ref.el && this.listsByEl.get(ref.el) === ref)
1921
+ this.listsByEl.delete(ref.el);
1922
+ }
1923
+ registerGroup(el, ref) {
1924
+ this.groupsByEl.set(el, ref);
1925
+ }
1926
+ removeGroup(el) {
1927
+ this.groupsByEl.delete(el);
1928
+ }
1929
+ /** Nearest registered drop list starting at (and including) `start`, walking up the DOM. */
1930
+ findAncestorDropList(start) {
1931
+ for (let n = start; n; n = n.parentElement) {
1932
+ const found = this.listsByEl.get(n);
1933
+ if (found)
1934
+ return found;
1935
+ }
1936
+ return null;
1937
+ }
1938
+ /** Nearest registered drop-list group starting at (and including) `start`, walking up the DOM. */
1939
+ findAncestorGroup(start) {
1940
+ for (let n = start; n; n = n.parentElement) {
1941
+ const found = this.groupsByEl.get(n);
1942
+ if (found)
1943
+ return found;
1944
+ }
1945
+ return null;
1629
1946
  }
1630
1947
  begin(ref) {
1631
1948
  this.activeDrag.set(ref);
@@ -1685,118 +2002,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
1685
2002
  args: [{ providedIn: 'root' }]
1686
2003
  }] });
1687
2004
 
1688
- const NGX_DROPLIST_GROUP = new InjectionToken('ngx-drop-list-group');
1689
- /** Wrap several `ngxDropList`s to let items move freely between them. */
1690
- class NgxDropListGroup {
1691
- _ref = new DropListGroupRef();
1692
- el = inject((ElementRef));
1693
- injector = inject(Injector);
1694
- constructor() {
1695
- this._ref.el = this.el.nativeElement;
1696
- }
1697
- ngOnDestroy() {
1698
- this._ref.lists.clear();
1699
- }
1700
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1701
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropListGroup, isStandalone: true, selector: "[NgxDropListGroup],[ngxDropListGroup]", providers: [{ provide: NGX_DROPLIST_GROUP, useExisting: NgxDropListGroup }], exportAs: ["ngxDropListGroup"], ngImport: i0 });
1702
- }
1703
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, decorators: [{
1704
- type: Directive,
1705
- args: [{
1706
- selector: '[NgxDropListGroup],[ngxDropListGroup]',
1707
- providers: [{ provide: NGX_DROPLIST_GROUP, useExisting: NgxDropListGroup }],
1708
- exportAs: 'ngxDropListGroup',
1709
- }]
1710
- }], ctorParameters: () => [] });
1711
-
1712
- const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
1713
- class NgxPlaceholder {
1714
- tpl;
1715
- _ref = new PlaceHolderRef();
1716
- list = inject(NGX_DROPLIST, { optional: true, skipSelf: true });
1717
- constructor(tpl) {
1718
- this.tpl = tpl;
1719
- this._ref.tpl = tpl;
1720
- this._ref.dropList = this.list?._ref;
1721
- }
1722
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxPlaceholder, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
1723
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxPlaceholder, isStandalone: true, selector: "[NgxPlaceholder],[ngxPlaceholder]", providers: [{ provide: NGX_PLACEHOLDER, useExisting: NgxPlaceholder }], ngImport: i0 });
1724
- }
1725
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxPlaceholder, decorators: [{
1726
- type: Directive,
1727
- args: [{
1728
- selector: '[NgxPlaceholder],[ngxPlaceholder]',
1729
- providers: [{ provide: NGX_PLACEHOLDER, useExisting: NgxPlaceholder }],
1730
- }]
1731
- }], ctorParameters: () => [{ type: i0.TemplateRef }] });
1732
-
1733
- const NGX_DROPLIST = new InjectionToken('ngx-drop-list');
1734
- class NgxDropList {
1735
- el;
1736
- _ref = new DropListRef();
1737
- set data(value) {
1738
- this._ref.data = value;
1739
- }
1740
- set connectedTo(value) {
1741
- this._ref.connectedTo = value ?? [];
1742
- }
1743
- set disableSort(value) {
1744
- this._ref.disableSort = value === true;
1745
- }
1746
- drop = new EventEmitter();
1747
- customPlaceholder;
1748
- sub;
1749
- service = inject(DragDropService);
1750
- group = inject(NGX_DROPLIST_GROUP, { optional: true, skipSelf: true });
1751
- constructor(el) {
1752
- this.el = el;
1753
- }
1754
- ngOnInit() {
1755
- this._ref.el = this.el.nativeElement;
1756
- this._ref.dropListGroup = this.group?._ref ?? null;
1757
- // console.log('list:', this._ref.el, 'group:', this._ref.dropListGroup?.el);
1758
- // Previously the group directive never actually tracked its lists — fixed here.
1759
- this.group?._ref.add(this._ref);
1760
- this.sub = this._ref.onDrop.subscribe((e) => this.drop.emit(e));
1761
- this.service.registerDropList(this._ref);
1762
- }
1763
- ngAfterContentInit() {
1764
- if (this.customPlaceholder) {
1765
- this._ref.customPlaceholder = this.customPlaceholder._ref;
1766
- }
1767
- }
1768
- ngOnDestroy() {
1769
- this.sub?.unsubscribe();
1770
- this._ref._release();
1771
- this.group?._ref.remove(this._ref);
1772
- this.service.removeDropList(this._ref);
1773
- }
1774
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
1775
- 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 });
1776
- }
1777
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
1778
- type: Directive,
1779
- args: [{
1780
- selector: '[NgxDropList],[ngxDropList]',
1781
- providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }],
1782
- host: { class: 'ngx-drop-list' },
1783
- }]
1784
- }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { data: [{
1785
- type: Input,
1786
- args: ['data']
1787
- }], connectedTo: [{
1788
- type: Input
1789
- }], disableSort: [{
1790
- type: Input
1791
- }], drop: [{
1792
- type: Output
1793
- }], customPlaceholder: [{
1794
- type: ContentChild,
1795
- args: [NgxPlaceholder, {
1796
- descendants: true,
1797
- }]
1798
- }] } });
1799
-
1800
2005
  /**
1801
2006
  * Auto-scrolls the nearest scrollable ancestor (or window) while the pointer
1802
2007
  * is dragged near the edge of the scrollable viewport. Direction-agnostic —
@@ -1894,7 +2099,6 @@ const NGX_DRAGGABLE = new InjectionToken('ngx-draggable');
1894
2099
  const DEFAULT_KEYBOARD_STEP = 8;
1895
2100
  class NgxDraggable {
1896
2101
  host;
1897
- renderer;
1898
2102
  disabled = model(false, /* @ts-ignore */
1899
2103
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1900
2104
  /** Element whose rect constrains the drag. Now actually enforced. */
@@ -1928,8 +2132,6 @@ class NgxDraggable {
1928
2132
  ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
1929
2133
  doc = inject(DOCUMENT);
1930
2134
  service = inject(DragDropService);
1931
- list = inject(NGX_DROPLIST, { optional: true, skipSelf: true });
1932
- group = inject(NGX_DROPLIST_GROUP, { optional: true, skipSelf: true });
1933
2135
  scroller = new AutoScroller();
1934
2136
  down = false;
1935
2137
  start = { x: 0, y: 0 };
@@ -1947,9 +2149,9 @@ class NgxDraggable {
1947
2149
  lastWindowScroll = { left: 0, top: 0 };
1948
2150
  removeContainerScroll;
1949
2151
  removeWindowScroll;
1950
- constructor(host, renderer) {
2152
+ renderer = inject(Renderer2);
2153
+ constructor(host) {
1951
2154
  this.host = host;
1952
- this.renderer = renderer;
1953
2155
  effect(() => {
1954
2156
  this._ref.el = this.dragRootElement()
1955
2157
  ? (this.host.nativeElement.closest(this.dragRootElement()) ?? this.host.nativeElement)
@@ -1962,10 +2164,15 @@ class NgxDraggable {
1962
2164
  : this.host.nativeElement;
1963
2165
  this._ref.boundary = this.boundary();
1964
2166
  this._ref.lockAxis = this.lockAxis();
1965
- this._ref.dropListGroup = this.group?._ref;
2167
+ // Found by walking the real DOM, not Angular's element-injector tree — see the matching
2168
+ // comment in `ngx-drop-list.directive.ts` for why: a draggable rendered by a recursively
2169
+ // invoked `ngTemplateOutlet` needs this just as much as a nested list does.
2170
+ const parentEl = this.host.nativeElement.parentElement;
2171
+ const list = this.service.findAncestorDropList(parentEl);
2172
+ this._ref.dropListGroup = list?.dropListGroup ?? this.service.findAncestorGroup(parentEl);
1966
2173
  this._ref.service = this.service;
1967
2174
  this._ref.init();
1968
- this._ref.withDropList(this.list?._ref ?? null);
2175
+ this._ref.withDropList(list ?? null);
1969
2176
  this.service.registerDragItem(this._ref);
1970
2177
  this.removeDown = this.renderer.listen(this.host.nativeElement, 'pointerdown', (e) => this.pointerDown(e));
1971
2178
  this.removeKeydown = this.renderer.listen(this.host.nativeElement, 'keydown', (e) => this.keyDown(e));
@@ -2144,7 +2351,9 @@ class NgxDraggable {
2144
2351
  // that keyboard sorting resolves against real geometry.
2145
2352
  const inList = !!this._ref.dropList && this._ref.dropList.el === this._ref.el.parentElement;
2146
2353
  const r = this._ref.el.getBoundingClientRect();
2147
- const origin = inList ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : { x: 0, y: 0 };
2354
+ const origin = inList
2355
+ ? { x: r.left + r.width / 2, y: r.top + r.height / 2 }
2356
+ : { x: 0, y: 0 };
2148
2357
  this._ref.pointerDown(origin);
2149
2358
  this._ref.boundary = this.boundary();
2150
2359
  this._ref.lockAxis = this.lockAxis();
@@ -2182,7 +2391,7 @@ class NgxDraggable {
2182
2391
  return (target instanceof HTMLElement &&
2183
2392
  !!target.closest('button,a,input,textarea,select,[data-no-drag]'));
2184
2393
  }
2185
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
2394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
2186
2395
  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 }], ngImport: i0 });
2187
2396
  }
2188
2397
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
@@ -2198,7 +2407,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2198
2407
  '[class.disable-drag]': 'disabled()',
2199
2408
  },
2200
2409
  }]
2201
- }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], 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: [{
2410
+ }], 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: [{
2202
2411
  type: Input,
2203
2412
  args: ['data']
2204
2413
  }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
@@ -2578,6 +2787,131 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2578
2787
  args: ['keydown', ['$event']]
2579
2788
  }] } });
2580
2789
 
2790
+ const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
2791
+ class NgxPlaceholder {
2792
+ tpl;
2793
+ _ref = new PlaceHolderRef();
2794
+ list = inject(NGX_DROPLIST, { optional: true, skipSelf: true });
2795
+ constructor(tpl) {
2796
+ this.tpl = tpl;
2797
+ this._ref.tpl = tpl;
2798
+ this._ref.dropList = this.list?._ref;
2799
+ // Lets the embedded view join Angular's change-detection tree (matters in zoneless apps,
2800
+ // and for app-stability tracking) instead of being rendered once and left dangling.
2801
+ this._ref.appRef = inject(ApplicationRef);
2802
+ }
2803
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxPlaceholder, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
2804
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxPlaceholder, isStandalone: true, selector: "[NgxPlaceholder],[ngxPlaceholder]", providers: [{ provide: NGX_PLACEHOLDER, useExisting: NgxPlaceholder }], ngImport: i0 });
2805
+ }
2806
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxPlaceholder, decorators: [{
2807
+ type: Directive,
2808
+ args: [{
2809
+ selector: '[NgxPlaceholder],[ngxPlaceholder]',
2810
+ providers: [{ provide: NGX_PLACEHOLDER, useExisting: NgxPlaceholder }],
2811
+ }]
2812
+ }], ctorParameters: () => [{ type: i0.TemplateRef }] });
2813
+
2814
+ const NGX_DROPLIST = new InjectionToken('ngx-drop-list');
2815
+ class NgxDropList {
2816
+ el;
2817
+ _ref = new DropListRef();
2818
+ set data(value) {
2819
+ this._ref.data = value;
2820
+ }
2821
+ set connectedTo(value) {
2822
+ this._ref.connectedTo = value ?? [];
2823
+ }
2824
+ set disableSort(value) {
2825
+ this._ref.disableSort = value === true;
2826
+ }
2827
+ drop = new EventEmitter();
2828
+ customPlaceholder;
2829
+ sub;
2830
+ service = inject(DragDropService);
2831
+ constructor(el) {
2832
+ this.el = el;
2833
+ }
2834
+ ngOnInit() {
2835
+ this._ref.el = this.el.nativeElement;
2836
+ // Found by walking the real DOM (not Angular's element-injector tree): a list rendered by a
2837
+ // recursively-invoked `ngTemplateOutlet` (a tree/outliner UI, where every level re-uses the
2838
+ // SAME `<ng-template>`) gets a fresh injector context per invocation, so `inject(TOKEN,
2839
+ // {skipSelf})` can never see a provider from another recursion level — the DOM position is
2840
+ // what actually determines nesting/grouping for this library's purposes anyway.
2841
+ const parentEl = this.el.nativeElement.parentElement;
2842
+ const group = this.service.findAncestorGroup(parentEl);
2843
+ const parent = this.service.findAncestorDropList(parentEl);
2844
+ this._ref.dropListGroup = group ?? null;
2845
+ group?.add(this._ref);
2846
+ parent?._registerChild(this._ref);
2847
+ this.sub = this._ref.onDrop.subscribe((e) => this.drop.emit(e));
2848
+ this.service.registerDropList(this._ref);
2849
+ }
2850
+ ngAfterContentInit() {
2851
+ if (this.customPlaceholder) {
2852
+ this._ref.customPlaceholder = this.customPlaceholder._ref;
2853
+ }
2854
+ }
2855
+ ngOnDestroy() {
2856
+ this.sub?.unsubscribe();
2857
+ this._ref._release();
2858
+ this._ref.parentList?._unregisterChild(this._ref);
2859
+ this._ref.dropListGroup?.remove(this._ref);
2860
+ this.service.removeDropList(this._ref);
2861
+ }
2862
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
2863
+ 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 });
2864
+ }
2865
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
2866
+ type: Directive,
2867
+ args: [{
2868
+ selector: '[NgxDropList],[ngxDropList]',
2869
+ providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }],
2870
+ host: { class: 'ngx-drop-list' },
2871
+ }]
2872
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { data: [{
2873
+ type: Input,
2874
+ args: ['data']
2875
+ }], connectedTo: [{
2876
+ type: Input
2877
+ }], disableSort: [{
2878
+ type: Input
2879
+ }], drop: [{
2880
+ type: Output
2881
+ }], customPlaceholder: [{
2882
+ type: ContentChild,
2883
+ args: [NgxPlaceholder, {
2884
+ descendants: true,
2885
+ }]
2886
+ }] } });
2887
+
2888
+ const NGX_DROPLIST_GROUP = new InjectionToken('ngx-drop-list-group');
2889
+ /** Wrap several `ngxDropList`s to let items move freely between them. */
2890
+ class NgxDropListGroup {
2891
+ _ref = new DropListGroupRef();
2892
+ el = inject((ElementRef));
2893
+ service = inject(DragDropService);
2894
+ ngOnInit() {
2895
+ // Registered by DOM element (not just provided via DI) so a list nested inside a recursive
2896
+ // `ngTemplateOutlet` tree can still find this group by walking its real ancestors — see the
2897
+ // comment on `DragDropService`'s registries for why DI alone can't do this.
2898
+ this.service.registerGroup(this.el.nativeElement, this._ref);
2899
+ }
2900
+ ngOnDestroy() {
2901
+ this._ref.lists.clear();
2902
+ this.service.removeGroup(this.el.nativeElement);
2903
+ }
2904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2905
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropListGroup, isStandalone: true, selector: "[NgxDropListGroup],[ngxDropListGroup]", providers: [{ provide: NGX_DROPLIST_GROUP, useExisting: NgxDropListGroup }], ngImport: i0 });
2906
+ }
2907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, decorators: [{
2908
+ type: Directive,
2909
+ args: [{
2910
+ selector: '[NgxDropListGroup],[ngxDropListGroup]',
2911
+ providers: [{ provide: NGX_DROPLIST_GROUP, useExisting: NgxDropListGroup }],
2912
+ }]
2913
+ }] });
2914
+
2581
2915
  function moveItemInArray(array, from, to) {
2582
2916
  const f = clamp(from, array.length - 1);
2583
2917
  const t = clamp(to, array.length - 1);