@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.
@@ -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
+ }