@hkdigital/lib-sveltekit 0.2.10 → 0.2.11

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.
@@ -12,7 +12,6 @@
12
12
  DROPPING
13
13
  } from '../../constants/state-labels/drag-states.js';
14
14
 
15
-
16
15
  /** @typedef {import('../../typedef').SimulatedDragEvent} SimulatedDragEvent */
17
16
 
18
17
  /**
@@ -26,7 +25,8 @@
26
25
  * classes?: string,
27
26
  * children: import('svelte').Snippet<[{
28
27
  * element: HTMLElement,
29
- * rect: DOMRect
28
+ * rect: DOMRect,
29
+ * isDragging: boolean
30
30
  * }]>,
31
31
  * draggingSnippet?: import('svelte').Snippet<[{
32
32
  * element: HTMLElement,
@@ -102,24 +102,27 @@
102
102
  let customPreviewSet = $state(false);
103
103
  let elementRect = $state(null);
104
104
 
105
- // Track if current draggable can drop in the active zone
106
- let canDropInActiveZone = $derived.by(() => {
107
- if (currentState !== DRAGGING || !dragState.activeDropZone) return false;
108
-
109
- const activeZone = dragState.dropZones.get(dragState.activeDropZone);
110
- return activeZone?.canDrop || false;
111
- });
112
-
113
- // Computed state object for CSS classes
114
- let stateObject = $derived({
115
- idle: currentState === IDLE,
116
- dragging: currentState === DRAGGING,
117
- 'drag-preview': currentState === DRAG_PREVIEW,
118
- dropping: currentState === DROPPING,
119
- 'drag-disabled': disabled || !canDrag(item),
120
- 'can-drop': currentState === DRAGGING && canDropInActiveZone,
121
- 'cannot-drop': currentState === DRAGGING && dragState.activeDropZone && !canDropInActiveZone
122
- });
105
+ // Track if current draggable can drop in the active zone
106
+ let canDropInActiveZone = $derived.by(() => {
107
+ if (currentState !== DRAGGING || !dragState.activeDropZone) return false;
108
+
109
+ const activeZone = dragState.dropZones.get(dragState.activeDropZone);
110
+ return activeZone?.canDrop || false;
111
+ });
112
+
113
+ // Computed state object for CSS classes
114
+ let stateObject = $derived({
115
+ idle: currentState === IDLE,
116
+ dragging: currentState === DRAGGING,
117
+ 'drag-preview': currentState === DRAG_PREVIEW,
118
+ dropping: currentState === DROPPING,
119
+ 'drag-disabled': disabled || !canDrag(item),
120
+ 'can-drop': currentState === DRAGGING && canDropInActiveZone,
121
+ 'cannot-drop':
122
+ currentState === DRAGGING &&
123
+ dragState.activeDropZone &&
124
+ !canDropInActiveZone
125
+ });
123
126
 
124
127
  let stateClasses = $derived(toStateClasses(stateObject));
125
128
 
@@ -180,10 +183,10 @@ let stateObject = $derived({
180
183
 
181
184
  let transparentPixel;
182
185
 
183
- if( browser )
184
- {
186
+ if (browser) {
185
187
  transparentPixel = new Image();
186
- transparentPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
188
+ transparentPixel.src =
189
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
187
190
  }
188
191
 
189
192
  /**
@@ -191,7 +194,6 @@ let stateObject = $derived({
191
194
  * @param {DragEvent} event - The drag event
192
195
  */
