@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
package/dist/Grid.svelte
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import type { GridParams } from './types';
|
|
4
|
+
|
|
5
|
+
const GRID_CONTEXT_NAME = Symbol('svelte-grid-extended-context');
|
|
6
|
+
|
|
7
|
+
export function getGridContext(): { current: GridParams } {
|
|
8
|
+
const context = getContext<{ current: GridParams }>(GRID_CONTEXT_NAME);
|
|
9
|
+
if (context === undefined) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`<GridItem /> is missing a parent <Grid /> component. Make sure you are using the component inside a <Grid />.`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { setContext, onMount, type Snippet } from 'svelte';
|
|
20
|
+
|
|
21
|
+
import { assertGridOptions } from './utils/assert';
|
|
22
|
+
import { findGridSize } from './utils/breakpoints';
|
|
23
|
+
import { getGridDimensions } from './utils/grid';
|
|
24
|
+
import { GridController } from './GridController';
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
Breakpoints,
|
|
28
|
+
ItemSize,
|
|
29
|
+
GridSize,
|
|
30
|
+
LayoutItem,
|
|
31
|
+
LayoutChangeDetail,
|
|
32
|
+
Collision,
|
|
33
|
+
GridController as GridControllerType
|
|
34
|
+
} from './types';
|
|
35
|
+
|
|
36
|
+
type Props = {
|
|
37
|
+
cols?: GridSize;
|
|
38
|
+
rows?: GridSize;
|
|
39
|
+
itemSize?: Partial<ItemSize>;
|
|
40
|
+
gap?: number;
|
|
41
|
+
breakpoints?: Breakpoints;
|
|
42
|
+
bounds?: boolean;
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
debug?: boolean;
|
|
45
|
+
class?: string;
|
|
46
|
+
style?: string;
|
|
47
|
+
collision?: Collision;
|
|
48
|
+
autoCompress?: boolean;
|
|
49
|
+
controller?: GridControllerType;
|
|
50
|
+
onchange?: (detail: LayoutChangeDetail) => void;
|
|
51
|
+
children?: Snippet;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
cols = 0,
|
|
56
|
+
rows = 0,
|
|
57
|
+
itemSize = {},
|
|
58
|
+
gap = 10,
|
|
59
|
+
breakpoints = {
|
|
60
|
+
xxl: 1536,
|
|
61
|
+
xl: 1280,
|
|
62
|
+
lg: 1024,
|
|
63
|
+
md: 768,
|
|
64
|
+
sm: 640,
|
|
65
|
+
xs: 320
|
|
66
|
+
},
|
|
67
|
+
bounds = false,
|
|
68
|
+
readOnly = false,
|
|
69
|
+
debug = false,
|
|
70
|
+
class: classes = '',
|
|
71
|
+
style = '',
|
|
72
|
+
collision = 'none',
|
|
73
|
+
autoCompress = true,
|
|
74
|
+
controller = $bindable(),
|
|
75
|
+
onchange,
|
|
76
|
+
children
|
|
77
|
+
}: Props = $props();
|
|
78
|
+
|
|
79
|
+
let items: Record<string, LayoutItem> = $state({});
|
|
80
|
+
|
|
81
|
+
$effect(() => {
|
|
82
|
+
assertGridOptions({ cols, rows, itemSize, collision });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let _cols = $state(typeof cols === 'number' ? cols : 0);
|
|
86
|
+
let _rows = $state(typeof rows === 'number' ? rows : 0);
|
|
87
|
+
let maxCols = $state(Infinity);
|
|
88
|
+
let maxRows = $state(Infinity);
|
|
89
|
+
let shouldExpandRows = $state(false);
|
|
90
|
+
let shouldExpandCols = $state(false);
|
|
91
|
+
let gridItemSize = $state<ItemSize | undefined>(
|
|
92
|
+
itemSize?.width && itemSize?.height ? { ...itemSize } as ItemSize : undefined
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const calculatedGridSize = $derived(getGridDimensions(Object.values(items)));
|
|
96
|
+
|
|
97
|
+
$effect(() => {
|
|
98
|
+
if (typeof cols === 'number') _cols = cols;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
$effect(() => {
|
|
102
|
+
if (typeof rows === 'number') _rows = rows;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
$effect(() => {
|
|
106
|
+
if (itemSize?.width && itemSize?.height) {
|
|
107
|
+
gridItemSize = { ...itemSize } as ItemSize;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
$effect(() => {
|
|
112
|
+
_cols = shouldExpandCols ? calculatedGridSize.cols : _cols;
|
|
113
|
+
maxCols = shouldExpandCols ? Infinity : _cols;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
$effect(() => {
|
|
117
|
+
_rows = shouldExpandRows ? calculatedGridSize.rows : _rows;
|
|
118
|
+
maxRows = shouldExpandRows ? Infinity : _rows;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (collision !== 'none') {
|
|
123
|
+
_rows = 0;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const containerWidth = $derived.by(() => {
|
|
128
|
+
if (gridItemSize && cols === 0) {
|
|
129
|
+
return _cols * (gridItemSize.width + gap + 1);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const containerHeight = $derived.by(() => {
|
|
135
|
+
if (gridItemSize && rows === 0) {
|
|
136
|
+
return _rows * (gridItemSize.height + gap + 1);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
let gridContainer: HTMLDivElement;
|
|
142
|
+
|
|
143
|
+
function updateGrid() {
|
|
144
|
+
items = { ...items };
|
|
145
|
+
|
|
146
|
+
if (autoCompress && collision === 'compress') {
|
|
147
|
+
_controller._internalCompress();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onMount(() => {
|
|
152
|
+
const sizeObserver = new ResizeObserver((entries) => {
|
|
153
|
+
if (entries.length > 1) {
|
|
154
|
+
throw new Error('that observer must have only one entry');
|
|
155
|
+
}
|
|
156
|
+
const entry = entries[0];
|
|
157
|
+
|
|
158
|
+
const width = entry.contentRect.width;
|
|
159
|
+
const height = entry.contentRect.height;
|
|
160
|
+
|
|
161
|
+
_cols = findGridSize(cols, width, breakpoints);
|
|
162
|
+
_rows = findGridSize(rows, height, breakpoints);
|
|
163
|
+
|
|
164
|
+
shouldExpandCols = _cols === 0;
|
|
165
|
+
shouldExpandRows = _rows === 0;
|
|
166
|
+
|
|
167
|
+
gridItemSize = {
|
|
168
|
+
width: itemSize.width ?? (width - (_cols + 1) * gap) / _cols,
|
|
169
|
+
height: itemSize.height ?? (height - (_rows + 1) * gap) / _rows
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
sizeObserver.observe(gridContainer);
|
|
174
|
+
|
|
175
|
+
return () => sizeObserver.disconnect();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
function registerItem(item: LayoutItem): void {
|
|
179
|
+
if (item.id in items) {
|
|
180
|
+
throw new Error(`Item with id ${item.id} already exists`);
|
|
181
|
+
}
|
|
182
|
+
items[item.id] = item;
|
|
183
|
+
updateGrid();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function unregisterItem(item: LayoutItem): void {
|
|
187
|
+
delete items[item.id];
|
|
188
|
+
updateGrid();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleChange(detail: LayoutChangeDetail): void {
|
|
192
|
+
onchange?.(detail);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Create a reactive object that can be accessed from context
|
|
196
|
+
const gridSettings = $derived<GridParams>({
|
|
197
|
+
cols: _cols,
|
|
198
|
+
rows: _rows,
|
|
199
|
+
maxCols,
|
|
200
|
+
maxRows,
|
|
201
|
+
gap,
|
|
202
|
+
itemSize: gridItemSize,
|
|
203
|
+
items,
|
|
204
|
+
bounds,
|
|
205
|
+
boundsTo: gridContainer!,
|
|
206
|
+
readOnly,
|
|
207
|
+
debug,
|
|
208
|
+
collision,
|
|
209
|
+
registerItem,
|
|
210
|
+
unregisterItem,
|
|
211
|
+
updateGrid,
|
|
212
|
+
onchange: handleChange
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Use a getter-based context object for proper reactivity
|
|
216
|
+
// The getter ensures we always get the current derived value
|
|
217
|
+
const contextRef = {
|
|
218
|
+
get current() {
|
|
219
|
+
return gridSettings;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const _controller = new GridController(contextRef);
|
|
224
|
+
|
|
225
|
+
controller = _controller as GridControllerType;
|
|
226
|
+
|
|
227
|
+
setContext(GRID_CONTEXT_NAME, contextRef);
|
|
228
|
+
</script>
|
|
229
|
+
|
|
230
|
+
<div
|
|
231
|
+
class={`svelte-grid-extended ${classes}`}
|
|
232
|
+
bind:this={gridContainer}
|
|
233
|
+
style={`width: ${containerWidth ? `${containerWidth}px` : '100%'};
|
|
234
|
+
height: ${containerHeight ? `${containerHeight}px` : '100%'}; ${style}`}
|
|
235
|
+
>
|
|
236
|
+
{#if gridItemSize}
|
|
237
|
+
{@render children?.()}
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<style>
|
|
242
|
+
.svelte-grid-extended {
|
|
243
|
+
position: relative !important;
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GridParams } from './types';
|
|
2
|
+
export declare function getGridContext(): {
|
|
3
|
+
current: GridParams;
|
|
4
|
+
};
|
|
5
|
+
import { type Snippet } from 'svelte';
|
|
6
|
+
import type { Breakpoints, ItemSize, GridSize, LayoutChangeDetail, Collision, GridController as GridControllerType } from './types';
|
|
7
|
+
type Props = {
|
|
8
|
+
cols?: GridSize;
|
|
9
|
+
rows?: GridSize;
|
|
10
|
+
itemSize?: Partial<ItemSize>;
|
|
11
|
+
gap?: number;
|
|
12
|
+
breakpoints?: Breakpoints;
|
|
13
|
+
bounds?: boolean;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
style?: string;
|
|
18
|
+
collision?: Collision;
|
|
19
|
+
autoCompress?: boolean;
|
|
20
|
+
controller?: GridControllerType;
|
|
21
|
+
onchange?: (detail: LayoutChangeDetail) => void;
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
};
|
|
24
|
+
declare const Grid: import("svelte").Component<Props, {}, "controller">;
|
|
25
|
+
type Grid = ReturnType<typeof Grid>;
|
|
26
|
+
export default Grid;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GridParams, GridController as GridControllerType } from './types';
|
|
2
|
+
export declare class GridController implements GridControllerType {
|
|
3
|
+
private _gridParamsRef;
|
|
4
|
+
constructor(gridParamsOrRef: GridParams | {
|
|
5
|
+
current: GridParams;
|
|
6
|
+
});
|
|
7
|
+
get gridParams(): GridParams;
|
|
8
|
+
set gridParams(value: GridParams);
|
|
9
|
+
getFirstAvailablePosition(w: number, h: number): {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
} | null;
|
|
13
|
+
compress(): void;
|
|
14
|
+
_internalCompress(): void;
|
|
15
|
+
private _compress;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getAvailablePosition, hasCollisions } from './utils/grid';
|
|
2
|
+
export class GridController {
|
|
3
|
+
_gridParamsRef;
|
|
4
|
+
constructor(gridParamsOrRef) {
|
|
5
|
+
// Support both direct gridParams (for tests/external use) and reference object (for internal use)
|
|
6
|
+
if ('current' in gridParamsOrRef) {
|
|
7
|
+
this._gridParamsRef = gridParamsOrRef;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// Wrap direct gridParams in a reference object
|
|
11
|
+
this._gridParamsRef = { current: gridParamsOrRef };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
get gridParams() {
|
|
15
|
+
return this._gridParamsRef.current;
|
|
16
|
+
}
|
|
17
|
+
set gridParams(value) {
|
|
18
|
+
// Update the reference for external updates
|
|
19
|
+
this._gridParamsRef.current = value;
|
|
20
|
+
}
|
|
21
|
+
getFirstAvailablePosition(w, h) {
|
|
22
|
+
const { items, maxCols, maxRows } = this.gridParams;
|
|
23
|
+
return getAvailablePosition({
|
|
24
|
+
id: '',
|
|
25
|
+
x: 0,
|
|
26
|
+
y: 0,
|
|
27
|
+
w,
|
|
28
|
+
h,
|
|
29
|
+
movable: true,
|
|
30
|
+
resizable: true,
|
|
31
|
+
invalidate: () => {
|
|
32
|
+
/* .. */
|
|
33
|
+
}
|
|
34
|
+
}, Object.values(items), maxCols, maxRows);
|
|
35
|
+
}
|
|
36
|
+
compress() {
|
|
37
|
+
this._compress(this.gridParams.items);
|
|
38
|
+
}
|
|
39
|
+
_internalCompress() {
|
|
40
|
+
this._compress(this.gridParams.items, true);
|
|
41
|
+
}
|
|
42
|
+
_compress(items, skipUpdate = false) {
|
|
43
|
+
const gridItems = Object.values(items);
|
|
44
|
+
const sortedItems = [...gridItems].sort((a, b) => a.y - b.y);
|
|
45
|
+
sortedItems.reduce((accItem, currentItem) => {
|
|
46
|
+
let newY = currentItem.y;
|
|
47
|
+
while (newY >= 0) {
|
|
48
|
+
if (hasCollisions({ ...currentItem, y: newY }, accItem)) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
newY--;
|
|
52
|
+
}
|
|
53
|
+
if (newY !== currentItem.y - 1) {
|
|
54
|
+
currentItem.y = newY + 1;
|
|
55
|
+
currentItem.invalidate();
|
|
56
|
+
}
|
|
57
|
+
accItem.push(currentItem);
|
|
58
|
+
return accItem;
|
|
59
|
+
}, []);
|
|
60
|
+
if (!skipUpdate) {
|
|
61
|
+
this.gridParams.updateGrid();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|