@hkdigital/lib-sveltekit 0.1.79 → 0.1.81

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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
4
+ */
5
+ export class DragController {
6
+ /**
7
+ * @param {DragEvent} event - The drag event
8
+ */
9
+ constructor(event: DragEvent);
10
+ event: DragEvent;
11
+ dataTransfer: DataTransfer;
12
+ targetElement: HTMLElement;
13
+ offsetX: number;
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
+ /**
36
+ * Apply the default preview (uses the draggable element itself)
37
+ * @returns {boolean}
38
+ */
39
+ applyDefaultPreview(): boolean;
40
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
4
+ */
5
+ export class DragController {
6
+ /**
7
+ * @param {DragEvent} event - The drag event
8
+ */
9
+ constructor(event) {
10
+ this.event = event;
11
+ this.dataTransfer = event.dataTransfer;
12
+ this.targetElement = /** @type {HTMLElement} */ (event.currentTarget);
13
+
14
+ // Calculate natural offsets by default (to prevent "jumping")
15
+ const rect = this.targetElement.getBoundingClientRect();
16
+ this.offsetX = event.clientX - rect.left;
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
+ }
87
+
88
+ /**
89
+ * Apply the default preview (uses the draggable element itself)
90
+ * @returns {boolean}
91
+ */
92
+ applyDefaultPreview() {
93
+ if (
94
+ !this.dataTransfer ||
95
+ !this.dataTransfer.setDragImage ||
96
+ this._previewSet
97
+ ) {
98
+ return false;
99
+ }
100
+
101
+ try {
102
+ this.dataTransfer.setDragImage(
103
+ this.targetElement,
104
+ this.offsetX,
105
+ this.offsetY
106
+ );
107
+ return true;
108
+ } catch (err) {
109
+ console.error('Failed to set default drag preview:', err);
110
+ return false;
111
+ }
112
+ }
113
+ }
@@ -1,10 +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
-
4
+ import { DragController } from './DragController.js';
6
5
  import { generateLocalId } from '../../util/unique';
7
-
6
+ import { onDestroy } from 'svelte';
8
7
  import {
9
8
  IDLE,
10
9
  DRAGGING,
@@ -23,6 +22,10 @@
23
22
  * base?: string,
24
23
  * classes?: string,
25
24
  * children: import('svelte').Snippet,
25
+ * draggingSnippet?: import('svelte').Snippet<[{
26
+ * element: HTMLElement,
27
+ * rect: DOMRect
28
+ * }]>,
26
29
  * contextKey?: import('../../typedef').ContextKey,
27
30
  * isDragging?: boolean,
28
31
  * isDropping?: boolean,
@@ -31,7 +34,8 @@
31
34
  * event: DragEvent,
32
35
  * item: any,
33
36
  * source: string,
34
- * group: string
37
+ * group: string,
38
+ * getController: () => DragController
35
39
  * }) => void,
