@appulsauce/svelte-grid 2.0.0 → 2.0.1
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/README.md +56 -506
- package/dist/Grid.svelte +5 -7
- package/dist/GridItem.svelte +17 -41
- package/dist/utils/breakpoints.js +1 -2
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,555 +1,105 @@
|
|
|
1
1
|
# @appulsauce/svelte-grid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A draggable and resizable grid layout component for Svelte 5.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import Grid, { GridItem, type GridController } from '@appulsauce/svelte-grid';
|
|
8
|
-
|
|
9
|
-
let items = $state([
|
|
10
|
-
{ id: '1', x: 0, y: 0, w: 2, h: 5 },
|
|
11
|
-
{ id: '2', x: 2, y: 2, w: 2, h: 2 }
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
let gridController: GridController;
|
|
15
|
-
|
|
16
|
-
function addNewItem() {
|
|
17
|
-
const w = Math.floor(Math.random() * 2) + 1;
|
|
18
|
-
const h = Math.floor(Math.random() * 5) + 1;
|
|
19
|
-
const newPosition = gridController.getFirstAvailablePosition(w, h);
|
|
20
|
-
items = newPosition
|
|
21
|
-
? [...items, { id: crypto.randomUUID(), x: newPosition.x, y: newPosition.y, w, h }]
|
|
22
|
-
: items;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const itemSize = { height: 40 };
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<button onclick={addNewItem}>Add New Item</button>
|
|
29
|
-
|
|
30
|
-
<Grid {itemSize} cols={10} collision="push" bind:controller={gridController}>
|
|
31
|
-
{#each items as { id, x, y, w, h } (id)}
|
|
32
|
-
<GridItem {id} bind:x bind:y bind:w bind:h>
|
|
33
|
-
{#snippet children()}
|
|
34
|
-
<div>{id}</div>
|
|
35
|
-
{/snippet}
|
|
36
|
-
</GridItem>
|
|
37
|
-
{/each}
|
|
38
|
-
</Grid>
|
|
39
|
-
```
|
|
40
|
-
- [GridItem props](#griditem-props)
|
|
41
|
-
- [Style related props:](#style-related-props)
|
|
42
|
-
- [Events](#events)
|
|
43
|
-
- [Grid Controller](#grid-controller)
|
|
44
|
-
- [Methods](#methods)
|
|
45
|
-
- [getFirstAvailablePosition(w, h)](#getfirstavailablepositionw-h)
|
|
46
|
-
- [Example](#example)
|
|
47
|
-
- [License](#-license)
|
|
48
|
-
|
|
49
|
-
## Usage
|
|
50
|
-
|
|
51
|
-
### Basic
|
|
52
|
-
|
|
53
|
-
```svelte
|
|
54
|
-
<script lang="ts">
|
|
55
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
56
|
-
</script>
|
|
57
|
-
|
|
58
|
-
<Grid cols={10} rows={10}>
|
|
59
|
-
<GridItem x={1} y={0} class="item">{#snippet children()}Hey{/snippet}</GridItem>
|
|
60
|
-
<GridItem x={3} y={3} w={4} class="item">{#snippet children()}Hoy{/snippet}</GridItem>
|
|
61
|
-
</Grid>
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Static grid
|
|
65
|
-
|
|
66
|
-
When `cols` or `rows` and `itemsSize` are set, grid becomes static and ignores the size of the container.
|
|
67
|
-
|
|
68
|
-
It can be set to both dimensions or just one.
|
|
69
|
-
|
|
70
|
-
Both:
|
|
71
|
-
|
|
72
|
-
```svelte
|
|
73
|
-
<script lang="ts">
|
|
74
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
75
|
-
|
|
76
|
-
const itemSize = { width: 100, height: 40 };
|
|
77
|
-
</script>
|
|
78
|
-
|
|
79
|
-
<Grid {itemSize} cols={10} rows={10}>
|
|
80
|
-
<GridItem x={1} y={0} class="item">{#snippet children()}Hey{/snippet}</GridItem>
|
|
81
|
-
<GridItem x={3} y={3} w={4} class="item">{#snippet children()}Hoy{/snippet}</GridItem>
|
|
82
|
-
</Grid>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Only rows:
|
|
86
|
-
|
|
87
|
-
```svelte
|
|
88
|
-
<script lang="ts">
|
|
89
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
90
|
-
|
|
91
|
-
const itemSize = { height: 40 };
|
|
92
|
-
</script>
|
|
93
|
-
|
|
94
|
-
<Grid {itemSize} cols={10} rows={10}>
|
|
95
|
-
<GridItem x={1} y={0} class="item">{#snippet children()}Hey{/snippet}</GridItem>
|
|
96
|
-
<GridItem x={3} y={3} w={4} class="item">{#snippet children()}Hoy{/snippet}</GridItem>
|
|
97
|
-
</Grid>
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Grid without bounds
|
|
101
|
-
|
|
102
|
-
When `cols` or/and `rows` set to 0, grid grows infinitely. The grid container adapts its width and height to fit all elements.
|
|
103
|
-
|
|
104
|
-
It can be set to both dimensions or just one.
|
|
105
|
-
|
|
106
|
-
```svelte
|
|
107
|
-
<script lang="ts">
|
|
108
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
109
|
-
|
|
110
|
-
const itemSize = { width: 100, height: 40 };
|
|
111
|
-
</script>
|
|
112
|
-
|
|
113
|
-
<Grid {itemSize} cols={0} rows={0}>
|
|
114
|
-
<GridItem x={1} y={0} class="item">{#snippet children()}Hey{/snippet}</GridItem>
|
|
115
|
-
<GridItem x={3} y={3} w={4} class="item">{#snippet children()}Hoy{/snippet}</GridItem>
|
|
116
|
-
</Grid>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Styling
|
|
120
|
-
|
|
121
|
-
Grid can be styled with classes passed to various props. Check [Style related props](#style-related-props) section for more info.
|
|
122
|
-
|
|
123
|
-
```svelte
|
|
124
|
-
<script lang="ts">
|
|
125
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<Grid class="grid-container" cols={10} rows={10}>
|
|
129
|
-
<GridItem
|
|
130
|
-
x={0}
|
|
131
|
-
y={0}
|
|
132
|
-
class="grid-item"
|
|
133
|
-
activeClass="grid-item-active"
|
|
134
|
-
previewClass="bg-green-500 rounded"
|
|
135
|
-
resizerClass=""
|
|
136
|
-
>
|
|
137
|
-
{#snippet children()}
|
|
138
|
-
<div class="item">Content</div>
|
|
139
|
-
{/snippet}
|
|
140
|
-
</GridItem>
|
|
141
|
-
</Grid>
|
|
142
|
-
|
|
143
|
-
<style>
|
|
144
|
-
:global(.grid-container) {
|
|
145
|
-
opacity: 0.7;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
:global(.grid-item) {
|
|
149
|
-
transition:
|
|
150
|
-
width 4s,
|
|
151
|
-
height 4s;
|
|
152
|
-
transition:
|
|
153
|
-
transform 4s,
|
|
154
|
-
opacity 4s;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
:global(.grid-item-active) {
|
|
158
|
-
opacity: 0.1;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
:global(.bg-green-500) {
|
|
162
|
-
background-color: rgb(34, 197, 94);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
:global(.rounded) {
|
|
166
|
-
border-radius: 0.25rem;
|
|
167
|
-
}
|
|
168
|
-
</style>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Disable interactions
|
|
172
|
-
|
|
173
|
-
To disable interactions, set `readOnly` prop to `true`. Or set `movable` and/or `resizable` to `false` on specific item.
|
|
174
|
-
|
|
175
|
-
Read Only grid:
|
|
176
|
-
|
|
177
|
-
```svelte
|
|
178
|
-
<script lang="ts">
|
|
179
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
180
|
-
</script>
|
|
181
|
-
|
|
182
|
-
<Grid cols={10} rows={10} readOnly>
|
|
183
|
-
<GridItem x={1} y={0} class="item">{#snippet children()}Hey{/snippet}</GridItem>
|
|
184
|
-
<GridItem x={3} y={3} w={4} class="item">{#snippet children()}Hoy{/snippet}</GridItem>
|
|
185
|
-
</Grid>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Make item non-interactive:
|
|
189
|
-
|
|
190
|
-
```svelte
|
|
191
|
-
<script lang="ts">
|
|
192
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
193
|
-
</script>
|
|
194
|
-
|
|
195
|
-
<Grid cols={10} rows={10}>
|
|
196
|
-
<GridItem x={1} y={0} class="item" movable={false}>{#snippet children()}Hey{/snippet}</GridItem>
|
|
197
|
-
<GridItem x={3} y={3} w={4} class="item" resizable={false}>{#snippet children()}Hoy{/snippet}</GridItem>
|
|
198
|
-
</Grid>
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Collision Behavior
|
|
202
|
-
|
|
203
|
-
The `collision` prop controls how the grid handles collisions. There are three available options: `none`, `push`, and `compress`.
|
|
204
|
-
|
|
205
|
-
#### None
|
|
206
|
-
|
|
207
|
-
Setting `collision` prop to `none` will ignore any collisions. This is the default behavior.
|
|
208
|
-
|
|
209
|
-
```svelte
|
|
210
|
-
<script lang="ts">
|
|
211
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
212
|
-
|
|
213
|
-
let items = $state([
|
|
214
|
-
{ id: '0', x: 0, y: 0, w: 2, h: 5 },
|
|
215
|
-
{ id: '1', x: 2, y: 2, w: 2, h: 2 },
|
|
216
|
-
{ id: '2', x: 2, y: 0, w: 1, h: 2 },
|
|
217
|
-
{ id: '3', x: 3, y: 0, w: 2, h: 2 },
|
|
218
|
-
{ id: '4', x: 4, y: 2, w: 1, h: 3 },
|
|
219
|
-
{ id: '5', x: 8, y: 0, w: 2, h: 8 }
|
|
220
|
-
]);
|
|
221
|
-
|
|
222
|
-
const itemSize = { height: 40 };
|
|
223
|
-
</script>
|
|
224
|
-
|
|
225
|
-
<Grid {itemSize} cols={10} collision="none">
|
|
226
|
-
{#each items as item (item.id)}
|
|
227
|
-
<GridItem x={item.x} y={item.y} w={item.w} h={item.h}>
|
|
228
|
-
{#snippet children()}
|
|
229
|
-
<div class="item">{item.id}</div>
|
|
230
|
-
{/snippet}
|
|
231
|
-
</GridItem>
|
|
232
|
-
{/each}
|
|
233
|
-
</Grid>
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
#### Push
|
|
237
|
-
|
|
238
|
-
Setting `collision` prop to `push` will cause grid items to move to the first available space when colliding. The grid will grow vertically as needed to accommodate all items.
|
|
239
|
-
|
|
240
|
-
```svelte
|
|
241
|
-
<script lang="ts">
|
|
242
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
243
|
-
|
|
244
|
-
let items = $state([
|
|
245
|
-
{ id: '0', x: 0, y: 0, w: 2, h: 5 },
|
|
246
|
-
{ id: '1', x: 2, y: 2, w: 2, h: 2 },
|
|
247
|
-
{ id: '2', x: 2, y: 0, w: 1, h: 2 },
|
|
248
|
-
{ id: '3', x: 3, y: 0, w: 2, h: 2 },
|
|
249
|
-
{ id: '4', x: 4, y: 2, w: 1, h: 3 },
|
|
250
|
-
{ id: '5', x: 8, y: 0, w: 2, h: 8 }
|
|
251
|
-
]);
|
|
252
|
-
|
|
253
|
-
const itemSize = { height: 40 };
|
|
254
|
-
</script>
|
|
255
|
-
|
|
256
|
-
<Grid {itemSize} cols={10} collision="push">
|
|
257
|
-
{#each items as item (item.id)}
|
|
258
|
-
<GridItem x={item.x} y={item.y} w={item.w} h={item.h}>
|
|
259
|
-
{#snippet children()}
|
|
260
|
-
<div class="item">{item.id}</div>
|
|
261
|
-
{/snippet}
|
|
262
|
-
</GridItem>
|
|
263
|
-
{/each}
|
|
264
|
-
</Grid>
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
#### Compress
|
|
268
|
-
|
|
269
|
-
Setting `collision` prop to `compress` will compress items vertically towards the top into any available space when colliding. The grid will grow vertically as needed to accommodate all items.
|
|
270
|
-
|
|
271
|
-
```svelte
|
|
272
|
-
<script lang="ts">
|
|
273
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
274
|
-
|
|
275
|
-
let items = $state([
|
|
276
|
-
{ id: '0', x: 0, y: 0, w: 2, h: 5 },
|
|
277
|
-
{ id: '1', x: 2, y: 2, w: 2, h: 2 },
|
|
278
|
-
{ id: '2', x: 2, y: 0, w: 1, h: 2 },
|
|
279
|
-
{ id: '3', x: 3, y: 0, w: 2, h: 2 },
|
|
280
|
-
{ id: '4', x: 4, y: 2, w: 1, h: 3 },
|
|
281
|
-
{ id: '5', x: 8, y: 0, w: 2, h: 8 }
|
|
282
|
-
]);
|
|
283
|
-
|
|
284
|
-
const itemSize = { height: 40 };
|
|
285
|
-
</script>
|
|
5
|
+
[](https://www.npmjs.com/package/@appulsauce/svelte-grid)
|
|
6
|
+
[](https://github.com/KaiErikNiermann/svelte-grid-extended/blob/main/LICENSE)
|
|
286
7
|
|
|
287
|
-
|
|
288
|
-
{#each items as item (item.id)}
|
|
289
|
-
<GridItem x={item.x} y={item.y} w={item.w} h={item.h}>
|
|
290
|
-
{#snippet children()}
|
|
291
|
-
<div class="item">{item.id}</div>
|
|
292
|
-
{/snippet}
|
|
293
|
-
</GridItem>
|
|
294
|
-
{/each}
|
|
295
|
-
</Grid>
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
> Setting `collision` to `push` or `compress` will set `rows` to `0` so `ItemSize.height` must be set.
|
|
299
|
-
|
|
300
|
-
### Custom move/resize handle
|
|
301
|
-
|
|
302
|
-
You can customize the move and resize handles using snippets:
|
|
303
|
-
|
|
304
|
-
```svelte
|
|
305
|
-
<script lang="ts">
|
|
306
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
307
|
-
</script>
|
|
8
|
+
## Installation
|
|
308
9
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
{#snippet moveHandle(moveStart)}
|
|
312
|
-
<div onpointerdown={moveStart}>MOVE</div>
|
|
313
|
-
{/snippet}
|
|
314
|
-
|
|
315
|
-
{#snippet resizeHandle(resizeStart)}
|
|
316
|
-
<div onpointerdown={resizeStart}>RESIZE</div>
|
|
317
|
-
{/snippet}
|
|
318
|
-
|
|
319
|
-
{#snippet children()}
|
|
320
|
-
<!-- content -->
|
|
321
|
-
<div>Content here</div>
|
|
322
|
-
{/snippet}
|
|
323
|
-
</GridItem>
|
|
324
|
-
</Grid>
|
|
10
|
+
```bash
|
|
11
|
+
npm install @appulsauce/svelte-grid
|
|
325
12
|
```
|
|
326
13
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
```svelte
|
|
330
|
-
<script lang="ts">
|
|
331
|
-
import Grid, { GridItem } from '@appulsauce/svelte-grid';
|
|
332
|
-
|
|
333
|
-
let items = $state([
|
|
334
|
-
{ x: 6, y: 0, w: 2, h: 2, data: { text: 'A' } },
|
|
335
|
-
{ x: 6, y: 2, w: 2, h: 2, data: { text: 'B' } }
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
const itemsBackup = structuredClone($state.snapshot(items));
|
|
339
|
-
|
|
340
|
-
function resetGrid() {
|
|
341
|
-
items = structuredClone(itemsBackup);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const itemSize = { height: 40 };
|
|
345
|
-
</script>
|
|
346
|
-
|
|
347
|
-
<button onclick={resetGrid}>RESET</button>
|
|
348
|
-
|
|
349
|
-
<Grid cols={10} {itemSize}>
|
|
350
|
-
{#each items as item (item.data.text)}
|
|
351
|
-
<GridItem bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
|
|
352
|
-
{#snippet children()}
|
|
353
|
-
{item.data.text}
|
|
354
|
-
{/snippet}
|
|
355
|
-
</GridItem>
|
|
356
|
-
{/each}
|
|
357
|
-
</Grid>
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
## API Documentation
|
|
361
|
-
|
|
362
|
-
### Grid props
|
|
363
|
-
|
|
364
|
-
| prop | description | type | default |
|
|
365
|
-
| ------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- |
|
|
366
|
-
| cols | Grid columns count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
|
|
367
|
-
| rows | Grid rows count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
|
|
368
|
-
| itemSize | Size of the grid item. If not set, grid will calculate it based on container size. | { width?: number, height?: number } | {} |
|
|
369
|
-
| gap | Gap between grid items. | number | 10 |
|
|
370
|
-
| bounds | Should grid items be bounded by the grid container. | boolean | false |
|
|
371
|
-
| readonly | If true disables interaction with grid items. | boolean | false |
|
|
372
|
-
| collision | Collision behavior of grid items. [About](#collision-behavior) | none \| push \| compress | none |
|
|
373
|
-
| autoCompress | Auto compress the grid items when programmatically changing grid items. Only works with 'compress' collision strategy. | boolean | true |
|
|
374
|
-
| onchange | Callback when grid items are changed. | (detail: LayoutChangeDetail) => void | undefined |
|
|
375
|
-
|
|
376
|
-
> If `cols` or/and `rows` are set to 0, `itemSize.width` or/and `itemSize.height` must be set.
|
|
377
|
-
|
|
378
|
-
> Setting `collision` to `push` or `compress` will set `rows` to `0` so `ItemSize.height` must be set.
|
|
379
|
-
|
|
380
|
-
### GridItem props
|
|
381
|
-
|
|
382
|
-
| prop | description | type | default |
|
|
383
|
-
| --------- | -------------------------------------------------------------------------------------------------- | ------------------------------------- | -------------- |
|
|
384
|
-
| id | Unique id of the item. Used to compare items during collision tests | string | uuid.v4 |
|
|
385
|
-
| x | X position of the item in grid units. | number | required |
|
|
386
|
-
| y | Y position of the item in grid units. | number | required |
|
|
387
|
-
| w | Width of the item in grid units. | number | 1 |
|
|
388
|
-
| h | Height of the item in grid units. | number | 1 |
|
|
389
|
-
| min | Minimum size of the item in Grid Units. | { w: number, h: number } | { w: 1, h: 1 } |
|
|
390
|
-
| max | Maximum size of the item in Grid Units. If not provided, the item will be able to grow infinitely. | { w: number, h: number } \| undefined | undefined |
|
|
391
|
-
| movable | If true, item can be moved by user. | boolean | true |
|
|
392
|
-
| resizable | If true, item can be resized by user. | boolean | true |
|
|
393
|
-
| onchange | Callback when item position/size changes. | (detail: LayoutChangeDetail) => void | undefined |
|
|
394
|
-
| onpreviewchange | Callback when preview position/size changes during drag. | (detail: LayoutChangeDetail) => void | undefined |
|
|
395
|
-
|
|
396
|
-
### Style related props:
|
|
397
|
-
|
|
398
|
-
Component can be styled with css framework of your choice, global classes or `style` prop. To do so, you can use the following props:
|
|
399
|
-
|
|
400
|
-
- `<Grid class="..." />` - class name for grid container.
|
|
401
|
-
- `<Grid style="..." />` - inline style for grid container.
|
|
402
|
-
- `<GridItem class="..." />` - class name for grid item.
|
|
403
|
-
- `<GridItem activeClass="..." />` - class name that applies when item is currently being dragged or resized. By default, it is used to make active grid item transparent.
|
|
404
|
-
- `<GridItem previewClass="..." />` - class name for preview where item will be placed after interaction.
|
|
405
|
-
- `<GridItem resizerClass="..." />` - class name for item's resize handle.
|
|
406
|
-
- `<GridItem style="..." />` - inline style for grid item.
|
|
407
|
-
|
|
408
|
-
To understand how to use these props, look at `<Grid />` component simplified structure.
|
|
409
|
-
|
|
410
|
-
> `active` is a variable that indicates if grid item is currently being dragged or resized:
|
|
411
|
-
|
|
412
|
-
```svelte
|
|
413
|
-
<!-- Grid -->
|
|
414
|
-
<div class={class}>
|
|
415
|
-
<!-- GridItem -->
|
|
416
|
-
<div class={itemClass} class:activeClass={active}>
|
|
417
|
-
{@render children?.()}
|
|
418
|
-
<!-- Resizer -->
|
|
419
|
-
<div class={resizerClass} />
|
|
420
|
-
<!-- Resizer -->
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
{#if active}
|
|
424
|
-
<!-- GridItemGhost (preview) -->
|
|
425
|
-
<div class={previewClass} />
|
|
426
|
-
{/if}
|
|
427
|
-
|
|
428
|
-
<!-- /GridItem -->
|
|
429
|
-
</div>
|
|
430
|
-
<!-- /Grid -->
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
##### Example
|
|
14
|
+
## Quick Start
|
|
435
15
|
|
|
436
16
|
```svelte
|
|
437
17
|
<script lang="ts">
|
|
438
18
|
import Grid, { GridItem, type GridController } from '@appulsauce/svelte-grid';
|
|
439
19
|
|
|
440
20
|
let items = $state([
|
|
441
|
-
{ id: '1', x: 0, y: 0, w: 2, h:
|
|
442
|
-
{ id: '2', x: 2, y:
|
|
21
|
+
{ id: '1', x: 0, y: 0, w: 2, h: 2 },
|
|
22
|
+
{ id: '2', x: 2, y: 0, w: 3, h: 1 }
|
|
443
23
|
]);
|
|
444
24
|
|
|
445
25
|
let gridController: GridController;
|
|
446
26
|
|
|
447
|
-
function
|
|
448
|
-
gridController.
|
|
27
|
+
function addItem() {
|
|
28
|
+
const position = gridController.getFirstAvailablePosition(2, 2);
|
|
29
|
+
if (position) {
|
|
30
|
+
items = [...items, { id: crypto.randomUUID(), ...position, w: 2, h: 2 }];
|
|
31
|
+
}
|
|
449
32
|
}
|
|
450
33
|
|
|
451
|
-
const itemSize = { height:
|
|
34
|
+
const itemSize = { height: 50 };
|
|
452
35
|
</script>
|
|
453
36
|
|
|
454
|
-
<button
|
|
37
|
+
<button onclick={addItem}>Add Item</button>
|
|
455
38
|
|
|
456
39
|
<Grid {itemSize} cols={10} collision="push" bind:controller={gridController}>
|
|
457
40
|
{#each items as item (item.id)}
|
|
458
41
|
<GridItem id={item.id} bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
|
|
459
42
|
{#snippet children()}
|
|
460
|
-
<div class="item">{item.id
|
|
43
|
+
<div class="item">{item.id}</div>
|
|
461
44
|
{/snippet}
|
|
462
45
|
</GridItem>
|
|
463
46
|
{/each}
|
|
464
47
|
</Grid>
|
|
465
48
|
```
|
|
466
|
-
### Methods
|
|
467
49
|
|
|
468
|
-
|
|
50
|
+
## Features
|
|
469
51
|
|
|
470
|
-
|
|
52
|
+
- **Draggable & Resizable** - Move and resize grid items with mouse or touch
|
|
53
|
+
- **Collision Detection** - Three modes: `none`, `push`, `compress`
|
|
54
|
+
- **Two-way Binding** - Bind item positions directly to your state
|
|
55
|
+
- **Custom Handles** - Use snippets for custom move/resize handles
|
|
56
|
+
- **Grid Controller** - Programmatic control with `getFirstAvailablePosition()` and `compress()`
|
|
57
|
+
- **TypeScript** - Full type support included
|
|
471
58
|
|
|
472
|
-
|
|
59
|
+
## Documentation
|
|
473
60
|
|
|
474
|
-
|
|
475
|
-
- `h` (number): Height of the item.
|
|
61
|
+
For full documentation and examples, visit the [docs site](https://github.com/KaiErikNiermann/svelte-grid-extended) or run the docs locally:
|
|
476
62
|
|
|
477
|
-
|
|
63
|
+
```bash
|
|
64
|
+
# Clone the repository
|
|
65
|
+
git clone https://github.com/KaiErikNiermann/svelte-grid-extended.git
|
|
66
|
+
cd svelte-grid-extended
|
|
478
67
|
|
|
479
|
-
|
|
68
|
+
# Install dependencies
|
|
69
|
+
pnpm install
|
|
70
|
+
pnpm docs:install
|
|
480
71
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
```svelte
|
|
484
|
-
<script lang="ts">
|
|
485
|
-
import Grid, { GridItem, type GridController } from '@appulsauce/svelte-grid';
|
|
486
|
-
|
|
487
|
-
let items = [
|
|
488
|
-
{ id: '1', x: 0, y: 0, w: 2, h: 5 },
|
|
489
|
-
{ id: '2', x: 2, y: 2, w: 2, h: 2 }
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
let gridController: GridController;
|
|
493
|
-
|
|
494
|
-
function addNewItem() {
|
|
495
|
-
const w = Math.floor(Math.random() * 2) + 1;
|
|
496
|
-
const h = Math.floor(Math.random() * 5) + 1;
|
|
497
|
-
const newPosition = gridController.getFirstAvailablePosition(w, h);
|
|
498
|
-
items = newPosition
|
|
499
|
-
? [...items, { id: crypto.randomUUID(), x: newPosition.x, y: newPosition.y, w, h }]
|
|
500
|
-
: items;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const itemSize = { height: 40 };
|
|
504
|
-
</script>
|
|
505
|
-
|
|
506
|
-
<button on:click={addNewItem}>Add New Item</button>
|
|
507
|
-
|
|
508
|
-
<Grid {itemSize} cols={10} collision="push" bind:controller={gridController}>
|
|
509
|
-
{#each items as { id, x, y, w, h } (id)}
|
|
510
|
-
<div transition:fade={{ duration: 300 }}>
|
|
511
|
-
<GridItem {id} bind:x bind:y bind:w bind:h>
|
|
512
|
-
<div>{id}</div>
|
|
513
|
-
</GridItem>
|
|
514
|
-
</div>
|
|
515
|
-
{/each}
|
|
516
|
-
</Grid>
|
|
72
|
+
# Run docs site
|
|
73
|
+
pnpm docs:dev
|
|
517
74
|
```
|
|
518
75
|
|
|
519
|
-
|
|
76
|
+
## API Overview
|
|
520
77
|
|
|
521
|
-
|
|
78
|
+
### Grid Props
|
|
522
79
|
|
|
523
|
-
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
| ----------- | -------------------------------- | -------- | -------------------------------- |
|
|
82
|
+
| `cols` | `number` | `0` | Number of columns (0 = infinite) |
|
|
83
|
+
| `rows` | `number` | `0` | Number of rows (0 = infinite) |
|
|
84
|
+
| `itemSize` | `{ width?, height? }` | `{}` | Fixed cell size in pixels |
|
|
85
|
+
| `gap` | `number` | `10` | Gap between items |
|
|
86
|
+
| `collision` | `'none' \| 'push' \| 'compress'` | `'none'` | Collision behavior |
|
|
87
|
+
| `readOnly` | `boolean` | `false` | Disable all interactions |
|
|
524
88
|
|
|
525
|
-
|
|
526
|
-
<script lang="ts">
|
|
527
|
-
import Grid, { GridItem, type GridController } from '@appulsauce/svelte-grid';
|
|
528
|
-
|
|
529
|
-
let items = [
|
|
530
|
-
{ id: '1', x: 0, y: 0, w: 2, h: 5 },
|
|
531
|
-
{ id: '2', x: 2, y: 2, w: 2, h: 2 }
|
|
532
|
-
];
|
|
533
|
-
|
|
534
|
-
let gridController: GridController;
|
|
89
|
+
### GridItem Props
|
|
535
90
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
91
|
+
| Prop | Type | Default | Description |
|
|
92
|
+
| ----------- | --------- | -------- | ------------------------ |
|
|
93
|
+
| `x` | `number` | required | X position in grid units |
|
|
94
|
+
| `y` | `number` | required | Y position in grid units |
|
|
95
|
+
| `w` | `number` | `1` | Width in grid units |
|
|
96
|
+
| `h` | `number` | `1` | Height in grid units |
|
|
97
|
+
| `movable` | `boolean` | `true` | Can be dragged |
|
|
98
|
+
| `resizable` | `boolean` | `true` | Can be resized |
|
|
542
99
|
|
|
543
|
-
|
|
100
|
+
## Requirements
|
|
544
101
|
|
|
545
|
-
|
|
546
|
-
{#each items as item (item.id)}
|
|
547
|
-
<GridItem id={item.id} bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
|
|
548
|
-
<div class="item">{item.id.slice(0, 5)}</div>
|
|
549
|
-
</GridItem>
|
|
550
|
-
{/each}
|
|
551
|
-
</Grid>
|
|
552
|
-
```
|
|
102
|
+
- Svelte 5.0.0+
|
|
553
103
|
|
|
554
104
|
## License
|
|
555
105
|
|
package/dist/Grid.svelte
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import type { GridParams } from './types';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
const GRID_CONTEXT_NAME = Symbol('svelte-grid-extended-context');
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
export function getGridContext(): { current: GridParams } {
|
|
8
8
|
const context = getContext<{ current: GridParams }>(GRID_CONTEXT_NAME);
|
|
9
9
|
if (context === undefined) {
|
|
@@ -82,15 +82,13 @@
|
|
|
82
82
|
assertGridOptions({ cols, rows, itemSize, collision });
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
let _cols = $state(
|
|
86
|
-
let _rows = $state(
|
|
85
|
+
let _cols = $state(0);
|
|
86
|
+
let _rows = $state(0);
|
|
87
87
|
let maxCols = $state(Infinity);
|
|
88
88
|
let maxRows = $state(Infinity);
|
|
89
89
|
let shouldExpandRows = $state(false);
|
|
90
90
|
let shouldExpandCols = $state(false);
|
|
91
|
-
let gridItemSize = $state<ItemSize | undefined>(
|
|
92
|
-
itemSize?.width && itemSize?.height ? { ...itemSize } as ItemSize : undefined
|
|
93
|
-
);
|
|
91
|
+
let gridItemSize = $state<ItemSize | undefined>(undefined);
|
|
94
92
|
|
|
95
93
|
const calculatedGridSize = $derived(getGridDimensions(Object.values(items)));
|
|
96
94
|
|
package/dist/GridItem.svelte
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
coordinate2size,
|
|
6
|
-
calcPosition,
|
|
7
|
-
snapOnMove,
|
|
8
|
-
snapOnResize,
|
|
9
|
-
type SnapGridParams
|
|
10
|
-
} from './utils/item';
|
|
4
|
+
import { coordinate2size, calcPosition, snapOnMove, snapOnResize, type SnapGridParams } from './utils/item';
|
|
11
5
|
import { hasCollisions, getCollisions, getAvailablePosition } from './utils/grid';
|
|
12
6
|
|
|
13
|
-
import type { LayoutItem,
|
|
7
|
+
import type { LayoutItem, GridItemProps } from './types';
|
|
14
8
|
import { getGridContext } from './Grid.svelte';
|
|
15
9
|
|
|
16
10
|
let {
|
|
@@ -37,7 +31,7 @@
|
|
|
37
31
|
|
|
38
32
|
// Get grid context - this returns a reactive reference wrapper
|
|
39
33
|
const gridParamsRef = getGridContext();
|
|
40
|
-
|
|
34
|
+
|
|
41
35
|
// Access current within reactive contexts ($effect, $derived, template)
|
|
42
36
|
const gridParams = $derived(gridParamsRef.current);
|
|
43
37
|
|
|
@@ -51,20 +45,21 @@
|
|
|
51
45
|
* Item object that is used in `gridParams.items`.
|
|
52
46
|
*/
|
|
53
47
|
let item = $state<LayoutItem>({
|
|
54
|
-
id,
|
|
55
|
-
x,
|
|
56
|
-
y,
|
|
57
|
-
w,
|
|
58
|
-
h,
|
|
59
|
-
min,
|
|
60
|
-
max,
|
|
61
|
-
movable,
|
|
62
|
-
resizable,
|
|
48
|
+
id: '',
|
|
49
|
+
x: 0,
|
|
50
|
+
y: 0,
|
|
51
|
+
w: 1,
|
|
52
|
+
h: 1,
|
|
53
|
+
min: { w: 1, h: 1 },
|
|
54
|
+
max: undefined,
|
|
55
|
+
movable: true,
|
|
56
|
+
resizable: true,
|
|
63
57
|
invalidate
|
|
64
58
|
});
|
|
65
59
|
|
|
66
60
|
// Sync props to item
|
|
67
61
|
$effect(() => {
|
|
62
|
+
item.id = id;
|
|
68
63
|
item.x = x;
|
|
69
64
|
item.y = y;
|
|
70
65
|
item.w = w;
|
|
@@ -157,14 +152,11 @@
|
|
|
157
152
|
|
|
158
153
|
const _movable = $derived(!gridParams.readOnly && movable);
|
|
159
154
|
|
|
160
|
-
let pointerShift = { left: 0, top: 0 };
|
|
161
|
-
|
|
162
155
|
function moveStart(event: PointerEvent) {
|
|
163
156
|
if (event.button !== 0) return;
|
|
164
157
|
event.stopPropagation();
|
|
165
158
|
initInteraction(event);
|
|
166
159
|
initialPosition = { left, top };
|
|
167
|
-
pointerShift = { left: event.pageX - left, top: event.pageY - top };
|
|
168
160
|
window.addEventListener('pointermove', move);
|
|
169
161
|
window.addEventListener('pointerup', moveEnd);
|
|
170
162
|
}
|
|
@@ -209,12 +201,7 @@
|
|
|
209
201
|
}
|
|
210
202
|
|
|
211
203
|
function updateCollItemPositionWithPush(collItem: LayoutItem, items: LayoutItem[]) {
|
|
212
|
-
const newPosition = getAvailablePosition(
|
|
213
|
-
collItem,
|
|
214
|
-
items,
|
|
215
|
-
gridParams.maxCols,
|
|
216
|
-
gridParams.maxRows
|
|
217
|
-
);
|
|
204
|
+
const newPosition = getAvailablePosition(collItem, items, gridParams.maxCols, gridParams.maxRows);
|
|
218
205
|
if (newPosition) {
|
|
219
206
|
const { x, y } = newPosition;
|
|
220
207
|
collItem.x = x;
|
|
@@ -224,12 +211,7 @@
|
|
|
224
211
|
gridParams.updateGrid();
|
|
225
212
|
}
|
|
226
213
|
|
|
227
|
-
function handleCollisionsForPreviewItemWithPush(newAttributes: {
|
|
228
|
-
x?: number;
|
|
229
|
-
y?: number;
|
|
230
|
-
w?: number;
|
|
231
|
-
h?: number;
|
|
232
|
-
}) {
|
|
214
|
+
function handleCollisionsForPreviewItemWithPush(newAttributes: { x?: number; y?: number; w?: number; h?: number }) {
|
|
233
215
|
const gridItems = Object.values(gridParams.items);
|
|
234
216
|
const itemsExceptPreview = gridItems.filter((item) => item.id != previewItem.id);
|
|
235
217
|
const collItems = getCollisions({ ...previewItem, ...newAttributes }, itemsExceptPreview);
|
|
@@ -306,7 +288,6 @@
|
|
|
306
288
|
function moveEnd(event: PointerEvent) {
|
|
307
289
|
if (event.button !== 0) return;
|
|
308
290
|
endInteraction(event);
|
|
309
|
-
pointerShift = { left: 0, top: 0 };
|
|
310
291
|
window.removeEventListener('pointermove', move);
|
|
311
292
|
window.removeEventListener('pointerup', moveEnd);
|
|
312
293
|
}
|
|
@@ -394,10 +375,7 @@
|
|
|
394
375
|
const hGap = newH - previewItem.h;
|
|
395
376
|
previewItem = { ...previewItem, w: newW, h: newH };
|
|
396
377
|
applyPreview();
|
|
397
|
-
const collItems = getCollisions(
|
|
398
|
-
{ ...previewItem, w: newW, h: 9999 },
|
|
399
|
-
Object.values(gridParams.items)
|
|
400
|
-
);
|
|
378
|
+
const collItems = getCollisions({ ...previewItem, w: newW, h: 9999 }, Object.values(gridParams.items));
|
|
401
379
|
collItems.forEach((i) => {
|
|
402
380
|
i.y += hGap;
|
|
403
381
|
i.invalidate();
|
|
@@ -432,9 +410,7 @@
|
|
|
432
410
|
} else if (previewItem.y < currentItem.y + currentItem.h) {
|
|
433
411
|
// compress items above previewItem
|
|
434
412
|
const maxY =
|
|
435
|
-
currentItem.y >= previewItem.y
|
|
436
|
-
? currentItem.y + previewItem.h + 1
|
|
437
|
-
: previewItem.y + currentItem.h + 1;
|
|
413
|
+
currentItem.y >= previewItem.y ? currentItem.y + previewItem.h + 1 : previewItem.y + currentItem.h + 1;
|
|
438
414
|
let newY = maxY;
|
|
439
415
|
while (newY >= 0) {
|
|
440
416
|
if (hasCollisions({ ...currentItem, y: newY }, accItem)) {
|
|
@@ -2,8 +2,7 @@ export function findGridSize(cols, width, breakpoints) {
|
|
|
2
2
|
if (typeof cols == 'number') {
|
|
3
3
|
return cols;
|
|
4
4
|
}
|
|
5
|
-
return Object.entries(cols).reduce((acc, obj) => Math.abs(width - breakpoints[obj[0]]) <
|
|
6
|
-
Math.abs(width - breakpoints[acc[0]])
|
|
5
|
+
return Object.entries(cols).reduce((acc, obj) => Math.abs(width - breakpoints[obj[0]]) < Math.abs(width - breakpoints[acc[0]])
|
|
7
6
|
? obj
|
|
8
7
|
: acc)[1];
|
|
9
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appulsauce/svelte-grid",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A draggable and resizable grid layout, for Svelte 5",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "vite dev",
|
|
11
11
|
"build": "vite build && pnpm package",
|
|
12
|
+
"package:build": "svelte-kit sync && svelte-package",
|
|
12
13
|
"preview": "vite preview",
|
|
13
14
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
14
15
|
"prepublishOnly": "pnpm package",
|
|
15
16
|
"test": "vitest",
|
|
17
|
+
"pretest:unit": "svelte-kit sync",
|
|
16
18
|
"test:unit": "vitest run",
|
|
17
19
|
"coverage": "vitest run --coverage",
|
|
18
20
|
"e2e": "playwright test",
|
|
@@ -20,7 +22,11 @@
|
|
|
20
22
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
21
23
|
"lint": "prettier --check . && eslint .",
|
|
22
24
|
"format": "prettier --write .",
|
|
23
|
-
"prepare": "husky"
|
|
25
|
+
"prepare": "husky",
|
|
26
|
+
"docs:dev": "pnpm --dir docs dev",
|
|
27
|
+
"docs:build": "pnpm --dir docs build",
|
|
28
|
+
"docs:preview": "pnpm --dir docs preview",
|
|
29
|
+
"docs:install": "pnpm --dir docs install"
|
|
24
30
|
},
|
|
25
31
|
"exports": {
|
|
26
32
|
".": {
|