svelte-flexiboards 0.1.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.
Files changed (44) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/components/flexi-add.svelte +35 -0
  3. package/dist/components/flexi-add.svelte.d.ts +16 -0
  4. package/dist/components/flexi-board.svelte +30 -0
  5. package/dist/components/flexi-board.svelte.d.ts +11 -0
  6. package/dist/components/flexi-delete.svelte +22 -0
  7. package/dist/components/flexi-delete.svelte.d.ts +13 -0
  8. package/dist/components/flexi-grab.svelte +20 -0
  9. package/dist/components/flexi-grab.svelte.d.ts +12 -0
  10. package/dist/components/flexi-grid.svelte +19 -0
  11. package/dist/components/flexi-grid.svelte.d.ts +8 -0
  12. package/dist/components/flexi-layout-loader.svelte +10 -0
  13. package/dist/components/flexi-layout-loader.svelte.d.ts +18 -0
  14. package/dist/components/flexi-resize.svelte +20 -0
  15. package/dist/components/flexi-resize.svelte.d.ts +12 -0
  16. package/dist/components/flexi-target-loader.svelte +10 -0
  17. package/dist/components/flexi-target-loader.svelte.d.ts +18 -0
  18. package/dist/components/flexi-target.svelte +93 -0
  19. package/dist/components/flexi-target.svelte.d.ts +42 -0
  20. package/dist/components/flexi-widget.svelte +37 -0
  21. package/dist/components/flexi-widget.svelte.d.ts +9 -0
  22. package/dist/components/index.d.ts +1 -0
  23. package/dist/components/index.js +1 -0
  24. package/dist/components/rendered-flexi-widget.svelte +44 -0
  25. package/dist/components/rendered-flexi-widget.svelte.d.ts +7 -0
  26. package/dist/index.d.ts +16 -0
  27. package/dist/index.js +10 -0
  28. package/dist/system/grid.svelte.d.ts +146 -0
  29. package/dist/system/grid.svelte.js +815 -0
  30. package/dist/system/index.d.ts +1 -0
  31. package/dist/system/index.js +1 -0
  32. package/dist/system/manage.svelte.d.ts +25 -0
  33. package/dist/system/manage.svelte.js +54 -0
  34. package/dist/system/provider.svelte.d.ts +51 -0
  35. package/dist/system/provider.svelte.js +290 -0
  36. package/dist/system/target.svelte.d.ts +179 -0
  37. package/dist/system/target.svelte.js +397 -0
  38. package/dist/system/types.d.ts +91 -0
  39. package/dist/system/types.js +1 -0
  40. package/dist/system/utils.svelte.d.ts +21 -0
  41. package/dist/system/utils.svelte.js +156 -0
  42. package/dist/system/widget.svelte.d.ts +225 -0
  43. package/dist/system/widget.svelte.js +447 -0
  44. package/package.json +57 -0