36
40
  * onDragging?: (detail: {
37
41
  * event: DragEvent,
@@ -60,6 +64,7 @@
60
64
  base = '',
61
65
  classes = '',
62
66
  children,
67
+ draggingSnippet,
63
68
  contextKey,
64
69
  isDragging = $bindable(false),
65
70
  isDropping = $bindable(false),
@@ -76,11 +81,21 @@
76
81
 
77
82
  const draggableId = generateLocalId();
78
83
 
79
- // console.debug('Draggable contextKey:', contextKey);
84
+ // svelte-ignore non_reactive_update
85
+ let draggableElement;
80
86
 
81
87
  let dragTimeout = null;
82
88
  let currentState = $state(IDLE);
83
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
+
84
99
  // Computed state object for CSS classes
85
100
  let stateObject = $derived({
86
101
  idle: currentState === IDLE,
@@ -99,6 +114,28 @@
99
114
  isDragPreview = currentState === DRAG_PREVIEW;
100
115
  });
101
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
+
102
139
  /**
103
140
  * Handle drag start
104
141
  * @param {DragEvent} event
@@ -143,24 +180,51 @@
143
180
  event.dataTransfer.effectAllowed = 'move';
144
181
  event.dataTransfer.setData('application/json', JSON.stringify(dragData));
145
182
 
146
- // Set custom drag image if needed
147
- if (event.dataTransfer.setDragImage) {
148
- // Custom drag image
149
- const dragEl = /** @type {HTMLElement} */ (event.currentTarget);
150
-
151
-
152
- // Get the bounding rectangle of the element
153
- const rect = dragEl.getBoundingClientRect();
154
-
155
- // Calculate offsets relative to the element's top-left corner
156
- const offsetX = event.clientX - rect.left;
157
- const offsetY = event.clientY - rect.top;
158
-
159
- // Use the element as drag image with calculated offsets
160
- event.dataTransfer.setDragImage(dragEl, offsetX, offsetY);
183
+ // Create the preview controller with natural offsets already calculated
184
+ const previewController = new DragController(event);
185
+
186
+ // Function to get the preview controller
187
+ const getController = () => previewController;
188
+
189
+ // Call onDragStart with the getController function
190
+ onDragStart?.({ event, item, source, group, getController });
191
+
192
+ // Check if we have a preview snippet and no custom preview was set by preview controller
193
+ if (draggingSnippet && !previewController.hasCustomPreview()) {
194
+ try {
195
+ // Get the element's bounding rectangle
196
+ const rect = draggableElement.getBoundingClientRect();
197
+ elementRect = rect;
198
+
199
+ // Calculate offsets - this is the natural position where the user grabbed
200
+ dragOffsetX = event.clientX - rect.left;
201
+ dragOffsetY = event.clientY - rect.top;
202
+
203
+ // Set initial position
204
+ previewX = event.clientX - dragOffsetX;
205
+ previewY = event.clientY - dragOffsetY;
206
+
207
+ // Set a transparent 1x1 pixel image as drag preview to hide browser preview
208
+ const emptyImg = new Image();
209
+ emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
210
+ event.dataTransfer.setDragImage(emptyImg, 0, 0);
211
+
212
+ // Add document level event listener to catch all dragover events
213
+ document.addEventListener('dragover', handleDocumentDragOver);
214
+
215
+ // Show our custom preview
216
+ showPreview = true;
217
+ customPreviewSet = true;
218
+ } catch (err) {
219
+ console.error('Error setting up custom preview:', err);
220
+ // Fallback to default preview
221
+ previewController.applyDefaultPreview();
222
+ }
223
+ }
224
+ // Apply default preview if no custom preview was set and no snippet
225
+ else if (!previewController.hasCustomPreview() && !customPreviewSet) {
226
+ previewController.applyDefaultPreview();
161
227
  }
162
-
163
- onDragStart?.({ event, item, source, group });
164
228
  }
165
229
 
166
230
  /**
@@ -173,33 +237,41 @@
173
237
  }
174
238
  }
175
239
 
176
- /**
177
- * Handle drag end
178
- * @param {DragEvent} event
179
- */
180
- function handleDragEnd(event) {
181
- clearTimeout(dragTimeout);
182
-
183
- // Clear global drag state
184
- dragState.end(draggableId);
240
+ /**
241
+ * Handle drag end
242
+ * @param {DragEvent} event
243
+ */
244
+ function handleDragEnd(event) {
245
+ clearTimeout(dragTimeout);
246
+
247
+ // Clear global drag state
248
+ dragState.end(draggableId);
249
+
250
+ // Clean up document event listener
251
+ if (customPreviewSet) {
252
+ document.removeEventListener('dragover', handleDocumentDragOver);
253
+ showPreview = false;
254
+ customPreviewSet = false;
255
+ elementRect = null;
256
+ }
185
257
 
186
- // Check if drop was successful
187
- const wasDropped = event.dataTransfer.dropEffect !== 'none';
258
+ // Check if drop was successful
259
+ const wasDropped = event.dataTransfer.dropEffect !== 'none';
188
260
 
189
- if (wasDropped) {
190
- currentState = DROPPING;
191
- onDrop?.({ event, item, wasDropped: true });
261
+ if (wasDropped) {
262
+ currentState = DROPPING;
263
+ onDrop?.({ event, item, wasDropped: true });
192
264
 
193
- // Brief dropping state before returning to idle
194
- setTimeout(() => {
265
+ // Brief dropping state before returning to idle
266
+ setTimeout(() => {
267
+ currentState = IDLE;
268
+ }, 100);
269
+ } else {
195
270
  currentState = IDLE;
196
- }, 100);
197
- } else {
198
- currentState = IDLE;
199
- }
271
+ }
200
272
 
201
- onDragEnd?.({ event, item, wasDropped });
202
- }
273
+ onDragEnd?.({ event, item, wasDropped });
274
+ }
203
275
 
204
276
  /**
205
277
  * Handle mouse down for drag delay
@@ -226,6 +298,7 @@ function handleDragEnd(event) {
226
298
  <div
227
299
  data-component="draggable"
228
300
  data-id={draggableId}
301
+ bind:this={draggableElement}
229
302
  draggable={!disabled && canDrag(item)}
230
303
  ondragstart={handleDragStart}
231
304
  ondrag={handleDrag}
@@ -237,3 +310,14 @@ function handleDragEnd(event) {
237
310
  >
238
311
  {@render children()}
239
312
  </div>
313
+
314
+ {#if draggingSnippet && showPreview && elementRect}
315
+ <div
316
+ data-companion="drag-preview-follower"
317
+ style="position: fixed; z-index: 9999; pointer-events: none;"
318
+ style:left="{previewX}px"
319
+ style:top="{previewY}px"
320
+ >
321
+ {@render draggingSnippet({ element: draggableElement, rect: elementRect })}
322
+ </div>
323
+ {/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,6 +24,7 @@ type Draggable = {
20
24
  item: any;
21
25
  source: string;
22
26
  group: string;
27
+ getController: () => DragController;
23
28
  }) => void;
24
29
  onDragging?: (detail: {
25
30
  event: DragEvent;
@@ -48,6 +53,10 @@ declare const Draggable: import("svelte").Component<{
48
53
  base?: string;
49
54
  classes?: string;
50
55
  children: import("svelte").Snippet;
56
+ draggingSnippet?: import("svelte").Snippet<[{
57
+ element: HTMLElement;
58
+ rect: DOMRect;
59
+ }]>;
51
60
  contextKey?: import("../../typedef").ContextKey;
52
61
  isDragging?: boolean;
53
62
  isDropping?: boolean;
@@ -57,6 +66,7 @@ declare const Draggable: import("svelte").Component<{
57
66
  item: any;
58
67
  source: string;
59
68
  group: string;
69
+ getController: () => DragController;
60
70
  }) => void;
61
71
  onDragging?: (detail: {
62
72
  event: DragEvent;
@@ -74,3 +84,4 @@ declare const Draggable: import("svelte").Component<{
74
84
  }) => void;
75
85
  canDrag?: (item: any) => boolean;
76
86
  }, {}, "isDragging" | "isDropping" | "isDragPreview">;
87
+ import { DragController } from './DragController.js';
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { createOrGetDragState } from './drag-state.svelte.js';
6
6
 
7
+ import { GridLayers } from '../layout';
8
+
7
9
  import {
8
10
  findDraggableSource,
9
11
  getDraggableIdFromEvent,
@@ -15,32 +17,26 @@
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
+
22
25
  /**
23
26
  * @type {{
24
27
  * zone?: string,
25
28
  * group?: string,
26
29
  * disabled?: boolean,
27
30
  * accepts?: (item: any) => boolean,
28
- * maxItems?: number,
29
31
  * base?: string,
30
32
  * classes?: string,
33
+ * height?: string,
34
+ * autoHeight?: boolean,
31
35
  * children?: import('svelte').Snippet,
32
36
  * 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
- * }]>,
37
+ * dropPreviewSnippet?: import('svelte').Snippet<[DragData]>,
40
38
  * isDragOver?: boolean,
41
39
  * canDrop?: boolean,
42
- * isDropping?: boolean,
43
- * itemCount?: number,
44
40
  * onDragEnter?: (detail: {
45
41
  * event: DragEvent,
46
42
  * zone: string,
@@ -81,17 +77,15 @@
81
77
  group = 'default',
82
78
  disabled = false,
83
79
  accepts = () => true,
84
- maxItems = Infinity,
85
80
  base = '',
86
81
  classes = '',
82
+ height = 'h-min',
83
+ autoHeight= false,
87
84
  children,
88
85
  contextKey,
89
- empty,
90
- preview,
86
+ dropPreviewSnippet,
91
87
  isDragOver = $bindable(false),
92
88
  canDrop = $bindable(false),
93
- isDropping = $bindable(false),
94
- itemCount = $bindable(0),
95
89
  onDragEnter,
96
90
  onDragOver,
97
91
  onDragLeave,
@@ -140,8 +134,7 @@
140
134
  'drag-over': currentState === DRAG_OVER,
141
135
  'can-drop': currentState === CAN_DROP,
142
136
  'cannot-drop': currentState === CANNOT_DROP,
143
- 'drop-disabled': disabled,
144
- 'active-drop': currentState === ACTIVE_DROP
137
+ 'drop-disabled': disabled
145
138
  });
146
139
 
147
140
  let stateClasses = $derived(toStateClasses(stateObject));
@@ -155,7 +148,6 @@
155
148
  ].includes(currentState);
156
149
 
157
150
  canDrop = currentState === CAN_DROP;
158
- isDropping = currentState === ACTIVE_DROP;
159
151
  });
160
152
 
161
153
  /**
@@ -172,7 +164,6 @@
172
164
  if (!data) return false;
173
165
  if (data.group !== group) return false;
174
166
  if (!accepts(data.item)) return false;
175
- if (itemCount >= maxItems) return false;
176
167
  return true;
177
168
  }
178
169
 
@@ -321,8 +312,7 @@ function handleDrop(event) {
321
312
 
322
313
  // Check if we can accept this drop
323
314
  if (dragData && canAcceptDrop(dragData)) {
324
- // Update state and notify listeners
325
- currentState = ACTIVE_DROP;
315
+ // Notify listener
326
316
  onDropStart?.({ event, zone, data: dragData });
327
317
 
328
318
  // Call the onDrop handler and handle Promise resolution
@@ -361,17 +351,41 @@ function handleDrop(event) {
361
351
  ondragover={handleDragOver}
362
352
  ondragleave={handleDragLeave}
363
353
  ondrop={handleDrop}
364
- class="{base} {classes} {stateClasses}"
354
+ class="{base} {height} {classes} {stateClasses}"
365
355
  data-zone={zone}
366
356
  {...attrs}
367
357
  >
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}
358
+ <GridLayers heightFrom={autoHeight ? 'content' : null}>
359
+ {#if children}
360
+ <div data-layer="content" class:auto-height={autoHeight}>
361
+ {@render children()}
362
+ </div>
363
+ {/if}
364
+
365
+ {#if currentState === CAN_DROP && dropPreviewSnippet}
366
+ <div data-layer="preview">
367
+ {@render dropPreviewSnippet(dragState.current)}
368
+ </div>
369
+ {/if}
370
+
371
+ </GridLayers>
377
372
  </div>
373
+
374
+ <style>
375
+ [data-layer='content']:not(.auto-height) {
376
+ position: absolute;
377
+ left: 0; right: 0; top: 0; bottom: 0;
378
+ }
379
+
380
+ [data-layer='content'].auto-height {
381
+ position: relative;
382
+ width: 100%;
383
+ }
384
+
385
+ [data-layer='preview']
386
+ {
387
+ position: absolute;
388
+ left: 0; right: 0; top: 0; bottom: 0;
389
+ pointer-events: none;
390
+ }
391
+ </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;
@@ -63,22 +56,15 @@ declare const DropZone: import("svelte").Component<{
63
56
  group?: string;
64
57
  disabled?: boolean;
65
58
  accepts?: (item: any) => boolean;
66
- maxItems?: number;
67
59
  base?: string;
68
60
  classes?: string;
61
+ height?: string;
62
+ autoHeight?: boolean;
69
63
  children?: import("svelte").Snippet;
70
64
  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
- }]>;
65
+ dropPreviewSnippet?: import("svelte").Snippet<[import("../../typedef").DragData]>;
78
66
  isDragOver?: boolean;
79
67
  canDrop?: boolean;
80
- isDropping?: boolean;
81
- itemCount?: number;
82
68
  onDragEnter?: (detail: {
83
69
  event: DragEvent;
84
70
  zone: string;
@@ -111,4 +97,4 @@ declare const DropZone: import("svelte").Component<{
111
97
  success: boolean;
112
98
  error?: Error;
113
99
  }) => void;
114
- }, {}, "isDropping" | "isDragOver" | "canDrop" | "itemCount">;
100
+ }, {}, "isDragOver" | "canDrop">;