@hkdigital/lib-sveltekit 0.1.91 → 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,
@@ -24,8 +21,9 @@
24
21
  * accepts?: (item: any) => boolean,
25
22
  * base?: string,
26
23
  * classes?: string,
27
- * height?: string,
28
- * autoHeight?: boolean,
24
+ * minHeight?: string,
25
+ * maxHeight?: string,
26
+ * heightMode?: 'fixed' | 'flexible' | 'fill',
29
27
  * children?: import('svelte').Snippet,
30
28
  * contextKey?: import('../../typedef').ContextKey,
31
29
  * dropPreviewSnippet?: import('svelte').Snippet<[DragData]>,
@@ -67,8 +65,9 @@
67
65
  accepts = () => true,
68
66
  base = '',
69
67
  classes = '',
70
- height = 'h-min',
71
- autoHeight = false,
68
+ minHeight = '',
69
+ maxHeight = '',
70
+ heightMode = 'fixed',
72
71
  children,
73
72
  contextKey,
74
73
  dropPreviewSnippet,
@@ -84,33 +83,34 @@
84
83
  } = $props();
85
84
 
86
85
  const dragState = createOrGetDragState(contextKey);
86
+ const dropZoneId = generateLocalId();
87
87
 
88
88
  let currentState = $state(READY);
89
- let dropZoneElement; // Reference to the drop zone DOM element
90
-
91
- // We'll use a flag to track if we're currently in the drop zone
92
- // without relying on a counter approach
93
- let isCurrentlyOver = $state(false);
94
-
95
- // Cleanup function
96
- let cleanup;
97
-
98
- onMount(() => {
99
- // Global dragend listener to ensure state cleanup
100
- const handleGlobalDragEnd = () => {
101
- isCurrentlyOver = false;
102
- currentState = READY;
103
- };
104
-
105
- document.addEventListener('dragend', handleGlobalDragEnd);
106
-
107
- cleanup = () => {
108
- document.removeEventListener('dragend', handleGlobalDragEnd);
109
- };
110
- });
89
+ let dropZoneElement = $state(null);
90
+
91
+ // Computed height classes based on mode
92
+ let heightClasses = $derived.by(() => {
93
+ const classes = [];
94
+
95
+ switch (heightMode) {
96
+ case 'flexible':
97
+ if (minHeight) classes.push(minHeight);
98
+ else classes.push('min-h-[100px]');
99
+ if (maxHeight) {
100
+ classes.push(maxHeight);
101
+ classes.push('overflow-y-auto');
102
+ }
103
+ break;
104
+ case 'fill':
105
+ classes.push('h-full');
106
+ break;
107
+ case 'fixed':
108
+ default:
109
+ classes.push('h-min');
110
+ break;
111
+ }
111
112
 
112
- onDestroy(() => {
113
- cleanup?.();
113
+ return classes.join(' ');
114
114
  });
115
115
 
116
116
  // Computed state object for CSS classes
@@ -127,253 +127,100 @@
127
127
  // Update bindable props
128
128
  $effect(() => {
129
129
  isDragOver = [DRAG_OVER, CAN_DROP, CANNOT_DROP].includes(currentState);
130
-
131
130
  canDrop = currentState === CAN_DROP;
132
131
  });
133
132
 
134
- /**
135
- * Check if we can accept the dragged item
136
- *
137
- * @param {Object} data
138
- *
139
- * @returns {boolean}
140
- */
141
- function canAcceptDrop(data) {
142
- if (disabled) return false;
143
- if (!data) return false;
144
- if (!accepts(data)) return false;
145
- return true;
146
- }
147
-
148
- /**
149
- * Get drag data from either drag state or handle file drops
150
- * @param {DragEvent} event
151
- * @returns {Object|null} The drag data, or null for file drops
152
- */
153
- function getDragData(event) {
154
- // Check if this is a file drop
155
- if (event.dataTransfer.types.includes('Files')) {
156
- // Handle file drop - you can extend this based on your needs
157
- // console.log('File drop detected:', event.dataTransfer.files);
158
- return null; // Return null to indicate this is not an internal drag
159
- }
160
-
161
- // Handle internal drag operations
162
- try {
163
- const jsonData = event.dataTransfer.getData('application/json');
164
- if (jsonData) {
165
- const transferData = JSON.parse(jsonData);
166
- const draggableId = transferData.draggableId;
167
-
168
- if (draggableId) {
169
- // Get the original instance from drag state
170
- const dragData = dragState.getDraggable(draggableId);
171
- if (dragData) {
172
- return dragData;
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;
173
185
  }
174
- }
175
- }
176
- } catch (error) {
177
- console.error('Error getting drag data:', error);
178
- }
179
-
180
- return null;
181
- }
182
-
183
- /**
184
- * Handle drag enter with improved DOM traversal check
185
- * @param {DragEvent} event
186
- */
187
- function handleDragEnter(event) {
188
- // Prevent default to allow drop
189
- event.preventDefault();
190
-
191
- // If we're already in a drag-over state, don't reset anything
192
- if (isCurrentlyOver) return;
193
-
194
- // Now we're over this drop zone
195
- isCurrentlyOver = true;
196
-
197
- // Get the drag data
198
- const dragData = getDragData(event);
199
-
200
- // Update state based on acceptance
201
- if (dragData) {
202
- currentState = canAcceptDrop(dragData) ? CAN_DROP : CANNOT_DROP;
203
- } else {
204
- currentState = DRAG_OVER;
205
- }
206
-
207
- // Notify listeners
208
- onDragEnter?.({ event, zone, canDrop: currentState === CAN_DROP });
209
- }
210
-
211
- /**
212
- * Handle drag over
213
- * @param {DragEvent} event
214
- */
215
- function handleDragOver(event) {
216
- // Prevent default to allow drop
217
- event.preventDefault();
218
-
219
- // If we're not currently over this drop zone (despite dragover firing),
220
- // treat it as an enter event
221
- if (!isCurrentlyOver) {
222
- handleDragEnter(event);
223
- return;
224
- }
225
-
226
- // Get the drag data
227
- const dragData = getDragData(event);
228
-
229
- // Re-evaluate acceptance
230
- if (dragData && [DRAG_OVER, CAN_DROP, CANNOT_DROP].includes(currentState)) {
231
- currentState = canAcceptDrop(dragData) ? CAN_DROP : CANNOT_DROP;
186
+ },
187
+ element: dropZoneElement
188
+ });
189
+
190
+ // Cleanup on unmount
191
+ return () => {
192
+ dragState.unregisterDropZone(dropZoneId);
193
+ };
232
194
  }
