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
@@ -6,8 +6,9 @@ import { WidgetPointerEventWatcher } from './triggers.svelte.js';
6
6
  export class InternalFlexiWidgetController extends FlexiWidgetController {
7
7
  #pointerService = getPointerService();
8
8
  // Grabber and resizer tracking
9
- #grabbers = $state(0);
10
- #resizers = $state(0);
9
+ #grabbers = 0;
10
+ #resizers = 0;
11
+ #disposed = false;
11
12
  // Movement interpolation
12
13
  interpolator;
13
14
  internalTarget = undefined;
@@ -15,6 +16,16 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
15
16
  mounted = $state(false);
16
17
  #eventBus;
17
18
  #unsubscribers = [];
19
+ #lastActionType = null;
20
+ #interpolationAnimationHint = null;
21
+ #type;
22
+ #userProvidedId;
23
+ get type() {
24
+ return this.#type;
25
+ }
26
+ get userProvidedId() {
27
+ return this.#userProvidedId;
28
+ }
18
29
  /**
19
30
  * The styling to apply to the widget.
20
31
  */
@@ -38,7 +49,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
38
49
  if (!this.mounted) {
39
50
  return '';
40
51
  }
41
- if (!this.draggable) {
52
+ if (!this.isGrabbable) {
42
53
  return '';
43
54
  }
44
55
  if (this.isGrabbed) {
@@ -67,10 +78,42 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
67
78
  return `pointer-events: none; user-select: none; cursor: grabbing; position: absolute; top: ${locationOffsetY}px; left: ${locationOffsetX}px; height: ${height}px; width: ${width}px;`;
68
79
  }
69
80
  #getResizingWidgetStyle(action) {
81
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
82
+ const parseGap = (value) => {
83
+ if (!value || value === 'normal') {
84
+ return 0;
85
+ }
86
+ const numeric = Number.parseFloat(value);
87
+ return Number.isFinite(numeric) ? numeric : 0;
88
+ };
89
+ const toPx = (units, unitSize, gapSize) => {
90
+ if (!Number.isFinite(units)) {
91
+ return Infinity;
92
+ }
93
+ return units * unitSize + Math.max(0, units - 1) * gapSize;
94
+ };
95
+ const gridRef = this.internalTarget?.grid?.ref;
96
+ const gridColumns = this.internalTarget?.columns ?? 0;
97
+ const gridRows = this.internalTarget?.rows ?? 0;
98
+ const gridStyle = gridRef && typeof window !== 'undefined' ? window.getComputedStyle(gridRef) : null;
99
+ const columnGapPx = parseGap(gridStyle?.columnGap);
100
+ const rowGapPx = parseGap(gridStyle?.rowGap);
70
101
  // Calculate size of one grid unit in pixels
71
- const unitSizeY = action.capturedHeightPx / action.initialHeightUnits;
102
+ const fallbackUnitSizeY = (action.capturedHeightPx - Math.max(0, action.initialHeightUnits - 1) * rowGapPx) /
103
+ action.initialHeightUnits;
72
104
  // Guard against division by zero if initial width is somehow 0
73
- const unitSizeX = action.initialWidthUnits > 0 ? action.capturedWidthPx / action.initialWidthUnits : 1;
105
+ const fallbackUnitSizeX = action.initialWidthUnits > 0
106
+ ? (action.capturedWidthPx - Math.max(0, action.initialWidthUnits - 1) * columnGapPx) /
107
+ action.initialWidthUnits
108
+ : 1;
109
+ const liveUnitSizeX = gridRef && gridColumns > 0
110
+ ? (gridRef.clientWidth - Math.max(0, gridColumns - 1) * columnGapPx) / gridColumns
111
+ : NaN;
112
+ const liveUnitSizeY = gridRef && gridRows > 0
113
+ ? (gridRef.clientHeight - Math.max(0, gridRows - 1) * rowGapPx) / gridRows
114
+ : NaN;
115
+ const unitSizeX = Number.isFinite(liveUnitSizeX) && liveUnitSizeX > 0 ? liveUnitSizeX : fallbackUnitSizeX;
116
+ const unitSizeY = Number.isFinite(liveUnitSizeY) && liveUnitSizeY > 0 ? liveUnitSizeY : fallbackUnitSizeY;
74
117
  const deltaX = this.#pointerService.position.x - action.offsetX - action.left;
75
118
  const deltaY = this.#pointerService.position.y - action.offsetY - action.top;
76
119
  // For resizing, top and left should remain fixed at their initial positions.
@@ -79,19 +122,31 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
79
122
  // Calculate new dimensions based on resizability
80
123
  let height = action.capturedHeightPx;
81
124
  let width = action.capturedWidthPx;
125
+ const minWidthPx = toPx(this.minWidth, unitSizeX, columnGapPx);
126
+ const minHeightPx = toPx(this.minHeight, unitSizeY, rowGapPx);
127
+ const gridMaxWidthUnits = this.internalTarget && this.internalTarget.columns > 0
128
+ ? Math.max(1, this.internalTarget.columns - this.x)
129
+ : Infinity;
130
+ const gridMaxHeightUnits = this.internalTarget && this.internalTarget.rows > 0
131
+ ? Math.max(1, this.internalTarget.rows - this.y)
132
+ : Infinity;
133
+ const configMaxWidthUnits = Number.isFinite(this.maxWidth) ? this.maxWidth : Infinity;
134
+ const configMaxHeightUnits = Number.isFinite(this.maxHeight) ? this.maxHeight : Infinity;
135
+ const maxWidthPx = toPx(Math.min(configMaxWidthUnits, gridMaxWidthUnits), unitSizeX, columnGapPx);
136
+ const maxHeightPx = toPx(Math.min(configMaxHeightUnits, gridMaxHeightUnits), unitSizeY, rowGapPx);
82
137
  switch (this.resizability) {
83
138
  case 'horizontal':
84
139
  // NOTE: Use the pre-calculated deltaX here
85
- width = Math.max(action.capturedWidthPx + deltaX, unitSizeX);
140
+ width = clamp(action.capturedWidthPx + deltaX, minWidthPx, maxWidthPx);
86
141
  break;
87
142
  case 'vertical':
88
143
  // NOTE: Use the pre-calculated deltaY here
89
- height = Math.max(action.capturedHeightPx + deltaY, unitSizeY);
144
+ height = clamp(action.capturedHeightPx + deltaY, minHeightPx, maxHeightPx);
90
145
  break;
91
146
  case 'both':
92
147
  // NOTE: Use the pre-calculated deltaX and deltaY here
93
- height = Math.max(action.capturedHeightPx + deltaY, unitSizeY);
94
- width = Math.max(action.capturedWidthPx + deltaX, unitSizeX);
148
+ height = clamp(action.capturedHeightPx + deltaY, minHeightPx, maxHeightPx);
149
+ width = clamp(action.capturedWidthPx + deltaX, minWidthPx, maxWidthPx);
95
150
  break;
96
151
  }
97
152
  // Return the style string for the absolutely positioned widget
@@ -113,6 +168,8 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
113
168
  this.internalTarget = params.target;
114
169
  }
115
170
  this.provider = params.provider;
171
+ this.#type = params.type;
172
+ this.#userProvidedId = params.config.id;
116
173
  // Create the widget's interpolator
117
174
  this.interpolator = new WidgetMoveInterpolator(this.provider, this);
118
175
  this.#eventBus = getFlexiEventBus();
@@ -128,6 +185,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
128
185
  if (event.widget !== this) {
129
186
  return;
130
187
  }
188
+ this.#lastActionType = 'grab';
131
189
  // We probably need to wait for the widget to be portalled before we can acquire its focus.
132
190
  setTimeout(() => {
133
191
  this.ref?.focus();
@@ -145,6 +203,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
145
203
  if (event.widget !== this) {
146
204
  return;
147
205
  }
206
+ this.#lastActionType = 'resize';
148
207
  // We probably need to wait for the widget to be portalled before we can acquire its focus.
149
208
  setTimeout(() => {
150
209
  this.ref?.focus();
@@ -177,15 +236,26 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
177
236
  * @param height The height of the widget.
178
237
  */
179
238
  setBounds(x, y, width, height, interpolate = true) {
180
- if (this.x == x && this.y == y && this.width == width && this.height == height) {
239
+ const previousDimensions = {
240
+ width: this.width,
241
+ height: this.height
242
+ };
243
+ const positionUnchanged = this.x == x && this.y == y && this.width == width && this.height == height;
244
+ if (positionUnchanged) {
245
+ // Still interpolate for drop animations even when position is unchanged
246
+ if (interpolate && this.backingState.currentAction?.action === 'grab') {
247
+ this.#interpolateMove(x, y, this.width, this.height, previousDimensions);
248
+ }
181
249
  return;
182
250
  }
251
+ const constrainedWidth = Math.max(this.minWidth, Math.min(this.maxWidth, width));
252
+ const constrainedHeight = Math.max(this.minHeight, Math.min(this.maxHeight, height));
183
253
  this.backingState.x = x;
184
254
  this.backingState.y = y;
185
- this.backingState.width = width;
186
- this.backingState.height = height;
255
+ this.backingState.width = constrainedWidth;
256
+ this.backingState.height = constrainedHeight;
187
257
  if (interpolate) {
188
- this.#interpolateMove(x, y, width, height);
258
+ this.#interpolateMove(x, y, constrainedWidth, constrainedHeight, previousDimensions);
189
259
  }
190
260
  }
191
261
  #getMovementAnimation() {
@@ -194,11 +264,19 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
194
264
  return 'drop';
195
265
  case 'resize':
196
266
  return 'resize';
197
- default:
198
- return 'move';
199
267
  }
268
+ // If action state has already been released but this update is part of a drop placement,
269
+ // preserve the last interaction animation type for interpolation.
270
+ if (this.backingState.isBeingDropped) {
271
+ return this.#lastActionType === 'resize' ? 'resize' : 'drop';
272
+ }
273
+ // Shadow/dropzone widgets can hint the desired interpolation mode even without an action state.
274
+ if (this.#interpolationAnimationHint) {
275
+ return this.#interpolationAnimationHint;
276
+ }
277
+ return 'move';
200
278
  }
201
- #interpolateMove(x, y, width, height) {
279
+ #interpolateMove(x, y, width, height, previousDimensions) {
202
280
  const rect = this.ref?.getBoundingClientRect();
203
281
  if (!rect || !this.interpolator) {
204
282
  return;
@@ -213,7 +291,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
213
291
  top: rect.top,
214
292
  width: rect.width,
215
293
  height: rect.height
216
- }, this.#getMovementAnimation());
294
+ }, this.#getMovementAnimation(), previousDimensions);
217
295
  this.backingState.isBeingDropped = false;
218
296
  }
219
297
  /**
@@ -228,6 +306,9 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
228
306
  * Unregisters a grabber from the widget.
229
307
  */
230
308
  removeGrabber() {
309
+ if (this.#disposed) {
310
+ return 0;
311
+ }
231
312
  this.#grabbers--;
232
313
  this.backingState.hasGrabbers = this.#grabbers > 0;
233
314
  return this.#grabbers;
@@ -244,6 +325,9 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
244
325
  * Unregisters a resizer from the widget.
245
326
  */
246
327
  removeResizer() {
328
+ if (this.#disposed) {
329
+ return 0;
330
+ }
247
331
  this.#resizers--;
248
332
  this.backingState.hasResizers = this.#resizers > 0;
249
333
  return this.#resizers;
@@ -299,6 +383,8 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
299
383
  * Cleanup method to be called when the widget is destroyed
300
384
  */
301
385
  destroy() {
386
+ // Mark as disposed to ignore any deferred cleanup callbacks
387
+ this.#disposed = true;
302
388
  // Reset counters
303
389
  this.#grabbers = 0;
304
390
  this.#resizers = 0;
@@ -312,4 +398,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
312
398
  get shouldDrawPlaceholder() {
313
399
  return this.interpolator?.active ?? false;
314
400
  }
401
+ set interpolationAnimationHint(value) {
402
+ this.#interpolationAnimationHint = value;
403
+ }
315
404
  }
@@ -30,6 +30,8 @@ export function widgetGrabberEvents(widget) {
30
30
  const grabWatcher = new WidgetPointerEventWatcher(widget, 'grab');
31
31
  return {
32
32
  onpointerdown: (event) => {
33
+ // Don't propagate to parent widget, preventing double-grab dispatch
34
+ event.stopPropagation();
33
35
  // Use the trigger watcher which respects trigger configuration (immediate vs long press)
34
36
  grabWatcher.onstartpointerdown(event);
35
37
  },
@@ -40,12 +42,17 @@ export function widgetResizerEvents(widget) {
40
42
  const eventBus = getFlexiEventBusCtx();
41
43
  const board = getInternalFlexiboardCtx();
42
44
  const resizeWatcher = new WidgetPointerEventWatcher(widget, 'resize');
45
+ // Don't propagate events upwards, so that it never also triggers a grab action.
43
46
  return {
44
47
  onpointerdown: (event) => {
48
+ event.stopPropagation();
45
49
  // Invoke the trigger watcher which respects trigger configuration (immediate vs long press).
46
50
  resizeWatcher.onstartpointerdown(event);
47
51
  },
48
- onkeydown: (event) => dispatchKeyDownResize(eventBus, widget, board, event)
52
+ onkeydown: (event) => {
53
+ event.stopPropagation();
54
+ dispatchKeyDownResize(eventBus, widget, board, event);
55
+ }
49
56
  };
50
57
  }
51
58
  /**
@@ -55,7 +62,7 @@ export function widgetResizerEvents(widget) {
55
62
  * @param event The keyboard event.
56
63
  */
57
64
  function dispatchKeyDownGrab(eventBus, widget, board, event) {
58
- if (!widget.draggable || !widget.ref || event.key !== 'Enter') {
65
+ if (!widget.isGrabbable || !widget.ref || event.key !== 'Enter') {
59
66
  return;
60
67
  }
61
68
  // If an action is already active, do not intercept Enter.
@@ -80,7 +87,7 @@ function dispatchKeyDownGrab(eventBus, widget, board, event) {
80
87
  * @param clientY The y-coordinate of the pointer event.
81
88
  */
82
89
  function dispatchGrab(eventBus, widget, board, { clientX, clientY }) {
83
- if (!widget.draggable || !widget.ref) {
90
+ if (!widget.isGrabbable || !widget.ref) {
84
91
  return;
85
92
  }
86
93
  const rect = widget.ref?.getBoundingClientRect();
@@ -0,0 +1,14 @@
1
+ export type InterpolationAnimation = 'move' | 'drop' | 'resize';
2
+ export type InterpolationSize = {
3
+ width: number;
4
+ height: number;
5
+ };
6
+ export type PlaceholderMinDimensionLocks = {
7
+ lockMinWidth: boolean;
8
+ lockMinHeight: boolean;
9
+ };
10
+ /**
11
+ * Keep min dimensions by default so auto-sized tracks remain stable while the placeholder is hidden.
12
+ * During resize, only unlock min dimensions on axes that actually changed.
13
+ */
14
+ export declare function getPlaceholderMinDimensionLocks(animation: InterpolationAnimation, newSize: InterpolationSize, previousSize?: InterpolationSize): PlaceholderMinDimensionLocks;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Keep min dimensions by default so auto-sized tracks remain stable while the placeholder is hidden.
3
+ * During resize, only unlock min dimensions on axes that actually changed.
4
+ */
5
+ export function getPlaceholderMinDimensionLocks(animation, newSize, previousSize) {
6
+ if (animation !== 'resize') {
7
+ return {
8
+ lockMinWidth: true,
9
+ lockMinHeight: true
10
+ };
11
+ }
12
+ if (!previousSize) {
13
+ // Fallback for legacy call sites: unlock both so shrinking resize animations still work.
14
+ return {
15
+ lockMinWidth: false,
16
+ lockMinHeight: false
17
+ };
18
+ }
19
+ const widthChanged = newSize.width !== previousSize.width;
20
+ const heightChanged = newSize.height !== previousSize.height;
21
+ // Nothing resized (e.g. clamped), keep both mins in place.
22
+ if (!widthChanged && !heightChanged) {
23
+ return {
24
+ lockMinWidth: true,
25
+ lockMinHeight: true
26
+ };
27
+ }
28
+ return {
29
+ lockMinWidth: !widthChanged,
30
+ lockMinHeight: !heightChanged
31
+ };
32
+ }
@@ -1,6 +1,7 @@
1
1
  import type { InternalFlexiBoardController } from '../board/controller.svelte.js';
2
2
  import type { Position } from '../types.js';
3
3
  import type { FlexiWidgetController } from './base.svelte.js';
4
+ import { type InterpolationSize } from './interpolation-utils.js';
4
5
  export declare class WidgetMoveInterpolator {
5
6
  #private;
6
7
  active: boolean;
@@ -8,7 +9,7 @@ export declare class WidgetMoveInterpolator {
8
9
  widgetStyle: string;
9
10
  placeholderStyle: string;
10
11
  constructor(provider: InternalFlexiBoardController, widget: FlexiWidgetController);
11
- interpolateMove(newDimensions: Dimensions, oldPosition: InterpolationPosition, animation?: WidgetMovementAnimation): void;
12
+ interpolateMove(newDimensions: Dimensions, oldPosition: InterpolationPosition, animation?: WidgetMovementAnimation, previousDimensions?: InterpolationSize): void;
12
13
  onPlaceholderMove(rect: DOMRect): void;
13
14
  onPlaceholderMount(ref: HTMLElement): () => void;
14
15
  onPlaceholderUnmount(): void;
@@ -1,4 +1,4 @@
1
- import { onMount } from 'svelte';
1
+ import { getPlaceholderMinDimensionLocks } from './interpolation-utils.js';
2
2
  export class WidgetMoveInterpolator {
3
3
  active = $state(false);
4
4
  #timeout;
@@ -12,7 +12,9 @@ export class WidgetMoveInterpolator {
12
12
  width: 1,
13
13
  height: 1,
14
14
  heightPx: 0,
15
- widthPx: 0
15
+ widthPx: 0,
16
+ lockMinWidth: true,
17
+ lockMinHeight: true
16
18
  });
17
19
  #interpolatedWidgetPosition = $state({
18
20
  left: 0,
@@ -32,7 +34,13 @@ export class WidgetMoveInterpolator {
32
34
  return `transition: all ${transitionConfig.duration}ms ${transitionConfig.easing}; position: absolute; top: ${this.#interpolatedWidgetPosition.top}px; left: ${this.#interpolatedWidgetPosition.left}px; width: ${this.#interpolatedWidgetPosition.width}px; height: ${this.#interpolatedWidgetPosition.height}px;`;
33
35
  });
34
36
  placeholderStyle = $derived.by(() => {
35
- return `grid-column: ${this.#placeholderPosition.x + 1} / span ${this.#placeholderPosition.width}; grid-row: ${this.#placeholderPosition.y + 1} / span ${this.#placeholderPosition.height}; visibility: hidden;`;
37
+ const minHeight = this.#placeholderPosition.lockMinHeight
38
+ ? ` min-height: ${this.#placeholderPosition.heightPx}px;`
39
+ : '';
40
+ const minWidth = this.#placeholderPosition.lockMinWidth
41
+ ? ` min-width: ${this.#placeholderPosition.widthPx}px;`
42
+ : '';
43
+ return `grid-column: ${this.#placeholderPosition.x + 1} / span ${this.#placeholderPosition.width}; grid-row: ${this.#placeholderPosition.y + 1} / span ${this.#placeholderPosition.height};${minHeight}${minWidth} visibility: hidden;`;
36
44
  });
37
45
  constructor(provider, widget) {
38
46
  this.#provider = provider;
@@ -40,7 +48,13 @@ export class WidgetMoveInterpolator {
40
48
  this.onPlaceholderMount = this.onPlaceholderMount.bind(this);
41
49
  this.onPlaceholderUnmount = this.onPlaceholderUnmount.bind(this);
42
50
  }
43
- interpolateMove(newDimensions, oldPosition, animation = 'move') {
51
+ #notifyStart() {
52
+ this.#provider?.notifyInterpolationStarted();
53
+ }
54
+ #notifyEnd() {
55
+ this.#provider?.notifyInterpolationEnded();
56
+ }
57
+ interpolateMove(newDimensions, oldPosition, animation = 'move', previousDimensions) {
44
58
  const containerRect = this.#containerRef?.getBoundingClientRect();
45
59
  if (!containerRect) {
46
60
  return;
@@ -50,29 +64,56 @@ export class WidgetMoveInterpolator {
50
64
  if (!transitionConfig || !transitionConfig.duration || !transitionConfig.easing) {
51
65
  return;
52
66
  }
53
- if (this.active) {
54
- clearTimeout(this.#timeout);
55
- }
56
- this.active = true;
57
- this.#animation = animation;
58
- this.#placeholderPosition = {
59
- x: newDimensions.x,
60
- y: newDimensions.y,
67
+ const minDimensionLocks = getPlaceholderMinDimensionLocks(animation, {
61
68
  width: newDimensions.width,
62
- height: newDimensions.height,
63
- heightPx: oldPosition.height,
64
- widthPx: oldPosition.width
65
- };
66
- this.#interpolatedWidgetPosition.top =
67
- oldPosition.top - containerRect.top + (this.#containerRef?.scrollTop ?? 0);
68
- this.#interpolatedWidgetPosition.left =
69
- oldPosition.left - containerRect.left + (this.#containerRef?.scrollLeft ?? 0);
70
- this.#interpolatedWidgetPosition.width = oldPosition.width;
71
- this.#interpolatedWidgetPosition.height = oldPosition.height;
69
+ height: newDimensions.height
70
+ }, previousDimensions);
71
+ const isInterruption = this.active;
72
+ clearTimeout(this.#timeout);
73
+ if (isInterruption) {
74
+ // INTERRUPTION PATH - only update target, keep transition flowing
75
+ // Don't change #animation to preserve CSS transition property
76
+ // Don't reset #interpolatedWidgetPosition
77
+ this.#placeholderPosition = {
78
+ x: newDimensions.x,
79
+ y: newDimensions.y,
80
+ width: newDimensions.width,
81
+ height: newDimensions.height,
82
+ heightPx: this.#interpolatedWidgetPosition.height,
83
+ widthPx: this.#interpolatedWidgetPosition.width,
84
+ lockMinWidth: minDimensionLocks.lockMinWidth,
85
+ lockMinHeight: minDimensionLocks.lockMinHeight
86
+ };
87
+ }
88
+ else {
89
+ // INITIAL MOVE PATH - set up starting position, then animate
90
+ this.#inInitialFrame = true; // Disable CSS transition for initial position
91
+ this.active = true;
92
+ this.#animation = animation;
93
+ this.#notifyStart();
94
+ this.#placeholderPosition = {
95
+ x: newDimensions.x,
96
+ y: newDimensions.y,
97
+ width: newDimensions.width,
98
+ height: newDimensions.height,
99
+ heightPx: oldPosition.height,
100
+ widthPx: oldPosition.width,
101
+ lockMinWidth: minDimensionLocks.lockMinWidth,
102
+ lockMinHeight: minDimensionLocks.lockMinHeight
103
+ };
104
+ this.#interpolatedWidgetPosition.top =
105
+ oldPosition.top - containerRect.top + (this.#containerRef?.scrollTop ?? 0);
106
+ this.#interpolatedWidgetPosition.left =
107
+ oldPosition.left - containerRect.left + (this.#containerRef?.scrollLeft ?? 0);
108
+ this.#interpolatedWidgetPosition.width = oldPosition.width;
109
+ this.#interpolatedWidgetPosition.height = oldPosition.height;
110
+ }
111
+ // Reset timeout for both paths
72
112
  requestAnimationFrame(() => {
73
113
  this.#timeout = setTimeout(() => {
74
114
  this.active = false;
75
115
  this.#animation = 'move';
116
+ this.#notifyEnd();
76
117
  }, transitionConfig.duration);
77
118
  });
78
119
  }
@@ -112,7 +112,7 @@ export class WidgetPointerEventWatcher {
112
112
  });
113
113
  return;
114
114
  }
115
- if (!this.#widget.draggable) {
115
+ if (!this.#widget.isGrabbable) {
116
116
  return;
117
117
  }
118
118
  this.#eventBus.dispatch('widget:grabbed', {
@@ -2,7 +2,7 @@ import type { ClassValue } from 'svelte/elements';
2
2
  import type { FlexiWidgetController } from './base.svelte.js';
3
3
  import type { Component, Snippet } from 'svelte';
4
4
  import { type PointerTriggerCondition } from './triggers.svelte.js';
5
- import type { WidgetAction, WidgetResizability } from '../types.js';
5
+ import type { WidgetAction, WidgetDraggability, WidgetResizability } from '../types.js';
6
6
  import type { InternalFlexiTargetController } from '../target/controller.svelte.js';
7
7
  import type { InternalFlexiBoardController } from '../board/controller.svelte.js';
8
8
  export type FlexiWidgetChildrenSnippetParameters = {
@@ -24,18 +24,25 @@ export type FlexiWidgetTriggerConfiguration = Record<string, PointerTriggerCondi
24
24
  export type FlexiWidgetDefaults = {
25
25
  /**
26
26
  * Whether the widget is draggable.
27
+ * @deprecated Prefer the use of `draggability` instead for finer control. When `true`, `draggability = 'full'`, when `false`, `draggability = 'none'`.
27
28
  */
28
29
  draggable?: boolean;
30
+ /**
31
+ * The draggability of the widget.
32
+ */
33
+ draggability?: WidgetDraggability;
29
34
  /**
30
35
  * The resizability of the widget.
31
36
  */
32
37
  resizability?: WidgetResizability;
33
38
  /**
34
39
  * The width of the widget in units.
40
+ * @deprecated This property does not work and will be removed in the next version.
35
41
  */
36
42
  width?: number;
37
43
  /**
38
44
  * The height of the widget in units.
45
+ * @deprecated This property does not work and will be removed in the next version.
39
46
  */
40
47
  height?: number;
41
48
  /**
@@ -68,10 +75,30 @@ export type FlexiWidgetDefaults = {
68
75
  * on the widget. E.g. a long press.
69
76
  */
70
77
  resizeTrigger?: FlexiWidgetTriggerConfiguration;
78
+ /**
79
+ * The minimum width of the widget in units. Defaults to 1, cannot be less than 1.
80
+ */
81
+ minWidth?: number;
82
+ /**
83
+ * The minimum height of the widget in units. Defaults to 1, cannot be less than 1.
84
+ */
85
+ minHeight?: number;
86
+ /**
87
+ * The maximum width of the widget in units. Defaults to Infinity, cannot be less than 1.
88
+ */
89
+ maxWidth?: number;
90
+ /**
91
+ * The maximum height of the widget in units. Defaults to Infinity, cannot be less than 1.
92
+ */
93
+ maxHeight?: number;
71
94
  };
72
95
  export type FlexiWidgetConfiguration = FlexiWidgetDefaults & {
96
+ id?: string;
97
+ type?: string;
73
98
  x?: number;
74
99
  y?: number;
100
+ width?: number;
101
+ height?: number;
75
102
  metadata?: Record<string, any>;
76
103
  };
77
104
  export type FlexiWidgetState = {
@@ -82,10 +109,6 @@ export type FlexiWidgetState = {
82
109
  y: number;
83
110
  };
84
111
  export type FlexiWidgetDerivedConfiguration = {
85
- /**
86
- * The name of the widget, which can be used to identify it in exported layouts.
87
- */
88
- name?: string;
89
112
  /**
90
113
  * The component that is rendered by this item. This is optional if a snippet is provided.
91
114
  */
@@ -103,9 +126,14 @@ export type FlexiWidgetDerivedConfiguration = {
103
126
  */
104
127
  resizability: WidgetResizability;
105
128
  /**
106
- * Whether the item is draggable.
129
+ * Whether the widget is draggable.
130
+ * @deprecated Prefer the use of `draggability` instead for finer control. When `true`, `draggability = 'full'`, when `false`, `draggability = 'none'`.
107
131
  */
108
132
  draggable: boolean;
133
+ /**
134
+ * The draggability of the widget.
135
+ */
136
+ draggability: WidgetDraggability;
109
137
  /**
110
138
  * The class name that is applied to this widget.
111
139
  */
@@ -128,11 +156,28 @@ export type FlexiWidgetDerivedConfiguration = {
128
156
  * The transition configuration for this widget.
129
157
  */
130
158
  transition: FlexiWidgetTransitionConfiguration;
159
+ /**
160
+ * The minimum width of the widget in units. Defaults to 1, cannot be less than 1.
161
+ */
162
+ minWidth: number;
163
+ /**
164
+ * The minimum height of the widget in units. Defaults to 1, cannot be less than 1.
165
+ */
166
+ minHeight: number;
167
+ /**
168
+ * The maximum width of the widget in units. Defaults to Infinity, cannot be less than 1.
169
+ */
170
+ maxWidth: number;
171
+ /**
172
+ * The maximum height of the widget in units. Defaults to Infinity, cannot be less than 1.
173
+ */
174
+ maxHeight: number;
131
175
  };
132
176
  export type FlexiWidgetConstructorParams = {
133
177
  config: FlexiWidgetConfiguration;
134
178
  provider: InternalFlexiBoardController;
135
179
  target?: InternalFlexiTargetController;
180
+ type?: string;
136
181
  isShadow?: boolean;
137
182
  };
138
183
  export declare const defaultTriggerConfig: FlexiWidgetTriggerConfiguration;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-flexiboards",
3
3
  "licence": "MIT",
4
- "version": "0.3.2",
4
+ "version": "0.4.1",
5
5
  "description": "The headless drag-and-drop toolkit for Svelte.",
6
6
  "scripts": {
7
7
  "dev": "pnpm watch",