Skip to content

Commit 35214cb

Browse files
authored
👹 Add gridController.compress() method and autoCompress prop (#69)
* demo * Add compress method to grid controller * update readme
1 parent d608bb0 commit 35214cb

File tree

7 files changed

+229
-15
lines changed

7 files changed

+229
-15
lines changed

README.md

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -374,15 +374,16 @@ Setting `collision` prop to `compress` will compress items vertically towards th
374374

375375
### Grid props
376376

377-
| prop | description | type | default |
378-
| --------- | ---------------------------------------------------------------------------------- | ----------------------------------- | ------- |
379-
| cols | Grid columns count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
380-
| rows | Grid rows count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
381-
| itemSize | Size of the grid item. If not set, grid will calculate it based on container size. | { width?: number, height?: number } | {} |
382-
| gap | Gap between grid items. | number | 10 |
383-
| bounds | Should grid items be bounded by the grid container. | boolean | false |
384-
| readonly | If true disables interaction with grid items. | boolean | false |
385-
| collision | Collision behavior of grid items. [About](#collision-behavior) | none \| push \| compress | none |
377+
| prop | description | type | default |
378+
| ------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- |
379+
| cols | Grid columns count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
380+
| rows | Grid rows count. If set to 0, grid will grow infinitly. Must be >= 0. | number | 0 |
381+
| itemSize | Size of the grid item. If not set, grid will calculate it based on container size. | { width?: number, height?: number } | {} |
382+
| gap | Gap between grid items. | number | 10 |
383+
| bounds | Should grid items be bounded by the grid container. | boolean | false |
384+
| readonly | If true disables interaction with grid items. | boolean | false |
385+
| collision | Collision behavior of grid items. [About](#collision-behavior) | none \| push \| compress | none |
386+
| autoCompress | Auto compress the grid items when programmatically changing grid items. Only works with 'compress' collision strategy. | boolean | true |
386387

387388
> ⚠️ if `cols` or/and `rows` are set to 0, `itemSize.width` or/and `itemSize.height` must be setted.
388389
@@ -519,6 +520,43 @@ Finds the first available position within the grid that can accommodate an item
519520
</Grid>
520521
```
521522

523+
#### compress()
524+
525+
Compresses all items vertically towards the top into any available space.
526+
527+
##### Example
528+
529+
[repl](https://svelte.dev/repl/79bcc70f11944d9e9b03970de731b3e2?version=4.2.11)
530+
531+
```svelte
532+
<script lang="ts">
533+
import Grid, { GridItem, type GridController } from 'svelte-grid-extended';
534+
535+
let items = [
536+
{ id: '1', x: 0, y: 0, w: 2, h: 5 },
537+
{ id: '2', x: 2, y: 2, w: 2, h: 2 }
538+
];
539+
540+
let gridController: GridController;
541+
542+
function compressItems() {
543+
gridController.compress();
544+
}
545+
546+
const itemSize = { height: 40 };
547+
</script>
548+
549+
<button class="btn" on:click={compressItems}>Compress Items</button>
550+
551+
<Grid {itemSize} cols={10} collision="push" bind:controller={gridController}>
552+
{#each items as item (item.id)}
553+
<GridItem id={item.id} bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
554+
<div class="item">{item.id.slice(0, 5)}</div>
555+
</GridItem>
556+
{/each}
557+
</Grid>
558+
```
559+
522560
## 📜 License
523561

524562
MIT

src/lib/Grid.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
LayoutItem,
2727
LayoutChangeDetail,
2828
GridParams,
29-
Collision
29+
Collision,
30+
GridController as GridControllerType
3031
} from './types';
3132
import { writable, type Readable, type Writable } from 'svelte/store';
3233
@@ -116,6 +117,13 @@
116117
*/
117118
export let collision: Collision = 'none';
118119
120+
/**
121+
* Auto compress the grid items when programmatically changing grid items.
122+
* Only works with 'compress' collision strategy.
123+
* @default true
124+
*/
125+
export let autoCompress = true;
126+
119127
let _cols: number;
120128
121129
let _rows: number;
@@ -175,6 +183,10 @@
175183
*/
176184
function updateGrid() {
177185
items = items;
186+
187+
if (autoCompress && collision === 'compress') {
188+
controller.compress();
189+
}
178190
}
179191
180192
onMount(() => {
@@ -209,12 +221,12 @@
209221
throw new Error(`Item with id ${item.id} already exists`);
210222
}
211223
items[item.id] = item;
212-
items = items;
224+
updateGrid();
213225
}
214226
215227
function unregisterItem(item: LayoutItem): void {
216228
delete items[item.id];
217-
items = items;
229+
updateGrid();
218230
}
219231
220232
const gridSettings = writable<GridParams>({
@@ -248,7 +260,7 @@
248260
collision
249261
}));
250262
251-
export const controller = new GridController($gridSettings);
263+
export const controller: GridControllerType = new GridController($gridSettings);
252264
$: controller.gridParams = $gridSettings;
253265
254266
setContext(GRID_CONTEXT_NAME, gridSettings);

src/lib/GridController.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getAvailablePosition } from './utils/grid';
2-
import type { GridParams, GridController as GridControllerType } from './types';
1+
import { getAvailablePosition, hasCollisions } from './utils/grid';
2+
import type { GridParams, GridController as GridControllerType, LayoutItem } from './types';
33

44
export class GridController implements GridControllerType {
55
gridParams: GridParams;
@@ -28,4 +28,31 @@ export class GridController implements GridControllerType {
2828
maxRows
2929
);
3030
}
31+
32+
compress(): void {
33+
this._compress(this.gridParams.items);
34+
}
35+
36+
private _compress(items: Record<string, LayoutItem>): void {
37+
const gridItems = Object.values(items);
38+
const sortedItems = [...gridItems].sort((a, b) => a.y - b.y);
39+
40+
sortedItems.reduce((accItem, currentItem) => {
41+
let newY = currentItem.y;
42+
while (newY >= 0) {
43+
if (hasCollisions({ ...currentItem, y: newY }, accItem)) {
44+
break;
45+
}
46+
newY--;
47+
}
48+
if (newY !== currentItem.y - 1) {
49+
currentItem.y = newY + 1;
50+
currentItem.invalidate();
51+
}
52+
accItem.push(currentItem);
53+
return accItem;
54+
}, [] as LayoutItem[]);
55+
56+
this.gridParams.updateGrid();
57+
}
3158
}

src/lib/GridItem.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
$: item.max = max;
114114
$: item.movable = movable;
115115
$: item.resizable = resizable;
116+
$: item, invalidate();
116117
117118
/**
118119
* Updates svelte-components props behind that item. Should be called when the item
@@ -122,6 +123,7 @@
122123
({ x, y, w, h } = item);
123124
dispatch('change', { item });
124125
$gridParams.dispatch('change', { item });
126+
$gridParams.updateGrid();
125127
}
126128
127129
onMount(() => {

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,5 @@ export type Collision = 'none' | 'push' | 'compress';
7373
export type GridController = {
7474
gridParams: GridParams;
7575
getFirstAvailablePosition: (w: number, h: number) => Position | null;
76+
compress: () => void;
7677
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script lang="ts">
2+
import Grid, { GridItem, type GridController } from '$lib';
3+
4+
let items = [
5+
{ id: '1', x: 0, y: 0, w: 2, h: 5 },
6+
{ id: '2', x: 2, y: 2, w: 2, h: 2 }
7+
];
8+
9+
let gridController: GridController;
10+
11+
function compressItems() {
12+
gridController.compress();
13+
}
14+
15+
const itemSize = { height: 40 };
16+
</script>
17+
18+
<button class="btn" on:click={compressItems}>Compress Items</button>
19+
20+
<Grid {itemSize} cols={10} collision="push" bind:controller={gridController}>
21+
{#each items as item (item.id)}
22+
<GridItem id={item.id} bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
23+
<div class="item">{item.id.slice(0, 5)}</div>
24+
</GridItem>
25+
{/each}
26+
</Grid>
27+
28+
<style>
29+
.item {
30+
display: grid;
31+
place-items: center;
32+
background-color: rgb(150, 150, 150);
33+
width: 100%;
34+
height: 100%;
35+
}
36+
.btn {
37+
margin-top: 10px;
38+
margin-left: 10px;
39+
right: 2px;
40+
top: 1px;
41+
}
42+
</style>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<script lang="ts">
2+
import { fade } from 'svelte/transition';
3+
import Grid, { GridItem, type GridController } from '$lib';
4+
5+
let items = [
6+
{ id: crypto.randomUUID(), x: 0, y: 0, w: 2, h: 5 },
7+
{ id: crypto.randomUUID(), x: 2, y: 2, w: 2, h: 2 },
8+
{ id: crypto.randomUUID(), x: 2, y: 0, w: 1, h: 2 },
9+
{ id: crypto.randomUUID(), x: 3, y: 0, w: 2, h: 2 },
10+
{ id: crypto.randomUUID(), x: 4, y: 2, w: 1, h: 3 },
11+
{ id: crypto.randomUUID(), x: 8, y: 0, w: 2, h: 8 },
12+
{ id: crypto.randomUUID(), x: 4, y: 5, w: 1, h: 1 },
13+
{ id: crypto.randomUUID(), x: 2, y: 6, w: 3, h: 2 },
14+
{ id: crypto.randomUUID(), x: 2, y: 4, w: 2, h: 2 }
15+
];
16+
17+
type Collision = 'none' | 'push' | 'compress';
18+
19+
let collision: Collision = 'compress';
20+
21+
const itemsBackup = structuredClone(items);
22+
23+
const itemSize = { height: 40 };
24+
25+
function resetGrid() {
26+
items = structuredClone(itemsBackup);
27+
}
28+
29+
function remove(id: string) {
30+
items = items.filter((i) => i.id !== id);
31+
}
32+
33+
let gridController: GridController;
34+
35+
function addNewItem() {
36+
const w = Math.floor(Math.random() * 2) + 1;
37+
const h = Math.floor(Math.random() * 5) + 1;
38+
const newPosition = gridController.getFirstAvailablePosition(w, h);
39+
items = newPosition
40+
? [...items, { id: crypto.randomUUID(), x: newPosition.x, y: newPosition.y, w, h }]
41+
: items;
42+
}
43+
44+
function moveAll() {
45+
items = items.map((i) => ({ ...i, x: i.x, y: i.y + 1 }));
46+
}
47+
</script>
48+
49+
<button class="btn" on:click={addNewItem}>Add New Item</button>
50+
<button class="btn" on:click={resetGrid}>Reset Grid</button>
51+
<button class="btn" on:click={moveAll}>Move all</button>
52+
53+
<button class="btn" on:click={() => (collision = 'none')}>No collision</button>
54+
<button class="btn" on:click={() => (collision = 'push')}>Push</button>
55+
<button class="btn" on:click={() => (collision = 'compress')}>Compress</button>
56+
57+
<Grid {itemSize} cols={10} {collision} bind:controller={gridController}>
58+
{#each items as item (item.id)}
59+
<GridItem id={item.id} bind:x={item.x} bind:y={item.y} bind:w={item.w} bind:h={item.h}>
60+
<button
61+
on:pointerdown={(e) => e.stopPropagation()}
62+
on:click={() => remove(item.id)}
63+
class="remove"
64+
>
65+
66+
</button>
67+
<div class="item">{item.id.slice(0, 5)}</div>
68+
</GridItem>
69+
{/each}
70+
</Grid>
71+
72+
<style>
73+
.item {
74+
display: grid;
75+
place-items: center;
76+
background-color: rgb(150, 150, 150);
77+
width: 100%;
78+
height: 100%;
79+
}
80+
.remove {
81+
cursor: pointer;
82+
position: absolute;
83+
right: 10px;
84+
top: 3px;
85+
}
86+
.btn {
87+
margin-top: 10px;
88+
margin-left: 10px;
89+
right: 2px;
90+
top: 1px;
91+
}
92+
</style>

0 commit comments

Comments
 (0)