@hkdigital/lib-sveltekit 0.2.6 → 0.2.7
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/dist/components/drag-drop/DragController.d.ts +0 -20
- package/dist/components/drag-drop/DragController.js +1 -70
- package/dist/components/drag-drop/DragDropContext.svelte +14 -7
- package/dist/components/drag-drop/Draggable.svelte +40 -20
- package/dist/components/drag-drop/Draggable.svelte.d.ts +8 -2
- package/dist/components/drag-drop/drag-state.svelte.js +23 -27
- package/dist/components/drag-drop/drag-state.svelte.js__ +323 -0
- package/package.json +1 -1
@@ -12,26 +12,6 @@ export class DragController {
|
|
12
12
|
targetElement: HTMLElement;
|
13
13
|
offsetX: number;
|
14
14
|
offsetY: number;
|
15
|
-
_previewSet: boolean;
|
16
|
-
/**
|
17
|
-
* Create a preview image from the current draggable element or a specific child element
|
18
|
-
* @param {string} [selector] - Optional CSS selector to target a specific child element
|
19
|
-
* @returns {HTMLElement} - The created preview element
|
20
|
-
*/
|
21
|
-
grabPreviewImage(selector?: string): HTMLElement;
|
22
|
-
/**
|
23
|
-
* Set a custom element as the drag preview image
|
24
|
-
* @param {HTMLElement} element - Element to use as drag preview
|
25
|
-
* @param {number} [offsetX] - Horizontal offset (uses natural offset if omitted)
|
26
|
-
* @param {number} [offsetY] - Vertical offset (uses natural offset if omitted)
|
27
|
-
* @returns {boolean} - Whether setting the preview was successful
|
28
|
-
*/
|
29
|
-
setPreviewImage(element: HTMLElement, offsetX?: number, offsetY?: number): boolean;
|
30
|
-
/**
|
31
|
-
* Check if a custom preview has been set
|
32
|
-
* @returns {boolean}
|
33
|
-
*/
|
34
|
-
hasCustomPreview(): boolean;
|
35
15
|
/**
|
36
16
|
* Apply the default preview (uses the draggable element itself)
|
37
17
|
* @returns {boolean}
|
@@ -15,74 +15,6 @@ export class DragController {
|
|
15
15
|
const rect = this.targetElement.getBoundingClientRect();
|
16
16
|
this.offsetX = event.clientX - rect.left;
|
17
17
|
this.offsetY = event.clientY - rect.top;
|
18
|
-
|
19
|
-
this._previewSet = false;
|
20
|
-
}
|
21
|
-
|
22
|
-
/**
|
23
|
-
* Create a preview image from the current draggable element or a specific child element
|
24
|
-
* @param {string} [selector] - Optional CSS selector to target a specific child element
|
25
|
-
* @returns {HTMLElement} - The created preview element
|
26
|
-
*/
|
27
|
-
grabPreviewImage(selector = null) {
|
28
|
-
// Find the source element (either the whole draggable or a specific child)
|
29
|
-
const sourceElement = selector
|
30
|
-
? this.targetElement.querySelector(selector)
|
31
|
-
: this.targetElement;
|
32
|
-
|
33
|
-
if (!sourceElement) {
|
34
|
-
console.error(`Element with selector "${selector}" not found`);
|
35
|
-
return this.grabPreviewImage(); // Fallback to the main element
|
36
|
-
}
|
37
|
-
|
38
|
-
// Clone the element to create the preview
|
39
|
-
const previewElement = /** @type {HTMLElement} */ (
|
40
|
-
sourceElement.cloneNode(true)
|
41
|
-
);
|
42
|
-
|
43
|
-
// Position off-screen (needed for setDragImage to work properly)
|
44
|
-
previewElement.style.position = 'absolute';
|
45
|
-
previewElement.style.top = '-9999px';
|
46
|
-
previewElement.style.left = '-9999px';
|
47
|
-
|
48
|
-
// Add to the document temporarily
|
49
|
-
document.body.appendChild(previewElement);
|
50
|
-
|
51
|
-
return previewElement;
|
52
|
-
}
|
53
|
-
|
54
|
-
/**
|
55
|
-
* Set a custom element as the drag preview image
|
56
|
-
* @param {HTMLElement} element - Element to use as drag preview
|
57
|
-
* @param {number} [offsetX] - Horizontal offset (uses natural offset if omitted)
|
58
|
-
* @param {number} [offsetY] - Vertical offset (uses natural offset if omitted)
|
59
|
-
* @returns {boolean} - Whether setting the preview was successful
|
60
|
-
*/
|
61
|
-
setPreviewImage(element, offsetX, offsetY) {
|
62
|
-
if (!this.dataTransfer || !this.dataTransfer.setDragImage) {
|
63
|
-
return false;
|
64
|
-
}
|
65
|
-
|
66
|
-
// Use provided offsets or fall back to natural offsets
|
67
|
-
const finalOffsetX = offsetX !== undefined ? offsetX : this.offsetX;
|
68
|
-
const finalOffsetY = offsetY !== undefined ? offsetY : this.offsetY;
|
69
|
-
|
70
|
-
try {
|
71
|
-
this.dataTransfer.setDragImage(element, finalOffsetX, finalOffsetY);
|
72
|
-
this._previewSet = true;
|
73
|
-
return true;
|
74
|
-
} catch (err) {
|
75
|
-
console.error('Failed to set drag preview image:', err);
|
76
|
-
return false;
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
/**
|
81
|
-
* Check if a custom preview has been set
|
82
|
-
* @returns {boolean}
|
83
|
-
*/
|
84
|
-
hasCustomPreview() {
|
85
|
-
return this._previewSet;
|
86
18
|
}
|
87
19
|
|
88
20
|
/**
|
@@ -92,8 +24,7 @@ export class DragController {
|
|
92
24
|
applyDefaultPreview() {
|
93
25
|
if (
|
94
26
|
!this.dataTransfer ||
|
95
|
-
!this.dataTransfer.setDragImage
|
96
|
-
this._previewSet
|
27
|
+
!this.dataTransfer.setDragImage
|
97
28
|
) {
|
98
29
|
return false;
|
99
30
|
}
|
@@ -25,7 +25,7 @@
|
|
25
25
|
dragState.updateActiveDropZone(event.clientX, event.clientY, event);
|
26
26
|
}
|
27
27
|
|
28
|
-
|
28
|
+
/**
|
29
29
|
* Handle drag over at context level
|
30
30
|
* @param {DragEvent} event
|
31
31
|
*/
|
@@ -33,15 +33,22 @@
|
|
33
33
|
event.preventDefault();
|
34
34
|
dragState.updateActiveDropZone(event.clientX, event.clientY, event);
|
35
35
|
|
36
|
-
// Set appropriate drop effect
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
// Set appropriate drop effect based on current drag operation
|
37
|
+
if (dragState.isDragging()) {
|
38
|
+
const activeZone = dragState.activeDropZone;
|
39
|
+
if (activeZone) {
|
40
|
+
const config = dragState.dropZones.get(activeZone);
|
41
|
+
if (config?.canDrop) {
|
42
|
+
event.dataTransfer.dropEffect = 'move';
|
43
|
+
} else {
|
44
|
+
event.dataTransfer.dropEffect = 'none';
|
45
|
+
}
|
42
46
|
} else {
|
43
47
|
event.dataTransfer.dropEffect = 'none';
|
44
48
|
}
|
49
|
+
} else {
|
50
|
+
// No internal drag operation, might be file drag
|
51
|
+
event.dataTransfer.dropEffect = 'copy';
|
45
52
|
}
|
46
53
|
}
|
47
54
|
|
@@ -23,7 +23,10 @@
|
|
23
23
|
* dragDelay?: number,
|
24
24
|
* base?: string,
|
25
25
|
* classes?: string,
|
26
|
-
* children: import('svelte').Snippet
|
26
|
+
* children: import('svelte').Snippet<[{
|
27
|
+
* element: HTMLElement,
|
28
|
+
* rect: DOMRect
|
29
|
+
* }]>,
|
27
30
|
* draggingSnippet?: import('svelte').Snippet<[{
|
28
31
|
* element: HTMLElement,
|
29
32
|
* rect: DOMRect
|
@@ -206,6 +209,9 @@ let stateObject = $derived({
|
|
206
209
|
JSON.stringify({ draggableId })
|
207
210
|
);
|
208
211
|
|
212
|
+
// Chrome also likes to have text/plain
|
213
|
+
event.dataTransfer.setData('text/plain', draggableId);
|
214
|
+
|
209
215
|
// Create the preview controller
|
210
216
|
const previewController = new DragController(event);
|
211
217
|
|
@@ -216,8 +222,8 @@ let stateObject = $derived({
|
|
216
222
|
onDragStart?.({ event, item, source, group, getController });
|
217
223
|
|
218
224
|
// Apply drag preview if available
|
219
|
-
if (draggingSnippet
|
220
|
-
try {
|
225
|
+
// if (draggingSnippet) {
|
226
|
+
// try {
|
221
227
|
// Store rectangle information for the snippet
|
222
228
|
elementRect = rect;
|
223
229
|
|
@@ -229,10 +235,20 @@ let stateObject = $derived({
|
|
229
235
|
previewX = rect.left;
|
230
236
|
previewY = rect.top;
|
231
237
|
|
232
|
-
// Set a transparent 1x1 pixel image to hide browser's
|
238
|
+
// Set a transparent 1x1 pixel image to hide browser's
|
239
|
+
// default preview
|
233
240
|
const emptyImg = new Image();
|
234
241
|
emptyImg.src =
|
235
|
-
|
242
|
+
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
243
|
+
|
244
|
+
// Chrome needs the image to be loaded before setting it
|
245
|
+
emptyImg.onload = () => {
|
246
|
+
if (event.dataTransfer) {
|
247
|
+
event.dataTransfer.setDragImage(emptyImg, 0, 0);
|
248
|
+
}
|
249
|
+
};
|
250
|
+
|
251
|
+
// Fallback: try to set it immediately too
|
236
252
|
event.dataTransfer.setDragImage(emptyImg, 0, 0);
|
237
253
|
|
238
254
|
// Add document level event listener to track mouse movement
|
@@ -241,15 +257,15 @@ let stateObject = $derived({
|
|
241
257
|
// Show custom preview
|
242
258
|
showPreview = true;
|
243
259
|
customPreviewSet = true;
|
244
|
-
} catch (err) {
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
}
|
249
|
-
} else
|
250
|
-
|
251
|
-
|
252
|
-
}
|
260
|
+
// } catch (err) {
|
261
|
+
// console.error('Error setting up custom preview:', err);
|
262
|
+
// // Fallback to default preview
|
263
|
+
// previewController.applyDefaultPreview();
|
264
|
+
// }
|
265
|
+
// } else {
|
266
|
+
// // Apply default preview if no custom preview was set
|
267
|
+
// previewController.applyDefaultPreview();
|
268
|
+
// }
|
253
269
|
}
|
254
270
|
|
255
271
|
/**
|
@@ -358,12 +374,12 @@ let stateObject = $derived({
|
|
358
374
|
dragState.start(draggableId, dragData);
|
359
375
|
|
360
376
|
// Show preview
|
361
|
-
if (draggingSnippet) {
|
377
|
+
// if (draggingSnippet) {
|
362
378
|
elementRect = rect;
|
363
379
|
previewX = rect.left;
|
364
380
|
previewY = rect.top;
|
365
381
|
showPreview = true;
|
366
|
-
}
|
382
|
+
// }
|
367
383
|
|
368
384
|
// Prevent scrolling while dragging
|
369
385
|
event.preventDefault();
|
@@ -398,7 +414,7 @@ function handleTouchMove(event) {
|
|
398
414
|
clientX: touch.clientX,
|
399
415
|
clientY: touch.clientY,
|
400
416
|
dataTransfer: {
|
401
|
-
types: ['application/json'],
|
417
|
+
types: ['application/json', 'text/plain'],
|
402
418
|
getData: () => JSON.stringify({ draggableId }),
|
403
419
|
dropEffect: 'move',
|
404
420
|
effectAllowed: 'move',
|
@@ -429,7 +445,7 @@ function handleTouchMove(event) {
|
|
429
445
|
clientX: touch.clientX,
|
430
446
|
clientY: touch.clientY,
|
431
447
|
dataTransfer: {
|
432
|
-
types: ['application/json'],
|
448
|
+
types: ['application/json', 'text'],
|
433
449
|
getData: () => JSON.stringify({ draggableId }),
|
434
450
|
dropEffect: 'move',
|
435
451
|
effectAllowed: 'move',
|
@@ -468,10 +484,10 @@ function handleTouchMove(event) {
|
|
468
484
|
style="touch-action: none;"
|
469
485
|
{...attrs}
|
470
486
|
>
|
471
|
-
{@render children()}
|
487
|
+
{@render children({ element: draggableElement, rect: elementRect })}
|
472
488
|
</div>
|
473
489
|
|
474
|
-
{#if
|
490
|
+
{#if showPreview && elementRect}
|
475
491
|
<div
|
476
492
|
data-companion="drag-preview-follower"
|
477
493
|
class={stateClasses}
|
@@ -479,7 +495,11 @@ function handleTouchMove(event) {
|
|
479
495
|
style:left="{previewX}px"
|
480
496
|
style:top="{previewY}px"
|
481
497
|
>
|
498
|
+
{#if draggingSnippet}
|
482
499
|
{@render draggingSnippet({ element: draggableElement, rect: elementRect })}
|
500
|
+
{:else}
|
501
|
+
{@render children({ element: draggableElement, rect: elementRect })}
|
502
|
+
{/if}
|
483
503
|
</div>
|
484
504
|
{/if}
|
485
505
|
|
@@ -10,7 +10,10 @@ type Draggable = {
|
|
10
10
|
dragDelay?: number;
|
11
11
|
base?: string;
|
12
12
|
classes?: string;
|
13
|
-
children: Snippet<[
|
13
|
+
children: Snippet<[{
|
14
|
+
element: HTMLElement;
|
15
|
+
rect: DOMRect;
|
16
|
+
}]>;
|
14
17
|
draggingSnippet?: Snippet<[{
|
15
18
|
element: HTMLElement;
|
16
19
|
rect: DOMRect;
|
@@ -52,7 +55,10 @@ declare const Draggable: import("svelte").Component<{
|
|
52
55
|
dragDelay?: number;
|
53
56
|
base?: string;
|
54
57
|
classes?: string;
|
55
|
-
children: import("svelte").Snippet
|
58
|
+
children: import("svelte").Snippet<[{
|
59
|
+
element: HTMLElement;
|
60
|
+
rect: DOMRect;
|
61
|
+
}]>;
|
56
62
|
draggingSnippet?: import("svelte").Snippet<[{
|
57
63
|
element: HTMLElement;
|
58
64
|
rect: DOMRect;
|
@@ -259,8 +259,26 @@ class DragState {
|
|
259
259
|
* @returns {Object|null} The drag data, or null for file drops
|
260
260
|
*/
|
261
261
|
getDraggable(event) {
|
262
|
-
// Check if this is a
|
263
|
-
if (event.dataTransfer &&
|
262
|
+
// Check if this is a file drop first
|
263
|
+
if (event.dataTransfer && event.dataTransfer.types) {
|
264
|
+
// Check if types is an array or DOMStringList
|
265
|
+
const types = Array.from(event.dataTransfer.types);
|
266
|
+
if (types.includes('Files')) {
|
267
|
+
return null; // This is a file drop, not an internal drag
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
// For dragover events, we can't read dataTransfer.getData in Chrome
|
272
|
+
// Instead, check if we have an active drag operation
|
273
|
+
if (event.type === 'dragover') {
|
274
|
+
if (this.draggables.size > 0) {
|
275
|
+
// Return the most recent drag operation
|
276
|
+
return this.current;
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
280
|
+
// For drop events, we can read the data
|
281
|
+
if (event.type === 'drop' && event.dataTransfer) {
|
264
282
|
try {
|
265
283
|
const jsonData = event.dataTransfer.getData('application/json');
|
266
284
|
if (jsonData) {
|
@@ -272,34 +290,12 @@ class DragState {
|
|
272
290
|
}
|
273
291
|
}
|
274
292
|
} catch (error) {
|
275
|
-
console.error('Error getting drag data:', error);
|
276
|
-
}
|
277
|
-
}
|
278
|
-
|
279
|
-
// Check if this is a file drop
|
280
|
-
if (event.dataTransfer.types.includes('Files')) {
|
281
|
-
return null;
|
282
|
-
}
|
283
|
-
|
284
|
-
// Handle internal drag operations
|
285
|
-
try {
|
286
|
-
const jsonData = event.dataTransfer.getData('application/json');
|
287
|
-
if (jsonData) {
|
288
|
-
const transferData = JSON.parse(jsonData);
|
289
|
-
const draggableId = transferData.draggableId;
|
290
|
-
|
291
|
-
if (draggableId) {
|
292
|
-
const dragData = this.getDraggableById(draggableId);
|
293
|
-
if (dragData) {
|
294
|
-
return dragData;
|
295
|
-
}
|
296
|
-
}
|
293
|
+
console.error('Error getting drag data from drop:', error);
|
297
294
|
}
|
298
|
-
} catch (error) {
|
299
|
-
console.error('Error getting drag data:', error);
|
300
295
|
}
|
301
296
|
|
302
|
-
|
297
|
+
// Fallback to checking active drags
|
298
|
+
return this.current;
|
303
299
|
}
|
304
300
|
|
305
301
|
/**
|
@@ -0,0 +1,323 @@
|
|
1
|
+
// drag-state.svelte.js
|
2
|
+
import { defineStateContext } from '$lib/util/svelte/state-context/index.js';
|
3
|
+
|
4
|
+
/** @typedef {import('$lib/typedef').SimulatedDragEvent} SimulatedDragEvent */
|
5
|
+
|
6
|
+
class DragState {
|
7
|
+
// Existing draggables map
|
8
|
+
draggables = $state(new Map());
|
9
|
+
|
10
|
+
// New: Registry for dropzones
|
11
|
+
dropZones = $state(new Map());
|
12
|
+
|
13
|
+
// Track which dropzone is currently active
|
14
|
+
activeDropZone = $state(null);
|
15
|
+
|
16
|
+
// Track the last active drop zone
|
17
|
+
// - activeDropZone gets cleared by dragLeavr
|
18
|
+
// - but we need it in 'end'
|
19
|
+
lastActiveDropZone = null;
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Register a dropzone
|
23
|
+
* @param {string} zoneId
|
24
|
+
* @param {Object} config
|
25
|
+
* @param {string} config.zone
|
26
|
+
* @param {string} config.group
|
27
|
+
* @param {Function} config.accepts
|
28
|
+
* @param {Function} config.onDragEnter
|
29
|
+
* @param {Function} config.onDragOver
|
30
|
+
* @param {Function} config.onDragLeave
|
31
|
+
* @param {(DropData) => void} config.onDrop
|
32
|
+
* @param {HTMLElement} config.element
|
33
|
+
*/
|
34
|
+
registerDropZone(zoneId, config) {
|
35
|
+
if (this.dropZones.has(zoneId)) {
|
36
|
+
throw new Error(`Zone [${zoneId}] is already registered`);
|
37
|
+
}
|
38
|
+
|
39
|
+
this.dropZones.set(zoneId, {
|
40
|
+
...config,
|
41
|
+
isOver: false,
|
42
|
+
canDrop: false
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Unregister a dropzone
|
48
|
+
* @param {string} zoneId
|
49
|
+
*/
|
50
|
+
unregisterDropZone(zoneId) {
|
51
|
+
if (this.activeDropZone === zoneId) {
|
52
|
+
this.activeDropZone = null;
|
53
|
+
}
|
54
|
+
this.dropZones.delete(zoneId);
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Get dropzone at coordinates
|
59
|
+
* @param {number} x
|
60
|
+
* @param {number} y
|
61
|
+
* @returns {Object|null}
|
62
|
+
*/
|
63
|
+
getDropZoneAtPoint(x, y) {
|
64
|
+
// Check all registered dropzones
|
65
|
+
for (const [zoneId, config] of this.dropZones) {
|
66
|
+
const rect = config.element.getBoundingClientRect();
|
67
|
+
|
68
|
+
if (
|
69
|
+
x >= rect.left &&
|
70
|
+
x <= rect.right &&
|
71
|
+
y >= rect.top &&
|
72
|
+
y <= rect.bottom
|
73
|
+
) {
|
74
|
+
// Found a dropzone at this point
|
75
|
+
// Check if it's the deepest one (for nested zones)
|
76
|
+
let deepestZone = { zoneId, config, depth: 0 };
|
77
|
+
|
78
|
+
// Check for nested dropzones
|
79
|
+
for (const [otherId, otherConfig] of this.dropZones) {
|
80
|
+
if (otherId === zoneId) continue;
|
81
|
+
|
82
|
+
const otherRect = otherConfig.element.getBoundingClientRect();
|
83
|
+
if (
|
84
|
+
x >= otherRect.left &&
|
85
|
+
x <= otherRect.right &&
|
86
|
+
y >= otherRect.top &&
|
87
|
+
y <= otherRect.bottom
|
88
|
+
) {
|
89
|
+
// Check if this zone is nested inside our current zone
|
90
|
+
if (config.element.contains(otherConfig.element)) {
|
91
|
+
deepestZone = {
|
92
|
+
zoneId: otherId,
|
93
|
+
config: otherConfig,
|
94
|
+
depth: deepestZone.depth + 1
|
95
|
+
};
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
return { zoneId: deepestZone.zoneId, config: deepestZone.config };
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
return null;
|
105
|
+
}
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Update active dropzone based on coordinates
|
109
|
+
*
|
110
|
+
* @param {number} x
|
111
|
+
* @param {number} y
|
112
|
+
* @param {DragEvent|SimulatedDragEvent} event
|
113
|
+
*/
|
114
|
+
updateActiveDropZone(x, y, event) {
|
115
|
+
const dropZone = this.getDropZoneAtPoint(x, y);
|
116
|
+
const newActiveId = dropZone?.zoneId || null;
|
117
|
+
|
118
|
+
// Handle leave/enter transitions
|
119
|
+
if (this.activeDropZone !== newActiveId) {
|
120
|
+
// Leave previous zone
|
121
|
+
if (this.activeDropZone) {
|
122
|
+
this.lastActiveDropZone = this.activeDropZone;
|
123
|
+
|
124
|
+
const prevConfig = this.dropZones.get(this.activeDropZone);
|
125
|
+
if (prevConfig) {
|
126
|
+
prevConfig.isOver = false;
|
127
|
+
prevConfig.canDrop = false;
|
128
|
+
prevConfig.onDragLeave?.({ event, zone: prevConfig.zone });
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
// Enter new zone
|
133
|
+
if (newActiveId && dropZone) {
|
134
|
+
const dragData = this.getDraggable(event);
|
135
|
+
const canDrop = dragData && dropZone.config.accepts(dragData);
|
136
|
+
|
137
|
+
dropZone.config.isOver = true;
|
138
|
+
dropZone.config.canDrop = canDrop;
|
139
|
+
dropZone.config.onDragEnter?.({
|
140
|
+
event,
|
141
|
+
zone: dropZone.config.zone,
|
142
|
+
canDrop
|
143
|
+
});
|
144
|
+
}
|
145
|
+
|
146
|
+
this.activeDropZone = newActiveId;
|
147
|
+
} else if (newActiveId) {
|
148
|
+
// Still in the same zone, just send dragOver
|
149
|
+
dropZone.config.onDragOver?.({ event, zone: dropZone.config.zone });
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Handle drop at coordinates
|
155
|
+
* @param {number} x
|
156
|
+
* @param {number} y
|
157
|
+
* @param {DragEvent|SimulatedDragEvent} event
|
158
|
+
*/
|
159
|
+
handleDropAtPoint(x, y, event) {
|
160
|
+
const dropZone = this.getDropZoneAtPoint(x, y);
|
161
|
+
|
162
|
+
if (dropZone && dropZone.config.canDrop) {
|
163
|
+
const dragData = this.getDraggable(event);
|
164
|
+
|
165
|
+
if (dragData && dropZone.config.element) {
|
166
|
+
// Calculate drop position relative to dropzone
|
167
|
+
const rect = dropZone.config.element.getBoundingClientRect();
|
168
|
+
|
169
|
+
const style = window.getComputedStyle(dropZone.config.element);
|
170
|
+
|
171
|
+
const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
|
172
|
+
const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
|
173
|
+
|
174
|
+
const dropOffsetX = x - rect.left - borderLeftWidth;
|
175
|
+
const dropOffsetY = y - rect.top - borderTopWidth;
|
176
|
+
|
177
|
+
const dropX = dropOffsetX - (dragData.offsetX ?? 0);
|
178
|
+
const dropY = dropOffsetY - (dragData.offsetY ?? 0);
|
179
|
+
|
180
|
+
// Call the dropzone's drop handler
|
181
|
+
dropZone.config.onDrop?.({
|
182
|
+
zone: dropZone.config.zone,
|
183
|
+
source: dragData.source,
|
184
|
+
item: dragData.item,
|
185
|
+
x: dropX,
|
186
|
+
y: dropY,
|
187
|
+
drag: dragData,
|
188
|
+
drop: {
|
189
|
+
offsetX: dropOffsetX,
|
190
|
+
offsetY: dropOffsetY,
|
191
|
+
target: dropZone.config.element
|
192
|
+
}
|
193
|
+
});
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
// Ensure we notify the active dropzone that drag ended
|
198
|
+
if (this.activeDropZone) {
|
199
|
+
const config = this.dropZones.get(this.activeDropZone);
|
200
|
+
if (config) {
|
201
|
+
config.isOver = false;
|
202
|
+
config.canDrop = false;
|
203
|
+
config.onDragLeave?.({ event, zone: config.zone });
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
// Reset active dropzone
|
208
|
+
this.activeDropZone = null;
|
209
|
+
}
|
210
|
+
|
211
|
+
/**
|
212
|
+
* @param {string} draggableId
|
213
|
+
* @param {import('$lib/typedef/drag.js').DragData} dragData
|
214
|
+
*/
|
215
|
+
start(draggableId, dragData) {
|
216
|
+
this.draggables.set(draggableId, dragData);
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
* @param {string} draggableId
|
221
|
+
*/
|
222
|
+
end(draggableId) {
|
223
|
+
this.draggables.delete(draggableId);
|
224
|
+
|
225
|
+
// Check both current AND last active dropzone
|
226
|
+
const zoneToNotify = this.activeDropZone || this.lastActiveDropZone;
|
227
|
+
|
228
|
+
if (zoneToNotify) {
|
229
|
+
const config = this.dropZones.get(zoneToNotify);
|
230
|
+
if (config && (config.isOver || config.canDrop)) {
|
231
|
+
config.isOver = false;
|
232
|
+
config.canDrop = false;
|
233
|
+
config.onDragLeave?.({
|
234
|
+
event: new DragEvent('dragend'),
|
235
|
+
zone: config.zone
|
236
|
+
});
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
this.activeDropZone = null;
|
241
|
+
this.lastActiveDropZone = null;
|
242
|
+
}
|
243
|
+
|
244
|
+
/**
|
245
|
+
* Get a drag data by draggable id
|
246
|
+
*
|
247
|
+
* @param {string} draggableId
|
248
|
+
* @returns {import('$lib/typedef/drag.js').DragData|undefined}
|
249
|
+
*/
|
250
|
+
getDraggableById(draggableId) {
|
251
|
+
return this.draggables.get(draggableId);
|
252
|
+
}
|
253
|
+
|
254
|
+
/**
|
255
|
+
* Get a drag data. Extracts draggable id from the supplied DragEvent
|
256
|
+
*
|
257
|
+
* @param {DragEvent|SimulatedDragEvent} event
|
258
|
+
*
|
259
|
+
* @returns {Object|null} The drag data, or null for file drops
|
260
|
+
*/
|
261
|
+
getDraggable(event) {
|
262
|
+
// Check if this is a touch-simulated event
|
263
|
+
if (event.dataTransfer && !event.dataTransfer.files) {
|
264
|
+
try {
|
265
|
+
const jsonData = event.dataTransfer.getData('application/json');
|
266
|
+
if (jsonData) {
|
267
|
+
const transferData = JSON.parse(jsonData);
|
268
|
+
const draggableId = transferData.draggableId;
|
269
|
+
|
270
|
+
if (draggableId) {
|
271
|
+
return this.getDraggableById(draggableId);
|
272
|
+
}
|
273
|
+
}
|
274
|
+
} catch (error) {
|
275
|
+
console.error('Error getting drag data:', error);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
// Check if this is a file drop
|
280
|
+
if (event.dataTransfer.types.includes('Files')) {
|
281
|
+
return null;
|
282
|
+
}
|
283
|
+
|
284
|
+
// Handle internal drag operations
|
285
|
+
try {
|
286
|
+
const jsonData = event.dataTransfer.getData('application/json');
|
287
|
+
if (jsonData) {
|
288
|
+
const transferData = JSON.parse(jsonData);
|
289
|
+
const draggableId = transferData.draggableId;
|
290
|
+
|
291
|
+
if (draggableId) {
|
292
|
+
const dragData = this.getDraggableById(draggableId);
|
293
|
+
if (dragData) {
|
294
|
+
return dragData;
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
} catch (error) {
|
299
|
+
console.error('Error getting drag data:', error);
|
300
|
+
}
|
301
|
+
|
302
|
+
return null;
|
303
|
+
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Get the most recently started drag operation (convenience method)
|
307
|
+
* @returns {import('$lib/typedef/drag.js').DragData|undefined}
|
308
|
+
*/
|
309
|
+
get current() {
|
310
|
+
const entries = Array.from(this.draggables.entries());
|
311
|
+
return entries.length > 0 ? entries[entries.length - 1][1] : undefined;
|
312
|
+
}
|
313
|
+
|
314
|
+
/**
|
315
|
+
* @returns {boolean}
|
316
|
+
*/
|
317
|
+
isDragging() {
|
318
|
+
return this.draggables.size > 0;
|
319
|
+
}
|
320
|
+
}
|
321
|
+
|
322
|
+
export const [createOrGetDragState, createDragState, getDragState] =
|
323
|
+
defineStateContext(DragState);
|