svelte-flexiboards 0.3.2 → 0.4.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.
Files changed (49) hide show
  1. package/dist/components/flexi-add.svelte +2 -15
  2. package/dist/components/flexi-add.svelte.d.ts +0 -12
  3. package/dist/components/flexi-delete.svelte +3 -16
  4. package/dist/components/flexi-delete.svelte.d.ts +0 -12
  5. package/dist/components/flexi-grab.svelte +2 -2
  6. package/dist/components/flexi-target.svelte +8 -19
  7. package/dist/components/flexi-widget.svelte +2 -2
  8. package/dist/components/responsive-flexi-board.svelte +83 -0
  9. package/dist/components/responsive-flexi-board.svelte.d.ts +34 -0
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.js +4 -1
  12. package/dist/system/board/base.svelte.d.ts +15 -0
  13. package/dist/system/board/controller.svelte.d.ts +26 -4
  14. package/dist/system/board/controller.svelte.js +237 -28
  15. package/dist/system/board/types.d.ts +26 -0
  16. package/dist/system/grid/base.svelte.d.ts +9 -0
  17. package/dist/system/grid/base.svelte.js +12 -1
  18. package/dist/system/grid/flow-grid.svelte.js +105 -36
  19. package/dist/system/grid/free-grid.svelte.d.ts +6 -2
  20. package/dist/system/grid/free-grid.svelte.js +139 -20
  21. package/dist/system/misc/deleter.svelte.d.ts +0 -4
  22. package/dist/system/misc/deleter.svelte.js +1 -6
  23. package/dist/system/portal.js +0 -1
  24. package/dist/system/responsive/base.svelte.d.ts +46 -0
  25. package/dist/system/responsive/base.svelte.js +1 -0
  26. package/dist/system/responsive/controller.svelte.d.ts +78 -0
  27. package/dist/system/responsive/controller.svelte.js +264 -0
  28. package/dist/system/responsive/index.d.ts +16 -0
  29. package/dist/system/responsive/index.js +36 -0
  30. package/dist/system/responsive/types.d.ts +56 -0
  31. package/dist/system/responsive/types.js +1 -0
  32. package/dist/system/shared/event-bus.d.ts +3 -1
  33. package/dist/system/shared/utils.svelte.d.ts +2 -0
  34. package/dist/system/shared/utils.svelte.js +39 -22
  35. package/dist/system/target/controller.svelte.d.ts +7 -2
  36. package/dist/system/target/controller.svelte.js +103 -30
  37. package/dist/system/types.d.ts +13 -2
  38. package/dist/system/widget/base.svelte.d.ts +40 -1
  39. package/dist/system/widget/base.svelte.js +84 -2
  40. package/dist/system/widget/controller.svelte.d.ts +4 -1
  41. package/dist/system/widget/controller.svelte.js +106 -17
  42. package/dist/system/widget/events.js +10 -3
  43. package/dist/system/widget/interpolation-utils.d.ts +14 -0
  44. package/dist/system/widget/interpolation-utils.js +32 -0
  45. package/dist/system/widget/interpolator.svelte.d.ts +2 -1
  46. package/dist/system/widget/interpolator.svelte.js +63 -22
  47. package/dist/system/widget/triggers.svelte.js +1 -1
  48. package/dist/system/widget/types.d.ts +51 -6
  49. package/package.json +1 -1
@@ -25,6 +25,7 @@ export class FlowFlexiGrid extends FlexiGrid {
25
25
  disallowInsert: this.#rawLayoutConfig.disallowInsert ?? false
26
26
  });
27
27
  #coordinateSystem = new FlowGridCoordinateSystem(this);
