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,
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
568
|
-
//
|
|
569
|
-
// to
|
|
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.
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
592
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
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
|
-
//
|
|
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.
|
|
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,
|
|
1532
|
-
const
|
|
1533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 }
|
|
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 }
|
|
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);
|