ng-hub-ui-utils 22.2.0 → 22.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,9 +1,553 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, Injectable, effect, InjectionToken, inject, makeEnvironmentProviders, ElementRef, TemplateRef, createComponent, ApplicationRef, Pipe, ChangeDetectorRef, Injector, ViewContainerRef, NgZone, input, Renderer2, RendererStyleFlags2, HostListener, Directive } from '@angular/core';
|
|
1
3
|
import { fromEvent, Observable, Subject, isObservable, EMPTY, of, timer, race } from 'rxjs';
|
|
2
4
|
import { takeUntil, map, filter, withLatestFrom, endWith, take, mergeMap, tap } from 'rxjs/operators';
|
|
3
|
-
import * as i0 from '@angular/core';
|
|
4
|
-
import { signal, effect, InjectionToken, inject, Injectable, makeEnvironmentProviders, ElementRef, TemplateRef, createComponent, ApplicationRef, Pipe, ChangeDetectorRef, Injector, ViewContainerRef, NgZone, input, Renderer2, RendererStyleFlags2, HostListener, Directive } from '@angular/core';
|
|
5
5
|
import { DOCUMENT } from '@angular/common';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Clamps a value into the `[0, max]` range.
|
|
9
|
+
*
|
|
10
|
+
* @param value Value to clamp.
|
|
11
|
+
* @param max Maximum allowed value.
|
|
12
|
+
* @returns The clamped value.
|
|
13
|
+
*/
|
|
14
|
+
function clamp(value, max) {
|
|
15
|
+
return Math.max(0, Math.min(max, value));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Moves an item within an array in place (mirrors `@angular/cdk`'s `moveItemInArray`).
|
|
19
|
+
*
|
|
20
|
+
* @param array Array to mutate.
|
|
21
|
+
* @param fromIndex Current index of the item.
|
|
22
|
+
* @param toIndex Target index of the item.
|
|
23
|
+
*/
|
|
24
|
+
function moveItemInArray(array, fromIndex, toIndex) {
|
|
25
|
+
const from = clamp(fromIndex, array.length - 1);
|
|
26
|
+
const to = clamp(toIndex, array.length - 1);
|
|
27
|
+
if (from === to) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const target = array[from];
|
|
31
|
+
const delta = to < from ? -1 : 1;
|
|
32
|
+
for (let i = from; i !== to; i += delta) {
|
|
33
|
+
array[i] = array[i + delta];
|
|
34
|
+
}
|
|
35
|
+
array[to] = target;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Transfers an item from one array to another in place (mirrors `transferArrayItem`).
|
|
39
|
+
*
|
|
40
|
+
* @param source Source array.
|
|
41
|
+
* @param target Target array.
|
|
42
|
+
* @param fromIndex Index of the item in the source array.
|
|
43
|
+
* @param toIndex Insertion index in the target array.
|
|
44
|
+
*/
|
|
45
|
+
function transferArrayItem(source, target, fromIndex, toIndex) {
|
|
46
|
+
const from = clamp(fromIndex, source.length - 1);
|
|
47
|
+
const to = clamp(toIndex, target.length);
|
|
48
|
+
if (source.length) {
|
|
49
|
+
target.splice(to, 0, source.splice(from, 1)[0]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Copies an item from one array into another in place, leaving the source untouched.
|
|
54
|
+
*
|
|
55
|
+
* @param source Source array.
|
|
56
|
+
* @param target Target array.
|
|
57
|
+
* @param fromIndex Index of the item in the source array.
|
|
58
|
+
* @param toIndex Insertion index in the target array.
|
|
59
|
+
*/
|
|
60
|
+
function copyArrayItem(source, target, fromIndex, toIndex) {
|
|
61
|
+
if (!source.length) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const from = clamp(fromIndex, source.length - 1);
|
|
65
|
+
const to = clamp(toIndex, target.length);
|
|
66
|
+
target.splice(to, 0, source[from]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Computes the destination index in the underlying collection from the hovered target index
|
|
70
|
+
* and the drop side. When reordering within the same container, the index is adjusted to
|
|
71
|
+
* account for the gap left by removing the dragged item.
|
|
72
|
+
*
|
|
73
|
+
* @param targetIndex Absolute index of the hovered target item.
|
|
74
|
+
* @param after Whether the item is dropped after (vs before) the target.
|
|
75
|
+
* @param sameContainer Whether source and target collections are the same.
|
|
76
|
+
* @param fromIndex Absolute index the dragged item occupied in the source collection.
|
|
77
|
+
* @returns The resolved destination index.
|
|
78
|
+
*/
|
|
79
|
+
function computeTargetIndex(targetIndex, after, sameContainer, fromIndex) {
|
|
80
|
+
let index = after ? targetIndex + 1 : targetIndex;
|
|
81
|
+
if (sameContainer && fromIndex < index) {
|
|
82
|
+
index -= 1;
|
|
83
|
+
}
|
|
84
|
+
return Math.max(0, index);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Maps a paginated visible index to its absolute index in the underlying collection.
|
|
88
|
+
*
|
|
89
|
+
* @param visibleIndex Index within the currently rendered slice.
|
|
90
|
+
* @param sliceStart Absolute index of the first item in the slice.
|
|
91
|
+
* @returns The absolute index.
|
|
92
|
+
*/
|
|
93
|
+
function toAbsoluteIndex(visibleIndex, sliceStart) {
|
|
94
|
+
return sliceStart + visibleIndex;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Determines whether `target` is `node` or any of its descendants in a tree, where children
|
|
98
|
+
* are stored under the `childrenKey` property. Used to forbid dropping a node into its own
|
|
99
|
+
* subtree (which would create a cycle).
|
|
100
|
+
*
|
|
101
|
+
* @param node Root node of the subtree to search.
|
|
102
|
+
* @param target Item to look for (e.g. the parent of a candidate drop container).
|
|
103
|
+
* @param childrenKey Property name holding the children collection.
|
|
104
|
+
* @returns `true` when `target` is `node` or one of its descendants.
|
|
105
|
+
*/
|
|
106
|
+
function containsNode(node, target, childrenKey) {
|
|
107
|
+
if (target == null) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (node === target) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const children = node?.[childrenKey];
|
|
114
|
+
if (!Array.isArray(children)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return children.some((child) => containsNode(child, target, childrenKey));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolves whether a dragged item should drop before or after the hovered target, based on
|
|
122
|
+
* the pointer position relative to the target's bounding rectangle and the layout axis.
|
|
123
|
+
*
|
|
124
|
+
* For `vertical` the Y axis decides; for `horizontal` the X axis decides (mirrored in RTL);
|
|
125
|
+
* for `grid` the vertical band decides across rows and the horizontal axis decides within
|
|
126
|
+
* the same row (mirrored in RTL).
|
|
127
|
+
*
|
|
128
|
+
* @param pointerX Pointer X in viewport coordinates.
|
|
129
|
+
* @param pointerY Pointer Y in viewport coordinates.
|
|
130
|
+
* @param rect Bounding rectangle of the target item.
|
|
131
|
+
* @param axis Layout axis of the collection.
|
|
132
|
+
* @param isRtl Whether the collection is in right-to-left mode.
|
|
133
|
+
* @returns `'before'` or `'after'`.
|
|
134
|
+
*/
|
|
135
|
+
function resolveDropPosition(pointerX, pointerY, rect, axis, isRtl) {
|
|
136
|
+
if (axis === 'horizontal') {
|
|
137
|
+
const midX = rect.left + rect.width / 2;
|
|
138
|
+
const before = isRtl ? pointerX > midX : pointerX < midX;
|
|
139
|
+
return before ? 'before' : 'after';
|
|
140
|
+
}
|
|
141
|
+
const midY = rect.top + rect.height / 2;
|
|
142
|
+
if (axis === 'vertical') {
|
|
143
|
+
return pointerY < midY ? 'before' : 'after';
|
|
144
|
+
}
|
|
145
|
+
// grid
|
|
146
|
+
if (pointerY < rect.top) {
|
|
147
|
+
return 'before';
|
|
148
|
+
}
|
|
149
|
+
if (pointerY > rect.bottom) {
|
|
150
|
+
return 'after';
|
|
151
|
+
}
|
|
152
|
+
const midX = rect.left + rect.width / 2;
|
|
153
|
+
const before = isRtl ? pointerX > midX : pointerX < midX;
|
|
154
|
+
return before ? 'before' : 'after';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Renders a template off-screen so it can be used as a native drag image
|
|
159
|
+
* (`dataTransfer.setDragImage`). The caller is responsible for calling `setDragImage` and,
|
|
160
|
+
* on `dragend`, the returned `destroy()`.
|
|
161
|
+
*
|
|
162
|
+
* @param template Template to render as the drag preview.
|
|
163
|
+
* @param context Template context (e.g. `{ item }`).
|
|
164
|
+
* @param container Optional host element to mount into; when omitted, an off-screen holder is
|
|
165
|
+
* appended to `document.body`.
|
|
166
|
+
* @returns The rendered image and its disposer, or `null` when nothing renders (e.g. SSR or
|
|
167
|
+
* an empty template).
|
|
168
|
+
*/
|
|
169
|
+
function createNativeDragImage(template, context, container) {
|
|
170
|
+
if (typeof document === 'undefined') {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const view = template.createEmbeddedView(context);
|
|
174
|
+
view.detectChanges();
|
|
175
|
+
const node = view.rootNodes.find((candidate) => candidate.nodeType === Node.ELEMENT_NODE);
|
|
176
|
+
if (!node) {
|
|
177
|
+
view.destroy();
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const mountedNodes = [...view.rootNodes];
|
|
181
|
+
let holder = null;
|
|
182
|
+
if (container) {
|
|
183
|
+
mountedNodes.forEach((rootNode) => container.appendChild(rootNode));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
holder = document.createElement('div');
|
|
187
|
+
holder.style.position = 'fixed';
|
|
188
|
+
holder.style.top = '-9999px';
|
|
189
|
+
holder.style.left = '-9999px';
|
|
190
|
+
holder.style.pointerEvents = 'none';
|
|
191
|
+
mountedNodes.forEach((rootNode) => holder.appendChild(rootNode));
|
|
192
|
+
document.body.appendChild(holder);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
node,
|
|
196
|
+
destroy: () => {
|
|
197
|
+
// Angular's `EmbeddedViewRef.destroy()` tears down the view but does NOT remove the
|
|
198
|
+
// DOM nodes it produced, so remove them explicitly to keep the helper self-contained
|
|
199
|
+
// (no caller-side `innerHTML` clearing needed). The off-screen holder is removed whole.
|
|
200
|
+
view.destroy();
|
|
201
|
+
if (holder) {
|
|
202
|
+
holder.remove();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
mountedNodes.forEach((rootNode) => rootNode.remove?.());
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const EDGE_MARGIN = 48;
|
|
212
|
+
const MAX_SCROLL_SPEED = 16;
|
|
213
|
+
/**
|
|
214
|
+
* Creates a Pointer Events drag session that mirrors native drag-and-drop on touch devices.
|
|
215
|
+
*
|
|
216
|
+
* The session waits for the pointer to pass a movement threshold (so taps still behave as
|
|
217
|
+
* taps), then renders a floating ghost that follows the finger, reports hover positions via
|
|
218
|
+
* `onMove`, autoscrolls when near a scroll container's edges, and commits on pointer up.
|
|
219
|
+
*
|
|
220
|
+
* @param config Session configuration.
|
|
221
|
+
* @returns A handle whose `destroy()` aborts the session.
|
|
222
|
+
*/
|
|
223
|
+
function createPointerDragSession(config) {
|
|
224
|
+
const threshold = config.threshold ?? 8;
|
|
225
|
+
const pointerId = config.startEvent.pointerId;
|
|
226
|
+
const startX = config.startEvent.clientX;
|
|
227
|
+
const startY = config.startEvent.clientY;
|
|
228
|
+
const rect = config.sourceEl.getBoundingClientRect();
|
|
229
|
+
const grabOffsetX = startX - rect.left;
|
|
230
|
+
const grabOffsetY = startY - rect.top;
|
|
231
|
+
let started = false;
|
|
232
|
+
let ghost = null;
|
|
233
|
+
let scrollContainer = window;
|
|
234
|
+
let rafId = null;
|
|
235
|
+
let scrollVelocity = 0;
|
|
236
|
+
/**
|
|
237
|
+
* Positions the ghost under the pointer.
|
|
238
|
+
*
|
|
239
|
+
* @param x Pointer X.
|
|
240
|
+
* @param y Pointer Y.
|
|
241
|
+
*/
|
|
242
|
+
const positionGhost = (x, y) => {
|
|
243
|
+
if (ghost) {
|
|
244
|
+
ghost.style.transform = `translate(${x - grabOffsetX}px, ${y - grabOffsetY}px)`;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Runs the autoscroll animation loop while the pointer sits in an edge zone.
|
|
249
|
+
*/
|
|
250
|
+
const scrollStep = () => {
|
|
251
|
+
if (scrollVelocity !== 0) {
|
|
252
|
+
if (scrollContainer === window) {
|
|
253
|
+
window.scrollBy(0, scrollVelocity);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
scrollContainer.scrollTop += scrollVelocity;
|
|
257
|
+
}
|
|
258
|
+
rafId = requestAnimationFrame(scrollStep);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
rafId = null;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Updates the autoscroll velocity from the pointer's proximity to the container edges.
|
|
266
|
+
*
|
|
267
|
+
* @param y Pointer Y.
|
|
268
|
+
*/
|
|
269
|
+
const updateAutoscroll = (y) => {
|
|
270
|
+
const bounds = scrollContainer === window
|
|
271
|
+
? { top: 0, bottom: window.innerHeight }
|
|
272
|
+
: scrollContainer.getBoundingClientRect();
|
|
273
|
+
if (y < bounds.top + EDGE_MARGIN) {
|
|
274
|
+
scrollVelocity = -Math.ceil((MAX_SCROLL_SPEED * (bounds.top + EDGE_MARGIN - y)) / EDGE_MARGIN);
|
|
275
|
+
}
|
|
276
|
+
else if (y > bounds.bottom - EDGE_MARGIN) {
|
|
277
|
+
scrollVelocity = Math.ceil((MAX_SCROLL_SPEED * (y - (bounds.bottom - EDGE_MARGIN))) / EDGE_MARGIN);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
scrollVelocity = 0;
|
|
281
|
+
}
|
|
282
|
+
if (scrollVelocity !== 0 && rafId === null) {
|
|
283
|
+
rafId = requestAnimationFrame(scrollStep);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Begins the actual drag once the threshold is exceeded.
|
|
288
|
+
*
|
|
289
|
+
* @param x Pointer X.
|
|
290
|
+
* @param y Pointer Y.
|
|
291
|
+
*/
|
|
292
|
+
const beginDrag = (x, y) => {
|
|
293
|
+
started = true;
|
|
294
|
+
scrollContainer = findScrollContainer(config.sourceEl);
|
|
295
|
+
ghost = config.ghostFactory();
|
|
296
|
+
ghost.classList.add('hub-drag-ghost');
|
|
297
|
+
ghost.style.position = 'fixed';
|
|
298
|
+
ghost.style.top = '0';
|
|
299
|
+
ghost.style.left = '0';
|
|
300
|
+
ghost.style.width = `${rect.width}px`;
|
|
301
|
+
ghost.style.pointerEvents = 'none';
|
|
302
|
+
ghost.style.zIndex = '2147483647';
|
|
303
|
+
ghost.style.margin = '0';
|
|
304
|
+
positionGhost(x, y);
|
|
305
|
+
document.body.appendChild(ghost);
|
|
306
|
+
config.onStart();
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Handles pointer movement: starts the drag past threshold, then tracks and autoscrolls.
|
|
310
|
+
*
|
|
311
|
+
* @param event Pointer move event.
|
|
312
|
+
*/
|
|
313
|
+
const onPointerMove = (event) => {
|
|
314
|
+
if (event.pointerId !== pointerId) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const { clientX, clientY } = event;
|
|
318
|
+
if (!started) {
|
|
319
|
+
if (Math.abs(clientX - startX) < threshold && Math.abs(clientY - startY) < threshold) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
beginDrag(clientX, clientY);
|
|
323
|
+
}
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
positionGhost(clientX, clientY);
|
|
326
|
+
config.onMove(clientX, clientY);
|
|
327
|
+
updateAutoscroll(clientY);
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Handles pointer up: commits the drop when a drag actually happened.
|
|
331
|
+
*
|
|
332
|
+
* @param event Pointer up event.
|
|
333
|
+
*/
|
|
334
|
+
const onPointerUp = (event) => {
|
|
335
|
+
if (event.pointerId !== pointerId) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (started) {
|
|
339
|
+
config.onDrop(event.clientX, event.clientY);
|
|
340
|
+
}
|
|
341
|
+
cleanup();
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Handles pointer cancellation.
|
|
345
|
+
*
|
|
346
|
+
* @param event Pointer cancel event.
|
|
347
|
+
*/
|
|
348
|
+
const onPointerCancel = (event) => {
|
|
349
|
+
if (event.pointerId !== pointerId) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (started) {
|
|
353
|
+
config.onCancel();
|
|
354
|
+
}
|
|
355
|
+
cleanup();
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Removes listeners, the ghost and any pending animation frame, then notifies the owner.
|
|
359
|
+
*/
|
|
360
|
+
const cleanup = () => {
|
|
361
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
362
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
363
|
+
window.removeEventListener('pointercancel', onPointerCancel);
|
|
364
|
+
if (rafId !== null) {
|
|
365
|
+
cancelAnimationFrame(rafId);
|
|
366
|
+
rafId = null;
|
|
367
|
+
}
|
|
368
|
+
scrollVelocity = 0;
|
|
369
|
+
ghost?.remove();
|
|
370
|
+
ghost = null;
|
|
371
|
+
try {
|
|
372
|
+
config.sourceEl.releasePointerCapture(pointerId);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Pointer capture may not be held; ignore.
|
|
376
|
+
}
|
|
377
|
+
config.onEnd();
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
config.sourceEl.setPointerCapture(pointerId);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Environments without pointer capture (e.g. tests) can still proceed.
|
|
384
|
+
}
|
|
385
|
+
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
|
386
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
387
|
+
window.addEventListener('pointercancel', onPointerCancel);
|
|
388
|
+
return { destroy: cleanup };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Finds the nearest vertically scrollable ancestor of an element, falling back to `window`.
|
|
392
|
+
*
|
|
393
|
+
* @param el Starting element.
|
|
394
|
+
* @returns The scroll container (an element) or `window`.
|
|
395
|
+
*/
|
|
396
|
+
function findScrollContainer(el) {
|
|
397
|
+
let node = el?.parentElement ?? null;
|
|
398
|
+
while (node && node !== document.body && node !== document.documentElement) {
|
|
399
|
+
const style = getComputedStyle(node);
|
|
400
|
+
const overflowY = style.overflowY;
|
|
401
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
|
|
402
|
+
return node;
|
|
403
|
+
}
|
|
404
|
+
node = node.parentElement;
|
|
405
|
+
}
|
|
406
|
+
return window;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Singleton coordinator that backs native HTML5 drag-and-drop reordering and cross-instance
|
|
411
|
+
* transfers (e.g. between two lists, or any two owners that share a drag group).
|
|
412
|
+
*
|
|
413
|
+
* A drag spans two component instances (source and target) and the native `dataTransfer`
|
|
414
|
+
* payload is unreadable during `dragover`, so a shared, root-provided service is the only
|
|
415
|
+
* reliable channel to know what is being dragged and from where while hovering. The service
|
|
416
|
+
* only coordinates state; it never mutates the underlying collections.
|
|
417
|
+
*/
|
|
418
|
+
class HubDragDropService {
|
|
419
|
+
#registrations = new Map();
|
|
420
|
+
#active = signal(null, /* @ts-ignore */
|
|
421
|
+
...(ngDevMode ? [{ debugName: "#active" }] : /* istanbul ignore next */ []));
|
|
422
|
+
#target = signal(null, /* @ts-ignore */
|
|
423
|
+
...(ngDevMode ? [{ debugName: "#target" }] : /* istanbul ignore next */ []));
|
|
424
|
+
/** The drag currently in progress, or `null`. */
|
|
425
|
+
active = this.#active.asReadonly();
|
|
426
|
+
/** The current drop target, or `null`. */
|
|
427
|
+
target = this.#target.asReadonly();
|
|
428
|
+
/** Whether a drag is in progress. */
|
|
429
|
+
isDragging = computed(() => this.#active() !== null, /* @ts-ignore */
|
|
430
|
+
...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
|
|
431
|
+
/**
|
|
432
|
+
* Registers an owner so it can participate in (and be a target of) cross-owner transfers.
|
|
433
|
+
*
|
|
434
|
+
* @param registration The owner registration.
|
|
435
|
+
*/
|
|
436
|
+
register(registration) {
|
|
437
|
+
this.#registrations.set(registration.ownerId, registration);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Removes an owner registration (call on destroy).
|
|
441
|
+
*
|
|
442
|
+
* @param ownerId Identifier of the owner to remove.
|
|
443
|
+
*/
|
|
444
|
+
unregister(ownerId) {
|
|
445
|
+
this.#registrations.delete(ownerId);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Starts a drag, recording the active item and clearing any previous target.
|
|
449
|
+
*
|
|
450
|
+
* @param drag The active drag snapshot.
|
|
451
|
+
*/
|
|
452
|
+
begin(drag) {
|
|
453
|
+
this.#active.set(drag);
|
|
454
|
+
this.#target.set(null);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Updates the transient drop target while hovering.
|
|
458
|
+
*
|
|
459
|
+
* @param target The hovered target, or `null` to clear it.
|
|
460
|
+
*/
|
|
461
|
+
setTarget(target) {
|
|
462
|
+
this.#target.set(target);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Ends the current drag and clears all transient state.
|
|
466
|
+
*/
|
|
467
|
+
end() {
|
|
468
|
+
this.#active.set(null);
|
|
469
|
+
this.#target.set(null);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Determines whether the active drag may be dropped on the given owner. An owner always
|
|
473
|
+
* accepts its own items (in-owner reorder); a different owner accepts only when both
|
|
474
|
+
* share the same non-null drag group.
|
|
475
|
+
*
|
|
476
|
+
* @param targetOwnerId Identifier of the candidate target owner.
|
|
477
|
+
* @returns `true` when the drop is allowed.
|
|
478
|
+
*/
|
|
479
|
+
canDrop(targetOwnerId) {
|
|
480
|
+
const active = this.#active();
|
|
481
|
+
if (!active) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
if (targetOwnerId === active.sourceId) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
const registration = this.#registrations.get(targetOwnerId);
|
|
488
|
+
if (!registration) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const targetGroup = registration.group();
|
|
492
|
+
return active.sourceGroup != null && targetGroup != null && active.sourceGroup === targetGroup;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Re-renders an owner on demand (used by the destination owner to refresh the source owner
|
|
496
|
+
* after a cross-owner transfer).
|
|
497
|
+
*
|
|
498
|
+
* @param ownerId Identifier of the owner to refresh.
|
|
499
|
+
*/
|
|
500
|
+
refreshSource(ownerId) {
|
|
501
|
+
this.#registrations.get(ownerId)?.refresh?.();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Asks an owner to commit the pending drop as the destination (Pointer Events fallback,
|
|
505
|
+
* where the source component drives the gesture but the destination must commit/emit).
|
|
506
|
+
*
|
|
507
|
+
* @param ownerId Identifier of the destination owner.
|
|
508
|
+
*/
|
|
509
|
+
requestCommit(ownerId) {
|
|
510
|
+
this.#registrations.get(ownerId)?.commit?.();
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Resolves the drop target under a viewport point by hit-testing the DOM and delegating to
|
|
514
|
+
* the owning component (which knows its own collections). Used by the Pointer Events
|
|
515
|
+
* fallback, including cross-owner hovers where the target is a different component.
|
|
516
|
+
*
|
|
517
|
+
* @param clientX Viewport X coordinate.
|
|
518
|
+
* @param clientY Viewport Y coordinate.
|
|
519
|
+
* @returns The resolved target, or `null` when the point is not over a droppable owner.
|
|
520
|
+
*/
|
|
521
|
+
resolveTargetAt(clientX, clientY) {
|
|
522
|
+
if (typeof document === 'undefined') {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const element = document.elementFromPoint(clientX, clientY);
|
|
526
|
+
const hostEl = element?.closest('[data-hub-drag-owner]');
|
|
527
|
+
const ownerId = hostEl?.getAttribute('data-hub-drag-owner');
|
|
528
|
+
if (!element || !ownerId || !this.canDrop(ownerId)) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
return this.#registrations.get(ownerId)?.resolveTarget?.(element, clientX, clientY) ?? null;
|
|
532
|
+
}
|
|
533
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubDragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
534
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubDragDropService, providedIn: 'root' });
|
|
535
|
+
}
|
|
536
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubDragDropService, decorators: [{
|
|
537
|
+
type: Injectable,
|
|
538
|
+
args: [{ providedIn: 'root' }]
|
|
539
|
+
}] });
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Native HTML5 drag-and-drop core shared across ng-hub-ui libraries.
|
|
543
|
+
*
|
|
544
|
+
* Provides the engine-agnostic, reusable pieces of a native drag-and-drop implementation:
|
|
545
|
+
* pure array helpers, drop-position geometry, drag-image rendering, a Pointer Events touch
|
|
546
|
+
* fallback, a singleton coordinator service (cross-instance transfers) and shared types.
|
|
547
|
+
* UI primitives (handle/placeholder/preview directives) stay per-library, since their
|
|
548
|
+
* selectors and data models differ.
|
|
549
|
+
*/
|
|
550
|
+
|
|
7
551
|
const FOCUSABLE_ELEMENTS_SELECTOR = [
|
|
8
552
|
'a[href]',
|
|
9
553
|
'button:not([disabled])',
|
|
@@ -1463,5 +2007,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
1463
2007
|
* Generated bundle index. Do not edit.
|
|
1464
2008
|
*/
|
|
1465
2009
|
|
|
1466
|
-
export { ContentRef, FOCUSABLE_ELEMENTS_SELECTOR, GetPipe, HUB_TRANSLATION_CONFIG, HubTranslationService, IsObjectPipe, IsObservablePipe, IsStringPipe, OverlayPosition, OverlayRef, OverlayService, PopupService, ScrollBar, TooltipDirective, TranslatePipe, UcfirstPipe, UnwrapAsyncPipe, closest, debouncedSignal, equals, generateUniqueId, getActiveElement, getFocusableBoundaryElements, getValue, getValueInRange, hubCompleteTransition, hubFocusTrap, hubRunTransition, interpolateString, isDefined, isInteger, isNumber, isObject, isPromise, isString, mergeDeep, padNumber, provideHubTranslation, reflow, regExpEscape, removeAccents, runInZone, toInteger, toString };
|
|
2010
|
+
export { ContentRef, FOCUSABLE_ELEMENTS_SELECTOR, GetPipe, HUB_TRANSLATION_CONFIG, HubDragDropService, HubTranslationService, IsObjectPipe, IsObservablePipe, IsStringPipe, OverlayPosition, OverlayRef, OverlayService, PopupService, ScrollBar, TooltipDirective, TranslatePipe, UcfirstPipe, UnwrapAsyncPipe, clamp, closest, computeTargetIndex, containsNode, copyArrayItem, createNativeDragImage, createPointerDragSession, debouncedSignal, equals, generateUniqueId, getActiveElement, getFocusableBoundaryElements, getValue, getValueInRange, hubCompleteTransition, hubFocusTrap, hubRunTransition, interpolateString, isDefined, isInteger, isNumber, isObject, isPromise, isString, mergeDeep, moveItemInArray, padNumber, provideHubTranslation, reflow, regExpEscape, removeAccents, resolveDropPosition, runInZone, toAbsoluteIndex, toInteger, toString, transferArrayItem };
|
|
1467
2011
|
//# sourceMappingURL=ng-hub-ui-utils.mjs.map
|