28
+ #dragSnapshot = null;
28
29
  constructor(target, targetConfig) {
29
30
  super(target, targetConfig);
30
31
  this.#targetConfig = targetConfig;
@@ -47,60 +48,82 @@ export class FlowFlexiGrid extends FlexiGrid {
47
48
  else if (!isRowFlow && height > this.rows) {
48
49
  height = this.rows;
49
50
  }
51
+ // Additionally, constrain the width/height of the widget to the min/max values.
52
+ width = Math.max(widget.minWidth, Math.min(widget.maxWidth, width));
53
+ height = Math.max(widget.minHeight, Math.min(widget.maxHeight, height));
50
54
  // Find the nearest widget to the proposed position, and determine the precise location based on it.
51
55
  const [index, nearestWidget] = this.#coordinateSystem.findNearestWidget(cellPosition, 0, this.#widgets.length - 1);
52
56
  // If there's no widgets in the grid, just trivially add ours to the start.
53
57
  if (!nearestWidget) {
54
- return this.#placeWidgetAt(widget, 0, 0);
58
+ return this.#placeWidgetAt(widget, 0, 0, width, height);
55
59
  }
56
60
  const nearestWidgetPosition = this.#coordinateSystem.to1D(nearestWidget.x, nearestWidget.y);
57
61
  // If the found widget's position is before our desired one, then our widget will be placed adjacent to it along the flow axis.
58
62
  if (nearestWidgetPosition < cellPosition) {
59
- return this.#placeWidgetAt(widget, nearestWidgetPosition + this.#coordinateSystem.getWidgetLength(nearestWidget), index + 1);
63
+ return this.#placeWidgetAt(widget, nearestWidgetPosition + this.#coordinateSystem.getWidgetLength(nearestWidget), index + 1, width, height);
60
64
  }
61
65
  // Otherwise, it'll look at the predecessor of the nearest widget and place it after it.
62
66
  // This prevents gaps from being persisted if widgets can fit adjacent to the predecessor.
63
67
  if (index > 0) {
64
68
  const predecessor = this.#widgets[index - 1];
65
69
  return this.#placeWidgetAt(widget, this.#coordinateSystem.to1D(predecessor.x, predecessor.y) +
66
- this.#coordinateSystem.getWidgetLength(predecessor), index);
70
+ this.#coordinateSystem.getWidgetLength(predecessor), index, width, height);
67
71
  }
68
- return this.#placeWidgetAt(widget, nearestWidgetPosition, index);
72
+ return this.#placeWidgetAt(widget, nearestWidgetPosition, index, width, height);
69
73
  }
70
74
  #commitOperations(operations) {
71
- const isRowFlow = this.isRowFlow;
72
75
  for (const operation of operations) {
73
76
  const [newX, newY] = this.#coordinateSystem.to2D(operation.newPosition);
74
- operation.widget.setBounds(newX, newY, isRowFlow ? operation.widget.width : 1, isRowFlow ? 1 : operation.widget.height);
77
+ operation.widget.setBounds(newX, newY, operation.width, operation.height);
75
78
  }
76
79
  return true;
77
80
  }
78
- #placeWidgetAt(widget, position, index) {
81
+ #placeWidgetAt(widget, position, index, width, height) {
79
82
  const operations = [];
80
83
  this.#widgets.splice(index, 0, widget);
81
- if (!this.#shiftWidget(index, position, operations)) {
84
+ if (!this.#shiftWidget(index, position, operations, width, height)) {
82
85
  // Undo the insertion.
83
86
  this.#widgets.splice(index, 1);
84
87
  return false;
85
88
  }
86
89
  return this.#commitOperations(operations);
87
90
  }
88
- #shiftWidget(index, position, operations) {
91
+ #shiftWidget(index, position, operations, width, height) {
89
92
  const widget = this.#widgets[index];
90
- const finalPosition = this.#coordinateSystem.findPositionToFitWidget(widget, position);
93
+ const isRowFlow = this.isRowFlow;
94
+ // Determine the dimensions to use for this widget.
95
+ // For flow grids, the cross-axis dimension is always 1.
96
+ // For the primary widget (first in chain), use provided flow-axis dimension if given.
97
+ // For displaced widgets, use their current flow-axis dimension.
98
+ let effectiveWidth;
99
+ let effectiveHeight;
100
+ if (isRowFlow) {
101
+ effectiveWidth = width ?? widget.width;
102
+ effectiveHeight = 1; // Cross-axis is always 1 for row flow
103
+ }
104
+ else {
105
+ effectiveWidth = 1; // Cross-axis is always 1 for column flow
106
+ effectiveHeight = height ?? widget.height;
107
+ }
108
+ // The "length" along the flow axis for positioning calculations.
109
+ const effectiveLength = isRowFlow ? effectiveWidth : effectiveHeight;
110
+ const finalPosition = this.#coordinateSystem.findPositionToFitWidget(widget, position, effectiveLength);
91
111
  // Expand the grid if the widget is being added past the current flow axis end.
92
112
  if (!this.#coordinateSystem.expandIfNeededToFit(finalPosition)) {
93
113
  return false;
94
114
  }
95
115
  operations.push({
96
116
  widget,
97
- newPosition: finalPosition
117
+ newPosition: finalPosition,
118
+ width: effectiveWidth,
119
+ height: effectiveHeight
98
120
  });
