@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.
- package/dist/components/drag-drop/DragDropContext.svelte +77 -0
- package/dist/components/drag-drop/Draggable.svelte +221 -70
- package/dist/components/drag-drop/DropZone.svelte +76 -365
- package/dist/components/drag-drop/drag-state.svelte.d.ts +66 -1
- package/dist/components/drag-drop/drag-state.svelte.js +276 -3
- package/dist/components/drag-drop/util.js +7 -7
- package/dist/components/layout/grid-layers/GridLayers.svelte +5 -2
- package/dist/features/image-box/ImageBox.svelte +1 -1
- package/dist/features/image-box/ImageBox.svelte.d.ts +1 -1
- package/dist/themes/hkdev/components/drag-drop/drop-zone.css +2 -2
- package/dist/typedef/drag.d.ts +14 -0
- package/dist/typedef/drag.js +15 -0
- package/dist/typedef/drop.d.ts +1 -1
- package/dist/typedef/drop.js +1 -1
- package/dist/typedef/image.d.ts +1 -0
- package/dist/typedef/image.js +25 -0
- package/package.json +1 -1
@@ -1,10 +1,213 @@
|
|
1
1
|
// drag-state.svelte.js
|
2
2
|
import { defineStateContext } from '../../util/svelte/state-context/index.js';
|
3
3
|
|
4
|
+
/** @typedef {import('../../typedef').SimulatedDragEvent} SimulatedDragEvent */
|
5
|
+
|
4
6
|
class DragState {
|
5
|
-
//
|
7
|
+
// Existing draggables map
|
6
8
|
draggables = $state(new Map());
|
7
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
|
+
|
8
211
|
/**
|
9
212
|
* @param {string} draggableId
|
10
213
|
* @param {import('../../typedef/drag.js').DragData} dragData
|
@@ -18,22 +221,92 @@ class DragState {
|
|
18
221
|
*/
|
19
222
|
end(draggableId) {
|
20
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;
|
21
242
|
}
|
22
243
|
|
23
244
|
/**
|
245
|
+
* Get a drag data by draggable id
|
246
|
+
*
|
24
247
|
* @param {string} draggableId
|
25
248
|
* @returns {import('../../typedef/drag.js').DragData|undefined}
|
26
249
|
*/
|
27
|
-
|
250
|
+
getDraggableById(draggableId) {
|
28
251
|
return this.draggables.get(draggableId);
|
29
252
|
}
|
30
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 touch-simulated event
|
263
|
+
if (event.dataTransfer && !event.dataTransfer.files) {
|
264
|
+
try {
|
265
|
+
const jsonData = event.dataTransfer.getData('application/json');
|
266
|
+
if (jsonData) {
|
267
|
+
const transferData = JSON.parse(jsonData);
|
268
|
+
const draggableId = transferData.draggableId;
|
269
|
+
|
270
|
+
if (draggableId) {
|
271
|
+
return this.getDraggableById(draggableId);
|
272
|
+
}
|
273
|
+
}
|
274
|
+
} catch (error) {
|
275
|
+
console.error('Error getting drag data:', error);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
// Check if this is a file drop
|
280
|
+
if (event.dataTransfer.types.includes('Files')) {
|
281
|
+
return null;
|
282
|
+
}
|
283
|
+
|
284
|
+
// Handle internal drag operations
|
285
|
+
try {
|
286
|
+
const jsonData = event.dataTransfer.getData('application/json');
|
287
|
+
if (jsonData) {
|
288
|
+
const transferData = JSON.parse(jsonData);
|
289
|
+
const draggableId = transferData.draggableId;
|
290
|
+
|
291
|
+
if (draggableId) {
|
292
|
+
const dragData = this.getDraggableById(draggableId);
|
293
|
+
if (dragData) {
|
294
|
+
return dragData;
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
} catch (error) {
|
299
|
+
console.error('Error getting drag data:', error);
|
300
|
+
}
|
301
|
+
|
302
|
+
return null;
|
303
|
+
}
|
304
|
+
|
31
305
|
/**
|
32
306
|
* Get the most recently started drag operation (convenience method)
|
33
307
|
* @returns {import('../../typedef/drag.js').DragData|undefined}
|
34
308
|
*/
|
35
309
|
get current() {
|
36
|
-
// For backward compatibility with existing code
|
37
310
|
const entries = Array.from(this.draggables.entries());
|
38
311
|
return entries.length > 0 ? entries[entries.length - 1][1] : undefined;
|
39
312
|
}
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { createOrGetDragState } from './drag-state.svelte.js';
|
2
|
+
|
1
3
|
/**
|
2
4
|
* Find the source draggable element from an event
|
3
5
|
*
|
@@ -48,13 +50,11 @@ export function getDraggableIdFromEvent(event) {
|
|
48
50
|
* @param {Function} options.setState Function to update component state
|
49
51
|
* @returns {Promise<boolean>} Success status
|
50
52
|
*/
|
51
|
-
export async function processDropWithData(
|
52
|
-
|
53
|
-
|
54
|
-
onDropEnd,
|
55
|
-
|
56
|
-
setState
|
57
|
-
}) {
|
53
|
+
export async function processDropWithData(
|
54
|
+
event,
|
55
|
+
data,
|
56
|
+
{ onDropStart, onDrop, onDropEnd, zone, setState }
|
57
|
+
) {
|
58
58
|
try {
|
59
59
|
// Update state and notify listeners
|
60
60
|
setState('ACTIVE_DROP');
|
@@ -29,7 +29,7 @@
|
|
29
29
|
bg = '',
|
30
30
|
padding = '',
|
31
31
|
margin = '',
|
32
|
-
height = '',
|
32
|
+
height = 'h-full',
|
33
33
|
classes = '',
|
34
34
|
style = '',
|
35
35
|
cellBase = '',
|
@@ -150,12 +150,15 @@
|
|
150
150
|
observer = null;
|
151
151
|
}
|
152
152
|
});
|
153
|
+
|
154
|
+
$inspect('heightFrom', heightFrom);
|
155
|
+
$inspect('containerStyle', containerStyle);
|
153
156
|
</script>
|
154
157
|
|
155
158
|
<div
|
156
159
|
data-component="grid-layers"
|
157
160
|
bind:this={gridContainer}
|
158
|
-
class="relative {isFirstRender ? 'invisible' : ''} {base} {bg} {
|
161
|
+
class="relative {isFirstRender ? 'invisible' : ''} {base} {bg} {heightFrom ? '' : height} {classes} {margin} {padding}"
|
159
162
|
style={containerStyle}
|
160
163
|
{...attrs}
|
161
164
|
>
|
@@ -13,7 +13,7 @@
|
|
13
13
|
* aspect?: string,
|
14
14
|
* overflow?: string,
|
15
15
|
* fit?: 'contain' | 'cover' | 'fill',
|
16
|
-
* position?:
|
16
|
+
* position?: import('../../typedef/image.js').ObjectPosition,
|
17
17
|
* imageMeta?: import('../../typedef').ImageSource,
|
18
18
|
* imageLoader?: import('../../classes/svelte/image/index.js').ImageLoader,
|
19
19
|
* alt?: string,
|
@@ -29,7 +29,7 @@ declare const ImageBox: import("svelte").Component<{
|
|
29
29
|
aspect?: string;
|
30
30
|
overflow?: string;
|
31
31
|
fit?: "contain" | "cover" | "fill";
|
32
|
-
position?:
|
32
|
+
position?: import("../../typedef/image.js").ObjectPosition;
|
33
33
|
imageMeta?: import("../../typedef").ImageSource;
|
34
34
|
imageLoader?: import("../../classes/svelte/image/index.js").ImageLoader;
|
35
35
|
alt?: string;
|
@@ -32,11 +32,11 @@
|
|
32
32
|
cursor: not-allowed;
|
33
33
|
}
|
34
34
|
|
35
|
-
|
35
|
+
/*&.state-drop-disabled {
|
36
36
|
opacity: 0.5;
|
37
37
|
cursor: not-allowed;
|
38
38
|
background-color: rgb(var(--color-surface-100));
|
39
|
-
}
|
39
|
+
}*/
|
40
40
|
}
|
41
41
|
|
42
42
|
/* Default styling for inner elements - all visual/customizable */
|
package/dist/typedef/drag.d.ts
CHANGED
@@ -13,3 +13,17 @@ export type DragData = {
|
|
13
13
|
*/
|
14
14
|
source?: string;
|
15
15
|
};
|
16
|
+
export type SimulatedDragEvent = {
|
17
|
+
type: "dragstart" | "dragover" | "dragleave" | "drop" | "dragend";
|
18
|
+
clientX: number;
|
19
|
+
clientY: number;
|
20
|
+
dataTransfer: {
|
21
|
+
types: Array<string>;
|
22
|
+
getData: Function;
|
23
|
+
dropEffect: "none" | "copy" | "link" | "move";
|
24
|
+
effectAllowed: "none" | "copy" | "copyLink" | "copyMove" | "link" | "linkMove" | "move" | "all" | "uninitialized";
|
25
|
+
files: FileList | any[];
|
26
|
+
};
|
27
|
+
preventDefault: Function;
|
28
|
+
stopPropagation: Function;
|
29
|
+
};
|
package/dist/typedef/drag.js
CHANGED
@@ -7,4 +7,19 @@
|
|
7
7
|
* @property {string} [source] - Source identifier
|
8
8
|
*/
|
9
9
|
|
10
|
+
/**
|
11
|
+
* @typedef {Object} SimulatedDragEvent
|
12
|
+
* @property {'dragstart'|'dragover'|'dragleave'|'drop'|'dragend'} type
|
13
|
+
* @property {number} clientX
|
14
|
+
* @property {number} clientY
|
15
|
+
* @property {Object} dataTransfer
|
16
|
+
* @property {Array<string>} dataTransfer.types
|
17
|
+
* @property {Function} dataTransfer.getData
|
18
|
+
* @property {'none'|'copy'|'link'|'move'} dataTransfer.dropEffect
|
19
|
+
* @property {'none'|'copy'|'copyLink'|'copyMove'|'link'|'linkMove'|'move'|'all'|'uninitialized'} dataTransfer.effectAllowed
|
20
|
+
* @property {FileList|Array} dataTransfer.files
|
21
|
+
* @property {Function} preventDefault
|
22
|
+
* @property {Function} stopPropagation
|
23
|
+
*/
|
24
|
+
|
10
25
|
export default {}
|
package/dist/typedef/drop.d.ts
CHANGED
package/dist/typedef/drop.js
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
* @property {string} source
|
7
7
|
* @property {any} item
|
8
8
|
* @property {import('./drag').DragData} drag
|
9
|
-
* @property {{offsetX: number, offsetY: number,
|
9
|
+
* @property {{offsetX: number, offsetY: number, target: Element}} drop
|
10
10
|
*/
|
11
11
|
|
12
12
|
export default {};
|
package/dist/typedef/image.d.ts
CHANGED
@@ -9,3 +9,4 @@ export type ImageMeta = {
|
|
9
9
|
* Single ImageMeta object or array of ImageMeta objects
|
10
10
|
*/
|
11
11
|
export type ImageSource = ImageMeta | ImageMeta[];
|
12
|
+
export type ObjectPosition = "center" | "top" | "bottom" | "left" | "right" | "left top" | "left center" | "left bottom" | "center top" | "center center" | "center bottom" | "right top" | "right center" | "right bottom" | string;
|
package/dist/typedef/image.js
CHANGED
@@ -10,4 +10,29 @@
|
|
10
10
|
* Single ImageMeta object or array of ImageMeta objects
|
11
11
|
*/
|
12
12
|
|
13
|
+
/**
|
14
|
+
* @typedef {"center" | "top" | "bottom" | "left" | "right" |
|
15
|
+
* "left top" | "left center" | "left bottom" |
|
16
|
+
* "center top" | "center center" | "center bottom" |
|
17
|
+
* "right top" | "right center" | "right bottom" |
|
18
|
+
* string} ObjectPosition
|
19
|
+
*
|
20
|
+
* @description Accepts valid CSS object-position values including:
|
21
|
+
* - Keywords: "center", "top", "bottom", "left", "right"
|
22
|
+
* - Length values: "10px", "2em", "50%", etc.
|
23
|
+
* - Percentage values: "25%", "100%", etc.
|
24
|
+
* - Two-value combinations: "left top", "center bottom", "25% 75%"
|
25
|
+
* - Mixed units: "left 20px", "10% center", "2em 50%"
|
26
|
+
*
|
27
|
+
* @example
|
28
|
+
* "center" // Single keyword (centers both axes)
|
29
|
+
* "top" // Single keyword
|
30
|
+
* "left center" // Two keywords
|
31
|
+
* "25% 75%" // Two percentages
|
32
|
+
* "10px 20px" // Two lengths
|
33
|
+
* "left 25%" // Keyword + percentage
|
34
|
+
* "50% top" // Percentage + keyword
|
35
|
+
* "2em center" // Length + keyword
|
36
|
+
*/
|
37
|
+
|
13
38
|
export default {};
|