193
196
  function startDrag(event) {
194
-
195
197
  // Set a transparent 1x1 pixel image to hide browser's default preview
196
198
  event.dataTransfer.setDragImage(transparentPixel, 0, 0);
197
199
 
@@ -363,10 +365,10 @@ let stateObject = $derived({
363
365
 
364
366
  // Show preview
365
367
  // if (draggingSnippet) {
366
- elementRect = rect;
367
- previewX = rect.left;
368
- previewY = rect.top;
369
- showPreview = true;
368
+ elementRect = rect;
369
+ previewX = rect.left;
370
+ previewY = rect.top;
371
+ showPreview = true;
370
372
  // }
371
373
 
372
374
  // Prevent scrolling while dragging
@@ -385,79 +387,83 @@ let stateObject = $derived({
385
387
  * Handle touch move
386
388
  * @param {TouchEvent} event
387
389
  */
388
- function handleTouchMove(event) {
389
- if (!touchDragging) return;
390
+ function handleTouchMove(event) {
391
+ if (!touchDragging) return;
390
392
 
391
- event.preventDefault();
392
- const touch = event.touches[0];
393
+ event.preventDefault();
394
+ const touch = event.touches[0];
393
395
 
394
- // Update preview position
395
- if (showPreview) {
396
- previewX = touch.clientX - dragOffsetX;
397
- previewY = touch.clientY - dragOffsetY;
398
- }
396
+ // Update preview position
397
+ if (showPreview) {
398
+ previewX = touch.clientX - dragOffsetX;
399
+ previewY = touch.clientY - dragOffsetY;
400
+ }
399
401
 
400
- /** @type {SimulatedDragEvent} */
401
- const simulatedEvent = {
402
- type: 'dragover',
403
- clientX: touch.clientX,
404
- clientY: touch.clientY,
405
- dataTransfer: {
406
- types: [`application/x-draggable-${draggableId}`, 'text/plain'],
407
- getData: () => 1,
408
- dropEffect: 'move',
409
- effectAllowed: 'move',
410
- files: []
411
- },
412
-
413
- preventDefault: () => {},
414
- stopPropagation: () => {}
415
- };
416
-
417
- // Update active dropzone in drag state
418
- dragState.updateActiveDropZone(touch.clientX, touch.clientY, simulatedEvent);
419
- }
402
+ /** @type {SimulatedDragEvent} */
403
+ const simulatedEvent = {
404
+ type: 'dragover',
405
+ clientX: touch.clientX,
406
+ clientY: touch.clientY,
407
+ dataTransfer: {
408
+ types: [`application/x-draggable-${draggableId}`, 'text/plain'],
409
+ getData: () => 1,
410
+ dropEffect: 'move',
411
+ effectAllowed: 'move',
412
+ files: []
413
+ },
414
+
415
+ preventDefault: () => {},
416
+ stopPropagation: () => {}
417
+ };
418
+
419
+ // Update active dropzone in drag state
420
+ dragState.updateActiveDropZone(
421
+ touch.clientX,
422
+ touch.clientY,
423
+ simulatedEvent
424
+ );
425
+ }
420
426
 
421
427
  /**
422
428
  * Handle touch end
423
429
  * @param {TouchEvent} event
424
430
  */
425
431
  function handleTouchEnd(event) {
426
- clearTimeout(dragTimeout);
427
-
428
- if (!touchDragging) return;
429
-
430
- const touch = event.changedTouches[0];
431
-
432
- /** @type {SimulatedDragEvent} */
433
- const simulatedEvent = {
434
- type: 'drop',
435
- clientX: touch.clientX,
436
- clientY: touch.clientY,
437
- dataTransfer: {
438
- types: [`application/x-draggable-${draggableId}`, 'text/plain'],
439
- getData: () => 1,
440
- dropEffect: 'move',
441
- effectAllowed: 'move',
442
- files: []
443
- },
444
- preventDefault: () => {}, // Add this!
445
- stopPropagation: () => {} // And this!
446
- };
447
-
448
- // Trigger drop at final touch position
449
- dragState.handleDropAtPoint(touch.clientX, touch.clientY, simulatedEvent);
450
-
451
- // Clean up
452
- touchDragging = false;
453
- currentState = IDLE;
454
- showPreview = false;
455
- dragState.end(draggableId);
456
-
457
- // Remove document handlers
458
- document.removeEventListener('touchmove', handleTouchMove);
459
- document.removeEventListener('touchend', handleTouchEnd);
460
- }
432
+ clearTimeout(dragTimeout);
433
+
434
+ if (!touchDragging) return;
435
+
436
+ const touch = event.changedTouches[0];
437
+
438
+ /** @type {SimulatedDragEvent} */
439
+ const simulatedEvent = {
440
+ type: 'drop',
441
+ clientX: touch.clientX,
442
+ clientY: touch.clientY,
443
+ dataTransfer: {
444
+ types: [`application/x-draggable-${draggableId}`, 'text/plain'],
445
+ getData: () => 1,
446
+ dropEffect: 'move',
447
+ effectAllowed: 'move',
448
+ files: []
449
+ },
450
+ preventDefault: () => {}, // Add this!
451
+ stopPropagation: () => {} // And this!
452
+ };
453
+
454
+ // Trigger drop at final touch position
455
+ dragState.handleDropAtPoint(touch.clientX, touch.clientY, simulatedEvent);
456
+
457
+ // Clean up
458
+ touchDragging = false;
459
+ currentState = IDLE;
460
+ showPreview = false;
461
+ dragState.end(draggableId);
462
+
463
+ // Remove document handlers
464
+ document.removeEventListener('touchmove', handleTouchMove);
465
+ document.removeEventListener('touchend', handleTouchEnd);
466
+ }
461
467
  </script>
462
468
 
463
469
  <div
@@ -474,7 +480,11 @@ function handleTouchMove(event) {
474
480
  style="touch-action: none;"
475
481
  {...attrs}
476
482
  >
477
- {@render children({ element: draggableElement, rect: elementRect })}
483
+ {@render children({
484
+ element: draggableElement,
485
+ rect: elementRect,
486
+ isDragging: false
487
+ })}
478
488
  </div>
479
489
 
480
490
  {#if showPreview && elementRect}
@@ -485,11 +495,18 @@ function handleTouchMove(event) {
485
495
  style:left="{previewX}px"
486
496
  style:top="{previewY}px"
487
497
  >
488
- {#if draggingSnippet}
489
- {@render draggingSnippet({ element: draggableElement, rect: elementRect })}
490
- {:else}
491
- {@render children({ element: draggableElement, rect: elementRect })}
492
- {/if}
498
+ {#if draggingSnippet}
499
+ {@render draggingSnippet({
500
+ element: draggableElement,
501
+ rect: elementRect
502
+ })}
503
+ {:else}
504
+ {@render children({
505
+ element: draggableElement,
506
+ rect: elementRect,
507
+ isDragging: true
508
+ })}
509
+ {/if}
493
510
  </div>
494
511
  {/if}
495
512
 
@@ -13,6 +13,7 @@ type Draggable = {
13
13
  children: Snippet<[{
14
14
  element: HTMLElement;
15
15
  rect: DOMRect;
16
+ isDragging: boolean;
16
17
  }]>;
17
18
  draggingSnippet?: Snippet<[{
18
19
  element: HTMLElement;
@@ -58,6 +59,7 @@ declare const Draggable: import("svelte").Component<{
58
59
  children: import("svelte").Snippet<[{
59
60
  element: HTMLElement;
60
61
  rect: DOMRect;
62
+ isDragging: boolean;
61
63
  }]>;
62
64
  draggingSnippet?: import("svelte").Snippet<[{
63
65
  element: HTMLElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -1,319 +0,0 @@
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 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'|| event.type === 'dragenter') {
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) {
282
- try {
283
- const jsonData = event.dataTransfer.getData('application/json');
284
- if (jsonData) {
285
- const transferData = JSON.parse(jsonData);
286
- const draggableId = transferData.draggableId;
287
-
288
- if (draggableId) {
289
- return this.getDraggableById(draggableId);
290
- }
291
- }
292
- } catch (error) {
293
- console.error('Error getting drag data from drop:', error);
294
- }
295
- }
296
-
297
- // Fallback to checking active drags
298
- return this.current;
299
- }
300
-
301
- /**
302
- * Get the most recently started drag operation (convenience method)
303
- * @returns {import('$lib/typedef/drag.js').DragData|undefined}
304
- */
305
- get current() {
306
- const entries = Array.from(this.draggables.entries());
307
- return entries.length > 0 ? entries[entries.length - 1][1] : undefined;
308
- }
309
-
310
- /**
311
- * @returns {boolean}
312
- */
313
- isDragging() {
314
- return this.draggables.size > 0;
315
- }
316
- }
317
-
318
- export const [createOrGetDragState, createDragState, getDragState] =
319
- defineStateContext(DragState);