195
+ });
233
196
 
234
- // Set visual feedback based on drop acceptance
235
- if (currentState === CAN_DROP) {
236
- event.dataTransfer.dropEffect = 'move';
237
- } else if (currentState === CANNOT_DROP) {
238
- event.dataTransfer.dropEffect = 'none';
239
- }
240
-
241
- // Notify listeners
242
- onDragOver?.({ event, zone });
243
- }
244
-
245
- /**
246
- * Handle drag leave with improved DOM traversal check
247
- * @param {DragEvent} event
248
- */
249
- function handleDragLeave(event) {
250
- // We need to check if we're actually leaving the drop zone or just
251
- // entering a child element within the drop zone
252
-
253
- // relatedTarget is the element we're moving to
254
- const relatedTarget = event.relatedTarget;
255
-
256
- // If relatedTarget is null or outside our drop zone, we're truly leaving
257
- const isActuallyLeaving =
258
- !relatedTarget || !dropZoneElement.contains(relatedTarget);
259
-
260
- if (isActuallyLeaving) {
261
- isCurrentlyOver = false;
262
- currentState = READY;
263
- onDragLeave?.({ event, zone });
264
- }
265
- }
266
-
267
- /**
268
- * Handle drop
269
- * @param {DragEvent} event
270
- */
271
- function handleDrop(event) {
272
- // Prevent default browser actions
273
- event.preventDefault();
274
-
275
- // Reset our tracking state
276
- isCurrentlyOver = false;
277
-
278
- try {
279
- // Check if this is a file drop first
280
- if (event.dataTransfer.types.includes('Files')) {
281
- // Handle file drops
282
- const files = Array.from(event.dataTransfer.files);
283
- // console.log('Files dropped:', files);
284
-
285
- // You can add custom file handling here
286
- // For now, just reset state and return
287
- currentState = READY;
288
- return;
289
- }
290
-
291
- // Get drag data for internal drag operations
292
- const dragData = getDragData(event);
293
-
294
- // Check if we can accept this drop
295
- if (dragData && canAcceptDrop(dragData)) {
296
- // Notify listener
297
- onDropStart?.({ event, zone, data: dragData });
298
-
299
- const style = window.getComputedStyle(dropZoneElement);
300
-
301
- // Parse border widths from computed style
302
- const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
303
- const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
304
-
305
- // Get drop zone rectangle
306
- const dropZoneRect = dropZoneElement.getBoundingClientRect();
307
-
308
- // Calculate position with both dragData.offsetX/Y adjustment and border adjustment
309
- const dropOffsetX =
310
- event.clientX -
311
- dropZoneRect.left -
312
- borderLeftWidth;
313
-
314
- const dropOffsetY =
315
- event.clientY -
316
- dropZoneRect.top -
317
- borderTopWidth;
318
-
319
- const x = dropOffsetX - (dragData.offsetX ?? 0);
320
- const y = dropOffsetY - (dragData.offsetY ?? 0);
321
-
322
- const dropResult = onDrop?.({
323
- zone,
324
- source: dragData.source,
325
- item: dragData.item,
326
- x,
327
- y,
328
- drag: dragData,
329
- drop: {
330
- offsetX: dropOffsetX,
331
- offsetY: dropOffsetY,
332
- event
333
- }
334
- });
335
-
336
- // Handle async or sync results
337
- Promise.resolve(dropResult)
338
- .then(() => {
339
- currentState = READY;
340
- onDropEnd?.({ event, zone, data: dragData, success: true });
341
- })
342
- .catch((error) => {
343
- currentState = READY;
344
- onDropEnd?.({ event, zone, data: dragData, success: false, error });
345
- });
346
- } else {
347
- // Not a valid drop, reset state
348
- currentState = READY;
349
- }
350
- } catch (error) {
351
- // Handle parsing errors
352
- console.error('Drop error:', error);
353
- currentState = READY;
354
- }
355
- }
197
+ // Monitor drag state to update preview
198
+ let showPreview = $derived(
199
+ dragState.activeDropZone === dropZoneId &&
200
+ currentState === CAN_DROP &&
201
+ dropPreviewSnippet
202
+ );
356
203
  </script>
