@appulsauce/svelte-grid 2.0.0
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/LICENSE +21 -0
- package/README.md +556 -0
- package/dist/Grid.svelte +245 -0
- package/dist/Grid.svelte.d.ts +26 -0
- package/dist/GridController.d.ts +16 -0
- package/dist/GridController.js +64 -0
- package/dist/GridItem.svelte +566 -0
- package/dist/GridItem.svelte.d.ts +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.js +1 -0
- package/dist/utils/assert.d.ts +8 -0
- package/dist/utils/assert.js +23 -0
- package/dist/utils/breakpoints.d.ts +2 -0
- package/dist/utils/breakpoints.js +9 -0
- package/dist/utils/grid.d.ts +9 -0
- package/dist/utils/grid.js +43 -0
- package/dist/utils/item.d.ts +13 -0
- package/dist/utils/item.js +42 -0
- package/dist/utils/types.d.ts +3 -0
- package/dist/utils/types.js +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
coordinate2size,
|
|
6
|
+
calcPosition,
|
|
7
|
+
snapOnMove,
|
|
8
|
+
snapOnResize,
|
|
9
|
+
type SnapGridParams
|
|
10
|
+
} from './utils/item';
|
|
11
|
+
import { hasCollisions, getCollisions, getAvailablePosition } from './utils/grid';
|
|
12
|
+
|
|
13
|
+
import type { LayoutItem, LayoutChangeDetail, Size, ItemSize, GridItemProps } from './types';
|
|
14
|
+
import { getGridContext } from './Grid.svelte';
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
id = crypto.randomUUID(),
|
|
18
|
+
x = $bindable(),
|
|
19
|
+
y = $bindable(),
|
|
20
|
+
w = $bindable(1),
|
|
21
|
+
h = $bindable(1),
|
|
22
|
+
min = { w: 1, h: 1 },
|
|
23
|
+
max,
|
|
24
|
+
movable = true,
|
|
25
|
+
resizable = true,
|
|
26
|
+
class: classes,
|
|
27
|
+
activeClass,
|
|
28
|
+
previewClass,
|
|
29
|
+
resizerClass,
|
|
30
|
+
style = '',
|
|
31
|
+
onchange,
|
|
32
|
+
onpreviewchange,
|
|
33
|
+
children,
|
|
34
|
+
moveHandle,
|
|
35
|
+
resizeHandle
|
|
36
|
+
}: GridItemProps = $props();
|
|
37
|
+
|
|
38
|
+
// Get grid context - this returns a reactive reference wrapper
|
|
39
|
+
const gridParamsRef = getGridContext();
|
|
40
|
+
|
|
41
|
+
// Access current within reactive contexts ($effect, $derived, template)
|
|
42
|
+
const gridParams = $derived(gridParamsRef.current);
|
|
43
|
+
|
|
44
|
+
let active = $state(false);
|
|
45
|
+
let left = $state(0);
|
|
46
|
+
let top = $state(0);
|
|
47
|
+
let width = $state(0);
|
|
48
|
+
let height = $state(0);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Item object that is used in `gridParams.items`.
|
|
52
|
+
*/
|
|
53
|
+
let item = $state<LayoutItem>({
|
|
54
|
+
id,
|
|
55
|
+
x,
|
|
56
|
+
y,
|
|
57
|
+
w,
|
|
58
|
+
h,
|
|
59
|
+
min,
|
|
60
|
+
max,
|
|
61
|
+
movable,
|
|
62
|
+
resizable,
|
|
63
|
+
invalidate
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Sync props to item
|
|
67
|
+
$effect(() => {
|
|
68
|
+
item.x = x;
|
|
69
|
+
item.y = y;
|
|
70
|
+
item.w = w;
|
|
71
|
+
item.h = h;
|
|
72
|
+
item.min = min;
|
|
73
|
+
item.max = max;
|
|
74
|
+
item.movable = movable;
|
|
75
|
+
item.resizable = resizable;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Updates svelte-components props behind that item.
|
|
80
|
+
*/
|
|
81
|
+
function invalidate() {
|
|
82
|
+
x = item.x;
|
|
83
|
+
y = item.y;
|
|
84
|
+
w = item.w;
|
|
85
|
+
h = item.h;
|
|
86
|
+
onchange?.({ item });
|
|
87
|
+
gridParams.onchange({ item });
|
|
88
|
+
gridParams.updateGrid();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMount(() => {
|
|
92
|
+
gridParams.registerItem(item);
|
|
93
|
+
return () => {
|
|
94
|
+
gridParams.unregisterItem(item);
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Reposition item on grid change
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (!active && gridParams.itemSize) {
|
|
101
|
+
const newPosition = calcPosition(item, {
|
|
102
|
+
itemSize: gridParams.itemSize,
|
|
103
|
+
gap: gridParams.gap
|
|
104
|
+
});
|
|
105
|
+
left = newPosition.left;
|
|
106
|
+
top = newPosition.top;
|
|
107
|
+
width = newPosition.width;
|
|
108
|
+
height = newPosition.height;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let previewItem = $state<LayoutItem>({ ...item });
|
|
113
|
+
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (!active && item) {
|
|
116
|
+
previewItem = { ...item };
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
$effect(() => {
|
|
121
|
+
onpreviewchange?.({ item: previewItem });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function applyPreview() {
|
|
125
|
+
item.x = previewItem.x;
|
|
126
|
+
item.y = previewItem.y;
|
|
127
|
+
item.w = previewItem.w;
|
|
128
|
+
item.h = previewItem.h;
|
|
129
|
+
item.invalidate();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// INTERACTION LOGIC
|
|
133
|
+
|
|
134
|
+
let itemRef: HTMLElement;
|
|
135
|
+
|
|
136
|
+
const initialPointerPosition = { left: 0, top: 0 };
|
|
137
|
+
|
|
138
|
+
function initInteraction(event: PointerEvent) {
|
|
139
|
+
active = true;
|
|
140
|
+
initialPointerPosition.left = event.pageX;
|
|
141
|
+
initialPointerPosition.top = event.pageY;
|
|
142
|
+
itemRef.setPointerCapture(event.pointerId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function endInteraction(event: PointerEvent) {
|
|
146
|
+
applyPreview();
|
|
147
|
+
active = false;
|
|
148
|
+
initialPointerPosition.left = 0;
|
|
149
|
+
initialPointerPosition.top = 0;
|
|
150
|
+
itemRef.releasePointerCapture(event.pointerId);
|
|
151
|
+
gridParams.updateGrid();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MOVE ITEM LOGIC
|
|
155
|
+
|
|
156
|
+
let initialPosition = { left: 0, top: 0 };
|
|
157
|
+
|
|
158
|
+
const _movable = $derived(!gridParams.readOnly && movable);
|
|
159
|
+
|
|
160
|
+
let pointerShift = { left: 0, top: 0 };
|
|
161
|
+
|
|
162
|
+
function moveStart(event: PointerEvent) {
|
|
163
|
+
if (event.button !== 0) return;
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
initInteraction(event);
|
|
166
|
+
initialPosition = { left, top };
|
|
167
|
+
pointerShift = { left: event.pageX - left, top: event.pageY - top };
|
|
168
|
+
window.addEventListener('pointermove', move);
|
|
169
|
+
window.addEventListener('pointerup', moveEnd);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function move(event: PointerEvent) {
|
|
173
|
+
if (!gridParams.itemSize) {
|
|
174
|
+
throw new Error('Grid is not mounted yet');
|
|
175
|
+
}
|
|
176
|
+
let _left = event.pageX - initialPointerPosition.left + initialPosition.left;
|
|
177
|
+
let _top = event.pageY - initialPointerPosition.top + initialPosition.top;
|
|
178
|
+
|
|
179
|
+
if (gridParams.bounds && gridParams.boundsTo) {
|
|
180
|
+
const parentRect = gridParams.boundsTo.getBoundingClientRect();
|
|
181
|
+
if (_left < parentRect.left) {
|
|
182
|
+
_left = parentRect.left;
|
|
183
|
+
}
|
|
184
|
+
if (_top < parentRect.top) {
|
|
185
|
+
_top = parentRect.top;
|
|
186
|
+
}
|
|
187
|
+
if (_left + width > parentRect.right) {
|
|
188
|
+
_left = parentRect.right - width;
|
|
189
|
+
}
|
|
190
|
+
if (_top + height > parentRect.bottom) {
|
|
191
|
+
_top = parentRect.bottom - height;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
left = _left;
|
|
196
|
+
top = _top;
|
|
197
|
+
|
|
198
|
+
// Snap and handle collisions
|
|
199
|
+
const snapParams = gridParams as SnapGridParams;
|
|
200
|
+
const { x: newX, y: newY } = snapOnMove(left, top, previewItem, snapParams);
|
|
201
|
+
|
|
202
|
+
if (gridParams.collision !== 'none') {
|
|
203
|
+
movePreviewWithCollisions(newX, newY);
|
|
204
|
+
} else {
|
|
205
|
+
if (!hasCollisions({ ...previewItem, x: newX, y: newY }, Object.values(gridParams.items))) {
|
|
206
|
+
previewItem = { ...previewItem, x: newX, y: newY };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function updateCollItemPositionWithPush(collItem: LayoutItem, items: LayoutItem[]) {
|
|
212
|
+
const newPosition = getAvailablePosition(
|
|
213
|
+
collItem,
|
|
214
|
+
items,
|
|
215
|
+
gridParams.maxCols,
|
|
216
|
+
gridParams.maxRows
|
|
217
|
+
);
|
|
218
|
+
if (newPosition) {
|
|
219
|
+
const { x, y } = newPosition;
|
|
220
|
+
collItem.x = x;
|
|
221
|
+
collItem.y = y;
|
|
222
|
+
collItem.invalidate();
|
|
223
|
+
}
|
|
224
|
+
gridParams.updateGrid();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleCollisionsForPreviewItemWithPush(newAttributes: {
|
|
228
|
+
x?: number;
|
|
229
|
+
y?: number;
|
|
230
|
+
w?: number;
|
|
231
|
+
h?: number;
|
|
232
|
+
}) {
|
|
233
|
+
const gridItems = Object.values(gridParams.items);
|
|
234
|
+
const itemsExceptPreview = gridItems.filter((item) => item.id != previewItem.id);
|
|
235
|
+
const collItems = getCollisions({ ...previewItem, ...newAttributes }, itemsExceptPreview);
|
|
236
|
+
|
|
237
|
+
collItems.forEach((collItem: LayoutItem) => {
|
|
238
|
+
const itemsExceptCollItem = gridItems.filter((item) => item.id != collItem.id);
|
|
239
|
+
const items = [
|
|
240
|
+
...itemsExceptCollItem.filter((item) => item.id != previewItem.id),
|
|
241
|
+
{ ...previewItem, ...newAttributes }
|
|
242
|
+
];
|
|
243
|
+
updateCollItemPositionWithPush(collItem, items);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
previewItem = { ...previewItem, ...newAttributes };
|
|
247
|
+
gridParams.updateGrid();
|
|
248
|
+
applyPreview();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function movePreviewWithCollisionsWithPush(x: number, y: number) {
|
|
252
|
+
handleCollisionsForPreviewItemWithPush({ x, y });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function movePreviewWithCollisionsWithCompress(newX: number, newY: number) {
|
|
256
|
+
const gridItems = Object.values(gridParams.items);
|
|
257
|
+
let computedY = newY;
|
|
258
|
+
const itemsExceptPreview = gridItems.filter((i) => i.id != previewItem.id);
|
|
259
|
+
while (computedY >= 0) {
|
|
260
|
+
const collItems = getCollisions({ ...previewItem, x: newX, y: computedY }, gridItems);
|
|
261
|
+
if (collItems.length > 0) {
|
|
262
|
+
const sortedItems = collItems.sort((a, b) => b.y - a.y);
|
|
263
|
+
let moved = false;
|
|
264
|
+
sortedItems.forEach((sortItem) => {
|
|
265
|
+
if (newY + previewItem.h / 2 >= sortItem.y + sortItem.h / 2) {
|
|
266
|
+
moved = true;
|
|
267
|
+
computedY = sortItem.y + sortItem.h;
|
|
268
|
+
sortedItems.forEach((itm) => {
|
|
269
|
+
if (
|
|
270
|
+
!hasCollisions({ ...itm, y: itm.y - previewItem.h }, itemsExceptPreview) &&
|
|
271
|
+
itm.y - previewItem.h >= 0
|
|
272
|
+
) {
|
|
273
|
+
itm.y -= previewItem.h;
|
|
274
|
+
itm.invalidate();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
if (!moved) {
|
|
281
|
+
computedY = previewItem.y;
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
computedY--;
|
|
286
|
+
}
|
|
287
|
+
if (computedY < 0 || newY === 0) {
|
|
288
|
+
computedY = 0;
|
|
289
|
+
}
|
|
290
|
+
const positionChanged = newX != previewItem.x || computedY != previewItem.y;
|
|
291
|
+
previewItem = { ...previewItem, x: newX, y: computedY };
|
|
292
|
+
if (positionChanged) {
|
|
293
|
+
compressItems();
|
|
294
|
+
applyPreview();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function movePreviewWithCollisions(x: number, y: number) {
|
|
299
|
+
if (gridParams.collision === 'compress') {
|
|
300
|
+
movePreviewWithCollisionsWithCompress(x, y);
|
|
301
|
+
} else {
|
|
302
|
+
movePreviewWithCollisionsWithPush(x, y);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function moveEnd(event: PointerEvent) {
|
|
307
|
+
if (event.button !== 0) return;
|
|
308
|
+
endInteraction(event);
|
|
309
|
+
pointerShift = { left: 0, top: 0 };
|
|
310
|
+
window.removeEventListener('pointermove', move);
|
|
311
|
+
window.removeEventListener('pointerup', moveEnd);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// RESIZE ITEM LOGIC
|
|
315
|
+
|
|
316
|
+
let initialSize = { width: 0, height: 0 };
|
|
317
|
+
|
|
318
|
+
const minSize = $derived.by(() => {
|
|
319
|
+
if (gridParams.itemSize) {
|
|
320
|
+
return {
|
|
321
|
+
width: coordinate2size(min.w, gridParams.itemSize.width, gridParams.gap),
|
|
322
|
+
height: coordinate2size(min.h, gridParams.itemSize.height, gridParams.gap)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const maxSize = $derived.by(() => {
|
|
329
|
+
if (gridParams.itemSize && max) {
|
|
330
|
+
return {
|
|
331
|
+
width: coordinate2size(max.w, gridParams.itemSize.width, gridParams.gap),
|
|
332
|
+
height: coordinate2size(max.h, gridParams.itemSize.height, gridParams.gap)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const _resizable = $derived(!gridParams.readOnly && item.resizable);
|
|
339
|
+
|
|
340
|
+
function resizeStart(event: PointerEvent) {
|
|
341
|
+
if (event.button !== 0) return;
|
|
342
|
+
event.stopPropagation();
|
|
343
|
+
initInteraction(event);
|
|
344
|
+
initialSize = { width, height };
|
|
345
|
+
window.addEventListener('pointermove', resize);
|
|
346
|
+
window.addEventListener('pointerup', resizeEnd);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function resize(event: PointerEvent) {
|
|
350
|
+
if (!gridParams.itemSize) {
|
|
351
|
+
throw new Error('Grid is not mounted yet');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
width = event.pageX + initialSize.width - initialPointerPosition.left;
|
|
355
|
+
height = event.pageY + initialSize.height - initialPointerPosition.top;
|
|
356
|
+
|
|
357
|
+
if (gridParams.bounds && gridParams.boundsTo) {
|
|
358
|
+
const parentRect = gridParams.boundsTo.getBoundingClientRect();
|
|
359
|
+
if (width + left > parentRect.width) {
|
|
360
|
+
width = parentRect.width - left;
|
|
361
|
+
}
|
|
362
|
+
if (height + top > parentRect.height) {
|
|
363
|
+
height = parentRect.height - top;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (minSize) {
|
|
368
|
+
width = Math.max(width, minSize.width);
|
|
369
|
+
height = Math.max(height, minSize.height);
|
|
370
|
+
}
|
|
371
|
+
if (maxSize) {
|
|
372
|
+
width = Math.min(width, maxSize.width);
|
|
373
|
+
height = Math.min(height, maxSize.height);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const snapParams = gridParams as SnapGridParams;
|
|
377
|
+
const { w: newW, h: newH } = snapOnResize(width, height, previewItem, snapParams);
|
|
378
|
+
if (gridParams.collision !== 'none') {
|
|
379
|
+
resizePreviewWithCollisions(newW, newH);
|
|
380
|
+
} else {
|
|
381
|
+
if (!hasCollisions({ ...previewItem, w: newW, h: newH }, Object.values(gridParams.items))) {
|
|
382
|
+
previewItem = { ...previewItem, w: newW, h: newH };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resizePreviewWithCollisionsWithPush(newW: number, newH: number) {
|
|
388
|
+
handleCollisionsForPreviewItemWithPush({ w: newW, h: newH });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function resizePreviewWithCollisionsWithCompress(newW: number, newH: number) {
|
|
392
|
+
const sizeChanged = newW != previewItem.w || newH != previewItem.h;
|
|
393
|
+
if (sizeChanged) {
|
|
394
|
+
const hGap = newH - previewItem.h;
|
|
395
|
+
previewItem = { ...previewItem, w: newW, h: newH };
|
|
396
|
+
applyPreview();
|
|
397
|
+
const collItems = getCollisions(
|
|
398
|
+
{ ...previewItem, w: newW, h: 9999 },
|
|
399
|
+
Object.values(gridParams.items)
|
|
400
|
+
);
|
|
401
|
+
collItems.forEach((i) => {
|
|
402
|
+
i.y += hGap;
|
|
403
|
+
i.invalidate();
|
|
404
|
+
gridParams.updateGrid();
|
|
405
|
+
});
|
|
406
|
+
compressItems();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function resizePreviewWithCollisions(w: number, h: number) {
|
|
411
|
+
if (gridParams.collision === 'compress') {
|
|
412
|
+
resizePreviewWithCollisionsWithCompress(w, h);
|
|
413
|
+
} else {
|
|
414
|
+
resizePreviewWithCollisionsWithPush(w, h);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function resizeEnd(event: PointerEvent) {
|
|
419
|
+
if (event.button !== 0) return;
|
|
420
|
+
endInteraction(event);
|
|
421
|
+
window.removeEventListener('pointermove', resize);
|
|
422
|
+
window.removeEventListener('pointerup', resizeEnd);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function compressItems() {
|
|
426
|
+
const gridItems = Object.values(gridParams.items);
|
|
427
|
+
const sortedItems = [...gridItems].sort((a, b) => a.y - b.y);
|
|
428
|
+
sortedItems.reduce(
|
|
429
|
+
(accItem, currentItem) => {
|
|
430
|
+
if (currentItem.id === previewItem.id) {
|
|
431
|
+
// if previewItem do nothing
|
|
432
|
+
} else if (previewItem.y < currentItem.y + currentItem.h) {
|
|
433
|
+
// compress items above previewItem
|
|
434
|
+
const maxY =
|
|
435
|
+
currentItem.y >= previewItem.y
|
|
436
|
+
? currentItem.y + previewItem.h + 1
|
|
437
|
+
: previewItem.y + currentItem.h + 1;
|
|
438
|
+
let newY = maxY;
|
|
439
|
+
while (newY >= 0) {
|
|
440
|
+
if (hasCollisions({ ...currentItem, y: newY }, accItem)) {
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
newY--;
|
|
444
|
+
}
|
|
445
|
+
currentItem.y = newY + 1;
|
|
446
|
+
currentItem.invalidate();
|
|
447
|
+
gridParams.updateGrid();
|
|
448
|
+
accItem.push(currentItem);
|
|
449
|
+
} else {
|
|
450
|
+
// compress items below previewItem
|
|
451
|
+
let newY = currentItem.y;
|
|
452
|
+
while (newY >= 0) {
|
|
453
|
+
if (hasCollisions({ ...currentItem, y: newY }, accItem)) {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
newY--;
|
|
457
|
+
}
|
|
458
|
+
if (newY < currentItem.y && newY > 0) {
|
|
459
|
+
currentItem.y = newY + 1;
|
|
460
|
+
}
|
|
461
|
+
currentItem.invalidate();
|
|
462
|
+
gridParams.updateGrid();
|
|
463
|
+
accItem.push(currentItem);
|
|
464
|
+
}
|
|
465
|
+
return accItem;
|
|
466
|
+
},
|
|
467
|
+
[previewItem as LayoutItem]
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const hasMoveHandle = $derived(!!moveHandle);
|
|
472
|
+
</script>
|
|
473
|
+
|
|
474
|
+
<div
|
|
475
|
+
role="group"
|
|
476
|
+
class={`${classes ?? ''} ${active ? (activeClass ?? '') : ''}`}
|
|
477
|
+
class:item-default={!classes}
|
|
478
|
+
class:active-default={!activeClass && active}
|
|
479
|
+
class:non-active-default={!active}
|
|
480
|
+
onpointerdown={_movable && !hasMoveHandle ? moveStart : undefined}
|
|
481
|
+
style={`position: absolute; left:${left}px; top:${top}px; width: ${width}px; height: ${height}px;
|
|
482
|
+
${_movable && !hasMoveHandle ? 'cursor: move;' : ''} touch-action: none; user-select: none;
|
|
483
|
+
${style}`}
|
|
484
|
+
bind:this={itemRef}
|
|
485
|
+
>
|
|
486
|
+
{#if _movable && moveHandle}
|
|
487
|
+
{@render moveHandle({ moveStart })}
|
|
488
|
+
{/if}
|
|
489
|
+
|
|
490
|
+
{#if children}
|
|
491
|
+
{@render children({ id, active, w, h })}
|
|
492
|
+
{/if}
|
|
493
|
+
|
|
494
|
+
{#if _resizable}
|
|
495
|
+
{#if resizeHandle}
|
|
496
|
+
{@render resizeHandle({ resizeStart })}
|
|
497
|
+
{:else}
|
|
498
|
+
<div
|
|
499
|
+
role="button"
|
|
500
|
+
tabindex="0"
|
|
501
|
+
class={resizerClass ?? ''}
|
|
502
|
+
class:resizer-default={!resizerClass}
|
|
503
|
+
onpointerdown={resizeStart}
|
|
504
|
+
></div>
|
|
505
|
+
{/if}
|
|
506
|
+
{/if}
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{#if active && gridParams.itemSize}
|
|
510
|
+
{@const preview = calcPosition(previewItem, {
|
|
511
|
+
itemSize: gridParams.itemSize,
|
|
512
|
+
gap: gridParams.gap
|
|
513
|
+
})}
|
|
514
|
+
<div
|
|
515
|
+
class={previewClass ?? ''}
|
|
516
|
+
class:item-preview-default={!previewClass}
|
|
517
|
+
style={`position: absolute; left:${preview.left}px; top:${preview.top}px;
|
|
518
|
+
width: ${preview.width}px; height: ${preview.height}px; z-index: -10;`}
|
|
519
|
+
></div>
|
|
520
|
+
{/if}
|
|
521
|
+
|
|
522
|
+
<style>
|
|
523
|
+
.item-default {
|
|
524
|
+
transition:
|
|
525
|
+
width 0.2s,
|
|
526
|
+
height 0.2s;
|
|
527
|
+
transition:
|
|
528
|
+
transform 0.2s,
|
|
529
|
+
opacity 0.2s;
|
|
530
|
+
}
|
|
531
|
+
.active-default {
|
|
532
|
+
opacity: 0.7;
|
|
533
|
+
}
|
|
534
|
+
.item-preview-default {
|
|
535
|
+
background-color: rgb(192, 127, 127);
|
|
536
|
+
transition: all 0.2s;
|
|
537
|
+
}
|
|
538
|
+
.non-active-default {
|
|
539
|
+
transition:
|
|
540
|
+
left 0.2s,
|
|
541
|
+
top 0.2s;
|
|
542
|
+
transition-timing-function: ease-in-out;
|
|
543
|
+
}
|
|
544
|
+
.resizer-default {
|
|
545
|
+
-webkit-user-select: none;
|
|
546
|
+
-moz-user-select: none;
|
|
547
|
+
user-select: none;
|
|
548
|
+
touch-action: none;
|
|
549
|
+
position: absolute;
|
|
550
|
+
width: 20px;
|
|
551
|
+
height: 20px;
|
|
552
|
+
right: 0;
|
|
553
|
+
bottom: 0;
|
|
554
|
+
cursor: se-resize;
|
|
555
|
+
}
|
|
556
|
+
.resizer-default::after {
|
|
557
|
+
content: '';
|
|
558
|
+
position: absolute;
|
|
559
|
+
right: 3px;
|
|
560
|
+
bottom: 3px;
|
|
561
|
+
width: 5px;
|
|
562
|
+
height: 5px;
|
|
563
|
+
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
|
564
|
+
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
|
565
|
+
}
|
|
566
|
+
</style>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import Grid from './Grid.svelte';
|
|
2
|
+
import type { LayoutItem, LayoutChangeDetail, GridController, GridItemProps, Collision } from './types';
|
|
3
|
+
export { Grid, type LayoutItem, type LayoutChangeDetail, type GridController, type GridItemProps, type Collision };
|
|
4
|
+
export { default as GridItem } from './GridItem.svelte';
|
|
5
|
+
export default Grid;
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { RequireAtLeastOne } from './utils/types';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
export type LayoutItem = Size & Position & {
|
|
4
|
+
id: string;
|
|
5
|
+
min?: Size;
|
|
6
|
+
max?: Size;
|
|
7
|
+
movable: boolean;
|
|
8
|
+
resizable: boolean;
|
|
9
|
+
invalidate: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Item position in grid units
|
|
13
|
+
*/
|
|
14
|
+
export type Size = {
|
|
15
|
+
w: number;
|
|
16
|
+
h: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Item position in grid units
|
|
20
|
+
*/
|
|
21
|
+
export type Position = {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Item position in pixels
|
|
27
|
+
*/
|
|
28
|
+
export type ItemPosition = {
|
|
29
|
+
left: number;
|
|
30
|
+
top: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Item size in pixels
|
|
34
|
+
*/
|
|
35
|
+
export type ItemSize = {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
};
|
|
39
|
+
export type ItemChangeEvent = {
|
|
40
|
+
id: number;
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
w: number;
|
|
44
|
+
h: number;
|
|
45
|
+
};
|
|
46
|
+
export type BreakpointKey = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
|
|
47
|
+
export type Breakpoints = Record<BreakpointKey, number>;
|
|
48
|
+
export type GridSize = number | RequireAtLeastOne<Breakpoints>;
|
|
49
|
+
export type GridDimensions = {
|
|
50
|
+
cols: number;
|
|
51
|
+
rows: number;
|
|
52
|
+
};
|
|
53
|
+
export type GridParams = {
|
|
54
|
+
cols: number;
|
|
55
|
+
rows: number;
|
|
56
|
+
itemSize?: ItemSize;
|
|
57
|
+
gap: number;
|
|
58
|
+
maxCols: number;
|
|
59
|
+
maxRows: number;
|
|
60
|
+
bounds: boolean;
|
|
61
|
+
boundsTo?: HTMLElement;
|
|
62
|
+
items: Record<string, LayoutItem>;
|
|
63
|
+
readOnly: boolean;
|
|
64
|
+
debug: boolean;
|
|
65
|
+
collision: Collision;
|
|
66
|
+
registerItem: (item: LayoutItem) => void;
|
|
67
|
+
unregisterItem: (item: LayoutItem) => void;
|
|
68
|
+
updateGrid: () => void;
|
|
69
|
+
onchange: (detail: LayoutChangeDetail) => void;
|
|
70
|
+
};
|
|
71
|
+
export type LayoutChangeDetail = {
|
|
72
|
+
item: LayoutItem;
|
|
73
|
+
};
|
|
74
|
+
export type Collision = 'none' | 'push' | 'compress';
|
|
75
|
+
export type GridController = {
|
|
76
|
+
gridParams: GridParams;
|
|
77
|
+
getFirstAvailablePosition: (w: number, h: number) => Position | null;
|
|
78
|
+
compress: () => void;
|
|
79
|
+
};
|
|
80
|
+
export type MoveHandleSnippetProps = {
|
|
81
|
+
moveStart: (event: PointerEvent) => void;
|
|
82
|
+
};
|
|
83
|
+
export type ResizeHandleSnippetProps = {
|
|
84
|
+
resizeStart: (event: PointerEvent) => void;
|
|
85
|
+
};
|
|
86
|
+
export type GridItemDefaultSnippetProps = {
|
|
87
|
+
id: string;
|
|
88
|
+
active: boolean;
|
|
89
|
+
w: number;
|
|
90
|
+
h: number;
|
|
91
|
+
};
|
|
92
|
+
export type GridItemProps = {
|
|
93
|
+
id?: string;
|
|
94
|
+
x: number;
|
|
95
|
+
y: number;
|
|
96
|
+
w?: number;
|
|
97
|
+
h?: number;
|
|
98
|
+
min?: Size;
|
|
99
|
+
max?: Size;
|
|
100
|
+
movable?: boolean;
|
|
101
|
+
resizable?: boolean;
|
|
102
|
+
class?: string;
|
|
103
|
+
activeClass?: string;
|
|
104
|
+
previewClass?: string;
|
|
105
|
+
resizerClass?: string;
|
|
106
|
+
style?: string;
|
|
107
|
+
onchange?: (detail: LayoutChangeDetail) => void;
|
|
108
|
+
onpreviewchange?: (detail: LayoutChangeDetail) => void;
|
|
109
|
+
children?: Snippet<[GridItemDefaultSnippetProps]>;
|
|
110
|
+
moveHandle?: Snippet<[MoveHandleSnippetProps]>;
|
|
111
|
+
resizeHandle?: Snippet<[ResizeHandleSnippetProps]>;
|
|
112
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|