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.
- package/dist/components/flexi-add.svelte +2 -15
- package/dist/components/flexi-add.svelte.d.ts +0 -12
- package/dist/components/flexi-delete.svelte +3 -16
- package/dist/components/flexi-delete.svelte.d.ts +0 -12
- package/dist/components/flexi-grab.svelte +2 -2
- package/dist/components/flexi-target.svelte +8 -19
- package/dist/components/flexi-widget.svelte +2 -2
- package/dist/components/responsive-flexi-board.svelte +83 -0
- package/dist/components/responsive-flexi-board.svelte.d.ts +34 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/system/board/base.svelte.d.ts +15 -0
- package/dist/system/board/controller.svelte.d.ts +26 -4
- package/dist/system/board/controller.svelte.js +237 -28
- package/dist/system/board/types.d.ts +26 -0
- package/dist/system/grid/base.svelte.d.ts +9 -0
- package/dist/system/grid/base.svelte.js +12 -1
- package/dist/system/grid/flow-grid.svelte.js +105 -36
- package/dist/system/grid/free-grid.svelte.d.ts +6 -2
- package/dist/system/grid/free-grid.svelte.js +139 -20
- package/dist/system/misc/deleter.svelte.d.ts +0 -4
- package/dist/system/misc/deleter.svelte.js +1 -6
- package/dist/system/portal.js +0 -1
- package/dist/system/responsive/base.svelte.d.ts +46 -0
- package/dist/system/responsive/base.svelte.js +1 -0
- package/dist/system/responsive/controller.svelte.d.ts +78 -0
- package/dist/system/responsive/controller.svelte.js +264 -0
- package/dist/system/responsive/index.d.ts +16 -0
- package/dist/system/responsive/index.js +36 -0
- package/dist/system/responsive/types.d.ts +56 -0
- package/dist/system/responsive/types.js +1 -0
- package/dist/system/shared/event-bus.d.ts +3 -1
- package/dist/system/shared/utils.svelte.d.ts +2 -0
- package/dist/system/shared/utils.svelte.js +39 -22
- package/dist/system/target/controller.svelte.d.ts +7 -2
- package/dist/system/target/controller.svelte.js +103 -30
- package/dist/system/types.d.ts +13 -2
- package/dist/system/widget/base.svelte.d.ts +40 -1
- package/dist/system/widget/base.svelte.js +84 -2
- package/dist/system/widget/controller.svelte.d.ts +4 -1
- package/dist/system/widget/controller.svelte.js +106 -17
- package/dist/system/widget/events.js +10 -3
- package/dist/system/widget/interpolation-utils.d.ts +14 -0
- package/dist/system/widget/interpolation-utils.js +32 -0
- package/dist/system/widget/interpolator.svelte.d.ts +2 -1
- package/dist/system/widget/interpolator.svelte.js +63 -22
- package/dist/system/widget/triggers.svelte.js +1 -1
- package/dist/system/widget/types.d.ts +51 -6
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
let
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.
|
|
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
|
|
430
|
-
const widgetsToShift = this.#grid.getWidgetsForModification();
|
|
542
|
+
// Update widget positions and re-add to coordinate system
|
|
431
543
|
for (const widget of widgetsToShift) {
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
494
|
-
const widgetsToShift = this.#grid.getWidgetsForModification();
|
|
613
|
+
// Update widget positions and re-add to coordinate system
|
|
495
614
|
for (const widget of widgetsToShift) {
|
|
496
|
-
|
|
497
|
-
|
|
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;
|
|
@@ -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
|
}
|
package/dist/system/portal.js
CHANGED
|
@@ -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 {};
|