99
121
  if (index + 1 >= this.#widgets.length) {
100
122
  return true;
101
123
  }
102
124
  // Prepare to shift the remaining widgets along relative to this one.
103
- return this.#shiftWidget(index + 1, finalPosition + this.#coordinateSystem.getWidgetLength(widget), operations);
125
+ // Displaced widgets use their current dimensions (no width/height override).
126
+ return this.#shiftWidget(index + 1, finalPosition + effectiveLength, operations);
104
127
  }
105
128
  #resolveNextPlacementPosition() {
106
129
  if (!this.#widgets.length) {
@@ -159,10 +182,23 @@ export class FlowFlexiGrid extends FlexiGrid {
159
182
  const widget = this.#widgets[index];
160
183
  this.#widgets.splice(index, 1);
161
184
  const operations = [];
162
- const widgetPosition = this.#coordinateSystem.to1D(widget.x, widget.y);
163
- // Shift the remaining widgets back if possible.
164
- if (index < this.#widgets.length && !this.#shiftWidget(index, widgetPosition, operations)) {
165
- return false;
185
+ // When removing a widget, we need to re-compact all remaining widgets
186
+ // starting from the beginning. This is because there may be gaps before
187
+ // the removed widget that widgets after it can now fill.
188
+ //
189
+ // Example: In a 3-column grid with [A(2-wide), B(2-wide), C(1-wide)]:
190
+ // - A occupies positions 0-1
191
+ // - B can't fit at position 2 (only 1 cell), so it goes to position 3
192
+ // - C goes to position 5
193
+ // State: AA- / BBC
194
+ //
195
+ // When B is removed, C should move to position 2 (the gap after A),
196
+ // NOT to position 3 (where B was).
197
+ if (this.#widgets.length > 0) {
198
+ // Start compaction from position 0
199
+ if (!this.#shiftWidget(0, 0, operations)) {
200
+ return false;
201
+ }
166
202
  }
167
203
  return this.#commitOperations(operations);
168
204
  }
@@ -190,31 +226,64 @@ export class FlowFlexiGrid extends FlexiGrid {
190
226
  restoreFromSnapshot(snapshot) {
191
227
  this.clear();
192
228
  for (const widget of snapshot.widgets) {
193
- widget.widget.setBounds(widget.x, widget.y, widget.width, widget.height);
229
+ widget.widget.setBounds(widget.x, widget.y, widget.width, widget.height, false);
194
230
  this.#widgets.push(widget.widget);
195
231
  }
196
232
  this.rows = snapshot.rows;
197
233
  this.columns = snapshot.columns;
198
234
  }
235
+ setDragSnapshot(snapshot) {
236
+ this.#dragSnapshot = snapshot;
237
+ }
238
+ clearDragSnapshot() {
239
+ this.#dragSnapshot = null;
240
+ }
199
241
  mapRawCellToFinalCell(x, y) {
200
- // This gets us most of the way there, but we need to account for the cases where different flow axis indexes may have different pixel sizes.
201
242
  const position = this.#coordinateSystem.getNormalisedHoverPosition(x, y);
202
- // This is somewhat hacky, but we're basically saying that if the shadow widget is anywhere before the current index,
203
- // then we need to adjust the drop position to offset the shadow widget's length.
204
- // This is pre-emptive as when the final placement happens, the shadow won't be present.
205
- // We do this to factor for different pixel sizes, as if we do not factor this then variable pixel sizes will cause flickering.
206
- // NEXT: This doesn't play very nice with 2D flow logic, because it's weird to displace widgets when the one we're moving doesn't fit anyway.
207
- const [index, _] = this.#coordinateSystem.findNearestWidgetFrom2D(position[0], position[1]);
208
- let position1D = this.#coordinateSystem.to1D(position[0], position[1]);
209
- // Hack: find if there's a shadow widget anywhere before the current index
210
- for (let i = 0; i < index; i++) {
211
- const widget = this.#widgets[i];
212
- if (widget.isShadow) {
213
- position1D -= this.#coordinateSystem.getWidgetLength(widget);
214
- break;
243
+ // Without a drag snapshot, just return the normalized position directly.
244
+ if (!this.#dragSnapshot) {
245
+ return position;
246
+ }
247
+ const position1D = this.#coordinateSystem.to1D(position[0], position[1]);
248
+ // Find the nearest widget to the cursor in the CURRENT (live) grid.
249
+ let [index, nearestWidget] = this.#coordinateSystem.findNearestWidgetFrom2D(position[0], position[1]);
250
+ // If the nearest widget is the shadow, look at neighbors and pick the closest non-shadow.
251
+ if (nearestWidget && nearestWidget.isShadow) {
252
+ nearestWidget = null;
253
+ // Check the widget after the shadow first, then before.
254
+ if (index + 1 < this.#widgets.length && !this.#widgets[index + 1].isShadow) {
255
+ nearestWidget = this.#widgets[index + 1];
256
+ index = index + 1;
257
+ }
258
+ else if (index - 1 >= 0 && !this.#widgets[index - 1].isShadow) {
259
+ nearestWidget = this.#widgets[index - 1];
260
+ index = index - 1;
215
261
  }
216
262
  }
217
- return this.#coordinateSystem.to2D(position1D);
263
+ // If we couldn't find a non-shadow widget, fall back to the normalized position.
264
+ if (!nearestWidget) {
265
+ return position;
266
+ }
267
+ // Look up this widget in the snapshot by identity (same object reference).
268
+ const snapshotEntry = this.#dragSnapshot.widgets.find((s) => s.widget === nearestWidget);
269
+ if (!snapshotEntry) {
270
+ return position;
271
+ }
272
+ // Determine: is the cursor BEFORE or AFTER the midpoint of this widget in the current grid?
273
+ const widgetPosition1D = this.#coordinateSystem.to1D(nearestWidget.x, nearestWidget.y);
274
+ const widgetLength = this.#coordinateSystem.getWidgetLength(nearestWidget);
275
+ const widgetMidpoint1D = widgetPosition1D + widgetLength / 2;
276
+ if (position1D < widgetMidpoint1D) {
277
+ // Cursor is before the widget → return the widget's snapshot position.
278
+ return [snapshotEntry.x, snapshotEntry.y];
279
+ }
280
+ else {
281
+ // Cursor is at or after the widget → return snapshot position + widget length.
282
+ const snapshotCoords = this.#coordinateSystem;
283
+ const snapshotPosition1D = snapshotCoords.to1D(snapshotEntry.x, snapshotEntry.y) +
284
+ (this.isRowFlow ? snapshotEntry.width : snapshotEntry.height);
285
+ return snapshotCoords.to2D(snapshotPosition1D);
286
+ }
218
287
  }
219
288
  get rows() {
220
289
  return this.#state.rows;
@@ -320,8 +389,8 @@ class FlowGridCoordinateSystem {
320
389
  }
321
390
  return !(!this.#isRowFlow && y !== undefined && y > this.#rows);
322
391
  }
323
- findPositionToFitWidget(widget, basePosition) {
324
- const widgetLength = this.getWidgetLength(widget);
392
+ findPositionToFitWidget(widget, basePosition, overrideLength) {
393
+ const widgetLength = overrideLength ?? this.getWidgetLength(widget);
325
394
  const crossPosition = this.getCrossAxisCoordinate(basePosition);
326
395
  const crossAxisLength = this.getCrossAxisLength();
327
396
  if (crossPosition + widgetLength <= crossAxisLength) {
@@ -369,11 +438,11 @@ class FlowGridCoordinateSystem {
369
438
  if (Math.ceil(x) == this.#grid.columns && y % 1 > 0.5) {
370
439
  return [0, Math.round(y)];
371
440
  }
372
- return [Math.round(x), Math.floor(y)];
441
+ return [Math.min(Math.round(x), this.#grid.columns - 1), Math.floor(y)];
373
442
  }
374
443
  if (Math.ceil(y) == this.#grid.rows && x % 1 > 0.5) {
375
444
  return [Math.round(x), 0];
376
445
  }
377
- return [Math.floor(x), Math.round(y)];
446
+ return [Math.floor(x), Math.min(Math.round(y), this.#grid.rows - 1)];
378
447
  }
379
448
  }
@@ -13,6 +13,7 @@ export declare class FreeFormFlexiGrid extends FlexiGrid {
13
13
  constructor(target: InternalFlexiTargetController, targetConfig: FlexiTargetConfiguration);
14
14
  tryPlaceWidget(widget: InternalFlexiWidgetController, inputX?: number, inputY?: number, inputWidth?: number, inputHeight?: number, isGrabbedWidget?: boolean): boolean;
15
15
  removeWidget(widget: InternalFlexiWidgetController): boolean;
16
+ applyPackingIfNeeded(): void;
16
17
  /**
17
18
  * Applies row and column collapsing, if needed.
18
19
  */
@@ -29,19 +30,22 @@ export declare class FreeFormFlexiGrid extends FlexiGrid {
29
30
  get rows(): number;
30
31
  get columns(): number;
31
32
  get collapsibility(): FreeGridCollapsibility;
33
+ get packing(): FreeGridPacking;
32
34
  get minRows(): number;
33
35
  get minColumns(): number;
34
36
  getWidgetsForModification(): InternalFlexiWidgetController[];
35
37
  }
36
38
  type FreeGridLayout = (InternalFlexiWidgetController | null)[][];
37
39
  type FreeGridCollapsibility = 'none' | 'leading' | 'trailing' | 'endings' | 'any';
40
+ type FreeGridPacking = 'none' | 'horizontal' | 'vertical';
38
41
  export type FreeFormTargetLayout = {
39
42
  type: 'free';
40
43
  minRows?: number;
41
44
  minColumns?: number;
42
45
  maxRows?: number;
43
46
  maxColumns?: number;
44
- colllapsibility?: FreeGridCollapsibility;
47
+ collapsibility?: FreeGridCollapsibility;
48
+ packing?: FreeGridPacking;
45
49
  };
46
50
  type FreeFormGridSnapshot = {
47
51
  layout: FreeGridLayout;
@@ -49,6 +53,6 @@ type FreeFormGridSnapshot = {
49
53
  rows: number;
50
54
  columns: number;
51
55
  widgets: WidgetSnapshot[];
52
- needsCollapsing: boolean;
56
+ needsPostEditOperations: boolean;
53
57
  };
54
58
  export {};
@@ -16,13 +16,14 @@ export class FreeFormFlexiGrid extends FlexiGrid {
16
16
  minRows: this.#rawLayoutConfig?.minRows ?? 1,
17
17
  maxColumns: this.#rawLayoutConfig?.maxColumns ?? Infinity,
18
18
  maxRows: this.#rawLayoutConfig?.maxRows ?? Infinity,
19
- colllapsibility: this.#rawLayoutConfig?.colllapsibility ?? 'any'
19
+ collapsibility: this.#rawLayoutConfig?.collapsibility ?? 'any',
20
+ packing: this.#rawLayoutConfig?.packing ?? 'none'
20
21
  });
21
22
  #rows = $state();
22
23
  #columns = $state();
23
24
  #coordinateSystem = $state();
24
25
  // Track whether collapsing is needed to defer it until operations complete
25
- #needsCollapsing = false;
26
+ #needsPostEditOperations = false;
26
27
  constructor(target, targetConfig) {
27
28
  super(target, targetConfig);
28
29
  this.#targetConfig = targetConfig;
@@ -34,6 +35,9 @@ export class FreeFormFlexiGrid extends FlexiGrid {
34
35
  }
35
36
  tryPlaceWidget(widget, inputX, inputY, inputWidth, inputHeight, isGrabbedWidget = false) {
36
37
  let [x, y, width, height] = this.#normalisePlacementDimensions(inputX, inputY, inputWidth, inputHeight, isGrabbedWidget);
38
+ // Constrain the width/height of the widget to the min/max values.
39
+ width = Math.max(widget.minWidth, Math.min(widget.maxWidth, width));
40
+ height = Math.max(widget.minHeight, Math.min(widget.maxHeight, height));
37
41
  // We need to try expand the grid if the widget is moving beyond the current bounds,
38
42
  // but if this is not possible then the operation fails.
39
43
  if (!this.adjustGridDimensionsToFit(x, y, width, height)) {
@@ -53,6 +57,7 @@ export class FreeFormFlexiGrid extends FlexiGrid {
53
57
  this.#coordinateSystem.addWidget(widget, x, y, width, height);
54
58
  widget.setBounds(x, y, width, height);
55
59
  this.#widgets.add(widget);
60
+ this.#needsPostEditOperations = true;
56
61
  return true;
57
62
  }
58
63
  #resolveCollisions(move, operations, displaceX = true, displaceY = true) {
@@ -71,7 +76,7 @@ export class FreeFormFlexiGrid extends FlexiGrid {
71
76
  if (!collidingWidget) {
72
77
  continue;
73
78
  }
74
- if (!collidingWidget.draggable) {
79
+ if (!collidingWidget.isMovable) {
75
80
  return false;
76
81
  }
77
82
  // Before relocating the colliding widget, remove it from the coordinate system so it can't collide with itself.
@@ -131,27 +136,34 @@ export class FreeFormFlexiGrid extends FlexiGrid {
131
136
  this.#widgets.delete(widget);
132
137
  this.#coordinateSystem.removeWidget(widget);
133
138
  // Mark that collapsing is needed, but don't apply it immediately
134
- this.#needsCollapsing = true;
139
+ this.#needsPostEditOperations = true;
135
140
  return true;
136
141
  }
142
+ applyPackingIfNeeded() {
143
+ if (!this.#needsPostEditOperations) {
144
+ return;
145
+ }
146
+ this.#coordinateSystem.applyPacking();
147
+ }
137
148
  /**
138
149
  * Applies row and column collapsing, if needed.
139
150
  */
140
151
  applyCollapsingIfNeeded() {
141
- if (!this.#needsCollapsing) {
152
+ if (!this.#needsPostEditOperations) {
142
153
  return;
143
154
  }
144
155
  const newRows = this.#coordinateSystem.applyRowCollapsibility();
145
156
  this.#setRows(newRows);
146
157
  const newColumns = this.#coordinateSystem.applyColumnCollapsibility();
147
158
  this.#setColumns(newColumns);
148
- this.#needsCollapsing = false;
149
159
  }
150
160
  /**
151
161
  * Collapse rows and columns if needed.
152
162
  */
153
163
  applyPostCompletionOperations() {
164
+ this.applyPackingIfNeeded();
154
165
  this.applyCollapsingIfNeeded();
166
+ this.#needsPostEditOperations = false;
155
167
  }
156
168
  takeSnapshot() {
157
169
  return {
@@ -166,7 +178,7 @@ export class FreeFormFlexiGrid extends FlexiGrid {
166
178
  width: widget.width,
167
179
  height: widget.height
168
180
  })),
169
- needsCollapsing: this.#needsCollapsing
181
+ needsPostEditOperations: this.#needsPostEditOperations
170
182
  };
171
183
  }
172
184
  clear() {
@@ -176,7 +188,7 @@ export class FreeFormFlexiGrid extends FlexiGrid {
176
188
  this.#rows = this.#layoutConfig.minRows;
177
189
  this.#columns = this.#layoutConfig.minColumns;
178
190
  this.#coordinateSystem.clear();
179
- this.#needsCollapsing = false;
191
+ this.#needsPostEditOperations = false;
180
192
  }
181
193
  restoreFromSnapshot(snapshot) {
182
194
  // Must deep copy these again, as the snapshot may be re-used.
@@ -192,7 +204,7 @@ export class FreeFormFlexiGrid extends FlexiGrid {
192
204
  widget.widget.setBounds(widget.x, widget.y, widget.width, widget.height);
193
205
  }
194
206
  // Restore the collapsing flag
195
- this.#needsCollapsing = snapshot.needsCollapsing;
207
+ this.#needsPostEditOperations = snapshot.needsPostEditOperations;
196
208
  }
197
209
  mapRawCellToFinalCell(x, y) {
198
210
  return [Math.floor(x), Math.floor(y)];
@@ -260,7 +272,10 @@ export class FreeFormFlexiGrid extends FlexiGrid {
260
272
  return this.#columns;
261
273
  }
262
274
  get collapsibility() {
263
- return this.#layoutConfig.colllapsibility;
275
+ return this.#layoutConfig.collapsibility;
276
+ }
277
+ get packing() {
278
+ return this.#layoutConfig.packing;
264
279
  }
265
280
  get minRows() {
266
281
  return this.#layoutConfig.minRows;
@@ -376,6 +391,95 @@ class FreeFormGridCoordinateSystem {
376
391
  #isRowEmpty(row) {
377
392
  return this.bitmaps[row] === 0;
378
393
  }
394
+ applyPacking() {
395
+ if (this.#grid.packing === 'none') {
396
+ return;
397
+ }
398
+ if (this.#grid.packing === 'horizontal') {
399
+ this.applyHorizontalPacking();
400
+ }
401
+ if (this.#grid.packing === 'vertical') {
402
+ this.applyVerticalPacking();
403
+ }
404
+ }
405
+ applyHorizontalPacking() {
406
+ // Pack the widgets that are closest to the left first.
407
+ const sortedWidgets = Array.from(this.#grid.getWidgetsForModification()).toSorted((a, b) => {
408
+ if (a.x == b.x) {
409
+ return a.y - b.y;
410
+ }
411
+ return a.x - b.x;
412
+ });
413
+ for (const widget of sortedWidgets) {
414
+ // We can already automatically eliminate any widget that's at x = 0.
415
+ if (widget.x === 0) {
416
+ continue;
417
+ }
418
+ const x = widget.x;
419
+ const y = widget.y;
420
+ let minimumAvailableShift = 0;
421
+ // Compute the best shift to the left we can achieve for this widget.
422
+ let blocked = false;
423
+ for (let j = x - 1; j >= 0; j--) {
424
+ for (let i = y; i < y + widget.height; i++) {
425
+ if (this.layout[i][j] !== null) {
426
+ blocked = true;
427
+ break;
428
+ }
429
+ }
430
+ if (blocked) {
431
+ break;
432
+ }
433
+ minimumAvailableShift++;
434
+ }
435
+ if (minimumAvailableShift == 0) {
436
+ continue;
437
+ }
438
+ // Remove and re-add the widget so the bitmaps are updated correctly.
439
+ this.removeWidget(widget);
440
+ widget.setBounds(widget.x - minimumAvailableShift, widget.y, widget.width, widget.height);
441
+ this.addWidget(widget, widget.x, widget.y, widget.width, widget.height);
442
+ }
443
+ }
444
+ applyVerticalPacking() {
445
+ // Pack the widgets that are closest to the top first.
446
+ const sortedWidgets = Array.from(this.#grid.getWidgetsForModification()).toSorted((a, b) => {
447
+ if (a.y == b.y) {
448
+ return a.x - b.x;
449
+ }
450
+ return a.y - b.y;
451
+ });
452
+ for (const widget of sortedWidgets) {
453
+ // We can already automatically eliminate any widget that's at y = 0.
454
+ if (widget.y === 0) {
455
+ continue;
456
+ }
457
+ const x = widget.x;
458
+ const y = widget.y;
459
+ let minimumAvailableShift = 0;
460
+ // Compute the best shift to the top we can achieve for this widget.
461
+ let blocked = false;
462
+ for (let i = y - 1; i >= 0; i--) {
463
+ for (let j = x; j < x + widget.width; j++) {
464
+ if (this.layout[i][j] !== null) {
465
+ blocked = true;
466
+ break;
467
+ }
468
+ }
469
+ if (blocked) {
470
+ break;
471
+ }
472
+ minimumAvailableShift++;
473
+ }
474
+ if (minimumAvailableShift == 0) {
475
+ continue;
476
+ }
477
+ // Remove and re-add the widget so the bitmaps are updated correctly.
478
+ this.removeWidget(widget);
479
+ widget.setBounds(widget.x, widget.y - minimumAvailableShift, widget.width, widget.height);
480
+ this.addWidget(widget, widget.x, widget.y, widget.width, widget.height);
481
+ }
482
+ }
379
483
  applyRowCollapsibility() {
380
484
  const currentRows = this.#rows;
381
485
  const minRows = this.#grid.minRows;
@@ -424,14 +528,22 @@ class FreeFormGridCoordinateSystem {
424
528
  rowsToRemove.sort((a, b) => b - a);
425
529
  // Remove rows and update widget positions
426
530
  for (const rowIndex of rowsToRemove) {
531
+ // Collect widgets to shift BEFORE modifying arrays
532
+ const widgetsToShift = this.#grid
533
+ .getWidgetsForModification()
534
+ .filter((widget) => widget.y > rowIndex);
535
+ // Remove widgets from coordinate system BEFORE splicing
536
+ for (const widget of widgetsToShift) {
537
+ this.removeWidget(widget);
538
+ }
539
+ // Splice the row from layout and bitmaps
427
540
  this.layout.splice(rowIndex, 1);
428
541
  this.bitmaps.splice(rowIndex, 1);
429
- // Update widget positions for all widgets that were below the removed row
430
- const widgetsToShift = this.#grid.getWidgetsForModification();
542
+ // Update widget positions and re-add to coordinate system
431
543
  for (const widget of widgetsToShift) {
432
- if (widget.y > rowIndex) {
433
- widget.setBounds(widget.x, widget.y - 1, widget.width, widget.height);
434
- }
544
+ const newY = widget.y - 1;
545
+ widget.setBounds(widget.x, newY, widget.width, widget.height);
546
+ this.addWidget(widget, widget.x, newY, widget.width, widget.height);
435
547
  }
436
548
  }
437
549
  return newRows;
@@ -484,18 +596,25 @@ class FreeFormGridCoordinateSystem {
484
596
  columnsToRemove.sort((a, b) => b - a);
485
597
  // Remove columns and update widget positions
486
598
  for (const columnIndex of columnsToRemove) {
599
+ // Collect widgets to shift BEFORE modifying arrays
600
+ const widgetsToShift = this.#grid
601
+ .getWidgetsForModification()
602
+ .filter((widget) => widget.x > columnIndex);
603
+ // Remove widgets from coordinate system BEFORE splicing
604
+ for (const widget of widgetsToShift) {
605
+ this.removeWidget(widget);
606
+ }
487
607
  // Remove the column from the layout
488
608
  this.layout.forEach((row) => row.splice(columnIndex, 1));
489
609
  // Update bitmaps by removing the column bit and shifting
490
610
  for (let rowIndex = 0; rowIndex < this.bitmaps.length; rowIndex++) {
491
611
  this.bitmaps[rowIndex] = this.#removeColumnFromBitmap(this.bitmaps[rowIndex], columnIndex);
492
612
  }
493
- // Update widget positions for all widgets that were to the right of the removed column
494
- const widgetsToShift = this.#grid.getWidgetsForModification();
613
+ // Update widget positions and re-add to coordinate system
495
614
  for (const widget of widgetsToShift) {
496
- if (widget.x > columnIndex) {
497
- widget.setBounds(widget.x - 1, widget.y, widget.width, widget.height);
498
- }
615
+ const newX = widget.x - 1;
616
+ widget.setBounds(newX, widget.y, widget.width, widget.height);
617
+ this.addWidget(widget, newX, widget.y, widget.width, widget.height);
499
618
  }
500
619
  }
501
620
  return newColumns;
@@ -10,8 +10,4 @@ export declare class FlexiDeleteController {
10
10
  }
11
11
  export declare function flexidelete(): {
12
12
  deleter: FlexiDeleteController;
13
- /** @deprecated */
14
- onpointerenter: () => void;
15
- /** @deprecated */
16
- onpointerleave: () => void;
17
13
  };
@@ -39,11 +39,6 @@ export function flexidelete() {
39
39
  const provider = getInternalFlexiboardCtx();
40
40
  const deleter = new FlexiDeleteController(provider);
41
41
  return {
42
- deleter,
43
- // TODO: remove in v0.4
44
- /** @deprecated */
45
- onpointerenter: () => { },
46
- /** @deprecated */
47
- onpointerleave: () => { }
42
+ deleter
48
43
  };
49
44
  }
@@ -45,7 +45,6 @@ export class FlexiPortalController {
45
45
  */
46
46
  moveWidgetToPortal(widget) {
47
47
  if (!widget.ref) {
48
- console.warn('moveWidgetToPortal() was called on a widget that has no ref. No widget will appear under the pointer.');
49
48
  return;
50
49
  }
51
50
  // Store original position info
@@ -0,0 +1,46 @@
1
+ import type { FlexiLayout } from '../board/types.js';
2
+ import type { ResponsiveFlexiLayout } from './types.js';
3
+ /**
4
+ * Public interface for the ResponsiveFlexiBoard controller.
5
+ * Manages multiple FlexiBoard layouts across different viewport breakpoints.
6
+ */
7
+ export interface ResponsiveFlexiBoardController {
8
+ /**
9
+ * The currently active breakpoint key.
10
+ */
11
+ readonly currentBreakpoint: string;
12
+ /**
13
+ * All breakpoint keys that have stored layouts.
14
+ */
15
+ readonly definedBreakpoints: string[];
16
+ /**
17
+ * All breakpoint keys from configuration.
18
+ */
19
+ readonly configuredBreakpoints: string[];
20
+ /**
21
+ * Imports layouts for all breakpoints.
22
+ * @param layout The responsive layout to import.
23
+ */
24
+ importLayout(layout: ResponsiveFlexiLayout): void;
25
+ /**
26
+ * Exports layouts for all breakpoints.
27
+ * @returns The complete responsive layout.
28
+ */
29
+ exportLayout(): ResponsiveFlexiLayout;
30
+ /**
31
+ * Gets the layout for a specific breakpoint.
32
+ * @param breakpoint The breakpoint key.
33
+ */
34
+ getLayoutForBreakpoint(breakpoint: string): FlexiLayout | undefined;
35
+ /**
36
+ * Sets the layout for a specific breakpoint.
37
+ * @param breakpoint The breakpoint key.
38
+ * @param layout The layout to set.
39
+ */
40
+ setLayoutForBreakpoint(breakpoint: string, layout: FlexiLayout): void;
41
+ /**
42
+ * Checks if a layout exists for a specific breakpoint.
43
+ * @param breakpoint The breakpoint key.
44
+ */
45
+ hasLayoutForBreakpoint(breakpoint: string): boolean;
46
+ }
@@ -0,0 +1 @@
1
+ export {};