@hkdigital/lib-sveltekit 0.1.80 → 0.1.82

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,7 +1,8 @@
1
1
  /**
2
- * Controller for customizing drag preview images
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
3
4
  */
4
- export class PreviewController {
5
+ export class DragController {
5
6
  /**
6
7
  * @param {DragEvent} event - The drag event
7
8
  */
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Controller for customizing drag preview images
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
3
4
  */
4
- export class PreviewController {
5
+ export class DragController {
5
6
  /**
6
7
  * @param {DragEvent} event - The drag event
7
8
  */
@@ -1,12 +1,9 @@
1
1
  <script>
2
2
  import { toStateClasses } from '../../util/design-system/index.js';
3
-
4
3
  import { createOrGetDragState } from './drag-state.svelte.js';
5
-
6
- import { PreviewController } from './PreviewController.js';
7
-
4
+ import { DragController } from './DragController.js';
8
5
  import { generateLocalId } from '../../util/unique';
9
-
6
+ import { onDestroy } from 'svelte';
10
7
  import {
11
8
  IDLE,
12
9
  DRAGGING,
@@ -25,6 +22,10 @@
25
22
  * base?: string,
26
23
  * classes?: string,
27
24
  * children: import('svelte').Snippet,
25
+ * draggingSnippet?: import('svelte').Snippet<[{
26
+ * element: HTMLElement,
27
+ * rect: DOMRect
28
+ * }]>,
28
29
  * contextKey?: import('../../typedef').ContextKey,
29
30
  * isDragging?: boolean,
30
31
  * isDropping?: boolean,
@@ -34,7 +35,7 @@
34
35
  * item: any,
35
36
  * source: string,
36
37
  * group: string,
37
- * getPreviewController: () => PreviewController
38
+ * getController: () => DragController
38
39
  * }) => void,
39
40
  * onDragging?: (detail: {
40
41
  * event: DragEvent,
@@ -63,6 +64,7 @@
63
64
  base = '',
64
65
  classes = '',
65
66
  children,
67
+ draggingSnippet,
66
68
  contextKey,
67
69
  isDragging = $bindable(false),
68
70
  isDropping = $bindable(false),
@@ -79,11 +81,21 @@
79
81
 
80
82
  const draggableId = generateLocalId();
81
83
 
82
- // console.debug('Draggable contextKey:', contextKey);
84
+ // svelte-ignore non_reactive_update
85
+ let draggableElement;
83
86
 
84
87
  let dragTimeout = null;
85
88
  let currentState = $state(IDLE);
86
89
 
90
+ // Custom preview follower state
91
+ let showPreview = $state(false);
92
+ let previewX = $state(0);
93
+ let previewY = $state(0);
94
+ let dragOffsetX = $state(0);
95
+ let dragOffsetY = $state(0);
96
+ let customPreviewSet = $state(false);
97
+ let elementRect = $state(null);
98
+
87
99
  // Computed state object for CSS classes
88
100
  let stateObject = $derived({
89
101
  idle: currentState === IDLE,
@@ -102,6 +114,28 @@
102
114
  isDragPreview = currentState === DRAG_PREVIEW;
103
115
  });
104
116
 
117
+ // Clean up on component destroy
118
+ onDestroy(() => {
119
+ if (showPreview) {
120
+ document.removeEventListener('dragover', handleDocumentDragOver);
121
+ }
122
+ });
123
+
124
+ /**
125
+ * Handle document level dragover to ensure we get position updates
126
+ * @param {DragEvent} event
127
+ */
128
+ function handleDocumentDragOver(event) {
129
+ if (showPreview && currentState === DRAGGING) {
130
+ // Update position for the custom preview
131
+ previewX = event.clientX - dragOffsetX;
132
+ previewY = event.clientY - dragOffsetY;
133
+
134
+ // Prevent default to allow drop
135
+ event.preventDefault();
136
+ }
137
+ }
138
+
105
139
  /**
106
140
  * Handle drag start
107
141
  * @param {DragEvent} event
@@ -128,40 +162,81 @@
128
162
  startDrag(event);
129
163
  }
130
164
 
131
- /**
132
- * Start the drag operation
133
- * @param {DragEvent} event
134
- */
135
- function startDrag(event) {
136
- const dragData = {
137
- item,
138
- source,
139
- group,
140
- metadata: { timestamp: Date.now() }
141
- };
165
+ /**
166
+ * Start the drag operation
167
+ * @param {DragEvent} event - The drag event
168
+ */
169
+ function startDrag(event) {
170
+ // Get the element's bounding rectangle
171
+ const rect = draggableElement.getBoundingClientRect();
172
+
173
+ // Calculate grab offsets - this is where the user grabbed the element
174
+ dragOffsetX = event.clientX - rect.left;
175
+ dragOffsetY = event.clientY - rect.top;
176
+
177
+ // Create drag data with your preferred structure
178
+ const dragData = {
179
+ offsetX: dragOffsetX,
180
+ offsetY: dragOffsetY,
181
+ item,
182
+ source,
183
+ group,
184
+ metadata: {
185
+ timestamp: Date.now()
186
+ }
187
+ };
188
+
189
+ // Set shared drag state
190
+ dragState.start(draggableId, dragData);
191
+
192
+ // Set data transfer for browser drag and drop API
193
+ event.dataTransfer.effectAllowed = 'move';
194
+ event.dataTransfer.setData('application/json', JSON.stringify(dragData));
142
195
 
143
- // Set shared drag state
144
- dragState.start(draggableId, dragData);
196
+ // Create the preview controller
197
+ const previewController = new DragController(event);
145
198
 
146
- event.dataTransfer.effectAllowed = 'move';
147
- event.dataTransfer.setData('application/json', JSON.stringify(dragData));
199
+ // Function to get the preview controller
200
+ const getController = () => previewController;
148
201
 
149
- // Create the preview controller with natural offsets already calculated
150
- const previewController = new PreviewController(event);
202
+ // Call onDragStart with the getController function
203
+ onDragStart?.({ event, item, source, group, getController });
151
204
 
152
- // Function to get the preview controller
153
- const getPreviewController = () => previewController;
205
+ // Apply drag preview if available
206
+ if (draggingSnippet && !previewController.hasCustomPreview()) {
207
+ try {
208
+ // Store rectangle information for the snippet
209
+ elementRect = rect;
154
210
 
155
- // Call onDragStart with the getPreviewController function
156
- onDragStart?.({ event, item, source, group, getPreviewController });
211
+ // These offsets represent where the user grabbed the element relative to its top-left corner
212
+ dragOffsetX = event.clientX - rect.left;
213
+ dragOffsetY = event.clientY - rect.top;
157
214
 
158
- // Apply default preview if no custom one was set
159
- if (!previewController.hasCustomPreview()) {
215
+ // Set initial position - this places the preview at the element's original position
216
+ previewX = rect.left;
217
+ previewY = rect.top;
218
+
219
+ // Set a transparent 1x1 pixel image to hide browser's default preview
220
+ const emptyImg = new Image();
221
+ emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
222
+ event.dataTransfer.setDragImage(emptyImg, 0, 0);
223
+
224
+ // Add document level event listener to track mouse movement
225
+ document.addEventListener('dragover', handleDocumentDragOver);
226
+
227
+ // Show custom preview
228
+ showPreview = true;
229
+ customPreviewSet = true;
230
+ } catch (err) {
231
+ console.error('Error setting up custom preview:', err);
232
+ // Fallback to default preview
160
233
  previewController.applyDefaultPreview();
161
234
  }
162
-
163
- // Additional handlers can be left unchanged
235
+ } else if (!previewController.hasCustomPreview()) {
236
+ // Apply default preview if no custom preview was set
237
+ previewController.applyDefaultPreview();
164
238
  }
239
+ }
165
240
 
166
241
  /**
167
242
  * Handle during drag
@@ -183,6 +258,14 @@
183
258
  // Clear global drag state
184
259
  dragState.end(draggableId);
185
260
 
261
+ // Clean up document event listener
262
+ if (customPreviewSet) {
263
+ document.removeEventListener('dragover', handleDocumentDragOver);
264
+ showPreview = false;
265
+ customPreviewSet = false;
266
+ elementRect = null;
267
+ }
268
+
186
269
  // Check if drop was successful
187
270
  const wasDropped = event.dataTransfer.dropEffect !== 'none';
188
271
 
@@ -226,6 +309,7 @@
226
309
  <div
227
310
  data-component="draggable"
228
311
  data-id={draggableId}
312
+ bind:this={draggableElement}
229
313
  draggable={!disabled && canDrag(item)}
230
314
  ondragstart={handleDragStart}
231
315
  ondrag={handleDrag}
@@ -237,3 +321,14 @@
237
321
  >
238
322
  {@render children()}
239
323
  </div>
324
+
325
+ {#if draggingSnippet && showPreview && elementRect}
326
+ <div
327
+ data-companion="drag-preview-follower"
328
+ style="position: fixed; z-index: 9999; pointer-events: none;"
329
+ style:left="{previewX}px"
330
+ style:top="{previewY}px"
331
+ >
332
+ {@render draggingSnippet({ element: draggableElement, rect: elementRect })}
333
+ </div>
334
+ {/if}
@@ -11,6 +11,10 @@ type Draggable = {
11
11
  base?: string;
12
12
  classes?: string;
13
13
  children: Snippet<[]>;
14
+ draggingSnippet?: Snippet<[{
15
+ element: HTMLElement;
16
+ rect: DOMRect;
17
+ }]>;
14
18
  contextKey?: ContextKey;
15
19
  isDragging?: boolean;
16
20
  isDropping?: boolean;
@@ -20,7 +24,7 @@ type Draggable = {
20
24
  item: any;
21
25
  source: string;
22
26
  group: string;
23
- getPreviewController: () => PreviewController;
27
+ getController: () => DragController;
24
28
  }) => void;
25
29
  onDragging?: (detail: {
26
30
  event: DragEvent;
@@ -49,6 +53,10 @@ declare const Draggable: import("svelte").Component<{
49
53
  base?: string;
50
54
  classes?: string;
51
55
  children: import("svelte").Snippet;
56
+ draggingSnippet?: import("svelte").Snippet<[{
57
+ element: HTMLElement;
58
+ rect: DOMRect;
59
+ }]>;
52
60
  contextKey?: import("../../typedef").ContextKey;
53
61
  isDragging?: boolean;
54
62
  isDropping?: boolean;
@@ -58,7 +66,7 @@ declare const Draggable: import("svelte").Component<{
58
66
  item: any;
59
67
  source: string;
60
68
  group: string;
61
- getPreviewController: () => PreviewController;
69
+ getController: () => DragController;
62
70
  }) => void;
63
71
  onDragging?: (detail: {
64
72
  event: DragEvent;
@@ -76,4 +84,4 @@ declare const Draggable: import("svelte").Component<{
76
84
  }) => void;
77
85
  canDrag?: (item: any) => boolean;
78
86
  }, {}, "isDragging" | "isDropping" | "isDragPreview">;
79
- import { PreviewController } from './PreviewController.js';
87
+ import { DragController } from './DragController.js';
@@ -4,10 +4,12 @@
4
4
 
5
5
  import { createOrGetDragState } from './drag-state.svelte.js';
6
6
 
7
+ import { GridLayers } from '../layout';
8
+
7
9
  import {
8
- findDraggableSource,
10
+ // findDraggableSource,
9
11
  getDraggableIdFromEvent,
10
- processDropWithData
12
+ // processDropWithData
11
13
  } from './util.js';
12
14
 
13
15
  import {
@@ -15,32 +17,27 @@
15
17
  DRAG_OVER,
16
18
  CAN_DROP,
17
19
  CANNOT_DROP,
18
- DROP_DISABLED,
19
- ACTIVE_DROP
20
+ // DROP_DISABLED
20
21
  } from '../../constants/state-labels/drop-states.js';
21
22
 
23
+ /** @typedef {import('../../typedef').DragData} DragData */
24
+ /** @typedef {import('../../typedef').DropData} DropData */
25
+
22
26
  /**
23
27
  * @type {{
24
28
  * zone?: string,
25
29
  * group?: string,
26
30
  * disabled?: boolean,
27
31
  * accepts?: (item: any) => boolean,
28
- * maxItems?: number,
29
32
  * base?: string,
30
33
  * classes?: string,
34
+ * height?: string,
35
+ * autoHeight?: boolean,
31
36
  * children?: import('svelte').Snippet,
32
37
  * contextKey?: import('../../typedef').ContextKey,
33
- * empty?: import('svelte').Snippet,
34
- * preview?: import('svelte').Snippet<[{
35
- * item: any,
36
- * source: string,
37
- * group: string,
38
- * metadata?: any
39
- * }]>,
38
+ * dropPreviewSnippet?: import('svelte').Snippet<[DragData]>,
40
39
  * isDragOver?: boolean,
41
40
  * canDrop?: boolean,
42
- * isDropping?: boolean,
43
- * itemCount?: number,
44
41
  * onDragEnter?: (detail: {
45
42
  * event: DragEvent,
46
43
  * zone: string,
@@ -54,13 +51,7 @@
54
51
  * event: DragEvent,
55
52
  * zone: string
56
53
  * }) => void,
57
- * onDrop?: (detail: {
58
- * event: DragEvent,
59
- * zone: string,
60
- * item: any,
61
- * source: string,
62
- * metadata?: any
63
- * }) => any | Promise<any>,
54
+ * onDrop?: (detail: DropData) => any | Promise<any>,
64
55
  * onDropStart?: (detail: {
65
56
  * event: DragEvent,
66
57
  * zone: string,
@@ -81,17 +72,15 @@
81
72
  group = 'default',
82
73
  disabled = false,
83
74
  accepts = () => true,
84
- maxItems = Infinity,
85
75
  base = '',
86
76
  classes = '',
77
+ height = 'h-min',
78
+ autoHeight= false,
87
79
  children,
88
80
  contextKey,
89
- empty,
90
- preview,
81
+ dropPreviewSnippet,
91
82
  isDragOver = $bindable(false),
92
83
  canDrop = $bindable(false),
93
- isDropping = $bindable(false),
94
- itemCount = $bindable(0),
95
84
  onDragEnter,
96
85
  onDragOver,
97
86
  onDragLeave,
@@ -140,8 +129,7 @@
140
129
  'drag-over': currentState === DRAG_OVER,
141
130
  'can-drop': currentState === CAN_DROP,
142
131
  'cannot-drop': currentState === CANNOT_DROP,
143
- 'drop-disabled': disabled,
144
- 'active-drop': currentState === ACTIVE_DROP
132
+ 'drop-disabled': disabled
145
133
  });
146
134
 
147
135
  let stateClasses = $derived(toStateClasses(stateObject));
@@ -155,7 +143,6 @@
155
143
  ].includes(currentState);
156
144
 
157
145
  canDrop = currentState === CAN_DROP;
158
- isDropping = currentState === ACTIVE_DROP;
159
146
  });
160
147
 
161
148
  /**
@@ -172,7 +159,6 @@
172
159
  if (!data) return false;
173
160
  if (data.group !== group) return false;
174
161
  if (!accepts(data.item)) return false;
175
- if (itemCount >= maxItems) return false;
176
162
  return true;
177
163
  }
178
164
 
@@ -321,13 +307,30 @@ function handleDrop(event) {
321
307
 
322
308
  // Check if we can accept this drop
323
309
  if (dragData && canAcceptDrop(dragData)) {
324
- // Update state and notify listeners
325
- currentState = ACTIVE_DROP;
310
+ // Notify listener
326
311
  onDropStart?.({ event, zone, data: dragData });
327
312
 
328
- // Call the onDrop handler and handle Promise resolution
313
+ const style = window.getComputedStyle(dropzoneElement);
314
+
315
+ // Parse border widths from computed style
316
+ const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
317
+ const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
318
+
319
+ // Get dropzone rectangle
320
+ const dropzoneRect = dropzoneElement.getBoundingClientRect();
321
+
322
+ // Calculate position with both dragData.offsetX/Y adjustment and border adjustment
323
+ // This combines your current approach with the border adjustment
324
+ const offsetX = event.clientX - dropzoneRect.left - borderLeftWidth - (dragData.offsetX ?? 0);
325
+
326
+ const offsetY = event.clientY - dropzoneRect.top - borderTopWidth - (dragData.offsetY ?? 0);
327
+
328
+ // console.debug("dragData", dragData);
329
+
329
330
  const dropResult = onDrop?.({
330
331
  event,
332
+ offsetX,
333
+ offsetY,
331
334
  zone,
332
335
  item: dragData.item,
333
336
  source: dragData.source,
@@ -361,17 +364,45 @@ function handleDrop(event) {
361
364
  ondragover={handleDragOver}
362
365
  ondragleave={handleDragLeave}
363
366
  ondrop={handleDrop}
364
- class="{base} {classes} {stateClasses}"
367
+ class="{base} {height} {classes} {stateClasses}"
365
368
  data-zone={zone}
366
369
  {...attrs}
367
370
  >
368
- {#if children}
369
- {@render children()}
370
- {:else if currentState === CAN_DROP && preview}
371
- {@render preview(dragState.current)}
372
- {:else if itemCount === 0 && empty}
373
- {@render empty()}
374
- {:else}
375
- <div data-element="drop-zone-empty">Drop items here</div>
376
- {/if}
371
+ <GridLayers heightFrom={autoHeight ? 'content' : null}>
372
+ {#if children}
373
+ <div data-layer="content" class:auto-height={autoHeight}>
374
+ {@render children()}
375
+ </div>
376
+ {/if}
377
+
378
+ {#if currentState === CAN_DROP && dropPreviewSnippet}
379
+ <div data-layer="preview">
380
+ {@render dropPreviewSnippet(dragState.current)}
381
+ </div>
382
+ {/if}
383
+ </GridLayers>
377
384
  </div>
385
+
386
+ <style>
387
+ [data-layer='content']:not(.auto-height) {
388
+ position: absolute;
389
+ left: 0;
390
+ right: 0;
391
+ top: 0;
392
+ bottom: 0;
393
+ }
394
+
395
+ [data-layer='content'].auto-height {
396
+ position: relative;
397
+ width: 100%;
398
+ }
399
+
400
+ [data-layer='preview'] {
401
+ position: absolute;
402
+ left: 0;
403
+ right: 0;
404
+ top: 0;
405
+ bottom: 0;
406
+ pointer-events: none;
407
+ }
408
+ </style>
@@ -7,22 +7,15 @@ type DropZone = {
7
7
  group?: string;
8
8
  disabled?: boolean;
9
9
  accepts?: (item: any) => boolean;
10
- maxItems?: number;
11
10
  base?: string;
12
11
  classes?: string;
12
+ height?: string;
13
+ autoHeight?: boolean;
13
14
  children?: Snippet<[]>;
14
15
  contextKey?: ContextKey;
15
- empty?: Snippet<[]>;
16
- preview?: Snippet<[{
17
- item: any;
18
- source: string;
19
- group: string;
20
- metadata?: any;
21
- }]>;
16
+ dropPreviewSnippet?: Snippet<[DragData]>;
22
17
  isDragOver?: boolean;
23
18
  canDrop?: boolean;
24
- isDropping?: boolean;
25
- itemCount?: number;
26
19
  onDragEnter?: (detail: {
27
20
  event: DragEvent;
28
21
  zone: string;
@@ -36,13 +29,7 @@ type DropZone = {
36
29
  event: DragEvent;
37
30
  zone: string;
38
31
  }) => void;
39
- onDrop?: (detail: {
40
- event: DragEvent;
41
- zone: string;
42
- item: any;
43
- source: string;
44
- metadata?: any;
45
- }) => any;
32
+ onDrop?: (detail: DropData) => any;
46
33
  onDropStart?: (detail: {
47
34
  event: DragEvent;
48
35
  zone: string;
@@ -63,22 +50,15 @@ declare const DropZone: import("svelte").Component<{
63
50
  group?: string;
64
51
  disabled?: boolean;
65
52
  accepts?: (item: any) => boolean;
66
- maxItems?: number;
67
53
  base?: string;
68
54
  classes?: string;
55
+ height?: string;
56
+ autoHeight?: boolean;
69
57
  children?: import("svelte").Snippet;
70
58
  contextKey?: import("../../typedef").ContextKey;
71
- empty?: import("svelte").Snippet;
72
- preview?: import("svelte").Snippet<[{
73
- item: any;
74
- source: string;
75
- group: string;
76
- metadata?: any;
77
- }]>;
59
+ dropPreviewSnippet?: import("svelte").Snippet<[import("../../typedef").DragData]>;
78
60
  isDragOver?: boolean;
79
61
  canDrop?: boolean;
80
- isDropping?: boolean;
81
- itemCount?: number;
82
62
  onDragEnter?: (detail: {
83
63
  event: DragEvent;
84
64
  zone: string;
@@ -92,13 +72,7 @@ declare const DropZone: import("svelte").Component<{
92
72
  event: DragEvent;
93
73
  zone: string;
94
74
  }) => void;
95
- onDrop?: (detail: {
96
- event: DragEvent;
97
- zone: string;
98
- item: any;
99
- source: string;
100
- metadata?: any;
101
- }) => any | Promise<any>;
75
+ onDrop?: (detail: import("../../typedef").DropData) => any | Promise<any>;
102
76
  onDropStart?: (detail: {
103
77
  event: DragEvent;
104
78
  zone: string;
@@ -111,4 +85,4 @@ declare const DropZone: import("svelte").Component<{
111
85
  success: boolean;
112
86
  error?: Error;
113
87
  }) => void;
114
- }, {}, "isDropping" | "isDragOver" | "canDrop" | "itemCount">;
88
+ }, {}, "isDragOver" | "canDrop">;