@@ -0,0 +1,815 @@
1
+ import { setContext, untrack } from "svelte";
2
+ import { getContext } from "svelte";
3
+ import { getInternalFlexitargetCtx } from "./target.svelte.js";
4
+ import { GridDimensionTracker } from "./utils.svelte.js";
5
+ const MAX_COLUMNS = 32;
6
+ export class FlexiGrid {
7
+ _target;
8
+ _targetConfig;
9
+ mouseCellPosition = $state({
10
+ x: 0,
11
+ y: 0
12
+ });
13
+ #ref = $state({ ref: null });
14
+ _dimensionTracker;
15
+ constructor(target, targetConfig) {
16
+ this._target = target;
17
+ this._targetConfig = targetConfig;
18
+ this._dimensionTracker = new GridDimensionTracker(this, targetConfig);
19
+ this.onpointermove = this.onpointermove.bind(this);
20
+ $effect(() => {
21
+ window.addEventListener('pointermove', this.onpointermove);
22
+ return () => {
23
+ window.removeEventListener('pointermove', this.onpointermove);
24
+ };
25
+ });
26
+ }
27
+ style = $derived.by(() => {
28
+ return `display: grid; grid-template-columns: ${this.#getSizing(this.columns, this._targetConfig.columnSizing)}; grid-template-rows: ${this.#getSizing(this.rows, this._targetConfig.rowSizing)};`;
29
+ });
30
+ #getSizing(axisCount, sizing) {
31
+ if (typeof sizing === "string") {
32
+ return `repeat(${axisCount}, ${sizing})`;
33
+ }
34
+ return sizing({ target: this._target, grid: this });
35
+ }
36
+ #updatePointerPosition(clientX, clientY) {
37
+ if (!this.ref) {
38
+ return;
39
+ }
40
+ const rawCell = this._dimensionTracker.getCellFromPointerPosition(clientX, clientY);
41
+ let cell = rawCell;
42
+ if (rawCell) {
43
+ const [x, y] = this.mapRawCellToFinalCell(rawCell.column, rawCell.row);
44
+ cell = {
45
+ row: y,
46
+ column: x
47
+ };
48
+ }
49
+ this.mouseCellPosition.x = cell?.column ?? 0;
50
+ this.mouseCellPosition.y = cell?.row ?? 0;
51
+ this._target.onmousegridcellmove({
52
+ cellX: this.mouseCellPosition.x,
53
+ cellY: this.mouseCellPosition.y
54
+ });
55
+ }
56
+ watchGridElementDimensions() {
57
+ if (!this.ref) {
58
+ return;
59
+ }
60
+ this._dimensionTracker.watchGrid();
61
+ }
62
+ onpointermove(event) {
63
+ this.#updatePointerPosition(event.clientX, event.clientY);
64
+ }
65
+ get ref() {
66
+ return this.#ref.ref;
67
+ }
68
+ set ref(ref) {
69
+ this.#ref.ref = ref;
70
+ }
71
+ }
72
+ /**
73
+ * Free-form Flexigrid Layout
74
+ *
75
+ * A grid layout where widgets are explicitly placed in particular cells, and the grid allows for gaps between widgets.
76
+ * A free grid can grow and shrink if required when enabled.
77
+ */
78
+ export class FreeFormFlexiGrid extends FlexiGrid {
79
+ #widgets = new Set();
80
+ #state = $state({
81
+ rows: 0,
82
+ columns: 0,
83
+ layout: [],
84
+ bitmaps: []
85
+ });
86
+ #targetConfig = $state();
87
+ #rawLayoutConfig = $derived(this.#targetConfig?.layout);
88
+ #layoutConfig = $derived({
89
+ type: "free",
90
+ expandColumns: this.#rawLayoutConfig?.expandColumns ?? false,
91
+ expandRows: this.#rawLayoutConfig?.expandRows ?? false,
92
+ minColumns: this.#rawLayoutConfig?.minColumns ?? this.#targetConfig.baseColumns ?? 1,
93
+ minRows: this.#rawLayoutConfig?.minRows ?? this.#targetConfig.baseRows ?? 1
94
+ });
95
+ constructor(target, targetConfig) {
96
+ super(target, targetConfig);
97
+ this.#targetConfig = targetConfig;
98
+ this.#rows = targetConfig.baseRows ?? 1;
99
+ this.#columns = targetConfig.baseColumns ?? 1;
100
+ this.#bitmaps = new Array(this.#rows).fill(0);
101
+ this.#layout = new Array(this.#rows).fill(new Array(this.#columns).fill(null));
102
+ }
103
+ tryPlaceWidget(widget, cellX, cellY, width, height) {
104
+ // Need both coordinates to place a widget.
105
+ if (cellX === undefined || cellY === undefined) {
106
+ throw new Error("Missing required x and y fields for a widget in a sparse target layout. The x- and y- coordinates of a widget cannot be automatically inferred in this context.");
107
+ }
108
+ // If no width or height is specified, default to 1.
109
+ width ??= 1;
110
+ height ??= 1;
111
+ // When placing a widget, we should constrain it so that it can only make so many more columns/rows more than the current grid size.
112
+ if (cellX >= this.#columns) {
113
+ cellX = this.#columns - 1;
114
+ }
115
+ if (cellY >= this.#rows) {
116
+ cellY = this.#rows - 1;
117
+ }
118
+ const widgetXBitmap = this.#createWidgetBitmap(widget, cellX, width);
119
+ const endCellX = cellX + width;
120
+ const endCellY = cellY + height;
121
+ // We need to try expand the grid if the widget is moving beyond the current bounds,
122
+ // but if this is not possible then the operation fails.
123
+ if (endCellX > this.#columns && !this.#tryExpandColumns(endCellX)) {
124
+ return false;
125
+ }
126
+ if (endCellY > this.#rows && !this.#tryExpandRows(endCellY)) {
127
+ return false;
128
+ }
129
+ // We'll use this to accumulate operations that will get carried out once all collisions are resolved
130
+ // successfully, in an all-or-nothing manner.
131
+ // Where a widget gets moved multiple times - ie it collides with multiple rows - we'll only
132
+ // carry out the operation once, for the maximal move.
133
+ const operations = new Map();
134
+ // Looking row-by-row, we can identify collisions using the bitmaps.
135
+ for (let i = cellY; i < cellY + height; i++) {
136
+ // Expand rows as necessary if this operation is supported.
137
+ while (this.#bitmaps.length <= i) {
138
+ this.#bitmaps.push(0);
139
+ this.#layout.push(new Array(this.#columns).fill(null));
140
+ }
141
+ // Intersection of the bitmaps will tell us whether there's a collision.
142
+ // If there isn't, check the next row.
143
+ if (!(this.#bitmaps[i] & widgetXBitmap)) {
144
+ continue;
145
+ }
146
+ // Find the first widget of this row that's intersecting, then push it out of the way
147
+ // which in turn will push any other widgets out of the way on this row.
148
+ for (let j = cellX; j < cellX + width; j++) {
149
+ const collidingWidget = this.#layout[i][j];
150
+ if (!collidingWidget) {
151
+ continue;
152
+ }
153
+ const xMoveBy = (cellX + width) - collidingWidget.x;
154
+ const yMoveBy = (cellY + height) - collidingWidget.y;
155
+ // PATCH: y-move seems to go weird, sometimes.
156
+ if (!this.#prepareMoveWidgetX(collidingWidget, xMoveBy, operations) && !this.#prepareMoveWidgetY(collidingWidget, yMoveBy, operations)) {
157
+ // If moving along either axis is not possible, then the overall operation is not possible.
158
+ return false;
159
+ }
160
+ // Don't need to do this again for this row.
161
+ break;
162
+ }
163
+ }
164
+ // No collisions, or we can resolve them.
165
+ // Apply all other necessary move operations.
166
+ for (const operation of operations.values()) {
167
+ this.#doMoveOperation(operation.widget, operation);
168
+ }
169
+ // Place the widget now that all other widgets have been moved out of the way.
170
+ for (let i = cellX; i < cellX + width; i++) {
171
+ for (let j = cellY; j < cellY + height; j++) {
172
+ this.#layout[j][i] = widget;
173
+ }
174
+ }
175
+ this.#widgets.add(widget);
176
+ // this._dimensionTracker.trackWidget(widget, widget.ref);
177
+ // Update the row bitmaps to reflect the widget's placement.
178
+ for (let i = cellY; i < cellY + height; i++) {
179
+ this.#bitmaps[i] |= widgetXBitmap;
180
+ }
181
+ widget.setBounds(cellX, cellY, width, height);
182
+ return true;
183
+ }
184
+ #prepareMoveWidgetX(widget, delta, operationMap) {
185
+ // If the widget is not draggable then we definitely can't push it.
186
+ if (!widget.draggable) {
187
+ return false;
188
+ }
189
+ const finalStartX = widget.x + delta;
190
+ const finalEndX = finalStartX + widget.width;
191
+ // Shortcut: if the widget is already being moved and this is further than the proposed move,
192
+ // then we can forgo remaining checks as the maximal move is the one that gets applied.
193
+ if (operationMap.has(widget)) {
194
+ const existingOperation = operationMap.get(widget);
195
+ if (existingOperation.newX >= finalStartX) {
196
+ return true;
197
+ }
198
+ }
199
+ // We need to try expand the grid if the widget is moving beyond the current bounds,
200
+ // but if this is not possible then the operation fails.
201
+ if (finalEndX > this.#columns && !this.#tryExpandColumns(finalEndX)) {
202
+ return false;
203
+ }
204
+ // If a widget lies between this widget's current position and its new end position then it's,
205
+ // by definition, colliding with it. We need to check if we can move the widget and collapse
206
+ // any gaps along the way.
207
+ for (let i = widget.y; i < widget.y + widget.height; i++) {
208
+ for (let j = widget.x + widget.width; j < finalEndX; j++) {
209
+ const cell = this.#layout[i][j];
210
+ // Empty cell
211
+ if (!cell) {
212
+ continue;
213
+ }
214
+ // A widget lies between this widget's current position and its new end position.
215
+ // Recurse to push it out of the way, and if that fails then the whole operation fails.
216
+ const xGapSize = j - (widget.x + widget.width);
217
+ const yGapSize = i - (widget.y + widget.height);
218
+ // NEXT: Heuristic to immediately cancel move if delta - gapSize > available space. Use #countSetBits for this
219
+ if (!this.#prepareMoveWidgetX(cell, delta - xGapSize, operationMap)) {
220
+ return false;
221
+ }
222
+ // We can move the colliding widget, we don't need to check this row any further as the move
223
+ // handles any subsequent collisions.
224
+ break;
225
+ }
226
+ }
227
+ // The move is possible. Add the operation to the accumulator so we can carry them out if all are OK.
228
+ const operation = {
229
+ widget,
230
+ oldX: widget.x,
231
+ oldY: widget.y,
232
+ newX: finalStartX,
233
+ newY: widget.y
234
+ };
235
+ operationMap.set(widget, operation);
236
+ return true;
237
+ }
238
+ #prepareMoveWidgetY(widget, delta, operationMap) {
239
+ // If the widget is not draggable then we definitely can't push it.
240
+ if (!widget.draggable) {
241
+ return false;
242
+ }
243
+ const finalStartY = widget.y + delta;
244
+ const finalEndY = finalStartY + widget.height;
245
+ // Shortcut: if the widget is already being moved and this is further than the proposed move,
246
+ // then we can forgo remaining checks as the maximal move is the one that gets applied.
247
+ if (operationMap.has(widget)) {
248
+ const existingOperation = operationMap.get(widget);
249
+ if (existingOperation.newY >= finalStartY) {
250
+ return true;
251
+ }
252
+ }
253
+ // We need to try expand the grid if the widget is moving beyond the current bounds,
254
+ // but if this is not possible then the operation fails.
255
+ if (finalEndY > this.#rows && !this.#tryExpandRows(finalEndY)) {
256
+ return false;
257
+ }
258
+ // If a widget lies between this widget's current position and its new end position then it's,
259
+ // by definition, colliding with it. We need to check if we can move the widget and collapse
260
+ // any gaps along the way.
261
+ for (let i = widget.y; i < widget.y + widget.height; i++) {
262
+ for (let j = widget.x + widget.width; j < finalEndY; j++) {
263
+ const cell = this.#layout[i][j];
264
+ // Empty cell
265
+ if (!cell) {
266
+ continue;
267
+ }
268
+ // A widget lies between this widget's current position and its new end position.
269
+ // Recurse to push it out of the way, and if that fails then the whole operation fails.
270
+ const gapSize = j - (widget.x + widget.width);
271
+ // NEXT: Heuristic to immediately cancel move if delta - gapSize > available space. Use #countSetBits for this
272
+ if (!this.#prepareMoveWidgetX(cell, delta - gapSize, operationMap)) {
273
+ return false;
274
+ }
275
+ // We can move the colliding widget, we don't need to check this row any further as the move
276
+ // handles any subsequent collisions.
277
+ break;
278
+ }
279
+ }
280
+ // The move is possible. Add the operation to the accumulator so we can carry them out if all are OK.
281
+ const operation = {
282
+ widget,
283
+ oldX: widget.x,
284
+ newX: widget.x,
285
+ oldY: widget.y,
286
+ newY: finalStartY
287
+ };
288
+ operationMap.set(widget, operation);
289
+ return true;
290
+ }
291
+ removeWidget(widget) {
292
+ // Refer to the widget's state to find where it is in the grid.
293
+ const [cellX, cellY] = [widget.x, widget.y];
294
+ // Remove the widget from the layout.
295
+ for (let i = cellX; i < cellX + widget.width; i++) {
296
+ for (let j = cellY; j < cellY + widget.height; j++) {
297
+ this.#layout[j][i] = null;
298
+ }
299
+ }
300
+ this.#widgets.delete(widget);
301
+ const widgetXBitmap = this.#createWidgetBitmap(widget, cellX, widget.width);
302
+ // Update the row bitmaps to reflect the widget's removal.
303
+ for (let i = cellY; i < cellY + widget.height; i++) {
304
+ this.#bitmaps[i] &= ~widgetXBitmap;
305
+ }
306
+ // If we now have empty rows or columns at the ends, remove them.
307
+ this.#removeTrailingEmptyRows();
308
+ this.#removeTrailingEmptyColumns();
309
+ return true;
310
+ }
311
+ takeSnapshot() {
312
+ // Deep copy the layout array, storing only the widget IDs
313
+ const layoutCopy = this.#layout.map(row => row.map(cell => cell));
314
+ return {
315
+ layout: layoutCopy,
316
+ bitmaps: [...this.#bitmaps],
317
+ rows: this.#rows,
318
+ columns: this.#columns,
319
+ widgets: Array.from(this.#widgets).map(widget => ({
320
+ widget,
321
+ x: widget.x,
322
+ y: widget.y,
323
+ width: widget.width,
324
+ height: widget.height
325
+ }))
326
+ };
327
+ }
328
+ clear() {
329
+ this.#widgets.clear();
330
+ this.#rows = this.#targetConfig.baseRows ?? 1;
331
+ this.#columns = this.#targetConfig.baseColumns ?? 1;
332
+ this.#bitmaps = new Array(this.#rows).fill(0);
333
+ this.#layout = new Array(this.#rows).fill(new Array(this.#columns).fill(null));
334
+ }
335
+ restoreFromSnapshot(snapshot) {
336
+ this.#layout = snapshot.layout;
337
+ this.#bitmaps = snapshot.bitmaps;
338
+ this.#rows = snapshot.rows;
339
+ this.#columns = snapshot.columns;
340
+ this.#widgets.clear();
341
+ for (const widget of snapshot.widgets) {
342
+ this.#widgets.add(widget.widget);
343
+ widget.widget.setBounds(widget.x, widget.y, widget.width, widget.height);
344
+ }
345
+ }
346
+ mapRawCellToFinalCell(x, y) {
347
+ return [Math.floor(x), Math.floor(y)];
348
+ }
349
+ #doMoveOperation(widget, operation) {
350
+ const removedBitmaps = Array(widget.height).fill(0);
351
+ // Remove the widget from the layout, if it's there.
352
+ // Another move operation may have already replaced it here.
353
+ for (let i = operation.oldY; i < operation.oldY + widget.height; i++) {
354
+ for (let j = operation.oldX; j < operation.oldX + widget.width; j++) {
355
+ if (this.#layout[i][j] === widget) {
356
+ this.#layout[i][j] = null;
357
+ removedBitmaps[i - operation.oldY] |= (1 << j);
358
+ }
359
+ }
360
+ }
361
+ // Update the bitmaps to reflect the widget's removal, again, only where this widget was removed.
362
+ for (let i = operation.oldY; i < operation.oldY + widget.height; i++) {
363
+ this.#bitmaps[i] &= ~removedBitmaps[i - operation.oldY];
364
+ }
365
+ // Place the widget in the new position.
366
+ for (let i = operation.newY; i < operation.newY + widget.height; i++) {
367
+ for (let j = operation.newX; j < operation.newX + widget.width; j++) {
368
+ this.#layout[i][j] = widget;
369
+ }
370
+ }
371
+ widget.setBounds(operation.newX, operation.newY, widget.width, widget.height);
372
+ // With the new bounds, update the row and column bitmaps to reflect the widget's placement.
373
+ const rowBitmap = this.#createWidgetBitmap(widget, operation.newX, widget.width);
374
+ for (let i = operation.newY; i < operation.newY + widget.height; i++) {
375
+ this.#bitmaps[i] |= rowBitmap;
376
+ }
377
+ }
378
+ #countSetBits(bitmap) {
379
+ // Uses Brian Kernighan's algorithm
380
+ let count = 0;
381
+ while (bitmap) {
382
+ bitmap &= bitmap - 1;
383
+ count++;
384
+ }
385
+ return count;
386
+ }
387
+ #createWidgetBitmap(widget, start, length) {
388
+ // Create a bitmap with 1s for the width of the widget starting at the given position
389
+ let bitmap = 0;
390
+ // Set bits from start to start + length
391
+ for (let i = start; i < start + length; i++) {
392
+ bitmap |= (1 << i);
393
+ }
394
+ return bitmap;
395
+ }
396
+ #tryExpandColumns(count) {
397
+ if (!this.#layoutConfig.expandColumns || count > MAX_COLUMNS) {
398
+ return false;
399
+ }
400
+ this.#columns = count;
401
+ this.#layout.forEach(row => row.push(...new Array(count - this.#columns).fill(null)));
402
+ return true;
403
+ }
404
+ #tryExpandRows(count) {
405
+ if (!this.#layoutConfig.expandRows) {
406
+ return false;
407
+ }
408
+ this.#rows = count;
409
+ this.#bitmaps.push(...new Array(count - this.#rows).fill(0));
410
+ this.#layout.push(...new Array(count - this.#rows).fill(new Array(this.#columns).fill(null)));
411
+ return true;
412
+ }
413
+ #removeTrailingEmptyRows() {
414
+ const minRows = this.#layoutConfig.minRows ?? 1;
415
+ for (let i = this.#rows - 1; i >= minRows; i--) {
416
+ if (this.#bitmaps[i]) {
417
+ break;
418
+ }
419
+ this.#bitmaps.pop();
420
+ this.#layout.pop();
421
+ this.#rows--;
422
+ }
423
+ }
424
+ #removeTrailingEmptyColumns() {
425
+ const minColumns = this.#layoutConfig.minColumns ?? 1;
426
+ for (let i = this.#columns - 1; i >= minColumns; i--) {
427
+ // If the ith bit is set on this column in any row, then it can't be removed.
428
+ const columnHasContent = this.#bitmaps.some(rowBitmap => {
429
+ return (rowBitmap & (1 << i)) !== 0;
430
+ });
431
+ if (columnHasContent) {
432
+ break;
433
+ }
434
+ // Remove the last column from each row
435
+ this.#layout.forEach(row => row.pop());
436
+ this.#columns--;
437
+ }
438
+ }
439
+ // Getters and setters
440
+ get layout() {
441
+ return this.#state.layout;
442
+ }
443
+ get #layout() {
444
+ return this.#state.layout;
445
+ }
446
+ set #layout(value) {
447
+ this.#state.layout = value;
448
+ }
449
+ get bitmaps() {
450
+ return this.#state.bitmaps;
451
+ }
452
+ get #bitmaps() {
453
+ return this.#state.bitmaps;
454
+ }
455
+ set #bitmaps(value) {
456
+ this.#state.bitmaps = value;
457
+ }
458
+ get rows() {
459
+ return this.#state.rows;
460
+ }
461
+ set rows(value) {
462
+ this.#state.rows = value;
463
+ }
464
+ get #rows() {
465
+ return this.#state.rows;
466
+ }
467
+ set #rows(value) {
468
+ this.#state.rows = value;
469
+ }
470
+ get columns() {
471
+ return this.#state.columns;
472
+ }
473
+ get #columns() {
474
+ return this.#state.columns;
475
+ }
476
+ set #columns(value) {
477
+ this.#state.columns = value;
478
+ }
479
+ }
480
+ /**
481
+ * Flow-based FlexiGrid Layout
482
+ *
483
+ * A grid layout where widgets are placed using a flow strategy. The flow axis determines which axis the widgets are placed along,
484
+ * and the cross axis can be configured to expand when the flow axis is full.
485
+ */
486
+ export class FlowFlexiGrid extends FlexiGrid {
487
+ #targetConfig = $state();
488
+ #rawLayoutConfig = $derived(this.#targetConfig?.layout);
489
+ #state = $state({
490
+ rows: 0,
491
+ columns: 0,
492
+ widgets: [],
493
+ });
494
+ #layoutConfig = $derived({
495
+ type: "flow",
496
+ maxFlowAxis: this.#rawLayoutConfig.maxFlowAxis ?? Infinity,
497
+ flowAxis: untrack(() => this.#rawLayoutConfig.flowAxis) ?? "row",
498
+ placementStrategy: this.#rawLayoutConfig.placementStrategy ?? "append",
499
+ disallowInsert: this.#rawLayoutConfig.disallowInsert ?? false,
500
+ disallowExpansion: this.#rawLayoutConfig.disallowExpansion ?? false
501
+ });
502
+ constructor(target, targetConfig) {
503
+ super(target, targetConfig);
504
+ this.#targetConfig = targetConfig;
505
+ this.#rows = this._targetConfig.baseRows ?? 1;
506
+ this.#columns = this._targetConfig.baseColumns ?? 1;
507
+ }
508
+ tryPlaceWidget(widget, cellX, cellY, width, height) {
509
+ const isRowFlow = this.#isRowFlow;
510
+ width ??= 1;
511
+ height ??= 1;
512
+ // If the coordinate for the non-extending axis is greater than the axis's length, then this operation fails.
513
+ if (isRowFlow && cellX !== undefined && cellX > this.columns) {
514
+ return false;
515
+ }
516
+ else if (!isRowFlow && cellY !== undefined && cellY > this.rows) {
517
+ return false;
518
+ }
519
+ let cellPosition = null;
520
+ if (cellX !== undefined && cellY !== undefined && !this.#layoutConfig.disallowInsert) {
521
+ // Ensures that it doesn't try to place a widget too far along or down.
522
+ cellX = Math.min(cellX, this.columns);
523
+ cellY = Math.min(cellY, this.rows);
524
+ cellPosition = this.#convert2DPositionTo1D(cellX, cellY);
525
+ }
526
+ // If it's null then coordinate is missing, or we can't allow insertion. Replace coordinates based on the placement strategy.
527
+ cellPosition ??= this.#resolveNextPlacementPosition();
528
+ // If the width/height of the widget is greater than the flow axis' length, then constrain it to the flow axis' length.
529
+ if (isRowFlow && width > this.columns) {
530
+ width = this.columns;
531
+ }
532
+ else if (!isRowFlow && height > this.rows) {
533
+ height = this.rows;
534
+ }
535
+ // Find the nearest widget to the proposed position, and determine the precise location based on it.
536
+ const [index, nearestWidget] = this.#findNearestWidget(cellPosition, 0, this.#widgets.length - 1);
537
+ // Easy - no widgets to search and none to shift. Just add ours at the start.
538
+ if (!nearestWidget) {
539
+ this.#widgets.push(widget);
540
+ widget.setBounds(0, 0, width, height);
541
+ return true;
542
+ }
543
+ const nearestWidgetPosition = this.#convert2DPositionTo1D(nearestWidget.x, nearestWidget.y);
544
+ const operations = [];
545
+ // If the found widget is to the left of the proposed position, then our widget will immediately succeed it.
546
+ if (nearestWidgetPosition < cellPosition) {
547
+ const cellPosition = nearestWidgetPosition + this.#getNonFlowLength(nearestWidget);
548
+ // Prepare to shift the remaining widgets to the right.
549
+ if (!this.#moveAndShiftWidgets(index + 1, cellPosition, operations)) {
550
+ return false;
551
+ }
552
+ // If this widget has placed itself at the end, then we need to make sure the flow axis fits it.
553
+ if (index + 1 === this.#widgets.length) {
554
+ if (isRowFlow && Math.floor(cellPosition / this.columns) >= this.rows) {
555
+ const newRows = Math.floor(cellPosition / this.columns) + 1;
556
+ if (newRows > this.#maxFlowAxis) {
557
+ return false;
558
+ }
559
+ this.#rows = newRows;
560
+ }
561
+ else {
562
+ const newColumns = Math.floor(cellPosition / this.rows) + 1;
563
+ if (newColumns > this.#maxFlowAxis) {
564
+ return false;
565
+ }
566
+ this.#columns = newColumns;
567
+ }
568
+ }
569
+ this.#widgets.splice(index + 1, 0, widget);
570
+ // Otherwise, our widget will immediately precede it and shift it along.
571
+ }
572
+ else if (nearestWidgetPosition >= cellPosition) {
573
+ // Prepare to shift the remaining widgets to the right.
574
+ if (!this.#moveAndShiftWidgets(index, nearestWidgetPosition + this.#getNonFlowLength(widget), operations)) {
575
+ return false;
576
+ }
577
+ this.#widgets.splice(index, 0, widget);
578
+ }
579
+ // Carry out the operations, as they all cleared.
580
+ for (const operation of operations) {
581
+ operation.widget.setBounds(operation.newX, operation.newY, isRowFlow ? operation.widget.width : 1, isRowFlow ? 1 : operation.widget.height);
582
+ }
583
+ // Finally, add the widget to the grid.
584
+ const [newX, newY] = this.#convert1DPositionTo2D(cellPosition);
585
+ widget.setBounds(newX, newY, isRowFlow ? 1 : width, isRowFlow ? height : 1);
586
+ return true;
587
+ }
588
+ #resolveNextPlacementPosition() {
589
+ if (!this.#widgets.length) {
590
+ return 0;
591
+ }
592
+ switch (this.#layoutConfig.placementStrategy) {
593
+ case "prepend":
594
+ {
595
+ return 0;
596
+ }
597
+ case "append":
598
+ {
599
+ // Find the last widget and place it after it.
600
+ const lastWidget = this.#widgets[this.#widgets.length - 1];
601
+ return this.#convert2DPositionTo1D(lastWidget.x, lastWidget.y) + this.#getNonFlowLength(lastWidget);
602
+ }
603
+ }
604
+ }
605
+ removeWidget(widget) {
606
+ const widgetPosition = this.#convert2DPositionTo1D(widget.x, widget.y);
607
+ // Find the widget in the grid.
608
+ const [index, foundWidget] = this.#findNearestWidget(widgetPosition, 0, this.#widgets.length - 1);
609
+ if (foundWidget !== widget) {
610
+ return false;
611
+ }
612
+ // Remove the widget from the grid.
613
+ this.#widgets.splice(index, 1);
614
+ const operations = [];
615
+ // Shift the remaining widgets to the left if possible.
616
+ if (!this.#moveAndShiftWidgets(index, widgetPosition, operations)) {
617
+ return false;
618
+ }
619
+ // Carry out the operations, as they all cleared.
620
+ for (const operation of operations) {
621
+ operation.widget.setBounds(operation.newX, operation.newY, widget.width, widget.height);
622
+ }
623
+ // Shrink the grid if necessary.
624
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
625
+ const [_, lastWidget] = this.#findNearestWidget(Infinity, 0, this.#widgets.length - 1);
626
+ if (lastWidget) {
627
+ if (this.#layoutConfig.flowAxis === "row" && this.rows > lastWidget.y + lastWidget.height) {
628
+ this.#rows = lastWidget.y + lastWidget.height;
629
+ }
630
+ else if (this.#layoutConfig.flowAxis === "column" && this.columns > lastWidget.x + lastWidget.width) {
631
+ this.#columns = lastWidget.x + lastWidget.width;
632
+ }
633
+ }
634
+ else {
635
+ this.#rows = 1;
636
+ this.#columns = 1;
637
+ }
638
+ return true;
639
+ }
640
+ clear() {
641
+ // Clear the grid without replacing it outright so reactivity proxies are preserved.
642
+ this.#widgets.length = 0;
643
+ }
644
+ takeSnapshot() {
645
+ // Copy the widget positions and sizes.
646
+ const widgets = this.#widgets.map(widget => {
647
+ return {
648
+ widget,
649
+ x: widget.x,
650
+ y: widget.y,
651
+ width: widget.width,
652
+ height: widget.height
653
+ };
654
+ });
655
+ return {
656
+ widgets,
657
+ rows: this.#rows,
658
+ columns: this.#columns
659
+ };
660
+ }
661
+ restoreFromSnapshot(snapshot) {
662
+ this.clear();
663
+ for (const widget of snapshot.widgets) {
664
+ widget.widget.setBounds(widget.x, widget.y, widget.width, widget.height);
665
+ this.#widgets.push(widget.widget);
666
+ }
667
+ this.#rows = snapshot.rows;
668
+ this.#columns = snapshot.columns;
669
+ }
670
+ mapRawCellToFinalCell(x, y) {
671
+ const position = [Math.round(x), Math.round(y)];
672
+ const position1D = this.#convert2DPositionTo1D(position[0], position[1]);
673
+ const [index, nearest] = this.#findNearestWidget(position1D, 0, this.#widgets.length - 1);
674
+ if (nearest?.isShadow) {
675
+ return [nearest.x, nearest.y];
676
+ }
677
+ if (index == 0) {
678
+ return [position[0], position[1]];
679
+ }
680
+ const predecessor = this.#widgets[index - 1];
681
+ if (predecessor.isShadow) {
682
+ return [predecessor.x, predecessor.y];
683
+ }
684
+ return [position[0], position[1]];
685
+ }
686
+ #findNearestWidget(position, searchStart, searchEnd) {
687
+ if (this.#widgets.length === 0) {
688
+ return [0, null];
689
+ }
690
+ if (searchStart === searchEnd) {
691
+ return [searchStart, this.#widgets[searchStart]];
692
+ }
693
+ const median = Math.floor((searchStart + searchEnd) / 2);
694
+ const widget = this.#widgets[median];
695
+ const widgetValue = this.#convert2DPositionTo1D(widget.x, widget.y);
696
+ if (widgetValue === position) {
697
+ return [median, widget];
698
+ }
699
+ else if (widgetValue < position) {
700
+ return this.#findNearestWidget(position, median + 1, searchEnd);
701
+ }
702
+ else {
703
+ return this.#findNearestWidget(position, searchStart, median);
704
+ }
705
+ }
706
+ #moveAndShiftWidgets(startIndex, basePosition, operations) {
707
+ if (startIndex >= this.#widgets.length) {
708
+ return true;
709
+ }
710
+ let lastPosition = basePosition;
711
+ for (let i = startIndex; i < this.#widgets.length; i++) {
712
+ const widget = this.#widgets[i];
713
+ const [newX, newY] = this.#convert1DPositionTo2D(lastPosition);
714
+ if (newY >= this.rows && !this.#tryExpandRows(newY + 1)) {
715
+ return false;
716
+ }
717
+ if (newX >= this.columns && !this.#tryExpandColumns(newX + 1)) {
718
+ return false;
719
+ }
720
+ operations.push({
721
+ widget,
722
+ oldX: widget.x,
723
+ oldY: widget.y,
724
+ newX,
725
+ newY
726
+ });
727
+ lastPosition = lastPosition + this.#getNonFlowLength(widget);
728
+ }
729
+ return true;
730
+ }
731
+ #convert2DPositionTo1D(x, y) {
732
+ if (this.#isRowFlow) {
733
+ return y * this.columns + x;
734
+ }
735
+ return x * this.rows + y;
736
+ }
737
+ #convert1DPositionTo2D(index) {
738
+ if (this.#isRowFlow) {
739
+ return [index % this.columns, Math.floor(index / this.columns)];
740
+ }
741
+ return [Math.floor(index / this.rows), index % this.rows];
742
+ }
743
+ #getNonFlowLength(widget) {
744
+ if (this.#isRowFlow) {
745
+ return widget.width;
746
+ }
747
+ return widget.height;
748
+ }
749
+ #tryExpandColumns(newCount) {
750
+ if (this.#layoutConfig.disallowExpansion || newCount > this.#maxFlowAxis || this.#isRowFlow) {
751
+ return false;
752
+ }
753
+ this.#columns = newCount;
754
+ return true;
755
+ }
756
+ #tryExpandRows(newCount) {
757
+ if (this.#layoutConfig.disallowExpansion || newCount > this.#maxFlowAxis || !this.#isRowFlow) {
758
+ return false;
759
+ }
760
+ this.#rows = newCount;
761
+ return true;
762
+ }
763
+ get rows() {
764
+ return this.#state.rows;
765
+ }
766
+ get columns() {
767
+ return this.#state.columns;
768
+ }
769
+ get #rows() {
770
+ return this.#state.rows;
771
+ }
772
+ set #rows(value) {
773
+ this.#state.rows = value;
774
+ }
775
+ get #columns() {
776
+ return this.#state.columns;
777
+ }
778
+ set #columns(value) {
779
+ this.#state.columns = value;
780
+ }
781
+ get widgets() {
782
+ return this.#state.widgets;
783
+ }
784
+ get #widgets() {
785
+ return this.#state.widgets;
786
+ }
787
+ set #widgets(value) {
788
+ this.#state.widgets = value;
789
+ }
790
+ get #isRowFlow() {
791
+ return this.#layoutConfig.flowAxis === "row";
792
+ }
793
+ get #maxFlowAxis() {
794
+ return this.#layoutConfig.maxFlowAxis;
795
+ }
796
+ }
797
+ const contextKey = Symbol('flexigrid');
798
+ export function flexigrid() {
799
+ const target = getInternalFlexitargetCtx();
800
+ if (!target) {
801
+ throw new Error("A FlexiGrid was instantiated outside of a FlexiTarget context. Ensure that flexigrid() is called within a FlexiTarget component.");
802
+ }
803
+ const grid = target.createGrid();
804
+ setContext(contextKey, grid);
805
+ // Tell the grid's dimension tracker to watch the grid element.
806
+ $effect(() => {
807
+ grid.watchGridElementDimensions();
808
+ });
809
+ return {
810
+ grid
811
+ };
812
+ }
813
+ export function getFlexigridCtx() {
814
+ return getContext(contextKey);
815
+ }