@hkdigital/lib-sveltekit 0.1.92 → 0.1.93

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,11 +1,8 @@
1
1
  <script>
2
- import { onMount, onDestroy } from 'svelte';
3
2
  import { toStateClasses } from '../../util/design-system/index.js';
4
-
5
3
  import { createOrGetDragState } from './drag-state.svelte.js';
6
-
7
4
  import { GridLayers } from '../layout';
8
-
5
+ import { generateLocalId } from '../../util/unique';
9
6
  import {
10
7
  READY,
11
8
  DRAG_OVER,
@@ -85,66 +82,30 @@
85
82
  ...attrs
86
83
  } = $props();
87
84
 
88
- // let debugEvents = [];
89
-
90
85
  const dragState = createOrGetDragState(contextKey);
86
+ const dropZoneId = generateLocalId();
91
87
 
92
88
  let currentState = $state(READY);
93
-
94
89
  let dropZoneElement = $state(null);
95
90
 
96
- // $effect( () => {
97
- // if(dropZoneElement) {
98
- // console.debug(dropZoneElement);
99
- // }
100
- // } )
101
-
102
- let isCurrentlyOver = $state(false);
103
-
104
- // $inspect({ zone, isCurrentlyOver });
105
-
106
- // Cleanup function
107
- let cleanup;
108
-
109
- onMount(() => {
110
- // Global dragend listener to ensure state cleanup
111
- const handleGlobalDragEnd = () => {
112
- isCurrentlyOver = false;
113
- currentState = READY;
114
- };
115
-
116
- document.addEventListener('dragend', handleGlobalDragEnd);
117
-
118
- // console.debug(`DropZone ${zone} mounted`);
119
-
120
- cleanup = () => {
121
- document.removeEventListener('dragend', handleGlobalDragEnd);
122
- };
123
-
124
- return cleanup;
125
- });
126
-
127
91
  // Computed height classes based on mode
