ngx-kit 7.4.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.
- package/drag-resize/README.md +25 -3
- package/fesm2022/ngx-kit-data-table.mjs +3 -3
- package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
- package/fesm2022/ngx-kit-dialog.mjs +11 -7
- package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
- package/fesm2022/ngx-kit-drag-resize.mjs +1710 -471
- package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -1
- package/fesm2022/ngx-kit-image-viewer.mjs +348 -0
- package/fesm2022/ngx-kit-image-viewer.mjs.map +1 -0
- package/fesm2022/ngx-kit-notify.mjs +2 -2
- package/fesm2022/ngx-kit-notify.mjs.map +1 -1
- package/fesm2022/ngx-kit-side-nav.mjs +170 -0
- package/fesm2022/ngx-kit-side-nav.mjs.map +1 -0
- package/{gallery → image-viewer}/README.md +43 -7
- package/package.json +9 -9
- package/side-nav/README.md +11 -0
- package/types/ngx-kit-dialog.d.ts +6 -2
- package/types/ngx-kit-drag-resize.d.ts +274 -44
- package/types/{ngx-kit-gallery.d.ts → ngx-kit-image-viewer.d.ts} +5 -39
- package/types/ngx-kit-side-nav.d.ts +53 -0
- package/drawer-menu/README.md +0 -125
- package/fesm2022/ngx-kit-drawer-menu.mjs +0 -456
- package/fesm2022/ngx-kit-drawer-menu.mjs.map +0 -1
- package/fesm2022/ngx-kit-gallery.mjs +0 -419
- package/fesm2022/ngx-kit-gallery.mjs.map +0 -1
- package/types/ngx-kit-drawer-menu.d.ts +0 -161
|
@@ -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
|
/**
|
|
@@ -79,22 +79,97 @@ function adjustForZoom(value) {
|
|
|
79
79
|
return zoom ? value / zoom : value;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Copy the browser-resolved visual state of an element.
|
|
84
|
+
*
|
|
85
|
+
* A drag preview is moved to <body>, so selectors depending on ancestors no
|
|
86
|
+
* longer apply. Copying computed styles (including custom properties) makes
|
|
87
|
+
* the preview visually independent from the original DOM subtree.
|
|
88
|
+
*/
|
|
89
|
+
function copyComputedStyle(source, target) {
|
|
90
|
+
copyStyleValues(source, target);
|
|
91
|
+
// Make the preview deterministic once it is taken out of normal flow (root only:
|
|
92
|
+
// descendants keep their own margins so the internal layout is unchanged).
|
|
93
|
+
target.style.setProperty('margin', '0', 'important');
|
|
94
|
+
target.style.setProperty('box-sizing', 'border-box', 'important');
|
|
95
|
+
}
|
|
96
|
+
function copyStyleValues(source, target) {
|
|
97
|
+
const computed = window.getComputedStyle(source);
|
|
98
|
+
for (let i = 0; i < computed.length; i++) {
|
|
99
|
+
const property = computed.item(i);
|
|
100
|
+
const value = computed.getPropertyValue(property);
|
|
101
|
+
if (value) {
|
|
102
|
+
target.style.setProperty(property, value, computed.getPropertyPriority(property));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function copyComputedStyleTree(source, target) {
|
|
107
|
+
copyComputedStyle(source, target);
|
|
108
|
+
copyDescendants(source, target);
|
|
109
|
+
}
|
|
110
|
+
function copyDescendants(source, target) {
|
|
111
|
+
const sourceChildren = Array.from(source.children);
|
|
112
|
+
const targetChildren = Array.from(target.children);
|
|
113
|
+
const count = Math.min(sourceChildren.length, targetChildren.length);
|
|
114
|
+
for (let i = 0; i < count; i++) {
|
|
115
|
+
copyStyleValues(sourceChildren[i], targetChildren[i]);
|
|
116
|
+
copyDescendants(sourceChildren[i], targetChildren[i]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Backward-compatible helper retained for consumers of older versions. */
|
|
120
|
+
function copyEssentialStyles(source, target) {
|
|
121
|
+
copyComputedStyle(source, target);
|
|
122
|
+
target.style.setProperty('box-sizing', 'border-box');
|
|
123
|
+
}
|
|
124
|
+
function combineTransforms(transform, initialTransform) {
|
|
125
|
+
return initialTransform && initialTransform !== 'none'
|
|
126
|
+
? transform + ' ' + initialTransform
|
|
127
|
+
: transform;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Above any app chrome (modals, sticky headers) but below the browser's own UI. */
|
|
131
|
+
const PREVIEW_Z_INDEX = '2147483000';
|
|
132
|
+
/**
|
|
133
|
+
* Creates the visual that follows the pointer while an item of a drop list is dragged.
|
|
134
|
+
*
|
|
135
|
+
* It is appended to `<body>` with `position: fixed`, so no ancestor of the original item
|
|
136
|
+
* (`overflow: hidden`, scroll containers, clipping, transformed/positioned parents) can clip
|
|
137
|
+
* or offset it. Computed styles are copied recursively first, because once the clone leaves
|
|
138
|
+
* its list every selector that depended on an ancestor (`.column .card {}`) and every
|
|
139
|
+
* inherited property (font, colour, `direction`) would otherwise be lost.
|
|
140
|
+
*
|
|
141
|
+
* Only used for drop-list drags. Free drag never calls this.
|
|
142
|
+
*/
|
|
82
143
|
function cloneDragElementInBody(dragEl, rect, pointerX, pointerY) {
|
|
144
|
+
const doc = dragEl.ownerDocument;
|
|
83
145
|
const preview = dragEl.cloneNode(true);
|
|
84
146
|
// Avoid duplicated ids in the document.
|
|
85
147
|
preview.removeAttribute('id');
|
|
148
|
+
preview.querySelectorAll('[id]').forEach((n) => n.removeAttribute('id'));
|
|
86
149
|
preview.classList.add('ngx-drag-preview');
|
|
150
|
+
preview.setAttribute('aria-hidden', 'true');
|
|
151
|
+
preview.setAttribute('inert', '');
|
|
152
|
+
copyComputedStyleTree(dragEl, preview);
|
|
87
153
|
Object.assign(preview.style, {
|
|
88
154
|
position: 'fixed',
|
|
89
155
|
top: `${rect.top}px`,
|
|
90
156
|
left: `${rect.left}px`,
|
|
157
|
+
right: 'auto',
|
|
158
|
+
bottom: 'auto',
|
|
91
159
|
width: `${rect.width}px`,
|
|
92
160
|
height: `${rect.height}px`,
|
|
93
161
|
margin: '0',
|
|
94
162
|
pointerEvents: 'none',
|
|
95
163
|
boxSizing: 'border-box',
|
|
164
|
+
transition: 'none',
|
|
165
|
+
animation: 'none',
|
|
166
|
+
transform: 'none',
|
|
167
|
+
visibility: 'visible',
|
|
168
|
+
display: preview.style.display === 'none' ? '' : preview.style.display,
|
|
169
|
+
willChange: 'transform',
|
|
170
|
+
zIndex: PREVIEW_Z_INDEX,
|
|
96
171
|
});
|
|
97
|
-
|
|
172
|
+
doc.body.appendChild(preview);
|
|
98
173
|
return {
|
|
99
174
|
element: preview,
|
|
100
175
|
offsetX: pointerX - rect.left,
|
|
@@ -108,8 +183,16 @@ class DragRef {
|
|
|
108
183
|
el;
|
|
109
184
|
boundary;
|
|
110
185
|
lockAxis;
|
|
186
|
+
/**
|
|
187
|
+
* The list this draggable BELONGS to (set by the template). Dragging never changes it.
|
|
188
|
+
* The list the pointer is currently over is `activeDropList`.
|
|
189
|
+
*/
|
|
111
190
|
dropList = null;
|
|
112
191
|
dropListGroup;
|
|
192
|
+
/** Used to find the drop list under the pointer. Set by the directive; optional. */
|
|
193
|
+
service;
|
|
194
|
+
/** Drop list currently under the pointer during a drop-list drag (null = outside every list). */
|
|
195
|
+
activeDropList = null;
|
|
113
196
|
placeholder;
|
|
114
197
|
originDropList = null;
|
|
115
198
|
originIndex = -1;
|
|
@@ -131,6 +214,21 @@ class DragRef {
|
|
|
131
214
|
sourceVisibility = '';
|
|
132
215
|
sourceDisplay = '';
|
|
133
216
|
sourcePointerEvents = '';
|
|
217
|
+
sourceTransition = '';
|
|
218
|
+
/** True while a drop-list drag (body preview + placeholder) is in progress. Free drag: always false. */
|
|
219
|
+
listDrag = false;
|
|
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;
|
|
229
|
+
lastEffective = { x: 0, y: 0 };
|
|
230
|
+
scrollListener;
|
|
231
|
+
resizeListener;
|
|
134
232
|
init() {
|
|
135
233
|
this.previousTransform = getComputedStyle(this.el).getPropertyValue('transform');
|
|
136
234
|
}
|
|
@@ -157,14 +255,18 @@ class DragRef {
|
|
|
157
255
|
this.moveDx = 0;
|
|
158
256
|
this.moveDy = 0;
|
|
159
257
|
this.scrollCompensation = { x: 0, y: 0 };
|
|
160
|
-
|
|
161
|
-
|
|
258
|
+
// Only draggables that are direct children of their drop list take part in sorting.
|
|
259
|
+
// Everything else is a plain free drag.
|
|
260
|
+
const list = this.dropList && this.dropList.el === this.el.parentElement ? this.dropList : null;
|
|
261
|
+
this.originDropList = list;
|
|
262
|
+
this.originIndex = list?.getItemIndex(this) ?? -1;
|
|
162
263
|
this.previousTransform = getComputedStyle(this.el).getPropertyValue('transform');
|
|
163
264
|
this.isDragging.set(true);
|
|
164
265
|
this.previousZIndex = this.el.style.zIndex;
|
|
165
266
|
this.sourceVisibility = this.el.style.visibility;
|
|
166
267
|
this.sourceDisplay = this.el.style.display;
|
|
167
268
|
this.sourcePointerEvents = this.el.style.pointerEvents;
|
|
269
|
+
this.sourceTransition = this.el.style.transition;
|
|
168
270
|
this.el.classList.add('ngx-draggable--dragging');
|
|
169
271
|
this.el.style.willChange = 'transform';
|
|
170
272
|
this.el.style.transition = 'none';
|
|
@@ -179,8 +281,10 @@ class DragRef {
|
|
|
179
281
|
* create a body-level preview because the original element can be
|
|
180
282
|
* inside an overflow:auto/hidden container such as a Kanban column.
|
|
181
283
|
*/
|
|
182
|
-
if (
|
|
183
|
-
this.
|
|
284
|
+
if (list) {
|
|
285
|
+
this.listDrag = true;
|
|
286
|
+
// Order matters: clone while the source is still visible and fully styled, hide it,
|
|
287
|
+
// and only THEN measure (the placeholder takes over the source's slot).
|
|
184
288
|
const preview = cloneDragElementInBody(this.el, this.startRect, pointer.x, pointer.y);
|
|
185
289
|
this.preview = preview.element;
|
|
186
290
|
this.previewOffsetX = preview.offsetX;
|
|
@@ -188,6 +292,7 @@ class DragRef {
|
|
|
188
292
|
this.el.style.visibility = 'hidden';
|
|
189
293
|
this.el.style.display = 'none';
|
|
190
294
|
this.el.style.pointerEvents = 'none';
|
|
295
|
+
this.beginListDrag(list, pointer);
|
|
191
296
|
this.updatePreview();
|
|
192
297
|
}
|
|
193
298
|
else {
|
|
@@ -215,8 +320,11 @@ class DragRef {
|
|
|
215
320
|
x: dx,
|
|
216
321
|
y: dy,
|
|
217
322
|
});
|
|
218
|
-
if (this.
|
|
323
|
+
if (this.listDrag) {
|
|
219
324
|
this.updatePreview();
|
|
325
|
+
// Sorting follows the (axis/boundary constrained) pointer, not the raw one.
|
|
326
|
+
this.lastEffective = { x: this.startPointer.x + dx, y: this.startPointer.y + dy };
|
|
327
|
+
this.updateDropTarget(this.lastEffective);
|
|
220
328
|
}
|
|
221
329
|
else {
|
|
222
330
|
this.applyTransform();
|
|
@@ -233,16 +341,15 @@ class DragRef {
|
|
|
233
341
|
endDrag() {
|
|
234
342
|
if (!this.isDragging())
|
|
235
343
|
return;
|
|
236
|
-
|
|
344
|
+
if (this.listDrag) {
|
|
345
|
+
this.isDragging.set(false);
|
|
346
|
+
this.finishListDrag(true);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// ---- free drag: unchanged ----
|
|
237
350
|
this.isDragging.set(false);
|
|
238
|
-
// Normal Drag MUST behave exactly like before.
|
|
239
351
|
this.placeholder?.detach();
|
|
240
352
|
this.placeholder = undefined;
|
|
241
|
-
if (dropList) {
|
|
242
|
-
dropList.finishDrag(this);
|
|
243
|
-
this.el.style.transform = this.previousTransform;
|
|
244
|
-
}
|
|
245
|
-
// Remove body preview after DropList has finished reading its state.
|
|
246
353
|
this.preview?.remove();
|
|
247
354
|
this.preview = undefined;
|
|
248
355
|
this.el.classList.remove('ngx-draggable--dragging');
|
|
@@ -258,14 +365,34 @@ class DragRef {
|
|
|
258
365
|
this.moveDx = 0;
|
|
259
366
|
this.moveDy = 0;
|
|
260
367
|
this.position.set({ x: 0, y: 0 });
|
|
261
|
-
this.
|
|
262
|
-
|
|
368
|
+
if (this.listDrag) {
|
|
369
|
+
// Rollback: nothing was ever committed to the DOM or the model, so restoring is just cleanup.
|
|
370
|
+
this.isDragging.set(false);
|
|
371
|
+
this.finishListDrag(false);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// ---- free drag: unchanged ----
|
|
263
375
|
this.el.style.transform = this.previousTransform;
|
|
264
376
|
this.el.style.visibility = this.sourceVisibility;
|
|
265
377
|
this.el.style.pointerEvents = this.sourcePointerEvents;
|
|
266
378
|
this.scrollCompensation = { x: 0, y: 0 };
|
|
267
379
|
this.endDrag();
|
|
268
380
|
}
|
|
381
|
+
/** Abort without emitting anything (the directive was destroyed mid-drag). Idempotent. */
|
|
382
|
+
dispose() {
|
|
383
|
+
if (!this.listDrag)
|
|
384
|
+
return;
|
|
385
|
+
this.isDragging.set(false);
|
|
386
|
+
this.finishListDrag(false);
|
|
387
|
+
}
|
|
388
|
+
/** True while a drop-list drag is running. */
|
|
389
|
+
get isListDrag() {
|
|
390
|
+
return this.listDrag;
|
|
391
|
+
}
|
|
392
|
+
/** Inline `display` the source element had before it was hidden for the drag. */
|
|
393
|
+
get originalDisplay() {
|
|
394
|
+
return this.sourceDisplay;
|
|
395
|
+
}
|
|
269
396
|
get x() {
|
|
270
397
|
return this.moveDx;
|
|
271
398
|
}
|
|
@@ -289,16 +416,15 @@ class DragRef {
|
|
|
289
416
|
notifyScroll(dx, dy) {
|
|
290
417
|
if (!this.isDragging())
|
|
291
418
|
return;
|
|
419
|
+
// Drop-list drags are position:fixed in <body> and follow the pointer in viewport space, so
|
|
420
|
+
// scrolling needs no compensation; sorting listens to scroll on its own (see beginListDrag).
|
|
421
|
+
if (this.listDrag)
|
|
422
|
+
return;
|
|
292
423
|
this.scrollCompensation = {
|
|
293
424
|
x: this.scrollCompensation.x + dx,
|
|
294
425
|
y: this.scrollCompensation.y + dy,
|
|
295
426
|
};
|
|
296
|
-
|
|
297
|
-
this.updatePreview();
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
this.applyTransform();
|
|
301
|
-
}
|
|
427
|
+
this.applyTransform();
|
|
302
428
|
}
|
|
303
429
|
applyTransform() {
|
|
304
430
|
const base = this.previousTransform && this.previousTransform !== 'none'
|
|
@@ -308,12 +434,848 @@ class DragRef {
|
|
|
308
434
|
const ty = this.moveDy + this.scrollCompensation.y;
|
|
309
435
|
this.el.style.transform = `${base}translate3d(${tx}px, ${ty}px, 0)`;
|
|
310
436
|
}
|
|
437
|
+
// -------------------------------------------------------------------------------------
|
|
438
|
+
// drop-list drag
|
|
439
|
+
// -------------------------------------------------------------------------------------
|
|
440
|
+
beginListDrag(origin, pointer) {
|
|
441
|
+
const doc = this.el.ownerDocument;
|
|
442
|
+
const win = doc.defaultView ?? window;
|
|
443
|
+
this.dropListsSnapshot = null; // force refreshDropCandidates() to do a full pass below
|
|
444
|
+
this.refreshDropCandidates(origin);
|
|
445
|
+
this.lastEffective = { ...pointer };
|
|
446
|
+
this.activeDropList = origin;
|
|
447
|
+
origin.enter(this, pointer.x, pointer.y);
|
|
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).
|
|
450
|
+
origin._cacheGeometry();
|
|
451
|
+
this.scrollListener = () => {
|
|
452
|
+
for (const l of this.cachedLists)
|
|
453
|
+
l._onScroll();
|
|
454
|
+
this.updateDropTarget(this.lastEffective);
|
|
455
|
+
};
|
|
456
|
+
this.resizeListener = () => {
|
|
457
|
+
for (const l of this.cachedLists)
|
|
458
|
+
l._refresh();
|
|
459
|
+
this.updateDropTarget(this.lastEffective);
|
|
460
|
+
};
|
|
461
|
+
// Capture: `scroll` does not bubble, and any ancestor (or the page) may be the one scrolling.
|
|
462
|
+
doc.addEventListener('scroll', this.scrollListener, { capture: true, passive: true });
|
|
463
|
+
win.addEventListener('resize', this.resizeListener, { passive: true });
|
|
464
|
+
}
|
|
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
|
+
*/
|
|
504
|
+
updateDropTarget(pointer) {
|
|
505
|
+
const origin = this.originDropList;
|
|
506
|
+
if (!origin)
|
|
507
|
+
return;
|
|
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
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
origin.exit(this); // truly outside the origin's territory now: reset it (idempotent)
|
|
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;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Single teardown path for every way a drop-list drag can end (drop on a list, drop outside,
|
|
553
|
+
* Escape, pointercancel, destroy). Everything transient is removed here, so nothing can leak
|
|
554
|
+
* regardless of how the drag ended. Only then is the drop event emitted.
|
|
555
|
+
*/
|
|
556
|
+
finishListDrag(commit) {
|
|
557
|
+
const doc = this.el.ownerDocument;
|
|
558
|
+
if (this.scrollListener)
|
|
559
|
+
doc.removeEventListener('scroll', this.scrollListener, { capture: true });
|
|
560
|
+
if (this.resizeListener)
|
|
561
|
+
(doc.defaultView ?? window).removeEventListener('resize', this.resizeListener);
|
|
562
|
+
this.scrollListener = this.resizeListener = undefined;
|
|
563
|
+
const target = commit ? this.activeDropList : null;
|
|
564
|
+
const event = target?._finish(this, true) ?? null;
|
|
565
|
+
for (const l of this.cachedLists)
|
|
566
|
+
l._release(this);
|
|
567
|
+
this.preview?.remove();
|
|
568
|
+
this.preview = undefined;
|
|
569
|
+
this.placeholder = undefined;
|
|
570
|
+
this.activeDropList = null;
|
|
571
|
+
this.cachedLists = [];
|
|
572
|
+
this.dropCandidates = [];
|
|
573
|
+
this.dropListsSnapshot = null;
|
|
574
|
+
this.listDrag = false;
|
|
575
|
+
// The source element is put back exactly as it was (its transform was never touched).
|
|
576
|
+
this.el.classList.remove('ngx-draggable--dragging');
|
|
577
|
+
this.el.style.visibility = this.sourceVisibility;
|
|
578
|
+
this.el.style.display = this.sourceDisplay;
|
|
579
|
+
this.el.style.pointerEvents = this.sourcePointerEvents;
|
|
580
|
+
this.el.style.transition = this.sourceTransition;
|
|
581
|
+
this.el.style.willChange = '';
|
|
582
|
+
this.el.style.zIndex = this.previousZIndex;
|
|
583
|
+
if (event && target)
|
|
584
|
+
target.onDrop.emit(event);
|
|
585
|
+
}
|
|
311
586
|
updatePreview() {
|
|
312
587
|
if (!this.preview)
|
|
313
588
|
return;
|
|
314
589
|
this.preview.style.transform = `translate3d(${this.moveDx}px, ${this.moveDy}px, 0)`;
|
|
315
590
|
}
|
|
316
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
|
+
}
|
|
600
|
+
|
|
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
|
+
*/
|
|
613
|
+
tpl;
|
|
614
|
+
/** Set by `NgxPlaceholder` so the embedded view can join Angular's change-detection tree. */
|
|
615
|
+
appRef;
|
|
616
|
+
dropList;
|
|
617
|
+
element;
|
|
618
|
+
_visible = false;
|
|
619
|
+
_view;
|
|
620
|
+
/** True once `this.element` was built from `tpl` (as opposed to the fallback clone). */
|
|
621
|
+
isCustom = false;
|
|
622
|
+
attach(container, source, reference) {
|
|
623
|
+
if (!this.element) {
|
|
624
|
+
if (this.tpl) {
|
|
625
|
+
this._view = this.tpl.createEmbeddedView({});
|
|
626
|
+
this.appRef?.attachView(this._view);
|
|
627
|
+
// Render bindings inside the embedded view before we move its DOM nodes.
|
|
628
|
+
this._view.detectChanges();
|
|
629
|
+
const element = this._view.rootNodes.find((node) => node instanceof HTMLElement);
|
|
630
|
+
if (!element) {
|
|
631
|
+
this.appRef?.detachView(this._view);
|
|
632
|
+
this._view.destroy();
|
|
633
|
+
this._view = undefined;
|
|
634
|
+
throw new Error('ngxPlaceholder template must contain exactly one HTMLElement root node ' +
|
|
635
|
+
'(other root nodes, if any, are ignored).');
|
|
636
|
+
}
|
|
637
|
+
this.element = element;
|
|
638
|
+
this.isCustom = true;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Fallback: keep the same tag/attributes as the dragged item so selectors such as
|
|
642
|
+
// ".list > article" and grid/flex item rules continue to apply.
|
|
643
|
+
this.element = source.cloneNode(false);
|
|
644
|
+
this.element.removeAttribute('id');
|
|
645
|
+
this.element.classList.add('ngx-drag-placeholder');
|
|
646
|
+
this.element.setAttribute('aria-hidden', 'true');
|
|
647
|
+
this.element.setAttribute('inert', '');
|
|
648
|
+
this.element.removeAttribute('tabindex');
|
|
649
|
+
this.isCustom = false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
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.
|
|
656
|
+
this.element.style.removeProperty('transform');
|
|
657
|
+
this.element.style.removeProperty('transition');
|
|
658
|
+
this.element.style.removeProperty('will-change');
|
|
659
|
+
this.element.style.setProperty('animation', 'none', 'important');
|
|
660
|
+
this.element.style.setProperty('pointer-events', 'none', 'important');
|
|
661
|
+
this.element.style.setProperty('visibility', 'visible', '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
|
+
}
|
|
669
|
+
this._visible = true;
|
|
670
|
+
if (reference && reference.parentNode === container) {
|
|
671
|
+
if (this.element !== reference)
|
|
672
|
+
container.insertBefore(this.element, reference);
|
|
673
|
+
}
|
|
674
|
+
else if (this.element.parentNode !== container) {
|
|
675
|
+
container.appendChild(this.element);
|
|
676
|
+
}
|
|
677
|
+
return this.element;
|
|
678
|
+
}
|
|
679
|
+
/** True while the currently attached element came from a custom `*ngxPlaceholder` template. */
|
|
680
|
+
get custom() {
|
|
681
|
+
return this.isCustom;
|
|
682
|
+
}
|
|
683
|
+
detach() {
|
|
684
|
+
this.element?.remove();
|
|
685
|
+
if (this._view) {
|
|
686
|
+
this.appRef?.detachView(this._view);
|
|
687
|
+
this._view.destroy();
|
|
688
|
+
this._view = undefined;
|
|
689
|
+
}
|
|
690
|
+
this.element = undefined;
|
|
691
|
+
this._visible = false;
|
|
692
|
+
}
|
|
693
|
+
get visible() {
|
|
694
|
+
return this._visible && !!this.element?.isConnected;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Pure geometry used by the drop-list sorting engine.
|
|
700
|
+
*
|
|
701
|
+
* Nothing in this file touches the DOM. Everything is a deterministic function of
|
|
702
|
+
* `(rectangles, pointer)`, which is what makes sorting reversible: the same pointer over the
|
|
703
|
+
* same snapshot always yields the same insertion index, regardless of what the previous
|
|
704
|
+
* pointer positions were.
|
|
705
|
+
*
|
|
706
|
+
* Layout direction is *derived from the rectangles* (row clustering + the order in which DOM
|
|
707
|
+
* siblings actually appear on screen) rather than from `flex-direction` / `direction`. That is
|
|
708
|
+
* what lets one algorithm handle block flow, inline flow, flex (row/column/wrap/reverse),
|
|
709
|
+
* grid, RTL and `order:`-reshuffled layouts. Computed style is only a fallback for lists that
|
|
710
|
+
* are too small (0 or 1 item) to reveal their own orientation.
|
|
711
|
+
*/
|
|
712
|
+
/**
|
|
713
|
+
* Hysteresis is the only "magic" number in the algorithm and it is deliberate: without it a
|
|
714
|
+
* pointer resting exactly on a sibling's mid-line flips the placeholder on every sub-pixel
|
|
715
|
+
* jitter (and on every rounding difference between two layouts). It is proportional to the
|
|
716
|
+
* sibling's size and capped, so it can never make a small item feel sticky.
|
|
717
|
+
*/
|
|
718
|
+
const HYSTERESIS_RATIO = 0.1;
|
|
719
|
+
const HYSTERESIS_MAX_PX = 6;
|
|
720
|
+
function hysteresis(size) {
|
|
721
|
+
return Math.min(HYSTERESIS_MAX_PX, Math.max(0, size) * HYSTERESIS_RATIO);
|
|
722
|
+
}
|
|
723
|
+
const cx = (b) => (b.left + b.right) / 2;
|
|
724
|
+
const cy = (b) => (b.top + b.bottom) / 2;
|
|
725
|
+
const width = (b) => b.right - b.left;
|
|
726
|
+
const height = (b) => b.bottom - b.top;
|
|
727
|
+
const sign = (n) => (n < 0 ? -1 : 1);
|
|
728
|
+
/**
|
|
729
|
+
* Builds a layout model from DOM-ordered boxes.
|
|
730
|
+
*
|
|
731
|
+
* @param fallbackAxis orientation to assume when there are fewer than two boxes.
|
|
732
|
+
* @param fallbackRtl inline direction to assume when no row contains two items.
|
|
733
|
+
*/
|
|
734
|
+
function analyzeLayout(boxes, fallbackAxis = 'y', fallbackRtl = false) {
|
|
735
|
+
const rows = [];
|
|
736
|
+
const rowOf = [];
|
|
737
|
+
boxes.forEach((b, i) => {
|
|
738
|
+
const row = rows[rows.length - 1];
|
|
739
|
+
if (row) {
|
|
740
|
+
const overlap = Math.min(b.bottom, row.bottom) - Math.max(b.top, row.top);
|
|
741
|
+
const smaller = Math.min(height(b), row.bottom - row.top);
|
|
742
|
+
// Same row when the boxes share at least half of the smaller one's height. Touching or
|
|
743
|
+
// sub-pixel-overlapping boxes (fractional heights) therefore start a new row.
|
|
744
|
+
if (overlap > 0 && overlap >= smaller * 0.5) {
|
|
745
|
+
row.top = Math.min(row.top, b.top);
|
|
746
|
+
row.bottom = Math.max(row.bottom, b.bottom);
|
|
747
|
+
row.last = i;
|
|
748
|
+
row.count++;
|
|
749
|
+
rowOf.push(rows.length - 1);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
rows.push({ top: b.top, bottom: b.bottom, first: i, last: i, count: 1 });
|
|
754
|
+
rowOf.push(rows.length - 1);
|
|
755
|
+
});
|
|
756
|
+
const rowCount = rows.length;
|
|
757
|
+
// Vertical direction: which way do successive rows travel?
|
|
758
|
+
const dirY = rowCount > 1
|
|
759
|
+
? sign((rows[rowCount - 1].top + rows[rowCount - 1].bottom) / 2 - (rows[0].top + rows[0].bottom) / 2)
|
|
760
|
+
: 1;
|
|
761
|
+
// Horizontal direction per row (from the first/last item of that row) + a global vote.
|
|
762
|
+
const rowDirX = [];
|
|
763
|
+
let votes = 0;
|
|
764
|
+
for (const r of rows) {
|
|
765
|
+
if (r.count > 1) {
|
|
766
|
+
const d = sign(cx(boxes[r.last]) - cx(boxes[r.first]));
|
|
767
|
+
rowDirX.push(d);
|
|
768
|
+
votes += d;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
rowDirX.push(0); // resolved below once the global vote is known
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const dirX = votes !== 0 ? sign(votes) : fallbackRtl ? -1 : 1;
|
|
775
|
+
for (let i = 0; i < rowDirX.length; i++)
|
|
776
|
+
if (!rowDirX[i])
|
|
777
|
+
rowDirX[i] = dirX;
|
|
778
|
+
const rowBounds = [];
|
|
779
|
+
for (let r = 0; r < rowCount - 1; r++) {
|
|
780
|
+
rowBounds.push(dirY === 1 ? (rows[r].bottom + rows[r + 1].top) / 2 : (rows[r].top + rows[r + 1].bottom) / 2);
|
|
781
|
+
}
|
|
782
|
+
const first = rows[0];
|
|
783
|
+
const lastRow = rows[rowCount - 1];
|
|
784
|
+
const yStart = first ? (dirY === 1 ? first.top : first.bottom) : 0;
|
|
785
|
+
const yEnd = lastRow ? (dirY === 1 ? lastRow.bottom : lastRow.top) : 0;
|
|
786
|
+
let axis;
|
|
787
|
+
if (boxes.length < 2)
|
|
788
|
+
axis = fallbackAxis;
|
|
789
|
+
else if (rowCount === 1)
|
|
790
|
+
axis = 'x';
|
|
791
|
+
else if (rows.every((r) => r.count === 1))
|
|
792
|
+
axis = 'y';
|
|
793
|
+
else
|
|
794
|
+
axis = 'grid';
|
|
795
|
+
return { axis, dirX, dirY, rowOf, rowDirX, rowBounds, yStart, yEnd, rowCount };
|
|
796
|
+
}
|
|
797
|
+
function rowAt(model, y) {
|
|
798
|
+
let r = 0;
|
|
799
|
+
for (const bound of model.rowBounds) {
|
|
800
|
+
if (model.dirY === 1 ? y > bound : y < bound)
|
|
801
|
+
r++;
|
|
802
|
+
else
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
return r;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Where should the dragged item be inserted, given the pointer?
|
|
809
|
+
*
|
|
810
|
+
* The result is the number of *other* items that end up before the dragged one, i.e. the
|
|
811
|
+
* dragged item's final index. `slot` is the index (in `boxes`) of the dragged item's own slot
|
|
812
|
+
* (its placeholder), or -1 when the list does not contain one yet.
|
|
813
|
+
*
|
|
814
|
+
* `current` is the index the slot currently has in "others space"; it only decides on which
|
|
815
|
+
* side of a sibling's mid-line the small hysteresis band applies. Pass `null` for a first
|
|
816
|
+
* placement (no history, no hysteresis).
|
|
817
|
+
*
|
|
818
|
+
* Pure: depends only on its arguments.
|
|
819
|
+
*/
|
|
820
|
+
function resolveInsertionIndex(model, boxes, slot, pointer, current) {
|
|
821
|
+
let before = 0;
|
|
822
|
+
let others = 0;
|
|
823
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
824
|
+
if (i === slot)
|
|
825
|
+
continue;
|
|
826
|
+
const o = others++;
|
|
827
|
+
const b = boxes[i];
|
|
828
|
+
// Sign of the hysteresis band: +1 favours "stays before the pointer", -1 favours "stays after".
|
|
829
|
+
const bias = current === null ? 0 : o < current ? 1 : -1;
|
|
830
|
+
let isBefore;
|
|
831
|
+
if (model.axis === 'x') {
|
|
832
|
+
isBefore = model.dirX * (pointer.x - cx(b)) + bias * hysteresis(width(b)) > 0;
|
|
833
|
+
}
|
|
834
|
+
else if (model.axis === 'y') {
|
|
835
|
+
isBefore = model.dirY * (pointer.y - cy(b)) + bias * hysteresis(height(b)) > 0;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
const row = model.rowOf[i];
|
|
839
|
+
const py = pointer.y + model.dirY * bias * hysteresis(height(b));
|
|
840
|
+
const pointerRow = rowAt(model, py);
|
|
841
|
+
if (model.dirY * (py - model.yStart) < 0) {
|
|
842
|
+
isBefore = false; // above the first row: nothing precedes the pointer
|
|
843
|
+
}
|
|
844
|
+
else if (model.dirY * (py - model.yEnd) > 0) {
|
|
845
|
+
isBefore = true; // below the last row: everything does (append)
|
|
846
|
+
}
|
|
847
|
+
else if (pointerRow !== row) {
|
|
848
|
+
isBefore = pointerRow > row;
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
isBefore = model.rowDirX[row] * (pointer.x - cx(b)) + bias * hysteresis(width(b)) > 0;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (isBefore)
|
|
855
|
+
before++;
|
|
856
|
+
}
|
|
857
|
+
return before;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Along-axis displacement of every box when the slot moves to `target` (others-space index).
|
|
861
|
+
*
|
|
862
|
+
* The result is where each box has to be translated so that the boxes, re-ordered, occupy the
|
|
863
|
+
* same run of space they occupied before, using each box's own size and the *measured* gaps
|
|
864
|
+
* between the original slots (so `gap`, margins and inline whitespace are handled by
|
|
865
|
+
* construction, not by assumption). Only meaningful for `x` / `y` axes; `grid` returns zeros.
|
|
866
|
+
*/
|
|
867
|
+
function computeAxisOffsets(model, boxes, slot, target) {
|
|
868
|
+
const n = boxes.length;
|
|
869
|
+
const offsets = new Array(n).fill(0);
|
|
870
|
+
if (model.axis === 'grid' || n < 2 || slot < 0)
|
|
871
|
+
return offsets;
|
|
872
|
+
const horizontal = model.axis === 'x';
|
|
873
|
+
const dir = horizontal ? model.dirX : model.dirY;
|
|
874
|
+
// Mirror into a frame where DOM order is ascending, so one code path serves LTR and RTL.
|
|
875
|
+
const lo = boxes.map((b) => (horizontal ? (dir === 1 ? b.left : -b.right) : dir === 1 ? b.top : -b.bottom));
|
|
876
|
+
const hi = boxes.map((b) => (horizontal ? (dir === 1 ? b.right : -b.left) : dir === 1 ? b.bottom : -b.top));
|
|
877
|
+
const order = [];
|
|
878
|
+
for (let i = 0; i < n; i++)
|
|
879
|
+
if (i !== slot)
|
|
880
|
+
order.push(i);
|
|
881
|
+
order.splice(Math.max(0, Math.min(target, n - 1)), 0, slot);
|
|
882
|
+
let cursor = lo[0];
|
|
883
|
+
for (let p = 0; p < n; p++) {
|
|
884
|
+
const item = order[p];
|
|
885
|
+
const delta = cursor - lo[item];
|
|
886
|
+
offsets[item] = Math.abs(delta) < 1e-6 ? 0 : dir * delta;
|
|
887
|
+
cursor += hi[item] - lo[item] + (p < n - 1 ? lo[p + 1] - hi[p] : 0);
|
|
888
|
+
}
|
|
889
|
+
return offsets;
|
|
890
|
+
}
|
|
891
|
+
/** Intersection of two boxes, or `null` when they do not overlap. */
|
|
892
|
+
function intersectBoxes(a, b) {
|
|
893
|
+
const left = Math.max(a.left, b.left);
|
|
894
|
+
const top = Math.max(a.top, b.top);
|
|
895
|
+
const right = Math.min(a.right, b.right);
|
|
896
|
+
const bottom = Math.min(a.bottom, b.bottom);
|
|
897
|
+
return right > left && bottom > top ? { left, top, right, bottom } : null;
|
|
898
|
+
}
|
|
899
|
+
function boxContains(b, x, y) {
|
|
900
|
+
return x >= b.left && x <= b.right && y >= b.top && y <= b.bottom;
|
|
901
|
+
}
|
|
902
|
+
function toBox(r) {
|
|
903
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Tracks how far the scrollable ancestors of an element (and the window) have scrolled since
|
|
908
|
+
* the frame was created.
|
|
909
|
+
*
|
|
910
|
+
* Geometry is measured once, when a drag enters a list. Scrolling afterwards moves the
|
|
911
|
+
* content under a stationary pointer, which is equivalent to moving the pointer by the scroll
|
|
912
|
+
* delta in the frame the geometry was measured in. Shifting the *pointer* (one point) instead
|
|
913
|
+
* of re-measuring every item keeps cached rectangles valid across page scroll, parent scroll
|
|
914
|
+
* and nested scroll without any layout reads on the hot path.
|
|
915
|
+
*
|
|
916
|
+
* `delta()` only reads `scrollLeft/Top` after `invalidate()` (called from a scroll event), so
|
|
917
|
+
* ordinary pointer moves perform zero scroll reads.
|
|
918
|
+
*/
|
|
919
|
+
class ScrollFrame {
|
|
920
|
+
win;
|
|
921
|
+
targets = [];
|
|
922
|
+
origin = { x: 0, y: 0 };
|
|
923
|
+
cached = { x: 0, y: 0 };
|
|
924
|
+
dirty = false;
|
|
925
|
+
/**
|
|
926
|
+
* @param start first element to inspect (walks up through its ancestors)
|
|
927
|
+
* @param includeStart whether `start` itself counts (true for a list whose own scroll moves its items)
|
|
928
|
+
*/
|
|
929
|
+
constructor(start, includeStart = true, win = window) {
|
|
930
|
+
this.win = win;
|
|
931
|
+
let node = includeStart ? start : (start?.parentElement ?? null);
|
|
932
|
+
const root = start?.ownerDocument.documentElement;
|
|
933
|
+
while (node) {
|
|
934
|
+
// <html> scrolling is reported through `window`; counting both would double the delta.
|
|
935
|
+
if (node !== root && scrolls(node))
|
|
936
|
+
this.targets.push(node);
|
|
937
|
+
node = node.parentElement;
|
|
938
|
+
}
|
|
939
|
+
this.targets.push(win);
|
|
940
|
+
this.origin = this.read();
|
|
941
|
+
}
|
|
942
|
+
/** Call when any scroll event fires. */
|
|
943
|
+
invalidate() {
|
|
944
|
+
this.dirty = true;
|
|
945
|
+
}
|
|
946
|
+
/** Total scroll distance (px) of all tracked containers since construction. */
|
|
947
|
+
delta() {
|
|
948
|
+
if (this.dirty) {
|
|
949
|
+
const now = this.read();
|
|
950
|
+
this.cached = { x: now.x - this.origin.x, y: now.y - this.origin.y };
|
|
951
|
+
this.dirty = false;
|
|
952
|
+
}
|
|
953
|
+
return this.cached;
|
|
954
|
+
}
|
|
955
|
+
read() {
|
|
956
|
+
let x = 0;
|
|
957
|
+
let y = 0;
|
|
958
|
+
for (const t of this.targets) {
|
|
959
|
+
if (t === this.win) {
|
|
960
|
+
x += this.win.scrollX;
|
|
961
|
+
y += this.win.scrollY;
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
x += t.scrollLeft;
|
|
965
|
+
y += t.scrollTop;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return { x, y };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function scrolls(el) {
|
|
972
|
+
const s = getComputedStyle(el);
|
|
973
|
+
// `hidden` containers still scroll programmatically; `clip` never does.
|
|
974
|
+
const scrollish = (v) => v === 'auto' || v === 'scroll' || v === 'overlay' || v === 'hidden';
|
|
975
|
+
return scrollish(s.overflowX) || scrollish(s.overflowY);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Inline / inline-block layouts get their inter-item gap from whitespace TEXT nodes. A
|
|
980
|
+
* placeholder that is moved (or freshly inserted) without one would collapse that gap and shift
|
|
981
|
+
* everything after it by a space width, so it carries a separator of its own. Returns `null`
|
|
982
|
+
* for layouts that have no whitespace nodes (flex, grid, Angular-trimmed templates).
|
|
983
|
+
*/
|
|
984
|
+
function makeSpacer(container) {
|
|
985
|
+
for (const n of Array.from(container.childNodes)) {
|
|
986
|
+
if (n.nodeType === Node.TEXT_NODE && n.nodeValue && /^\s+$/.test(n.nodeValue)) {
|
|
987
|
+
return container.ownerDocument.createTextNode(n.nodeValue);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const EASING = 'cubic-bezier(0, 0, 0.2, 1)';
|
|
993
|
+
/**
|
|
994
|
+
* One drag's view of one drop list.
|
|
995
|
+
*
|
|
996
|
+
* Lifecycle
|
|
997
|
+
* ---------
|
|
998
|
+
* snapshot ──► resolve(pointer) ──► apply(index) ──► ... ──► dispose()
|
|
999
|
+
*
|
|
1000
|
+
* The snapshot (`boxes`) is taken once, with the dragged item's slot already in place, and is
|
|
1001
|
+
* never re-read while the drag is over the list. `resolve` is a pure function of the pointer
|
|
1002
|
+
* and that snapshot, so moving the pointer back always restores the previous state.
|
|
1003
|
+
*
|
|
1004
|
+
* Two ways to *show* the result, chosen from the measured layout:
|
|
1005
|
+
*
|
|
1006
|
+
* - `transform` (one row or one column; the common case): the DOM is never touched. Siblings
|
|
1007
|
+
* and the placeholder are displaced with `translate3d`, computed from measured sizes and
|
|
1008
|
+
* gaps, exactly like Angular CDK's single-axis strategy.
|
|
1009
|
+
* - `flow` (grid / wrapping layouts): an exact translate cannot be derived without
|
|
1010
|
+
* re-implementing the browser's layout engine, so the single placeholder node is moved and
|
|
1011
|
+
* the browser lays the list out. Siblings are then animated from their old to new position
|
|
1012
|
+
* with FLIP `translate3d` so the motion is still transform-driven and smooth. Decisions
|
|
1013
|
+
* keep using the immutable snapshot, so the reflow can never feed back into them.
|
|
1014
|
+
*/
|
|
1015
|
+
class SortSession {
|
|
1016
|
+
mode;
|
|
1017
|
+
container;
|
|
1018
|
+
placeholder;
|
|
1019
|
+
itemElements;
|
|
1020
|
+
animationMs;
|
|
1021
|
+
fallbackAxis;
|
|
1022
|
+
fallbackRtl;
|
|
1023
|
+
entries = [];
|
|
1024
|
+
boxes = [];
|
|
1025
|
+
model;
|
|
1026
|
+
/** Index of the slot (placeholder) inside `entries`/`boxes` at snapshot time. */
|
|
1027
|
+
slotIdx = -1;
|
|
1028
|
+
/** For each visible "other" item: its index among ALL other items (hidden ones included). */
|
|
1029
|
+
otherModelIndex = [];
|
|
1030
|
+
frame;
|
|
1031
|
+
applied = new Map();
|
|
1032
|
+
targetIdx = 0;
|
|
1033
|
+
disposed = false;
|
|
1034
|
+
/** undefined = not created yet, null = this layout has no whitespace separators. */
|
|
1035
|
+
spacer;
|
|
1036
|
+
onOffsetChange;
|
|
1037
|
+
constructor(init) {
|
|
1038
|
+
this.spacer = init.spacer;
|
|
1039
|
+
this.onOffsetChange = init.onOffsetChange;
|
|
1040
|
+
this.container = init.container;
|
|
1041
|
+
this.placeholder = init.placeholder;
|
|
1042
|
+
this.itemElements = init.itemElements;
|
|
1043
|
+
this.animationMs = init.animationMs;
|
|
1044
|
+
this.fallbackAxis = init.fallbackAxis;
|
|
1045
|
+
this.fallbackRtl = init.fallbackRtl;
|
|
1046
|
+
this.snapshot();
|
|
1047
|
+
this.frame = new ScrollFrame(this.container, true);
|
|
1048
|
+
this.mode = this.model.axis === 'grid' ? 'flow' : 'transform';
|
|
1049
|
+
this.targetIdx = this.slotIdx;
|
|
1050
|
+
const transition = this.animationMs > 0 ? `transform ${this.animationMs}ms ${EASING}` : '';
|
|
1051
|
+
for (const e of this.entries) {
|
|
1052
|
+
e.el.style.willChange = 'transform';
|
|
1053
|
+
if (transition)
|
|
1054
|
+
e.el.style.transition = transition;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/** Detected orientation (useful for tests/diagnostics). */
|
|
1058
|
+
get axis() {
|
|
1059
|
+
return this.model.axis;
|
|
1060
|
+
}
|
|
1061
|
+
/** Current insertion index among the *visible other* items. */
|
|
1062
|
+
get target() {
|
|
1063
|
+
return this.targetIdx;
|
|
1064
|
+
}
|
|
1065
|
+
/** Index at which the session started (the slot's original position). */
|
|
1066
|
+
get origin() {
|
|
1067
|
+
return this.slotIdx;
|
|
1068
|
+
}
|
|
1069
|
+
/** Insertion index among ALL other items of the list (what consumers see as `currentIndex`). */
|
|
1070
|
+
get modelIndex() {
|
|
1071
|
+
const idx = this.otherModelIndex;
|
|
1072
|
+
if (this.targetIdx < idx.length)
|
|
1073
|
+
return idx[this.targetIdx];
|
|
1074
|
+
return idx.length ? idx[idx.length - 1] + 1 : 0;
|
|
1075
|
+
}
|
|
1076
|
+
/** Where would the dragged item be inserted if released at `pointer`? Pure; no side effects. */
|
|
1077
|
+
resolve(pointer) {
|
|
1078
|
+
const d = this.frame.delta();
|
|
1079
|
+
return resolveInsertionIndex(this.model, this.boxes, this.slotIdx, { x: pointer.x + d.x, y: pointer.y + d.y }, this.targetIdx);
|
|
1080
|
+
}
|
|
1081
|
+
/** Visually place the slot at `index`. Idempotent. */
|
|
1082
|
+
apply(index) {
|
|
1083
|
+
if (this.disposed || index === this.targetIdx)
|
|
1084
|
+
return;
|
|
1085
|
+
this.targetIdx = index;
|
|
1086
|
+
if (this.mode === 'transform')
|
|
1087
|
+
this.applyTransforms(index);
|
|
1088
|
+
else
|
|
1089
|
+
this.applyFlow(index);
|
|
1090
|
+
}
|
|
1091
|
+
/** Put everything back where it started (slot at its original index). */
|
|
1092
|
+
reset() {
|
|
1093
|
+
this.apply(this.slotIdx);
|
|
1094
|
+
}
|
|
1095
|
+
/** A scroll event happened somewhere: cached scroll deltas must be re-read once. */
|
|
1096
|
+
onScroll() {
|
|
1097
|
+
this.frame.invalidate();
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Re-baseline from the live layout (e.g. the window was resized mid-drag). Not called during
|
|
1101
|
+
* normal sorting, because re-measuring while items are displaced is exactly what would
|
|
1102
|
+
* re-introduce feedback into the decision.
|
|
1103
|
+
*/
|
|
1104
|
+
refresh() {
|
|
1105
|
+
if (this.disposed)
|
|
1106
|
+
return;
|
|
1107
|
+
const target = this.targetIdx;
|
|
1108
|
+
this.clearDisplacement();
|
|
1109
|
+
this.snapshot();
|
|
1110
|
+
this.frame = new ScrollFrame(this.container, true);
|
|
1111
|
+
if (this.mode === 'transform') {
|
|
1112
|
+
this.targetIdx = this.slotIdx;
|
|
1113
|
+
this.apply(Math.min(target, this.boxes.length - 1));
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
this.targetIdx = this.slotIdx;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/** Restore every inline style this session touched. Safe to call more than once. */
|
|
1120
|
+
dispose() {
|
|
1121
|
+
if (this.disposed)
|
|
1122
|
+
return;
|
|
1123
|
+
this.disposed = true;
|
|
1124
|
+
for (const e of this.entries) {
|
|
1125
|
+
e.el.style.transform = e.savedTransform;
|
|
1126
|
+
e.el.style.transition = e.savedTransition;
|
|
1127
|
+
e.el.style.willChange = e.savedWillChange;
|
|
1128
|
+
if (this.applied.has(e.el))
|
|
1129
|
+
this.onOffsetChange?.(e.el, { x: 0, y: 0 });
|
|
1130
|
+
}
|
|
1131
|
+
this.applied.clear();
|
|
1132
|
+
this.spacer?.remove();
|
|
1133
|
+
}
|
|
1134
|
+
// ---------------------------------------------------------------------------------------
|
|
1135
|
+
// snapshot
|
|
1136
|
+
// ---------------------------------------------------------------------------------------
|
|
1137
|
+
snapshot() {
|
|
1138
|
+
const previous = new Map(this.entries.map((e) => [e.el, e]));
|
|
1139
|
+
const entries = [];
|
|
1140
|
+
const boxes = [];
|
|
1141
|
+
const otherModelIndex = [];
|
|
1142
|
+
let slot = -1;
|
|
1143
|
+
let allOthers = 0;
|
|
1144
|
+
for (const node of Array.from(this.container.children)) {
|
|
1145
|
+
if (!(node instanceof HTMLElement))
|
|
1146
|
+
continue;
|
|
1147
|
+
const isSlot = node === this.placeholder;
|
|
1148
|
+
if (!isSlot && !this.itemElements.has(node))
|
|
1149
|
+
continue;
|
|
1150
|
+
const modelIdx = isSlot ? -1 : allOthers++;
|
|
1151
|
+
const rect = node.getBoundingClientRect();
|
|
1152
|
+
const off = this.applied.get(node);
|
|
1153
|
+
const box = toBox(rect);
|
|
1154
|
+
if (off) {
|
|
1155
|
+
// Strip our own displacement so the snapshot always describes the *undisplaced* layout.
|
|
1156
|
+
box.left -= off.x;
|
|
1157
|
+
box.right -= off.x;
|
|
1158
|
+
box.top -= off.y;
|
|
1159
|
+
box.bottom -= off.y;
|
|
1160
|
+
}
|
|
1161
|
+
const hidden = !isSlot && rect.width === 0 && rect.height === 0;
|
|
1162
|
+
if (hidden)
|
|
1163
|
+
continue;
|
|
1164
|
+
if (isSlot)
|
|
1165
|
+
slot = entries.length;
|
|
1166
|
+
else
|
|
1167
|
+
otherModelIndex.push(modelIdx);
|
|
1168
|
+
const known = previous.get(node);
|
|
1169
|
+
entries.push(known ?? {
|
|
1170
|
+
el: node,
|
|
1171
|
+
isSlot,
|
|
1172
|
+
base: initialTransform(node),
|
|
1173
|
+
savedTransform: node.style.transform,
|
|
1174
|
+
savedTransition: node.style.transition,
|
|
1175
|
+
savedWillChange: node.style.willChange,
|
|
1176
|
+
});
|
|
1177
|
+
boxes.push(box);
|
|
1178
|
+
}
|
|
1179
|
+
this.entries = entries;
|
|
1180
|
+
this.boxes = boxes;
|
|
1181
|
+
this.slotIdx = slot;
|
|
1182
|
+
this.otherModelIndex = otherModelIndex;
|
|
1183
|
+
this.model = analyzeLayout(boxes, this.fallbackAxis, this.fallbackRtl);
|
|
1184
|
+
}
|
|
1185
|
+
// ---------------------------------------------------------------------------------------
|
|
1186
|
+
// transform strategy (DOM stays untouched)
|
|
1187
|
+
// ---------------------------------------------------------------------------------------
|
|
1188
|
+
applyTransforms(target) {
|
|
1189
|
+
const offsets = computeAxisOffsets(this.model, this.boxes, this.slotIdx, target);
|
|
1190
|
+
const horizontal = this.model.axis === 'x';
|
|
1191
|
+
this.entries.forEach((e, i) => {
|
|
1192
|
+
const o = offsets[i];
|
|
1193
|
+
this.setOffset(e, horizontal ? { x: o, y: 0 } : { x: 0, y: o });
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
setOffset(e, off) {
|
|
1197
|
+
const prev = this.applied.get(e.el);
|
|
1198
|
+
if ((prev?.x ?? 0) === off.x && (prev?.y ?? 0) === off.y)
|
|
1199
|
+
return;
|
|
1200
|
+
if (off.x === 0 && off.y === 0) {
|
|
1201
|
+
this.applied.delete(e.el);
|
|
1202
|
+
e.el.style.transform = e.savedTransform;
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
this.applied.set(e.el, off);
|
|
1206
|
+
e.el.style.transform = `translate3d(${off.x}px, ${off.y}px, 0)${e.base}`;
|
|
1207
|
+
}
|
|
1208
|
+
this.onOffsetChange?.(e.el, off);
|
|
1209
|
+
}
|
|
1210
|
+
// ---------------------------------------------------------------------------------------
|
|
1211
|
+
// flow strategy (single placeholder move + FLIP transforms)
|
|
1212
|
+
// ---------------------------------------------------------------------------------------
|
|
1213
|
+
applyFlow(target) {
|
|
1214
|
+
const entries = this.entries;
|
|
1215
|
+
// 1. READ: where everything visually is right now (including in-flight animations).
|
|
1216
|
+
const first = entries.map((e) => e.el.getBoundingClientRect());
|
|
1217
|
+
// 2. WRITE: move the one placeholder node; drop any FLIP transforms.
|
|
1218
|
+
this.placeAt(target);
|
|
1219
|
+
for (const e of entries) {
|
|
1220
|
+
e.el.style.transition = 'none';
|
|
1221
|
+
e.el.style.transform = e.savedTransform;
|
|
1222
|
+
}
|
|
1223
|
+
// 3. READ: where everything is after the browser re-flowed the list.
|
|
1224
|
+
const last = entries.map((e) => e.el.getBoundingClientRect());
|
|
1225
|
+
// 4. WRITE: invert (put each item back where it was), then play (animate to its new home).
|
|
1226
|
+
const moved = [];
|
|
1227
|
+
entries.forEach((e, i) => {
|
|
1228
|
+
const dx = first[i].left - last[i].left;
|
|
1229
|
+
const dy = first[i].top - last[i].top;
|
|
1230
|
+
if (this.animationMs > 0 && (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)) {
|
|
1231
|
+
e.el.style.transform = `translate3d(${dx}px, ${dy}px, 0)${e.base}`;
|
|
1232
|
+
moved.push(e);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
const transition = this.animationMs > 0 ? `transform ${this.animationMs}ms ${EASING}` : '';
|
|
1236
|
+
if (moved.length)
|
|
1237
|
+
void this.container.offsetWidth; // commit the inverted frame
|
|
1238
|
+
for (const e of entries)
|
|
1239
|
+
e.el.style.transition = transition || e.savedTransition;
|
|
1240
|
+
for (const e of moved)
|
|
1241
|
+
e.el.style.transform = e.savedTransform;
|
|
1242
|
+
}
|
|
1243
|
+
placeAt(target) {
|
|
1244
|
+
const container = this.container;
|
|
1245
|
+
if (this.spacer === undefined)
|
|
1246
|
+
this.spacer = makeSpacer(container);
|
|
1247
|
+
const spacer = this.spacer;
|
|
1248
|
+
spacer?.remove();
|
|
1249
|
+
const others = this.entries.filter((e) => !e.isSlot);
|
|
1250
|
+
const ref = others[target]?.el;
|
|
1251
|
+
if (ref) {
|
|
1252
|
+
// ... [placeholder][separator][ref]: the separator that used to sit before `ref` still
|
|
1253
|
+
// separates it from its previous sibling, the new one separates it from the placeholder.
|
|
1254
|
+
container.insertBefore(this.placeholder, ref);
|
|
1255
|
+
if (spacer)
|
|
1256
|
+
container.insertBefore(spacer, ref);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const last = others[others.length - 1]?.el;
|
|
1260
|
+
const after = last ? last.nextSibling : null;
|
|
1261
|
+
if (spacer)
|
|
1262
|
+
container.insertBefore(spacer, after);
|
|
1263
|
+
container.insertBefore(this.placeholder, after);
|
|
1264
|
+
}
|
|
1265
|
+
clearDisplacement() {
|
|
1266
|
+
for (const e of this.entries) {
|
|
1267
|
+
e.el.style.transition = 'none';
|
|
1268
|
+
e.el.style.transform = e.savedTransform;
|
|
1269
|
+
if (this.applied.has(e.el))
|
|
1270
|
+
this.onOffsetChange?.(e.el, { x: 0, y: 0 });
|
|
1271
|
+
}
|
|
1272
|
+
this.applied.clear();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function initialTransform(el) {
|
|
1276
|
+
const t = getComputedStyle(el).transform;
|
|
1277
|
+
return t && t !== 'none' ? ` ${t}` : '';
|
|
1278
|
+
}
|
|
317
1279
|
|
|
318
1280
|
/** Base (LTR) physical direction for every logical direction. */
|
|
319
1281
|
const LTR_BASE = {
|
|
@@ -355,262 +1317,30 @@ function isRtl(element) {
|
|
|
355
1317
|
* Resolves a logical direction ('start' | 'end' | 'n-start' | 'n-end' | 's-start' | 's-end')
|
|
356
1318
|
* to a physical, viewport-space direction ('n' | 'e' | 's' | 'w' | 'ne' | 'nw' | 'se' | 'sw')
|
|
357
1319
|
* based on `rtl`.
|
|
358
|
-
*
|
|
359
|
-
* Physical directions pass through untouched, so it's always safe to call this
|
|
360
|
-
* even when `dir` might already be physical (e.g. resolving a direction that was
|
|
361
|
-
* already resolved once before).
|
|
362
|
-
*/
|
|
363
|
-
function resolveLogicalDirection(dir, rtl) {
|
|
364
|
-
if (!isLogicalDirection(dir)) {
|
|
365
|
-
return dir;
|
|
366
|
-
}
|
|
367
|
-
const base = LTR_BASE[dir];
|
|
368
|
-
return rtl ? MIRROR[base] : base;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Live geometry sorting.
|
|
373
|
-
*
|
|
374
|
-
* Unlike the old snapshot-only implementation, every sort pass reads the
|
|
375
|
-
* current DOM order and current client rects. This matters when:
|
|
376
|
-
* - the placeholder changes layout,
|
|
377
|
-
* - a Kanban column is scrolled while dragging,
|
|
378
|
-
* - the dragged item enters another list,
|
|
379
|
-
* - CSS grid/flex reflows occur.
|
|
380
|
-
*/
|
|
381
|
-
class PositionalSortStrategy {
|
|
382
|
-
container;
|
|
383
|
-
dragging = null;
|
|
384
|
-
index = -1;
|
|
385
|
-
flow = 'free';
|
|
386
|
-
rtl = false;
|
|
387
|
-
withElementContainer(el) {
|
|
388
|
-
this.container = el;
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
start(_items) {
|
|
392
|
-
this.flow = detectFlow(this.container);
|
|
393
|
-
this.rtl = isRtl(this.container);
|
|
394
|
-
this.index = -1;
|
|
395
|
-
}
|
|
396
|
-
enter(drag, x, y) {
|
|
397
|
-
this.dragging = drag;
|
|
398
|
-
this.index = this.findInsertionIndex(drag, x, y);
|
|
399
|
-
this.movePlaceholder(drag, this.index);
|
|
400
|
-
}
|
|
401
|
-
sort(drag, x, y) {
|
|
402
|
-
this.dragging = drag;
|
|
403
|
-
const next = this.findInsertionIndex(drag, x, y);
|
|
404
|
-
const previous = this.getCurrentIndex();
|
|
405
|
-
if (next === previous)
|
|
406
|
-
return null;
|
|
407
|
-
this.movePlaceholder(drag, next);
|
|
408
|
-
return {
|
|
409
|
-
previousIndex: previous,
|
|
410
|
-
currentIndex: next,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
getCurrentIndex() {
|
|
414
|
-
if (!this.container)
|
|
415
|
-
return Math.max(0, this.index);
|
|
416
|
-
const placeholder = this.dragging?.getPlaceholderElement();
|
|
417
|
-
if (placeholder?.parentElement === this.container) {
|
|
418
|
-
const refs = new Set(this.container.__ngxDragItems ?? []);
|
|
419
|
-
let position = 0;
|
|
420
|
-
const itemElements = new Set(Array.from(refs, (item) => item.el));
|
|
421
|
-
for (const node of Array.from(this.container.children)) {
|
|
422
|
-
if (node === placeholder)
|
|
423
|
-
return position;
|
|
424
|
-
if (node instanceof HTMLElement && itemElements.has(node) && node !== this.dragging?.el) {
|
|
425
|
-
position++;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return Math.max(0, this.index);
|
|
430
|
-
}
|
|
431
|
-
getItemIndex(item) {
|
|
432
|
-
let index = 0;
|
|
433
|
-
const refs = new Set(this.container.__ngxDragItems ?? []);
|
|
434
|
-
const itemElements = new Set(Array.from(refs, (ref) => ref.el));
|
|
435
|
-
for (const node of Array.from(this.container.children)) {
|
|
436
|
-
if (node === item.el)
|
|
437
|
-
return index;
|
|
438
|
-
if (node instanceof HTMLElement &&
|
|
439
|
-
!node.classList.contains('ngx-drag-placeholder') &&
|
|
440
|
-
!node.classList.contains('ngx-drag-in-body') &&
|
|
441
|
-
(!refs.size || itemElements.has(node))) {
|
|
442
|
-
index++;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return -1;
|
|
446
|
-
}
|
|
447
|
-
reset() {
|
|
448
|
-
this.dragging = null;
|
|
449
|
-
this.index = -1;
|
|
450
|
-
}
|
|
451
|
-
movePlaceholder(drag, index) {
|
|
452
|
-
const ph = drag.getPlaceholderElement();
|
|
453
|
-
if (!ph || !this.container)
|
|
454
|
-
return;
|
|
455
|
-
const items = this.readItems(drag);
|
|
456
|
-
const reference = items[index]?.drag.el ?? null;
|
|
457
|
-
if (reference) {
|
|
458
|
-
if (ph !== reference.previousSibling) {
|
|
459
|
-
this.container.insertBefore(ph, reference);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
else if (ph.parentElement !== this.container || ph !== this.container.lastElementChild) {
|
|
463
|
-
this.container.appendChild(ph);
|
|
464
|
-
}
|
|
465
|
-
this.index = Math.max(0, Math.min(index, items.length));
|
|
466
|
-
}
|
|
467
|
-
findInsertionIndex(drag, x, y) {
|
|
468
|
-
const items = this.readItems(drag);
|
|
469
|
-
if (!items.length)
|
|
470
|
-
return 0;
|
|
471
|
-
switch (this.flow) {
|
|
472
|
-
case 'flex-row':
|
|
473
|
-
return this.findFlexRow(items, x);
|
|
474
|
-
case 'flex-column':
|
|
475
|
-
return this.findFlexColumn(items, y);
|
|
476
|
-
case 'grid':
|
|
477
|
-
case 'free':
|
|
478
|
-
default:
|
|
479
|
-
return this.findGrid(items, x, y);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
readItems(drag) {
|
|
483
|
-
const itemSet = new Set(
|
|
484
|
-
// DropListRef maintains the authoritative set; DOM order is authoritative
|
|
485
|
-
// for placement, so we only inspect direct children here.
|
|
486
|
-
this.container.__ngxDragItems ?? []);
|
|
487
|
-
const byElement = new Map(Array.from(itemSet, (item) => [item.el, item]));
|
|
488
|
-
const result = [];
|
|
489
|
-
Array.from(this.container.children).forEach((element, domIndex) => {
|
|
490
|
-
if (!(element instanceof HTMLElement) ||
|
|
491
|
-
element === drag.el ||
|
|
492
|
-
element.classList.contains('ngx-drag-placeholder') ||
|
|
493
|
-
element.classList.contains('ngx-drag-in-body')) {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
const ref = byElement.get(element);
|
|
497
|
-
if (!ref)
|
|
498
|
-
return;
|
|
499
|
-
result.push({
|
|
500
|
-
drag: ref,
|
|
501
|
-
rect: element.getBoundingClientRect(),
|
|
502
|
-
domIndex,
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
// Fallback for environments where the private bridge is not available.
|
|
506
|
-
if (!result.length) {
|
|
507
|
-
return [];
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
findFlexRow(items, x) {
|
|
512
|
-
for (let i = 0; i < items.length; i++) {
|
|
513
|
-
const r = items[i].rect;
|
|
514
|
-
const mid = r.left + r.width / 2;
|
|
515
|
-
const before = this.rtl ? x > mid : x < mid;
|
|
516
|
-
if (before)
|
|
517
|
-
return i;
|
|
518
|
-
}
|
|
519
|
-
return items.length;
|
|
520
|
-
}
|
|
521
|
-
findFlexColumn(items, y) {
|
|
522
|
-
for (let i = 0; i < items.length; i++) {
|
|
523
|
-
const r = items[i].rect;
|
|
524
|
-
if (y < r.top + r.height / 2)
|
|
525
|
-
return i;
|
|
526
|
-
}
|
|
527
|
-
return items.length;
|
|
528
|
-
}
|
|
529
|
-
findGrid(items, x, y) {
|
|
530
|
-
let best = items[0];
|
|
531
|
-
let bestScore = Number.POSITIVE_INFINITY;
|
|
532
|
-
for (const item of items) {
|
|
533
|
-
const cx = item.rect.left + item.rect.width / 2;
|
|
534
|
-
const cy = item.rect.top + item.rect.height / 2;
|
|
535
|
-
const dx = Math.abs(x - cx) / Math.max(item.rect.width, 1);
|
|
536
|
-
const dy = Math.abs(y - cy) / Math.max(item.rect.height, 1);
|
|
537
|
-
const score = dx + dy * 1.2;
|
|
538
|
-
if (score < bestScore) {
|
|
539
|
-
bestScore = score;
|
|
540
|
-
best = item;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
const target = best.rect;
|
|
544
|
-
const sameRow = y >= target.top - target.height * 0.35 && y <= target.bottom + target.height * 0.35;
|
|
545
|
-
const before = sameRow
|
|
546
|
-
? this.rtl
|
|
547
|
-
? x > target.left + target.width / 2
|
|
548
|
-
: x < target.left + target.width / 2
|
|
549
|
-
: y < target.top + target.height / 2;
|
|
550
|
-
return Math.max(0, Math.min(items.length, items.findIndex((x) => x === best) + (before ? 0 : 1)));
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function detectFlow(el) {
|
|
554
|
-
const s = getComputedStyle(el);
|
|
555
|
-
if (s.display.includes('grid'))
|
|
556
|
-
return 'grid';
|
|
557
|
-
if (s.display.includes('flex')) {
|
|
558
|
-
if (s.flexWrap !== 'nowrap')
|
|
559
|
-
return 'grid';
|
|
560
|
-
return s.flexDirection.startsWith('column') ? 'flex-column' : 'flex-row';
|
|
561
|
-
}
|
|
562
|
-
return 'free';
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
class PlaceHolderRef {
|
|
566
|
-
tpl;
|
|
567
|
-
dropList;
|
|
568
|
-
element;
|
|
569
|
-
_visible = false;
|
|
570
|
-
attach(container, source, reference) {
|
|
571
|
-
if (!this.element) {
|
|
572
|
-
// Keep the same tag/attributes as the dragged item so selectors such as
|
|
573
|
-
// ".list > article" and grid/flex item rules continue to apply.
|
|
574
|
-
this.element = source.cloneNode(false);
|
|
575
|
-
this.element.removeAttribute('id');
|
|
576
|
-
this.element.classList.add('ngx-drag-placeholder');
|
|
577
|
-
this.element.setAttribute('aria-hidden', 'true');
|
|
578
|
-
this.element.setAttribute('inert', '');
|
|
579
|
-
this.element.removeAttribute('tabindex');
|
|
580
|
-
}
|
|
581
|
-
// The placeholder must never inherit a live drag transform/animation.
|
|
582
|
-
this.element.style.setProperty('transform', 'none', 'important');
|
|
583
|
-
this.element.style.setProperty('transition', 'none', 'important');
|
|
584
|
-
this.element.style.setProperty('animation', 'none', 'important');
|
|
585
|
-
this.element.style.setProperty('pointer-events', 'none', 'important');
|
|
586
|
-
this.element.style.setProperty('visibility', 'visible', 'important');
|
|
587
|
-
this.element.style.setProperty('opacity', '0.16', 'important');
|
|
588
|
-
this.element.style.setProperty('outline', '2px dashed currentColor', 'important');
|
|
589
|
-
this.element.style.setProperty('outline-offset', '-2px', 'important');
|
|
590
|
-
this._visible = true;
|
|
591
|
-
if (reference && reference.parentNode === container) {
|
|
592
|
-
if (this.element !== reference)
|
|
593
|
-
container.insertBefore(this.element, reference);
|
|
594
|
-
}
|
|
595
|
-
else if (this.element.parentNode !== container) {
|
|
596
|
-
container.appendChild(this.element);
|
|
597
|
-
}
|
|
598
|
-
return this.element;
|
|
599
|
-
}
|
|
600
|
-
detach() {
|
|
601
|
-
this.element?.remove();
|
|
602
|
-
this._visible = false;
|
|
603
|
-
}
|
|
604
|
-
get visible() {
|
|
605
|
-
return this._visible && !!this.element?.isConnected;
|
|
1320
|
+
*
|
|
1321
|
+
* Physical directions pass through untouched, so it's always safe to call this
|
|
1322
|
+
* even when `dir` might already be physical (e.g. resolving a direction that was
|
|
1323
|
+
* already resolved once before).
|
|
1324
|
+
*/
|
|
1325
|
+
function resolveLogicalDirection(dir, rtl) {
|
|
1326
|
+
if (!isLogicalDirection(dir)) {
|
|
1327
|
+
return dir;
|
|
606
1328
|
}
|
|
1329
|
+
const base = LTR_BASE[dir];
|
|
1330
|
+
return rtl ? MIRROR[base] : base;
|
|
607
1331
|
}
|
|
608
1332
|
|
|
609
1333
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
1334
|
+
* A drop list.
|
|
1335
|
+
*
|
|
1336
|
+
* Membership (`_draggables`) only ever reflects which draggables *belong* to this list in the
|
|
1337
|
+
* template. It is never changed by dragging. Everything that is transient during a drag lives
|
|
1338
|
+
* in a single `SortSession` (see sorting/sort-session.ts) that is created when a drag enters
|
|
1339
|
+
* the list and disposed when it leaves or the drag ends:
|
|
1340
|
+
*
|
|
1341
|
+
* - the list the drag started in keeps its session for the whole drag (its placeholder holds
|
|
1342
|
+
* the item's original slot, so leaving and re-entering is stable);
|
|
1343
|
+
* - any other list gets a fresh session on every entry and drops it on every exit.
|
|
614
1344
|
*/
|
|
615
1345
|
class DropListRef {
|
|
616
1346
|
data;
|
|
@@ -618,19 +1348,125 @@ class DropListRef {
|
|
|
618
1348
|
disableSort = false;
|
|
619
1349
|
connectedTo = [];
|
|
620
1350
|
dropListGroup;
|
|
1351
|
+
/** Duration (ms) of the sibling slide animation. `0` disables animation. */
|
|
1352
|
+
sortAnimationDuration = 200;
|
|
621
1353
|
_draggables = new Set();
|
|
622
1354
|
onDrop = new EventEmitter();
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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();
|
|
1365
|
+
session = null;
|
|
626
1366
|
placeholder;
|
|
1367
|
+
customPlaceholder;
|
|
1368
|
+
activeDrag = null;
|
|
1369
|
+
isOrigin = false;
|
|
1370
|
+
hovered = false;
|
|
1371
|
+
hit = null;
|
|
1372
|
+
/** Pre-insertion geometry of a foreign list, restored when the drag leaves it. */
|
|
1373
|
+
naturalHit = null;
|
|
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
|
+
}
|
|
627
1465
|
addItem(item) {
|
|
628
1466
|
this._draggables.add(item);
|
|
629
|
-
this.publishDragItems();
|
|
630
1467
|
}
|
|
631
1468
|
removeItem(item) {
|
|
632
1469
|
this._draggables.delete(item);
|
|
633
|
-
this.publishDragItems();
|
|
634
1470
|
}
|
|
635
1471
|
isConnectedTo(other) {
|
|
636
1472
|
if (other === this)
|
|
@@ -639,76 +1475,322 @@ class DropListRef {
|
|
|
639
1475
|
return true;
|
|
640
1476
|
return !!this.dropListGroup && this.dropListGroup.has(other);
|
|
641
1477
|
}
|
|
1478
|
+
// ---------------------------------------------------------------------------------------
|
|
1479
|
+
// hit testing (cached for the duration of a drag)
|
|
1480
|
+
// ---------------------------------------------------------------------------------------
|
|
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
|
+
*/
|
|
1490
|
+
_cacheGeometry() {
|
|
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;
|
|
1499
|
+
this.hitCached = true;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Is the point over the *visible* part of this list? Scroll-aware without re-measuring.
|
|
1503
|
+
*
|
|
1504
|
+
* The list box and every clipping ancestor are tracked separately because they react to scroll
|
|
1505
|
+
* differently: scrolling a container moves the list but not the container's own clip window.
|
|
1506
|
+
*/
|
|
1507
|
+
_containsPoint(x, y) {
|
|
1508
|
+
const hit = this.hitCached ? this.hit : measureHit(this.el);
|
|
1509
|
+
if (!hit)
|
|
1510
|
+
return false;
|
|
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))
|
|
1516
|
+
return false;
|
|
1517
|
+
for (const clip of hit.clips) {
|
|
1518
|
+
const d = clip.frame.delta();
|
|
1519
|
+
if (!boxContains(clip.box, x + d.x, y + d.y))
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
return true;
|
|
1523
|
+
}
|
|
1524
|
+
/** Approximate area, used to break ties between overlapping, unrelated lists. */
|
|
1525
|
+
_area() {
|
|
1526
|
+
const b = (this.hit ?? measureHit(this.el))?.box;
|
|
1527
|
+
return b ? (b.right - b.left) * (b.bottom - b.top) : 0;
|
|
1528
|
+
}
|
|
1529
|
+
_onScroll() {
|
|
1530
|
+
this.hit?.frame.invalidate();
|
|
1531
|
+
this.hit?.clips.forEach((c) => c.frame.invalidate());
|
|
1532
|
+
this.session?.onScroll();
|
|
1533
|
+
}
|
|
1534
|
+
/** Re-measure after a viewport resize. */
|
|
1535
|
+
_refresh() {
|
|
1536
|
+
if (this.hitCached)
|
|
1537
|
+
this._cacheGeometry();
|
|
1538
|
+
this.session?.refresh();
|
|
1539
|
+
}
|
|
1540
|
+
// ---------------------------------------------------------------------------------------
|
|
1541
|
+
// drag lifecycle
|
|
1542
|
+
// ---------------------------------------------------------------------------------------
|
|
1543
|
+
/** Backwards-compatible entry point: makes this list the drag's target. */
|
|
642
1544
|
createPlaceholder(drag) {
|
|
643
|
-
// There is exactly one placeholder for the active drag.
|
|
644
|
-
this.placeholder?.detach();
|
|
645
|
-
const ref = new PlaceHolderRef();
|
|
646
|
-
ref.dropList = this;
|
|
647
|
-
const source = drag.el;
|
|
648
|
-
const rect = source.getBoundingClientRect();
|
|
649
|
-
ref.attach(this.el, source);
|
|
650
|
-
this.copyPlaceholderGeometry(source, ref.element, rect);
|
|
651
|
-
this.placeholder = ref;
|
|
652
|
-
drag.placeholder = ref;
|
|
653
1545
|
this.enter(drag, drag.pointer.x, drag.pointer.y);
|
|
654
|
-
return
|
|
1546
|
+
return drag.placeholder;
|
|
655
1547
|
}
|
|
1548
|
+
/** The drag pointer entered this list (or the drag started inside it). */
|
|
656
1549
|
enter(drag, x = 0, y = 0) {
|
|
657
|
-
if (
|
|
658
|
-
this
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
this.publishDragItems();
|
|
663
|
-
this.strategy.withElementContainer(this.el).start(this.getStrategyItems());
|
|
1550
|
+
if (this.session && this.activeDrag === drag) {
|
|
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).
|
|
1554
|
+
this.hovered = true;
|
|
664
1555
|
this.el.classList.add('ngx-drop-list--active');
|
|
1556
|
+
drag.placeholder = this.placeholder;
|
|
1557
|
+
this.sortItem(drag, { x, y });
|
|
1558
|
+
return;
|
|
665
1559
|
}
|
|
666
|
-
this.
|
|
1560
|
+
this.releaseSession(); // never keep stale state from a previous drag (geometry cache stays)
|
|
1561
|
+
this.begin(drag, { x, y });
|
|
1562
|
+
this.hovered = true;
|
|
1563
|
+
this.el.classList.add('ngx-drop-list--active');
|
|
667
1564
|
}
|
|
1565
|
+
/** Re-evaluate the insertion index for the pointer. Returns the new index, or null if unchanged. */
|
|
668
1566
|
sortItem(drag, position) {
|
|
669
|
-
|
|
1567
|
+
const s = this.session;
|
|
1568
|
+
if (!s || this.activeDrag !== drag || !this.hovered || this.disableSort)
|
|
670
1569
|
return null;
|
|
671
|
-
|
|
1570
|
+
const next = s.resolve(position);
|
|
1571
|
+
if (next === s.target)
|
|
1572
|
+
return null;
|
|
1573
|
+
s.apply(next);
|
|
1574
|
+
return s.modelIndex;
|
|
672
1575
|
}
|
|
673
|
-
|
|
674
|
-
|
|
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
|
+
*/
|
|
1582
|
+
exit(drag) {
|
|
1583
|
+
if (!this.session || this.activeDrag !== drag)
|
|
675
1584
|
return;
|
|
676
|
-
|
|
677
|
-
this.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1585
|
+
this.hovered = false;
|
|
1586
|
+
this.el.classList.remove('ngx-drop-list--active');
|
|
1587
|
+
if (this.isOrigin) {
|
|
1588
|
+
// Keep the original slot (the item logically still lives here) but stop displacing siblings.
|
|
1589
|
+
this.session.reset();
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
// A foreign list forgets the drag on exit; its geometry stays cached until the drag ends.
|
|
1593
|
+
this.releaseSession(drag);
|
|
1594
|
+
}
|
|
685
1595
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Ends the drag for this list. Returns the drop event when the pointer was released over this
|
|
1609
|
+
* list, `null` otherwise (nothing to emit). All transient state is disposed either way.
|
|
1610
|
+
*/
|
|
1611
|
+
_finish(drag, commit) {
|
|
1612
|
+
const s = this.session;
|
|
1613
|
+
if (!s || this.activeDrag !== drag)
|
|
1614
|
+
return null;
|
|
1615
|
+
const event = commit && this.hovered
|
|
1616
|
+
? {
|
|
1617
|
+
previousIndex: Math.max(0, drag.originIndex),
|
|
1618
|
+
currentIndex: s.modelIndex,
|
|
1619
|
+
item: drag,
|
|
1620
|
+
container: this,
|
|
1621
|
+
previousContainer: drag.originDropList ?? this,
|
|
1622
|
+
isPointerOverContainer: true,
|
|
1623
|
+
}
|
|
1624
|
+
: null;
|
|
1625
|
+
this.releaseSession(drag);
|
|
1626
|
+
return event;
|
|
1627
|
+
}
|
|
1628
|
+
/** Compatibility wrapper: finish + emit. */
|
|
1629
|
+
finishDrag(drag) {
|
|
1630
|
+
const event = this._finish(drag, true);
|
|
1631
|
+
if (event)
|
|
1632
|
+
this.onDrop.emit(event);
|
|
691
1633
|
}
|
|
692
1634
|
resetSortTransforms() {
|
|
693
|
-
this.reset();
|
|
1635
|
+
this.session?.reset();
|
|
694
1636
|
}
|
|
695
1637
|
getFinalIndex() {
|
|
696
|
-
return this.
|
|
1638
|
+
return this.session ? this.session.modelIndex : 0;
|
|
697
1639
|
}
|
|
698
1640
|
getItemIndex(item) {
|
|
699
1641
|
return this.indexOf(item);
|
|
700
1642
|
}
|
|
1643
|
+
/** End of the drag: dispose EVERYTHING this list holds, including cached geometry. Idempotent. */
|
|
1644
|
+
_release(drag) {
|
|
1645
|
+
if (drag && this.activeDrag && this.activeDrag !== drag)
|
|
1646
|
+
return;
|
|
1647
|
+
this.releaseSession(drag);
|
|
1648
|
+
this.hit = null;
|
|
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}`).
|
|
1656
|
+
}
|
|
1657
|
+
/** Dispose the sort session, placeholder and drag ownership. */
|
|
1658
|
+
releaseSession(drag) {
|
|
1659
|
+
if (drag && this.activeDrag && this.activeDrag !== drag)
|
|
1660
|
+
return;
|
|
1661
|
+
this.session?.dispose();
|
|
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
|
+
}
|
|
1671
|
+
this.placeholder?.detach();
|
|
1672
|
+
if (this.activeDrag && this.activeDrag.placeholder === this.placeholder) {
|
|
1673
|
+
this.activeDrag.placeholder = undefined;
|
|
1674
|
+
}
|
|
1675
|
+
this.placeholder = undefined;
|
|
1676
|
+
if (this.naturalHit) {
|
|
1677
|
+
this.hit = this.naturalHit;
|
|
1678
|
+
this.naturalHit = null;
|
|
1679
|
+
}
|
|
1680
|
+
this.activeDrag = null;
|
|
1681
|
+
this.isOrigin = false;
|
|
1682
|
+
this.hovered = false;
|
|
1683
|
+
this.el?.classList.remove('ngx-drop-list--active');
|
|
1684
|
+
}
|
|
1685
|
+
// ---------------------------------------------------------------------------------------
|
|
1686
|
+
// internals
|
|
1687
|
+
// ---------------------------------------------------------------------------------------
|
|
1688
|
+
begin(drag, pointer) {
|
|
1689
|
+
const origin = drag.originDropList === this;
|
|
1690
|
+
const others = this.otherSlots(drag);
|
|
1691
|
+
const rtl = isRtl(this.el);
|
|
1692
|
+
const axis = fallbackAxis(this.el);
|
|
1693
|
+
const ph = this.customPlaceholder ?? new PlaceHolderRef();
|
|
1694
|
+
ph.dropList = this;
|
|
1695
|
+
let spacer;
|
|
1696
|
+
if (origin) {
|
|
1697
|
+
// The placeholder takes over the exact slot of the (now hidden) source element.
|
|
1698
|
+
ph.attach(this.el, drag.el, drag.el);
|
|
1699
|
+
this.lockPlaceholderGeometry(drag, ph, drag.start);
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
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;
|
|
1712
|
+
const reference = this.foreignInsertionReference(others, pointer, axis, rtl);
|
|
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);
|
|
1719
|
+
spacer = makeSpacer(this.el);
|
|
1720
|
+
if (spacer)
|
|
1721
|
+
this.el.insertBefore(spacer, reference ?? ph.element);
|
|
1722
|
+
}
|
|
1723
|
+
this.placeholder = ph;
|
|
1724
|
+
this.activeDrag = drag;
|
|
1725
|
+
this.isOrigin = origin;
|
|
1726
|
+
drag.placeholder = ph;
|
|
1727
|
+
this.session = new SortSession({
|
|
1728
|
+
container: this.el,
|
|
1729
|
+
placeholder: ph.element,
|
|
1730
|
+
itemElements: others,
|
|
1731
|
+
fallbackAxis: axis,
|
|
1732
|
+
fallbackRtl: rtl,
|
|
1733
|
+
animationMs: this.sortAnimationDuration,
|
|
1734
|
+
spacer,
|
|
1735
|
+
onOffsetChange: this.childLists.size
|
|
1736
|
+
? (el, offset) => this.propagateOffset(el, offset)
|
|
1737
|
+
: undefined,
|
|
1738
|
+
});
|
|
1739
|
+
if (!origin && this.hitCached) {
|
|
1740
|
+
// Inserting the placeholder made this list bigger; the grown box is what the user sees
|
|
1741
|
+
// (and must be able to reach, e.g. to append below the last item). The natural box comes
|
|
1742
|
+
// back on exit, so entering/leaving at the old edge cannot flicker.
|
|
1743
|
+
this.naturalHit = this.hit;
|
|
1744
|
+
this._cacheGeometry();
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* First placement into a list that does not contain the item yet: the node the placeholder
|
|
1749
|
+
* must be inserted before (null = append). Same pure resolver as sorting, no hysteresis.
|
|
1750
|
+
*/
|
|
1751
|
+
foreignInsertionReference(others, pointer, axis, rtl) {
|
|
1752
|
+
const els = [];
|
|
1753
|
+
const boxes = [];
|
|
1754
|
+
for (const node of Array.from(this.el.children)) {
|
|
1755
|
+
if (!(node instanceof HTMLElement) || !others.has(node))
|
|
1756
|
+
continue;
|
|
1757
|
+
const r = node.getBoundingClientRect();
|
|
1758
|
+
if (r.width === 0 && r.height === 0)
|
|
1759
|
+
continue;
|
|
1760
|
+
els.push(node);
|
|
1761
|
+
boxes.push(toBox(r));
|
|
1762
|
+
}
|
|
1763
|
+
if (!els.length)
|
|
1764
|
+
return null;
|
|
1765
|
+
const index = this.disableSort
|
|
1766
|
+
? els.length
|
|
1767
|
+
: resolveInsertionIndex(analyzeLayout(boxes, axis, rtl), boxes, -1, pointer, null);
|
|
1768
|
+
return index < els.length ? els[index] : els[els.length - 1].nextSibling;
|
|
1769
|
+
}
|
|
1770
|
+
otherSlots(drag) {
|
|
1771
|
+
const set = new Set();
|
|
1772
|
+
for (const d of this._draggables) {
|
|
1773
|
+
if (d !== drag && d.el?.parentElement === this.el)
|
|
1774
|
+
set.add(d.el);
|
|
1775
|
+
}
|
|
1776
|
+
return set;
|
|
1777
|
+
}
|
|
701
1778
|
indexOf(item) {
|
|
702
|
-
const
|
|
1779
|
+
const byElement = new Map();
|
|
1780
|
+
for (const d of this._draggables)
|
|
1781
|
+
if (d.el)
|
|
1782
|
+
byElement.set(d.el, d);
|
|
703
1783
|
let index = 0;
|
|
704
1784
|
for (const node of Array.from(this.el.children)) {
|
|
705
1785
|
if (!(node instanceof HTMLElement))
|
|
706
1786
|
continue;
|
|
707
1787
|
if (node === this.placeholder?.element)
|
|
708
1788
|
continue;
|
|
709
|
-
if (node.classList.contains('ngx-drag-
|
|
1789
|
+
if (node.classList.contains('ngx-drag-placeholder'))
|
|
710
1790
|
continue;
|
|
711
|
-
|
|
1791
|
+
if (node.classList.contains('ngx-drag-preview'))
|
|
1792
|
+
continue;
|
|
1793
|
+
const ref = byElement.get(node);
|
|
712
1794
|
if (!ref)
|
|
713
1795
|
continue;
|
|
714
1796
|
if (ref === item)
|
|
@@ -717,44 +1799,67 @@ class DropListRef {
|
|
|
717
1799
|
}
|
|
718
1800
|
return -1;
|
|
719
1801
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
placeholder.style.setProperty('animation', 'none', 'important');
|
|
742
|
-
placeholder.style.setProperty('box-sizing', 'border-box', 'important');
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* The sorting strategy only needs a read-only collection of draggable refs.
|
|
746
|
-
* Using DragRef<any> at this internal boundary avoids the invariant generic
|
|
747
|
-
* relationship between DragRef<T> and DragRef<unknown>.
|
|
748
|
-
*/
|
|
749
|
-
getStrategyItems() {
|
|
750
|
-
return Array.from(this._draggables);
|
|
751
|
-
}
|
|
752
|
-
asStrategyDrag(drag) {
|
|
753
|
-
return drag;
|
|
1802
|
+
lockPlaceholderGeometry(drag, ph, rect) {
|
|
1803
|
+
const placeholder = ph.element;
|
|
1804
|
+
const set = (k, v) => placeholder.style.setProperty(k, v, 'important');
|
|
1805
|
+
placeholder.style.zIndex = '';
|
|
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.
|
|
1812
|
+
set('width', `${rect.width}px`);
|
|
1813
|
+
set('height', `${rect.height}px`);
|
|
1814
|
+
// set('min-width', `${rect.width}px`);
|
|
1815
|
+
// set('min-height', `${rect.height}px`);
|
|
1816
|
+
// set('max-width', `${rect.width}px`);
|
|
1817
|
+
// set('max-height', `${rect.height}px`);
|
|
1818
|
+
set('visibility', 'visible');
|
|
1819
|
+
set('opacity', '0.16');
|
|
1820
|
+
set('pointer-events', 'none');
|
|
1821
|
+
set('animation', 'none');
|
|
1822
|
+
set('box-sizing', 'border-box');
|
|
754
1823
|
}
|
|
755
|
-
|
|
756
|
-
|
|
1824
|
+
}
|
|
1825
|
+
/** Orientation to assume while a list has fewer than two items to measure. */
|
|
1826
|
+
function fallbackAxis(container) {
|
|
1827
|
+
const s = getComputedStyle(container);
|
|
1828
|
+
if (s.display.includes('flex'))
|
|
1829
|
+
return s.flexDirection.startsWith('column') ? 'y' : 'x';
|
|
1830
|
+
if (s.display.includes('grid'))
|
|
1831
|
+
return 'y';
|
|
1832
|
+
const child = container.firstElementChild;
|
|
1833
|
+
if (child && getComputedStyle(child).display.startsWith('inline'))
|
|
1834
|
+
return 'x';
|
|
1835
|
+
return 'y';
|
|
1836
|
+
}
|
|
1837
|
+
/** Rect of the list plus one clip window per clipping (overflow != visible) ancestor. */
|
|
1838
|
+
function measureHit(el) {
|
|
1839
|
+
const r = el.getBoundingClientRect();
|
|
1840
|
+
if (r.right <= r.left || r.bottom <= r.top)
|
|
1841
|
+
return null;
|
|
1842
|
+
const clips = [];
|
|
1843
|
+
const root = el.ownerDocument.documentElement;
|
|
1844
|
+
for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
|
|
1845
|
+
const s = getComputedStyle(node);
|
|
1846
|
+
const cx = s.overflowX !== 'visible';
|
|
1847
|
+
const cy = s.overflowY !== 'visible';
|
|
1848
|
+
if (!cx && !cy)
|
|
1849
|
+
continue;
|
|
1850
|
+
const c = node.getBoundingClientRect();
|
|
1851
|
+
clips.push({
|
|
1852
|
+
box: {
|
|
1853
|
+
left: cx ? c.left : -Infinity,
|
|
1854
|
+
right: cx ? c.right : Infinity,
|
|
1855
|
+
top: cy ? c.top : -Infinity,
|
|
1856
|
+
bottom: cy ? c.bottom : Infinity,
|
|
1857
|
+
},
|
|
1858
|
+
// A clip window only moves when something ABOVE the clipping element scrolls.
|
|
1859
|
+
frame: new ScrollFrame(node, false),
|
|
1860
|
+
});
|
|
757
1861
|
}
|
|
1862
|
+
return { box: toBox(r), frame: new ScrollFrame(el, false), clips };
|
|
758
1863
|
}
|
|
759
1864
|
|
|
760
1865
|
/**
|
|
@@ -782,6 +1887,21 @@ class DragDropService {
|
|
|
782
1887
|
...(ngDevMode ? [{ debugName: "dropLists" }] : /* istanbul ignore next */ []));
|
|
783
1888
|
activeDrag = signal(null, /* @ts-ignore */
|
|
784
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();
|
|
785
1905
|
registerDragItem(ref) {
|
|
786
1906
|
this.drags.update((items) => (items.includes(ref) ? items : [...items, ref]));
|
|
787
1907
|
}
|
|
@@ -792,9 +1912,37 @@ class DragDropService {
|
|
|
792
1912
|
}
|
|
793
1913
|
registerDropList(ref) {
|
|
794
1914
|
this.dropLists.update((items) => (items.includes(ref) ? items : [...items, ref]));
|
|
1915
|
+
if (ref.el)
|
|
1916
|
+
this.listsByEl.set(ref.el, ref);
|
|
795
1917
|
}
|
|
796
1918
|
removeDropList(ref) {
|
|
797
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;
|
|
798
1946
|
}
|
|
799
1947
|
begin(ref) {
|
|
800
1948
|
this.activeDrag.set(ref);
|
|
@@ -810,23 +1958,41 @@ class DragDropService {
|
|
|
810
1958
|
ref.cancelDrag();
|
|
811
1959
|
this.activeDrag.set(null);
|
|
812
1960
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1961
|
+
/**
|
|
1962
|
+
* Which drop list is under `point`?
|
|
1963
|
+
*
|
|
1964
|
+
* - Only lists connected to the drag's ORIGIN list qualify (connectivity is not transitive, so
|
|
1965
|
+
* A→B→C never lets an item hop A→C). Without an origin (a free-drag element) nothing
|
|
1966
|
+
* qualifies: free dragging is never captured by a list.
|
|
1967
|
+
* - Only the visible (clip-aware) part of a list counts.
|
|
1968
|
+
* - A list inside the dragged element itself is never a target.
|
|
1969
|
+
* - Nested lists: the innermost qualifying list wins. Among unrelated overlapping lists the
|
|
1970
|
+
* current one is kept (no flicker), otherwise the smallest.
|
|
1971
|
+
*/
|
|
1972
|
+
findDropList(point, current, origin, dragEl) {
|
|
1973
|
+
const source = origin ?? null;
|
|
1974
|
+
if (!source)
|
|
1975
|
+
return null;
|
|
1976
|
+
let best = null;
|
|
1977
|
+
for (const list of this.dropLists()) {
|
|
1978
|
+
if (!list.el || !source.isConnectedTo(list))
|
|
1979
|
+
continue;
|
|
1980
|
+
if (dragEl && dragEl !== list.el && dragEl.contains(list.el))
|
|
1981
|
+
continue;
|
|
1982
|
+
if (!list._containsPoint(point.x, point.y))
|
|
1983
|
+
continue;
|
|
1984
|
+
if (!best)
|
|
1985
|
+
best = list;
|
|
1986
|
+
else if (best.el.contains(list.el))
|
|
1987
|
+
best = list; // deeper wins
|
|
1988
|
+
else if (list.el.contains(best.el))
|
|
1989
|
+
continue;
|
|
1990
|
+
else if (list === current)
|
|
1991
|
+
best = list;
|
|
1992
|
+
else if (best !== current && list._area() < best._area())
|
|
1993
|
+
best = list;
|
|
1994
|
+
}
|
|
1995
|
+
return best;
|
|
830
1996
|
}
|
|
831
1997
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
832
1998
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DragDropService, providedIn: 'root' });
|
|
@@ -836,76 +2002,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
836
2002
|
args: [{ providedIn: 'root' }]
|
|
837
2003
|
}] });
|
|
838
2004
|
|
|
839
|
-
const NGX_DROPLIST_GROUP = new InjectionToken('ngx-drop-list-group');
|
|
840
|
-
/** Wrap several `ngxDropList`s to let items move freely between them. */
|
|
841
|
-
class NgxDropListGroup {
|
|
842
|
-
_ref = new DropListGroupRef();
|
|
843
|
-
ngOnDestroy() {
|
|
844
|
-
this._ref.lists.clear();
|
|
845
|
-
}
|
|
846
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
847
|
-
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 });
|
|
848
|
-
}
|
|
849
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropListGroup, decorators: [{
|
|
850
|
-
type: Directive,
|
|
851
|
-
args: [{
|
|
852
|
-
selector: '[NgxDropListGroup],[ngxDropListGroup]',
|
|
853
|
-
providers: [{ provide: NGX_DROPLIST_GROUP, useExisting: NgxDropListGroup }],
|
|
854
|
-
}]
|
|
855
|
-
}] });
|
|
856
|
-
|
|
857
|
-
const NGX_DROPLIST = new InjectionToken('ngx-drop-list');
|
|
858
|
-
class NgxDropList {
|
|
859
|
-
el;
|
|
860
|
-
_ref = new DropListRef();
|
|
861
|
-
set data(value) {
|
|
862
|
-
this._ref.data = value;
|
|
863
|
-
}
|
|
864
|
-
set connectedTo(value) {
|
|
865
|
-
this._ref.connectedTo = value ?? [];
|
|
866
|
-
}
|
|
867
|
-
set disableSort(value) {
|
|
868
|
-
this._ref.disableSort = value === true;
|
|
869
|
-
}
|
|
870
|
-
drop = new EventEmitter();
|
|
871
|
-
service = inject(DragDropService);
|
|
872
|
-
group = inject(NGX_DROPLIST_GROUP, { optional: true, skipSelf: true });
|
|
873
|
-
constructor(el) {
|
|
874
|
-
this.el = el;
|
|
875
|
-
}
|
|
876
|
-
ngOnInit() {
|
|
877
|
-
this._ref.el = this.el.nativeElement;
|
|
878
|
-
this._ref.dropListGroup = this.group?._ref ?? null;
|
|
879
|
-
// Previously the group directive never actually tracked its lists — fixed here.
|
|
880
|
-
this.group?._ref.add(this._ref);
|
|
881
|
-
this._ref.onDrop.subscribe((e) => this.drop.emit(e));
|
|
882
|
-
this.service.registerDropList(this._ref);
|
|
883
|
-
}
|
|
884
|
-
ngOnDestroy() {
|
|
885
|
-
this.group?._ref.remove(this._ref);
|
|
886
|
-
this.service.removeDropList(this._ref);
|
|
887
|
-
}
|
|
888
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
889
|
-
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 }], ngImport: i0 });
|
|
890
|
-
}
|
|
891
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
|
|
892
|
-
type: Directive,
|
|
893
|
-
args: [{
|
|
894
|
-
selector: '[NgxDropList],[ngxDropList]',
|
|
895
|
-
providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }],
|
|
896
|
-
host: { class: 'ngx-drop-list' },
|
|
897
|
-
}]
|
|
898
|
-
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { data: [{
|
|
899
|
-
type: Input,
|
|
900
|
-
args: ['data']
|
|
901
|
-
}], connectedTo: [{
|
|
902
|
-
type: Input
|
|
903
|
-
}], disableSort: [{
|
|
904
|
-
type: Input
|
|
905
|
-
}], drop: [{
|
|
906
|
-
type: Output
|
|
907
|
-
}] } });
|
|
908
|
-
|
|
909
2005
|
/**
|
|
910
2006
|
* Auto-scrolls the nearest scrollable ancestor (or window) while the pointer
|
|
911
2007
|
* is dragged near the edge of the scrollable viewport. Direction-agnostic —
|
|
@@ -997,11 +2093,12 @@ function getScrollPosition(container) {
|
|
|
997
2093
|
return { left: container.scrollLeft, top: container.scrollTop };
|
|
998
2094
|
}
|
|
999
2095
|
|
|
2096
|
+
/** Pointer events already claimed by an (inner) draggable. */
|
|
2097
|
+
const claimedPointerEvents = new WeakSet();
|
|
1000
2098
|
const NGX_DRAGGABLE = new InjectionToken('ngx-draggable');
|
|
1001
2099
|
const DEFAULT_KEYBOARD_STEP = 8;
|
|
1002
2100
|
class NgxDraggable {
|
|
1003
2101
|
host;
|
|
1004
|
-
renderer;
|
|
1005
2102
|
disabled = model(false, /* @ts-ignore */
|
|
1006
2103
|
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1007
2104
|
/** Element whose rect constrains the drag. Now actually enforced. */
|
|
@@ -1035,8 +2132,6 @@ class NgxDraggable {
|
|
|
1035
2132
|
...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
|
|
1036
2133
|
doc = inject(DOCUMENT);
|
|
1037
2134
|
service = inject(DragDropService);
|
|
1038
|
-
list = inject(NGX_DROPLIST, { optional: true, skipSelf: true });
|
|
1039
|
-
group = inject(NGX_DROPLIST_GROUP, { optional: true, skipSelf: true });
|
|
1040
2135
|
scroller = new AutoScroller();
|
|
1041
2136
|
down = false;
|
|
1042
2137
|
start = { x: 0, y: 0 };
|
|
@@ -1047,14 +2142,16 @@ class NgxDraggable {
|
|
|
1047
2142
|
removeUp;
|
|
1048
2143
|
removeKeydown;
|
|
1049
2144
|
removeEscape;
|
|
2145
|
+
removeCancel;
|
|
2146
|
+
removeLostCapture;
|
|
1050
2147
|
scrollTarget = window;
|
|
1051
2148
|
lastScroll = { left: 0, top: 0 };
|
|
1052
2149
|
lastWindowScroll = { left: 0, top: 0 };
|
|
1053
2150
|
removeContainerScroll;
|
|
1054
2151
|
removeWindowScroll;
|
|
1055
|
-
|
|
2152
|
+
renderer = inject(Renderer2);
|
|
2153
|
+
constructor(host) {
|
|
1056
2154
|
this.host = host;
|
|
1057
|
-
this.renderer = renderer;
|
|
1058
2155
|
effect(() => {
|
|
1059
2156
|
this._ref.el = this.dragRootElement()
|
|
1060
2157
|
? (this.host.nativeElement.closest(this.dragRootElement()) ?? this.host.nativeElement)
|
|
@@ -1067,9 +2164,15 @@ class NgxDraggable {
|
|
|
1067
2164
|
: this.host.nativeElement;
|
|
1068
2165
|
this._ref.boundary = this.boundary();
|
|
1069
2166
|
this._ref.lockAxis = this.lockAxis();
|
|
1070
|
-
|
|
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);
|
|
2173
|
+
this._ref.service = this.service;
|
|
1071
2174
|
this._ref.init();
|
|
1072
|
-
this._ref.withDropList(
|
|
2175
|
+
this._ref.withDropList(list ?? null);
|
|
1073
2176
|
this.service.registerDragItem(this._ref);
|
|
1074
2177
|
this.removeDown = this.renderer.listen(this.host.nativeElement, 'pointerdown', (e) => this.pointerDown(e));
|
|
1075
2178
|
this.removeKeydown = this.renderer.listen(this.host.nativeElement, 'keydown', (e) => this.keyDown(e));
|
|
@@ -1080,7 +2183,12 @@ class NgxDraggable {
|
|
|
1080
2183
|
this.removeUp?.();
|
|
1081
2184
|
this.removeKeydown?.();
|
|
1082
2185
|
this.removeEscape?.();
|
|
2186
|
+
this.removeCancel?.();
|
|
2187
|
+
this.removeLostCapture?.();
|
|
1083
2188
|
this.scroller.stop();
|
|
2189
|
+
this.stopScrollTracking();
|
|
2190
|
+
// Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
|
|
2191
|
+
this._ref.dispose();
|
|
1084
2192
|
this.service.removeDragItem(this._ref);
|
|
1085
2193
|
this._ref.dropList?.removeItem(this._ref);
|
|
1086
2194
|
}
|
|
@@ -1090,6 +2198,11 @@ class NgxDraggable {
|
|
|
1090
2198
|
// this.isInteractive(e.target) ||
|
|
1091
2199
|
!this.isOnHandle(e.target))
|
|
1092
2200
|
return;
|
|
2201
|
+
// Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
|
|
2202
|
+
// its own drag from the very same gesture. The innermost enabled draggable claims it.
|
|
2203
|
+
if (claimedPointerEvents.has(e))
|
|
2204
|
+
return;
|
|
2205
|
+
claimedPointerEvents.add(e);
|
|
1093
2206
|
e.preventDefault();
|
|
1094
2207
|
this.down = true;
|
|
1095
2208
|
this.pointerId = e.pointerId;
|
|
@@ -1103,24 +2216,20 @@ class NgxDraggable {
|
|
|
1103
2216
|
if (ev.key === 'Escape' && this.dragging())
|
|
1104
2217
|
this.cancel();
|
|
1105
2218
|
});
|
|
2219
|
+
this.removeCancel = this.renderer.listen(this.doc, 'pointercancel', (ev) => this.pointerCancel(ev));
|
|
2220
|
+
this.removeLostCapture = this.renderer.listen(this.doc, 'lostpointercapture', (ev) => this.pointerCancel(ev));
|
|
1106
2221
|
}
|
|
1107
2222
|
applyDragUpdate(p) {
|
|
2223
|
+
const before = this._ref.activeDropList;
|
|
2224
|
+
// DragRef decides everything (preview, target list, placeholder). A free drag never
|
|
2225
|
+
// interacts with drop lists.
|
|
1108
2226
|
this._ref.dragMove(p);
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
this._ref.withDropList(target);
|
|
1115
|
-
target.createPlaceholder(this._ref);
|
|
1116
|
-
if (this.autoScroll())
|
|
1117
|
-
this.scroller.retarget(target.el);
|
|
1118
|
-
}
|
|
1119
|
-
else if (this.autoScroll()) {
|
|
1120
|
-
this.scroller.stop();
|
|
1121
|
-
}
|
|
2227
|
+
if (this._ref.isListDrag && this.autoScroll()) {
|
|
2228
|
+
const after = this._ref.activeDropList;
|
|
2229
|
+
// Follow the hovered list; outside every list, scroll the page.
|
|
2230
|
+
if (after !== before)
|
|
2231
|
+
this.scroller.retarget(after?.el ?? this.doc.body);
|
|
1122
2232
|
}
|
|
1123
|
-
this._ref.dropList?.sortItem(this._ref, p);
|
|
1124
2233
|
this.dragMove.emit(p);
|
|
1125
2234
|
}
|
|
1126
2235
|
/** فراخوانی میشود دقیقاً وقتی drag واقعاً شروع میشود (چه با موس، چه با کیبورد). */
|
|
@@ -1181,7 +2290,13 @@ class NgxDraggable {
|
|
|
1181
2290
|
if (!this.down || e.pointerId !== this.pointerId)
|
|
1182
2291
|
return;
|
|
1183
2292
|
if (this.dragging()) {
|
|
1184
|
-
|
|
2293
|
+
// Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
|
|
2294
|
+
if (this._ref.isListDrag) {
|
|
2295
|
+
// The release position is authoritative for the drop target (a fast flick may end with
|
|
2296
|
+
// a pointerup that has no preceding pointermove at that position).
|
|
2297
|
+
const p = { x: e.clientX, y: e.clientY };
|
|
2298
|
+
if (p.x !== this.lastPointer.x || p.y !== this.lastPointer.y)
|
|
2299
|
+
this._ref.dragMove(p);
|
|
1185
2300
|
this._ref.endDrag();
|
|
1186
2301
|
}
|
|
1187
2302
|
this.service.end(this._ref);
|
|
@@ -1189,6 +2304,16 @@ class NgxDraggable {
|
|
|
1189
2304
|
this.finish();
|
|
1190
2305
|
this.dragEnd.emit(this._ref.pointer);
|
|
1191
2306
|
}
|
|
2307
|
+
/** Browser cancelled the pointer (touch takeover, lost capture...). */
|
|
2308
|
+
pointerCancel(e) {
|
|
2309
|
+
if (!this.down || e.pointerId !== this.pointerId)
|
|
2310
|
+
return;
|
|
2311
|
+
if (this.dragging() && this._ref.isListDrag) {
|
|
2312
|
+
this.cancel();
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
this.pointerUp(e);
|
|
2316
|
+
}
|
|
1192
2317
|
cancel() {
|
|
1193
2318
|
this._ref.cancelDrag();
|
|
1194
2319
|
this.service.end(this._ref);
|
|
@@ -1202,7 +2327,10 @@ class NgxDraggable {
|
|
|
1202
2327
|
this.removeMove?.();
|
|
1203
2328
|
this.removeUp?.();
|
|
1204
2329
|
this.removeEscape?.();
|
|
2330
|
+
this.removeCancel?.();
|
|
2331
|
+
this.removeLostCapture?.();
|
|
1205
2332
|
this.removeMove = this.removeUp = this.removeEscape = undefined;
|
|
2333
|
+
this.removeCancel = this.removeLostCapture = undefined;
|
|
1206
2334
|
}
|
|
1207
2335
|
/** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
|
|
1208
2336
|
keyDown(e) {
|
|
@@ -1219,14 +2347,21 @@ class NgxDraggable {
|
|
|
1219
2347
|
if (delta) {
|
|
1220
2348
|
e.preventDefault();
|
|
1221
2349
|
if (!this.dragging()) {
|
|
1222
|
-
|
|
2350
|
+
// Free drag keeps its (0,0) pseudo pointer. A list item starts from its own centre so
|
|
2351
|
+
// that keyboard sorting resolves against real geometry.
|
|
2352
|
+
const inList = !!this._ref.dropList && this._ref.dropList.el === this._ref.el.parentElement;
|
|
2353
|
+
const r = this._ref.el.getBoundingClientRect();
|
|
2354
|
+
const origin = inList
|
|
2355
|
+
? { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
2356
|
+
: { x: 0, y: 0 };
|
|
2357
|
+
this._ref.pointerDown(origin);
|
|
1223
2358
|
this._ref.boundary = this.boundary();
|
|
1224
2359
|
this._ref.lockAxis = this.lockAxis();
|
|
1225
|
-
this._ref.startDrag(
|
|
2360
|
+
this._ref.startDrag(origin);
|
|
1226
2361
|
this.dragging.set(true);
|
|
1227
2362
|
this.service.begin(this._ref);
|
|
1228
2363
|
this.startScrollTracking();
|
|
1229
|
-
this.dragStart.emit(
|
|
2364
|
+
this.dragStart.emit(origin);
|
|
1230
2365
|
}
|
|
1231
2366
|
this._ref.nudge(delta.x, delta.y);
|
|
1232
2367
|
this.dragMove.emit(this._ref.pointer);
|
|
@@ -1256,7 +2391,7 @@ class NgxDraggable {
|
|
|
1256
2391
|
return (target instanceof HTMLElement &&
|
|
1257
2392
|
!!target.closest('button,a,input,textarea,select,[data-no-drag]'));
|
|
1258
2393
|
}
|
|
1259
|
-
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 });
|
|
1260
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 });
|
|
1261
2396
|
}
|
|
1262
2397
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
|
|
@@ -1272,7 +2407,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
1272
2407
|
'[class.disable-drag]': 'disabled()',
|
|
1273
2408
|
},
|
|
1274
2409
|
}]
|
|
1275
|
-
}], 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: [{
|
|
1276
2411
|
type: Input,
|
|
1277
2412
|
args: ['data']
|
|
1278
2413
|
}], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
|
|
@@ -1661,6 +2796,9 @@ class NgxPlaceholder {
|
|
|
1661
2796
|
this.tpl = tpl;
|
|
1662
2797
|
this._ref.tpl = tpl;
|
|
1663
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);
|
|
1664
2802
|
}
|
|
1665
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 });
|
|
1666
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 });
|
|
@@ -1673,6 +2811,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
1673
2811
|
}]
|
|
1674
2812
|
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
|
|
1675
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
|
+
|
|
1676
2915
|
function moveItemInArray(array, from, to) {
|
|
1677
2916
|
const f = clamp(from, array.length - 1);
|
|
1678
2917
|
const t = clamp(to, array.length - 1);
|