@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.
@@ -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
- // Replace the single 'current' with a Map of draggable IDs
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
- getDraggable(draggableId) {
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(event, data, {
52
- onDropStart,
53
- onDrop,
54
- onDropEnd,
55
- zone,
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} {!heightFrom ? height : ''} {classes} {margin} {padding}"
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?: string,
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?: string;
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
- &.state-drop-disabled {
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 */
@@ -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
+ };
@@ -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 {}
@@ -10,6 +10,6 @@ export type DropData = {
10
10
  drop: {
11
11
  offsetX: number;
12
12
  offsetY: number;
13
- event: DragEvent;
13
+ target: Element;
14
14
  };
15
15
  };
@@ -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, event: DragEvent}} drop
9
+ * @property {{offsetX: number, offsetY: number, target: Element}} drop
10
10
  */
11
11
 
12
12
  export default {};
@@ -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;
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"