357
204
 
358
205
  <div
359
206
  data-component="drop-zone"
360
207
  bind:this={dropZoneElement}
361
- ondragenter={handleDragEnter}
362
- ondragover={handleDragOver}
363
- ondragleave={handleDragLeave}
364
- ondrop={handleDrop}
365
- class="{base} {height} {classes} {stateClasses}"
208
+ class="{base} {heightClasses} {classes} {stateClasses}"
366
209
  data-zone={zone}
367
210
  {...attrs}
368
211
  >
369
- <GridLayers heightFrom={autoHeight ? 'content' : null}>
212
+ <GridLayers heightFrom={heightMode === 'flexible' ? 'content' : null}>
370
213
  {#if children}
371
- <div data-layer="content" class:auto-height={autoHeight}>
214
+ <div
215
+ data-layer="content"
216
+ class:relative={heightMode === 'flexible'}
217
+ class:w-full={heightMode === 'flexible'}
218
+ >
372
219
  {@render children()}
373
220
  </div>
374
221
  {/if}
375
222
 
376
- {#if currentState === CAN_DROP && dropPreviewSnippet}
223
+ {#if showPreview && dropPreviewSnippet}
377
224
  <div data-layer="preview">
378
225
  {@render dropPreviewSnippet(dragState.current)}
379
226
  </div>
@@ -382,7 +229,11 @@
382
229
  </div>
383
230
 
384
231
  <style>
385
- [data-layer='content']:not(.auto-height) {
232
+ [data-component="drop-zone"] {
233
+ -webkit-tap-highlight-color: transparent;
234
+ }
235
+
236
+ [data-layer='content']:not(.relative) {
386
237
  position: absolute;
387
238
  left: 0;
388
239
  right: 0;
@@ -390,7 +241,7 @@
390
241
  bottom: 0;
391
242
  }
392
243
 
393
- [data-layer='content'].auto-height {
244
+ [data-layer='content'].relative {
394
245
  position: relative;
395
246
  width: 100%;
396
247
  }
@@ -9,8 +9,9 @@ type DropZone = {
9
9
  accepts?: (item: any) => boolean;
10
10
  base?: string;
11
11
  classes?: string;
12
- height?: string;
13
- autoHeight?: boolean;
12
+ minHeight?: string;
13
+ maxHeight?: string;
14
+ heightMode?: "fill" | "fixed" | "flexible";
14
15
  children?: Snippet<[]>;
15
16
  contextKey?: ContextKey;
16
17
  dropPreviewSnippet?: Snippet<[DragData]>;
@@ -52,8 +53,9 @@ declare const DropZone: import("svelte").Component<{
52
53
  accepts?: (item: any) => boolean;
53
54
  base?: string;
54
55
  classes?: string;
55
- height?: string;
56
- autoHeight?: boolean;
56
+ minHeight?: string;
57
+ maxHeight?: string;
58
+ heightMode?: "fixed" | "flexible" | "fill";
57
59
  children?: import("svelte").Snippet;
58
60
  contextKey?: import("../../typedef").ContextKey;
59
61
  dropPreviewSnippet?: import("svelte").Snippet<[import("../../typedef").DragData]>;
@@ -0,0 +1,119 @@
1
+ <script>
2
+ import { DropZone } from './index.js';
3
+
4
+ /**
5
+ * @type {{
6
+ * zone?: string,
7
+ * group?: string,
8
+ * disabled?: boolean,
9
+ * accepts?: (item: any) => boolean,
10
+ * fillContainer?: boolean,
11
+ * aspectRatio?: string,
12
+ * overflow?: 'auto' | 'hidden' | 'visible' | 'scroll',
13
+ * base?: string,
14
+ * classes?: string,
15
+ * children?: import('svelte').Snippet,
16
+ * contextKey?: import('../../typedef').ContextKey,
17
+ * dropPreviewSnippet?: import('svelte').Snippet<[import('../../typedef').DragData]>,
18
+ * isDragOver?: boolean,
19
+ * canDrop?: boolean,
20
+ * onDragEnter?: (detail: {
21
+ * event: DragEvent,
22
+ * zone: string,
23
+ * canDrop: boolean
24
+ * }) => void,
25
+ * onDragOver?: (detail: {
26
+ * event: DragEvent,
27
+ * zone: string
28
+ * }) => void,
29
+ * onDragLeave?: (detail: {
30
+ * event: DragEvent,
31
+ * zone: string
32
+ * }) => void,
33
+ * onDrop?: (detail: import('../../typedef').DropData) => any | Promise<any>,
34
+ * onDropStart?: (detail: {
35
+ * event: DragEvent,
36
+ * zone: string,
37
+ * data: any
38
+ * }) => void,
39
+ * onDropEnd?: (detail: {
40
+ * event: DragEvent,
41
+ * zone: string,
42
+ * data: any,
43
+ * success: boolean,
44
+ * error?: Error
45
+ * }) => void,
46
+ * [key: string]: any
47
+ * }}
48
+ */
49
+ let {
50
+ zone = 'default',
51
+ group = 'default',
52
+ disabled = false,
53
+ accepts = () => true,
54
+ fillContainer = true,
55
+ aspectRatio = '',
56
+ overflow = 'hidden',
57
+ base = '',
58
+ classes = '',
59
+ children,
60
+ contextKey,
61
+ dropPreviewSnippet,
62
+ isDragOver = $bindable(false),
63
+ canDrop = $bindable(false),
64
+ onDragEnter,
65
+ onDragOver,
66
+ onDragLeave,
67
+ onDrop,
68
+ onDropStart,
69
+ onDropEnd,
70
+ ...attrs
71
+ } = $props();
72
+
73
+ // Build overflow classes based on prop
74
+ let overflowClasses = $derived.by(() => {
75
+ switch (overflow) {
76
+ case 'auto':
77
+ return 'overflow-auto';
78
+ case 'scroll':
79
+ return 'overflow-scroll';
80
+ case 'visible':
81
+ return 'overflow-visible';
82
+ case 'hidden':
83
+ default:
84
+ return 'overflow-hidden';
85
+ }
86
+ });
87
+
88
+ // Combine all classes for the drop zone
89
+ let combinedClasses = $derived(
90
+ `${overflowClasses} ${aspectRatio} ${classes}`.trim()
91
+ );
92
+ </script>
93
+
94
+ <DropZone
95
+ data-component="drop-zone"
96
+ data-type="area"
97
+ {zone}
98
+ {group}
99
+ {disabled}
100
+ {accepts}
101
+ heightMode={fillContainer ? 'fill' : 'fixed'}
102
+ {base}
103
+ classes={combinedClasses}
104
+ {contextKey}
105
+ {dropPreviewSnippet}
106
+ bind:isDragOver
107
+ bind:canDrop
108
+ {onDragEnter}
109
+ {onDragOver}
110
+ {onDragLeave}
111
+ {onDrop}
112
+ {onDropStart}
113
+ {onDropEnd}
114
+ {...attrs}
115
+ >
116
+ {#if children}
117
+ {@render children()}
118
+ {/if}
119
+ </DropZone>
@@ -0,0 +1,90 @@
1
+ export default DropZoneArea;
2
+ type DropZoneArea = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<{
5
+ [key: string]: any;
6
+ zone?: string;
7
+ group?: string;
8
+ disabled?: boolean;
9
+ accepts?: (item: any) => boolean;
10
+ fillContainer?: boolean;
11
+ aspectRatio?: string;
12
+ overflow?: "scroll" | "auto" | "hidden" | "visible";
13
+ base?: string;
14
+ classes?: string;
15
+ children?: Snippet<[]>;
16
+ contextKey?: ContextKey;
17
+ dropPreviewSnippet?: Snippet<[DragData]>;
18
+ isDragOver?: boolean;
19
+ canDrop?: boolean;
20
+ onDragEnter?: (detail: {
21
+ event: DragEvent;
22
+ zone: string;
23
+ canDrop: boolean;
24
+ }) => void;
25
+ onDragOver?: (detail: {
26
+ event: DragEvent;
27
+ zone: string;
28
+ }) => void;
29
+ onDragLeave?: (detail: {
30
+ event: DragEvent;
31
+ zone: string;
32
+ }) => void;
33
+ onDrop?: (detail: DropData) => any;
34
+ onDropStart?: (detail: {
35
+ event: DragEvent;
36
+ zone: string;
37
+ data: any;
38
+ }) => void;
39
+ onDropEnd?: (detail: {
40
+ event: DragEvent;
41
+ zone: string;
42
+ data: any;
43
+ success: boolean;
44
+ error?: Error;
45
+ }) => void;
46
+ }>): void;
47
+ };
48
+ declare const DropZoneArea: import("svelte").Component<{
49
+ [key: string]: any;
50
+ zone?: string;
51
+ group?: string;
52
+ disabled?: boolean;
53
+ accepts?: (item: any) => boolean;
54
+ fillContainer?: boolean;
55
+ aspectRatio?: string;
56
+ overflow?: "auto" | "hidden" | "visible" | "scroll";
57
+ base?: string;
58
+ classes?: string;
59
+ children?: import("svelte").Snippet;
60
+ contextKey?: import("../../typedef").ContextKey;
61
+ dropPreviewSnippet?: import("svelte").Snippet<[import("../../typedef").DragData]>;
62
+ isDragOver?: boolean;
63
+ canDrop?: boolean;
64
+ onDragEnter?: (detail: {
65
+ event: DragEvent;
66
+ zone: string;
67
+ canDrop: boolean;
68
+ }) => void;
69
+ onDragOver?: (detail: {
70
+ event: DragEvent;
71
+ zone: string;
72
+ }) => void;
73
+ onDragLeave?: (detail: {
74
+ event: DragEvent;
75
+ zone: string;
76
+ }) => void;
77
+ onDrop?: (detail: import("../../typedef").DropData) => any | Promise<any>;
78
+ onDropStart?: (detail: {
79
+ event: DragEvent;
80
+ zone: string;
81
+ data: any;
82
+ }) => void;
83
+ onDropEnd?: (detail: {
84
+ event: DragEvent;
85
+ zone: string;
86
+ data: any;
87
+ success: boolean;
88
+ error?: Error;
89
+ }) => void;
90
+ }, {}, "isDragOver" | "canDrop">;