128
92
  let heightClasses = $derived.by(() => {
129
93
  const classes = [];
130
94
 
131
95
  switch (heightMode) {
132
96
  case 'flexible':
133
- // Flexible height with optional min/max constraints
134
97
  if (minHeight) classes.push(minHeight);
135
- else classes.push('min-h-[100px]'); // Default minimum
98
+ else classes.push('min-h-[100px]');
136
99
  if (maxHeight) {
137
100
  classes.push(maxHeight);
138
101
  classes.push('overflow-y-auto');
139
102
  }
140
103
  break;
141
104
  case 'fill':
142
- // Fill the parent container
143
105
  classes.push('h-full');
144
106
  break;
145
107
  case 'fixed':
146
108
  default:
147
- // Fixed height (default to min-height if not filling)
148
109
  classes.push('h-min');
149
110
  break;
150
111
  }
@@ -166,338 +127,84 @@
166
127
  // Update bindable props
167
128
  $effect(() => {
168
129
  isDragOver = [DRAG_OVER, CAN_DROP, CANNOT_DROP].includes(currentState);
169
-
170
130
  canDrop = currentState === CAN_DROP;
171
131
  });
172
132
 
173
- /**
174
- * Check if we can accept the dragged item
175
- *
176
- * @param {Object} data
177
- *
178
- * @returns {boolean}
179
- */
180
- function canAcceptDrop(data) {
181
- if (disabled) return false;
182
- if (!data) return false;
183
- if (!accepts(data)) return false;
184
- return true;
185
- }
186
-
187
- /**
188
- * Get drag data from either drag state or handle file drops
189
- * @param {DragEvent} event
190
- * @returns {Object|null} The drag data, or null for file drops
191
- */
192
- function getDragData(event) {
193
- // Check if this is a file drop
194
- if (event.dataTransfer.types.includes('Files')) {
195
- // Handle file drop - you can extend this based on your needs
196
- // console.log('File drop detected:', event.dataTransfer.files);
197
- return null; // Return null to indicate this is not an internal drag
198
- }
199
-
200
- // Handle internal drag operations
201
- try {
202
- const jsonData = event.dataTransfer.getData('application/json');
203
- if (jsonData) {
204
- const transferData = JSON.parse(jsonData);
205
- const draggableId = transferData.draggableId;
206
-
207
- if (draggableId) {
208
- // Get the original instance from drag state
209
- const dragData = dragState.getDraggable(draggableId);
210
- if (dragData) {
211
- return dragData;
212
- }
213
- }
214
- }
215
- } catch (error) {
216
- console.error('Error getting drag data:', error);
217
- }
218
-
219
- return null;
220
- }
221
-
222
- /**
223
- * Handle drag enter with improved DOM traversal check
224
- * @param {DragEvent} event
225
- */
226
- function handleDragEnter(event) {
227
- // debugEvents.push({
228
- // event: 'dragEnter',
229
- // target: event.target,
230
- // currentTarget: event.currentTarget,
231
- // isCurrentlyOver
232
- // });
233
-
234
- // console.log('dragEnter:', { zone });
235
-
236
- // Prevent default to allow drop
237
- event.preventDefault();
238
-
239
- // Check if mouse is actually within drop zone boundaries
240
- const rect = dropZoneElement.getBoundingClientRect();
241
- const x = event.clientX;
242
- const y = event.clientY;
243
-
244
- const isWithinBounds =
245
- x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
246
-
247
- if (!isWithinBounds) {
248
- // Don't enter if mouse is outside bounds
249
- // console.debug('Mouse outside bounds', {
250
- // x,
251
- // y,
252
- // top: rect.top,
253
- // bottom: rect.bottom,
254
- // left: rect.left,
255
- // right: rect.right
256
- // });
257
-
258
- dropZoneElement.style.border = "solid 10px purple";
259
- return;
260
- }
261
-
262
- if (isCurrentlyOver) {
263
- // console.debug('Already over');
264
- return;
265
- }
266
-
267
- isCurrentlyOver = true;
268
-
269
- // Get the drag data
270
- const dragData = getDragData(event);
271
-
272
- // Update state based on acceptance
273
- if (dragData) {
274
- currentState = canAcceptDrop(dragData) ? CAN_DROP : CANNOT_DROP;
275
- } else {
276
- currentState = DRAG_OVER;
277
- }
278
-
279
- // Notify listeners
280
- onDragEnter?.({ event, zone, canDrop: currentState === CAN_DROP });
281
- }
282
-
283
- /**
284
- * Handle drag over
285
- * @param {DragEvent} event
286
- */
287
- function handleDragOver(event) {
288
- // console.log('dragOver', {zone}, event );
289
- // console.log('dragOver', {
290
- // zone,
291
- // eventTarget: event.target.closest('[data-zone]')?.dataset?.zone,
292
- // currentTarget: event.currentTarget.dataset?.zone,
293
- // path: event.composedPath().map(el => el.dataset?.zone || el.tagName)
294
- // });
295
-
296
- // debugEvents.push({
297
- // event: 'handleDragOver',
298
- // target: event.target,
299
- // currentTarget: event.currentTarget,
300
- // isCurrentlyOver,
301
- // contains: dropZoneElement.contains(event.target),
302
- // mouseX: event.clientX,
303
- // mouseY: event.clientY,
304
- // dropZoneBounds: dropZoneElement.getBoundingClientRect()
305
- // });
306
-
307
- // console.log('dragOver:', {
308
- // target: event.target,
309
- // currentTarget: event.currentTarget,
310
- // isCurrentlyOver,
311
- // contains: dropZoneElement.contains(event.target)
312
- // });
313
-
314
- // Check if mouse is actually within drop zone boundaries
315
- const rect = dropZoneElement.getBoundingClientRect();
316
- const x = event.clientX;
317
- const y = event.clientY;
318
-
319
- const isWithinBounds =
320
- x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
321
-
322
- if (!isWithinBounds) {
323
- // Mouse is outside bounds, treat as leave
324
- if (isCurrentlyOver) {
325
- isCurrentlyOver = false;
326
- currentState = READY;
327
- onDragLeave?.({ event, zone });
328
- }
329
- return;
330
- }
331
-
332
- // console.log('dragOver (accepted)', {zone});
333
-
334
- // Prevent default to allow drop
335
- event.preventDefault();
336
-
337
- // If we're not currently over this drop zone, treat it as an enter
338
- if (!isCurrentlyOver) {
339
- handleDragEnter(event);
340
- return;
341
- }
342
-
343
- // Get the drag data
344
- const dragData = getDragData(event);
345
-
346
- // Re-evaluate acceptance
347
- if (dragData && [DRAG_OVER, CAN_DROP, CANNOT_DROP].includes(currentState)) {
348
- currentState = canAcceptDrop(dragData) ? CAN_DROP : CANNOT_DROP;
349
- }
350
-
351
- // Set visual feedback based on drop acceptance
352
- if (currentState === CAN_DROP) {
353
- event.dataTransfer.dropEffect = 'move';
354
- } else if (currentState === CANNOT_DROP) {
355
- event.dataTransfer.dropEffect = 'none';
356
- }
357
-
358
- // Notify listeners
359
- onDragOver?.({ event, zone });
360
- }
361
-
362
- /**
363
- * Handle drag leave with improved DOM traversal check
364
- * @param {DragEvent} event
365
- */
366
- function handleDragLeave(event) {
367
- // debugEvents.push({
368
- // event: 'handleDragLeave',
369
- // target: event.target,
370
- // currentTarget: event.currentTarget,
371
- // relatedTarget: event.relatedTarget,
372
- // isCurrentlyOver,
373
- // isActuallyLeaving: !event.relatedTarget || !dropZoneElement.contains(event.relatedTarget)
374
- // });
375
-
376
- // console.log('dragLeave:', {
377
- // target: event.target,
378
- // currentTarget: event.currentTarget,
379
- // relatedTarget: event.relatedTarget,
380
- // isCurrentlyOver,
381
- // isActuallyLeaving: !event.relatedTarget || !dropZoneElement.contains(event.relatedTarget)
382
- // });
383
-
384
- // We need to check if we're actually leaving the drop zone or just
385
- // entering a child element within the drop zone
386
-
387
- // relatedTarget is the element we're moving to
388
- const relatedTarget = event.relatedTarget;
389
-
390
- // If relatedTarget is null or outside our drop zone, we're truly leaving
391
- const isActuallyLeaving =
392
- !relatedTarget || !dropZoneElement.contains(relatedTarget);
393
-
394
- if (isActuallyLeaving) {
395
- isCurrentlyOver = false;
396
- currentState = READY;
397
- onDragLeave?.({ event, zone });
398
- }
399
- }
400
-
401
- /**
402
- * Handle drop
403
- * @param {DragEvent} event
404
- */
405
- function handleDrop(event) {
406
- // console.debug( JSON.stringify(debugEvents, null, 2));
407
-
408
- // Prevent default browser actions
409
- event.preventDefault();
410
-
411
- if (!isCurrentlyOver) {
412
- // Prevent drop if not currently over
413
- return;
414
- }
415
-
416
- // Reset our tracking state
417
- isCurrentlyOver = false;
418
-
419
- try {
420
- // Check if this is a file drop first
421
- if (event.dataTransfer.types.includes('Files')) {
422
- // Handle file drops
423
- const files = Array.from(event.dataTransfer.files);
424
- // console.log('Files dropped:', files);
425
-
426
- // You can add custom file handling here
427
- // For now, just reset state and return
428
- currentState = READY;
429
- return;
430
- }
431
-
432
- // Get drag data for internal drag operations
433
- const dragData = getDragData(event);
434
-
435
- // Check if we can accept this drop
436
- if (dragData && canAcceptDrop(dragData)) {
437
- // Notify listener
438
- onDropStart?.({ event, zone, data: dragData });
439
-
440
- const style = window.getComputedStyle(dropZoneElement);
441
-
442
- // Parse border widths from computed style
443
- const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
444
- const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
445
-
446
- // Get drop zone rectangle
447
- const dropZoneRect = dropZoneElement.getBoundingClientRect();
448
-
449
- // Calculate position with both dragData.offsetX/Y adjustment and border adjustment
450
- const dropOffsetX = event.clientX - dropZoneRect.left - borderLeftWidth;
451
-
452
- const dropOffsetY = event.clientY - dropZoneRect.top - borderTopWidth;
453
-
454
- const x = dropOffsetX - (dragData.offsetX ?? 0);
455
- const y = dropOffsetY - (dragData.offsetY ?? 0);
456
-
457
- const dropResult = onDrop?.({
458
- zone,
459
- source: dragData.source,
460
- item: dragData.item,
461
- x,
462
- y,
463
- drag: dragData,
464
- drop: {
465
- offsetX: dropOffsetX,
466
- offsetY: dropOffsetY,
467
- event
133
+ // Register/unregister with drag state
134
+ $effect(() => {
135
+ if (dropZoneElement) {
136
+ // Register this dropzone
137
+ dragState.registerDropZone(dropZoneId, {
138
+ zone,
139
+ group,
140
+ accepts: (dragData) => {
141
+ if (disabled) return false;
142
+ if (!dragData) return false;
143
+ return accepts(dragData);
144
+ },
145
+ onDragEnter: (detail) => {
146
+ currentState = detail.canDrop ? CAN_DROP : CANNOT_DROP;
147
+ onDragEnter?.(detail);
148
+ },
149
+ onDragOver: (detail) => {
150
+ onDragOver?.(detail);
151
+ },
152
+ onDragLeave: (detail) => {
153
+ currentState = READY;
154
+ onDragLeave?.(detail);
155
+ },
156
+ onDrop: async (dropData) => {
157
+ currentState = READY;
158
+
159
+ try {
160
+ onDropStart?.({
161
+ event: dropData.drop.event,
162
+ zone: dropData.zone,
163
+ data: dropData.drag
164
+ });
165
+
166
+ const result = await onDrop?.(dropData);
167
+
168
+ onDropEnd?.({
169
+ event: dropData.drop.event,
170
+ zone: dropData.zone,
171
+ data: dropData.drag,
172
+ success: true
173
+ });
174
+
175
+ return result;
176
+ } catch (error) {
177
+ onDropEnd?.({
178
+ event: dropData.drop.event,
179
+ zone: dropData.zone,
180
+ data: dropData.drag,
181
+ success: false,
182
+ error
183
+ });
184
+ throw error;
468
185
  }
469
- });
470
-
471
- // Handle async or sync results
472
- Promise.resolve(dropResult)
473
- .then(() => {
474
- currentState = READY;
475
- onDropEnd?.({ event, zone, data: dragData, success: true });
476
- })
477
- .catch((error) => {
478
- currentState = READY;
479
- onDropEnd?.({ event, zone, data: dragData, success: false, error });
480
- });
481
- } else {
482
- // Not a valid drop, reset state
483
- currentState = READY;
484
- }
485
- } catch (error) {
486
- // Handle parsing errors
487
- console.error('Drop error:', error);
488
- currentState = READY;
186
+ },
187
+ element: dropZoneElement
188
+ });
189
+
190
+ // Cleanup on unmount
191
+ return () => {
192
+ dragState.unregisterDropZone(dropZoneId);
193
+ };
489
194
  }
490
- }
195
+ });
491
196
 
492
- // console.log(`DropZone ${zone} script run`);
197
+ // Monitor drag state to update preview
198
+ let showPreview = $derived(
199
+ dragState.activeDropZone === dropZoneId &&
200
+ currentState === CAN_DROP &&
201
+ dropPreviewSnippet
202
+ );
493
203
  </script>
204
+
494
205
  <div
495
206
  data-component="drop-zone"
496
207
  bind:this={dropZoneElement}
497
- ondragenter={handleDragEnter}
498
- ondragover={handleDragOver}
499
- ondragleave={handleDragLeave}
500
- ondrop={handleDrop}
501
208
  class="{base} {heightClasses} {classes} {stateClasses}"
502
209
  data-zone={zone}
503
210
  {...attrs}
@@ -513,7 +220,7 @@
513
220
  </div>
514
221
  {/if}
515
222
 
516
- {#if currentState === CAN_DROP && dropPreviewSnippet}
223
+ {#if showPreview && dropPreviewSnippet}
517
224
  <div data-layer="preview">
518
225
  {@render dropPreviewSnippet(dragState.current)}
519
226
  </div>
@@ -522,6 +229,10 @@
522
229
  </div>
523
230
 
524
231
  <style>
232
+ [data-component="drop-zone"] {
233
+ -webkit-tap-highlight-color: transparent;
234
+ }
235
+
525
236
  [data-layer='content']:not(.relative) {
526
237
  position: absolute;
527
238
  left: 0;
@@ -1,8 +1,63 @@
1
1
  export const createOrGetDragState: (contextKey?: import("../../typedef").ContextKey) => DragState;
2
2
  export const createDragState: (contextKey?: import("../../typedef").ContextKey) => DragState;
3
3
  export const getDragState: (contextKey?: import("../../typedef").ContextKey) => DragState;
4
+ export type SimulatedDragEvent = import("../../typedef").SimulatedDragEvent;
5
+ /** @typedef {import('../../typedef').SimulatedDragEvent} SimulatedDragEvent */
4
6
  declare class DragState {
5
7
  draggables: Map<any, any>;
8
+ dropZones: Map<any, any>;
9
+ activeDropZone: any;
10
+ lastActiveDropZone: any;
11
+ /**
12
+ * Register a dropzone
13
+ * @param {string} zoneId
14
+ * @param {Object} config
15
+ * @param {string} config.zone
16
+ * @param {string} config.group
17
+ * @param {Function} config.accepts
18
+ * @param {Function} config.onDragEnter
19
+ * @param {Function} config.onDragOver
20
+ * @param {Function} config.onDragLeave
21
+ * @param {(DropData) => void} config.onDrop
22
+ * @param {HTMLElement} config.element
23
+ */
24
+ registerDropZone(zoneId: string, config: {
25
+ zone: string;
26
+ group: string;
27
+ accepts: Function;
28
+ onDragEnter: Function;
29
+ onDragOver: Function;
30
+ onDragLeave: Function;
31
+ onDrop: (DropData: any) => void;
32
+ element: HTMLElement;
33
+ }): void;
34
+ /**
35
+ * Unregister a dropzone
36
+ * @param {string} zoneId
37
+ */
38
+ unregisterDropZone(zoneId: string): void;
39
+ /**
40
+ * Get dropzone at coordinates
41
+ * @param {number} x
42
+ * @param {number} y
43
+ * @returns {Object|null}
44
+ */
45
+ getDropZoneAtPoint(x: number, y: number): any | null;
46
+ /**
47
+ * Update active dropzone based on coordinates
48
+ *
49
+ * @param {number} x
50
+ * @param {number} y
51
+ * @param {DragEvent|SimulatedDragEvent} event
52
+ */
53
+ updateActiveDropZone(x: number, y: number, event: DragEvent | SimulatedDragEvent): void;
54
+ /**
55
+ * Handle drop at coordinates
56
+ * @param {number} x
57
+ * @param {number} y
58
+ * @param {DragEvent|SimulatedDragEvent} event
59
+ */
60
+ handleDropAtPoint(x: number, y: number, event: DragEvent | SimulatedDragEvent): void;
6
61
  /**
7
62
  * @param {string} draggableId
8
63
  * @param {import('../../typedef/drag.js').DragData} dragData
@@ -13,10 +68,20 @@ declare class DragState {
13
68
  */
14
69
  end(draggableId: string): void;
15
70
  /**
71
+ * Get a drag data by draggable id
72
+ *
16
73
  * @param {string} draggableId
17
74
  * @returns {import('../../typedef/drag.js').DragData|undefined}
18
75
  */
19
- getDraggable(draggableId: string): import("../../typedef/drag.js").DragData | undefined;
76
+ getDraggableById(draggableId: string): import("../../typedef/drag.js").DragData | undefined;
77
+ /**
78
+ * Get a drag data. Extracts draggable id from the supplied DragEvent
79
+ *
80
+ * @param {DragEvent|SimulatedDragEvent} event
81
+ *
82
+ * @returns {Object|null} The drag data, or null for file drops
83
+ */
84
+ getDraggable(event: DragEvent | SimulatedDragEvent): any | null;
20
85
  /**
21
86
  * Get the most recently started drag operation (convenience method)
22
87
  * @returns {import('../../typedef/drag.js').DragData|undefined}