svelte-flexiboards 0.3.0 → 0.3.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,7 @@ export class InternalFlexiTargetController {
26
26
  value: null
27
27
  });
28
28
  #isDropzoneWidgetAdded = $state(false);
29
+ #dropzoneWidgetEffectCleanup = null;
29
30
  #mouseCellPosition = $state({
30
31
  x: 0,
31
32
  y: 0
@@ -100,11 +101,7 @@ export class InternalFlexiTargetController {
100
101
  this.#updateOrderedWidgets();
101
102
  widget.target = this;
102
103
  widget.internalTarget = this;
103
- // Ensure reactive state is created immediately for proper grid integration
104
- // This is especially important for adder widgets that weren't created via target.createWidget()
105
- if (!widget.interpolator) {
106
- widget.createReactiveState();
107
- }
104
+ // Widget is now fully reactive by default - no additional setup needed
108
105
  }
109
106
  return added;
110
107
  }
@@ -388,7 +385,12 @@ export class InternalFlexiTargetController {
388
385
  const grid = this.grid;
389
386
  // Take a snapshot of the grid so we can restore its state if the hover stops.
390
387
  this.#gridSnapshot = grid.takeSnapshot();
391
- this.dropzoneWidget = this.#createShadow(this.actionWidget.widget);
388
+ // Wrap only the widget creation in $effect.root to ensure proper reactivity context
389
+ let shadowWidget;
390
+ this.#dropzoneWidgetEffectCleanup = $effect.root(() => {
391
+ shadowWidget = this.#createShadow(this.actionWidget.widget);
392
+ });
393
+ this.dropzoneWidget = shadowWidget;
392
394
  let [x, y, width, height] = this.#getDropzoneLocation(this.actionWidget);
393
395
  const added = this.grid.tryPlaceWidget(this.dropzoneWidget, x, y, width, height, true);
394
396
  if (added) {
@@ -478,6 +480,11 @@ export class InternalFlexiTargetController {
478
480
  }
479
481
  grid.restoreFromSnapshot(this.#gridSnapshot);
480
482
  this.#gridSnapshot = null;
483
+ // Cleanup the effect root
484
+ if (this.#dropzoneWidgetEffectCleanup) {
485
+ this.#dropzoneWidgetEffectCleanup();
486
+ this.#dropzoneWidgetEffectCleanup = null;
487
+ }
481
488
  this.dropzoneWidget = null;
482
489
  }
483
490
  // State-related getters and setters
@@ -553,6 +560,11 @@ export class InternalFlexiTargetController {
553
560
  }
554
561
  });
555
562
  this.widgets.clear();
563
+ // Clean up dropzone widget effect
564
+ if (this.#dropzoneWidgetEffectCleanup) {
565
+ this.#dropzoneWidgetEffectCleanup();
566
+ this.#dropzoneWidgetEffectCleanup = null;
567
+ }
556
568
  // Clean up event subscriptions
557
569
  this.#unsubscribers.forEach((unsubscribe) => unsubscribe());
558
570
  this.#unsubscribers = [];
@@ -2,8 +2,7 @@ import type { Component } from 'svelte';
2
2
  import type { FlexiTargetController } from '../target/index.js';
3
3
  import type { WidgetAction, WidgetResizability } from '../types.js';
4
4
  import { type FlexiWidgetChildrenSnippet, type FlexiWidgetClasses, type FlexiWidgetConstructorParams } from './types.js';
5
- import type { WidgetReactiveState } from './state.svelte.js';
6
- export declare class FlexiWidgetController {
5
+ export declare abstract class FlexiWidgetController {
7
6
  #private;
8
7
  /**
9
8
  * The target this widget is under. This is not defined if the widget has not yet been dropped in the board.
@@ -16,24 +15,24 @@ export declare class FlexiWidgetController {
16
15
  /**
17
16
  * Whether this widget is a shadow dropzone widget.
18
17
  */
19
- isShadow: boolean;
18
+ abstract get isShadow(): boolean;
19
+ abstract get currentAction(): WidgetAction | null;
20
+ abstract get width(): number;
21
+ abstract get height(): number;
22
+ abstract get x(): number;
23
+ abstract get y(): number;
24
+ abstract get isBeingDropped(): boolean;
25
+ abstract get hasGrabbers(): boolean;
26
+ abstract get hasResizers(): boolean;
20
27
  /**
21
28
  * Whether this widget is grabbed.
22
29
  */
23
- isGrabbed: boolean;
30
+ abstract get isGrabbed(): boolean;
24
31
  /**
25
32
  * Whether this widget is being resized.
26
33
  */
27
- isResizing: boolean;
28
- protected reactiveState?: WidgetReactiveState;
29
- backingState: WidgetStateData;
30
- constructor(state: WidgetStateData, params: FlexiWidgetConstructorParams);
31
- /**
32
- * When the widget is being grabbed, this contains information that includes its position, size and offset.
33
- * When this is null, the widget is not being grabbed.
34
- */
35
- get currentAction(): WidgetAction | null;
36
- set currentAction(value: WidgetAction | null);
34
+ abstract get isResizing(): boolean;
35
+ constructor(params: FlexiWidgetConstructorParams);
37
36
  /**
38
37
  * Whether the widget is draggable.
39
38
  */
@@ -48,14 +47,6 @@ export declare class FlexiWidgetController {
48
47
  * Whether the widget is resizable.
49
48
  */
50
49
  get resizable(): boolean;
51
- /**
52
- * The width in units of the widget.
53
- */
54
- get width(): number;
55
- /**
56
- * The height in units of the widget.
57
- */
58
- get height(): number;
59
50
  /**
60
51
  * The component that is rendered by this widget.
61
52
  */
@@ -76,14 +67,6 @@ export declare class FlexiWidgetController {
76
67
  */
77
68
  get className(): FlexiWidgetClasses | undefined;
78
69
  set className(value: FlexiWidgetClasses | undefined);
79
- /**
80
- * Gets the column (x-coordinate) of the widget. This value is readonly and is managed by the target.
81
- */
82
- get x(): number;
83
- /**
84
- * Gets the row (y-coordinate) of the widget. This value is readonly and is managed by the target.
85
- */
86
- get y(): number;
87
70
  /**
88
71
  * The metadata associated with this widget, if any.
89
72
  */
@@ -102,18 +85,6 @@ export declare class FlexiWidgetController {
102
85
  * Gets the transition configuration for this widget.
103
86
  */
104
87
  get transitionConfig(): import("./types.js").FlexiWidgetTransitionConfiguration;
105
- /**
106
- * Whether the widget has any grabbers attached.
107
- */
108
- get hasGrabbers(): boolean;
109
- /**
110
- * Whether the widget has any resizers attached
111
- */
112
- get hasResizers(): boolean;
113
- /**
114
- * Whether the widget is currently being dropped after a drag operation.
115
- */
116
- get isBeingDropped(): boolean;
117
88
  }
118
89
  export type WidgetStateData = {
119
90
  currentAction: WidgetAction | null;
@@ -13,18 +13,6 @@ export class FlexiWidgetController {
13
13
  #providerWidgetDefaults = $derived(this.target?.providerWidgetDefaults);
14
14
  #targetWidgetDefaults = $derived(this.target?.config.widgetDefaults);
15
15
  #rawConfig = $state();
16
- /**
17
- * Whether this widget is a shadow dropzone widget.
18
- */
19
- isShadow = $state(false);
20
- /**
21
- * Whether this widget is grabbed.
22
- */
23
- isGrabbed = $derived(this.currentAction?.action == 'grab');
24
- /**
25
- * Whether this widget is being resized.
26
- */
27
- isResizing = $derived(this.currentAction?.action == 'resize');
28
16
  /**
29
17
  * The reactive configuration of the widget. When these properties are changed, either due to a change in the widget's configuration,
30
18
  * or a change in the target's, or the board's, they will be updated to reflect the new values.
@@ -64,33 +52,13 @@ export class FlexiWidgetController {
64
52
  this.#providerWidgetDefaults?.resizeTrigger ??
65
53
  defaultTriggerConfig
66
54
  });
67
- reactiveState;
68
- backingState;
69
- constructor(state, params) {
70
- this.backingState = state;
55
+ constructor(params) {
71
56
  this.#rawConfig = params.config;
72
57
  if (params.target) {
73
58
  this.target = params.target;
74
- this.isShadow = params.isShadow ?? false;
75
59
  }
76
60
  }
77
61
  // Getters and setters
78
- /**
79
- * When the widget is being grabbed, this contains information that includes its position, size and offset.
80
- * When this is null, the widget is not being grabbed.
81
- */
82
- get currentAction() {
83
- if (this.reactiveState) {
84
- return this.reactiveState.currentAction;
85
- }
86
- return this.backingState.currentAction;
87
- }
88
- set currentAction(value) {
89
- if (this.reactiveState) {
90
- this.reactiveState.currentAction = value;
91
- }
92
- this.backingState.currentAction = value;
93
- }
94
62
  /**
95
63
  * Whether the widget is draggable.
96
64
  */
@@ -115,24 +83,6 @@ export class FlexiWidgetController {
115
83
  get resizable() {
116
84
  return this.resizability !== 'none';
117
85
  }
118
- /**
119
- * The width in units of the widget.
120
- */
121
- get width() {
122
- if (this.reactiveState) {
123
- return this.reactiveState.width;
124
- }
125
- return this.backingState.width;
126
- }
127
- /**
128
- * The height in units of the widget.
129
- */
130
- get height() {
131
- if (this.reactiveState) {
132
- return this.reactiveState.height;
133
- }
134
- return this.backingState.height;
135
- }
136
86
  /**
137
87
  * The component that is rendered by this widget.
138
88
  */
@@ -169,24 +119,6 @@ export class FlexiWidgetController {
169
119
  set className(value) {
170
120
  this.#rawConfig.className = value;
171
121
  }
172
- /**
173
- * Gets the column (x-coordinate) of the widget. This value is readonly and is managed by the target.
174
- */
175
- get x() {
176
- if (this.reactiveState) {
177
- return this.reactiveState.x;
178
- }
179
- return this.backingState.x;
180
- }
181
- /**
182
- * Gets the row (y-coordinate) of the widget. This value is readonly and is managed by the target.
183
- */
184
- get y() {
185
- if (this.reactiveState) {
186
- return this.reactiveState.y;
187
- }
188
- return this.backingState.y;
189
- }
190
122
  /**
191
123
  * The metadata associated with this widget, if any.
192
124
  */
@@ -215,31 +147,4 @@ export class FlexiWidgetController {
215
147
  get transitionConfig() {
216
148
  return this.#config.transition;
217
149
  }
218
- /**
219
- * Whether the widget has any grabbers attached.
220
- */
221
- get hasGrabbers() {
222
- if (this.reactiveState) {
223
- return this.reactiveState.hasGrabbers;
224
- }
225
- return this.backingState.hasGrabbers;
226
- }
227
- /**
228
- * Whether the widget has any resizers attached
229
- */
230
- get hasResizers() {
231
- if (this.reactiveState) {
232
- return this.reactiveState.hasResizers;
233
- }
234
- return this.backingState.hasResizers;
235
- }
236
- /**
237
- * Whether the widget is currently being dropped after a drag operation.
238
- */
239
- get isBeingDropped() {
240
- if (this.reactiveState) {
241
- return this.reactiveState.isBeingDropped;
242
- }
243
- return this.backingState.isBeingDropped;
244
- }
245
150
  }
@@ -1,12 +1,14 @@
1
- import type { WidgetDroppedEvent, WidgetEvent, WidgetGrabbedEvent, WidgetResizingEvent } from '../types.js';
1
+ import type { WidgetAction, WidgetDroppedEvent, WidgetEvent, WidgetGrabbedEvent, WidgetResizingEvent } from '../types.js';
2
2
  import { FlexiWidgetController } from './base.svelte.js';
3
3
  import type { InternalFlexiTargetController } from '../target/controller.svelte.js';
4
+ import { WidgetMoveInterpolator } from './interpolator.svelte.js';
4
5
  import type { FlexiWidgetConstructorParams } from './types.js';
5
6
  import type { InternalFlexiBoardController } from '../board/controller.svelte.js';
6
7
  export declare class InternalFlexiWidgetController extends FlexiWidgetController {
7
8
  #private;
8
9
  internalTarget?: InternalFlexiTargetController;
9
10
  provider: InternalFlexiBoardController;
11
+ interpolator: WidgetMoveInterpolator;
10
12
  mounted: boolean;
11
13
  /**
12
14
  * The styling to apply to the widget.
@@ -39,47 +41,25 @@ export declare class InternalFlexiWidgetController extends FlexiWidgetController
39
41
  */
40
42
  delete(): void;
41
43
  onDelete(event: WidgetEvent): void;
42
- /**
43
- * Creates the reactive state container when the widget component mounts.
44
- * This should be called from the component's onMount lifecycle.
45
- */
46
- createReactiveState(): void;
47
44
  /**
48
45
  * Cleanup method to be called when the widget is destroyed
49
46
  */
50
47
  destroy(): void;
51
- /**
52
- * Destroys the reactive state container when the widget component unmounts.
53
- * This should be called from the component's onDestroy lifecycle.
54
- */
55
- destroyReactiveState(): void;
56
- /**
57
- * The width in units of the widget.
58
- */
48
+ get currentAction(): WidgetAction | null;
59
49
  get width(): number;
60
- /**
61
- * The height in units of the widget.
62
- */
63
50
  get height(): number;
64
- set width(value: number);
65
- set height(value: number);
66
- /**
67
- * Gets the column (x-coordinate) of the widget. This value is readonly and is managed by the target.
68
- */
69
51
  get x(): number;
70
- /**
71
- * Gets the row (y-coordinate) of the widget. This value is readonly and is managed by the target.
72
- */
73
52
  get y(): number;
74
- set x(value: number);
75
- set y(value: number);
76
- /**
77
- * Gets the widget's interpolator for transitions.
78
- */
79
- get interpolator(): import("./interpolator.svelte.js").WidgetMoveInterpolator | undefined;
53
+ get isBeingDropped(): boolean;
54
+ get hasGrabbers(): boolean;
55
+ get hasResizers(): boolean;
56
+ get isShadow(): boolean;
57
+ get isGrabbed(): boolean;
58
+ get isResizing(): boolean;
59
+ set currentAction(value: WidgetAction | null);
60
+ set isBeingDropped(value: boolean);
80
61
  /**
81
62
  * Whether the widget should draw a placeholder widget in the DOM.
82
63
  */
83
64
  get shouldDrawPlaceholder(): boolean;
84
- set isBeingDropped(value: boolean);
85
65
  }
@@ -1,19 +1,28 @@
1
1
  import { getFlexiEventBus } from '../shared/event-bus.js';
2
2
  import { generateUniqueId, getElementMidpoint, getPointerService } from '../shared/utils.svelte.js';
3
3
  import { FlexiWidgetController } from './base.svelte.js';
4
- import {} from './interpolator.svelte.js';
4
+ import { WidgetMoveInterpolator } from './interpolator.svelte.js';
5
5
  import { WidgetPointerEventWatcher } from './triggers.svelte.js';
6
- import { WidgetReactiveState } from './state.svelte.js';
7
6
  export class InternalFlexiWidgetController extends FlexiWidgetController {
8
7
  #pointerService = getPointerService();
9
- // Legacy non-reactive state tracking (for when reactive state is not available)
10
- #grabbersCount = 0;
11
- #resizersCount = 0;
12
8
  internalTarget = undefined;
13
9
  provider;
14
- // hasGrabbers and hasResizers are now implemented in the base class
15
- // TODO: the "state-subclass" system is quite hacky, and isn't really achieving what we want right now anyway.
10
+ // Private reactive state properties
11
+ #currentAction = $state(null);
12
+ #width = $state(1);
13
+ #height = $state(1);
14
+ #x = $state(0);
15
+ #y = $state(0);
16
+ #isBeingDropped = $state(false);
17
+ #hasGrabbers = $state(false);
18
+ #hasResizers = $state(false);
19
+ #isShadow = $state(false);
20
+ // Movement interpolation
21
+ interpolator;
16
22
  mounted = $state(false);
23
+ // Grabber and resizer tracking
24
+ #grabbersCount = 0;
25
+ #resizersCount = 0;
17
26
  #eventBus;
18
27
  #unsubscribers = [];
19
28
  /**
@@ -48,7 +57,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
48
57
  if (this.isResizing) {
49
58
  return 'pointer-events: none; user-select: none; cursor: nwse-resize;';
50
59
  }
51
- const grabberCount = this.reactiveState?.grabberCount ?? this.#grabbersCount;
60
+ const grabberCount = this.#grabbersCount;
52
61
  if (grabberCount == 0) {
53
62
  return 'user-select: none; cursor: grab; touch-action: none;';
54
63
  }
@@ -100,21 +109,18 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
100
109
  return `pointer-events: none; user-select: none; cursor: nwse-resize; position: absolute; top: ${top}px; left: ${left}px; height: ${height}px; width: ${width}px;`;
101
110
  }
102
111
  constructor(params) {
103
- // Initialise the state proxy.
104
- super({
105
- currentAction: null,
106
- width: params.config.width ?? 1,
107
- height: params.config.height ?? 1,
108
- x: 0,
109
- y: 0,
110
- hasGrabbers: false,
111
- hasResizers: false,
112
- isBeingDropped: false
113
- }, params);
112
+ // Initialize base class
113
+ super(params);
114
+ // Initialize private state with config values
115
+ this.#width = params.config.width ?? 1;
116
+ this.#height = params.config.height ?? 1;
114
117
  if (params.target) {
115
118
  this.internalTarget = params.target;
119
+ this.#isShadow = params.isShadow ?? false;
116
120
  }
117
121
  this.provider = params.provider;
122
+ // Create the widget's interpolator.
123
+ this.interpolator = new WidgetMoveInterpolator(params.provider, this);
118
124
  this.#eventBus = getFlexiEventBus();
119
125
  this.#unsubscribers.push(this.#eventBus.subscribe('widget:grabbed', this.onGrabbed.bind(this)), this.#eventBus.subscribe('widget:resizing', this.onResizing.bind(this)), this.#eventBus.subscribe('widget:release', this.onReleased.bind(this)), this.#eventBus.subscribe('widget:cancel', this.onReleased.bind(this)), this.#eventBus.subscribe('widget:delete', this.onDelete.bind(this)), this.#eventBus.subscribe('widget:dropped', this.onDropped.bind(this)));
120
126
  }
@@ -132,7 +138,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
132
138
  setTimeout(() => {
133
139
  this.ref?.focus();
134
140
  }, 0);
135
- this.currentAction = {
141
+ this.#currentAction = {
136
142
  action: 'grab',
137
143
  widget: this,
138
144
  offsetX: event.xOffset,
@@ -149,7 +155,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
149
155
  setTimeout(() => {
150
156
  this.ref?.focus();
151
157
  }, 0);
152
- this.currentAction = {
158
+ this.#currentAction = {
153
159
  action: 'resize',
154
160
  widget: this,
155
161
  offsetX: event.offsetX,
@@ -166,7 +172,7 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
166
172
  if (event.widget !== this) {
167
173
  return;
168
174
  }
169
- this.currentAction = null;
175
+ this.#currentAction = null;
170
176
  }
171
177
  /**
172
178
  * Sets the bounds of the widget.
@@ -180,10 +186,10 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
180
186
  if (this.x == x && this.y == y && this.width == width && this.height == height) {
181
187
  return;
182
188
  }
183
- this.x = x;
184
- this.y = y;
185
- this.width = width;
186
- this.height = height;
189
+ this.#x = x;
190
+ this.#y = y;
191
+ this.#width = width;
192
+ this.#height = height;
187
193
  if (interpolate) {
188
194
  this.#interpolateMove(x, y, width, height);
189
195
  }
@@ -214,55 +220,35 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
214
220
  width: rect.width,
215
221
  height: rect.height
216
222
  }, this.#getMovementAnimation());
217
- this.isBeingDropped = false;
223
+ this.#isBeingDropped = false;
218
224
  }
219
225
  /**
220
226
  * Registers a grabber to the widget.
221
227
  */
222
228
  addGrabber() {
223
- if (this.reactiveState) {
224
- this.reactiveState.addGrabber();
225
- }
226
- else {
227
- this.#grabbersCount++;
228
- this.backingState.hasGrabbers = this.#grabbersCount > 0;
229
- }
229
+ this.#grabbersCount++;
230
+ this.#hasGrabbers = this.#grabbersCount > 0;
230
231
  }
231
232
  /**
232
233
  * Unregisters a grabber from the widget.
233
234
  */
234
235
  removeGrabber() {
235
- if (this.reactiveState) {
236
- this.reactiveState.removeGrabber();
237
- }
238
- else {
239
- this.#grabbersCount--;
240
- this.backingState.hasGrabbers = this.#grabbersCount > 0;
241
- }
236
+ this.#grabbersCount--;
237
+ this.#hasGrabbers = this.#grabbersCount > 0;
242
238
  }
243
239
  /**
244
240
  * Registers a resizer to the widget.
245
241
  */
246
242
  addResizer() {
247
- if (this.reactiveState) {
248
- this.reactiveState.addResizer();
249
- }
250
- else {
251
- this.#resizersCount++;
252
- this.backingState.hasResizers = this.#resizersCount > 0;
253
- }
243
+ this.#resizersCount++;
244
+ this.#hasResizers = this.#resizersCount > 0;
254
245
  }
255
246
  /**
256
247
  * Unregisters a resizer from the widget.
257
248
  */
258
249
  removeResizer() {
259
- if (this.reactiveState) {
260
- this.reactiveState.removeResizer();
261
- }
262
- else {
263
- this.#resizersCount--;
264
- this.backingState.hasResizers = this.#resizersCount > 0;
265
- }
250
+ this.#resizersCount--;
251
+ this.#hasResizers = this.#resizersCount > 0;
266
252
  }
267
253
  /**
268
254
  * Deletes this widget from its target and board.
@@ -295,124 +281,66 @@ export class InternalFlexiWidgetController extends FlexiWidgetController {
295
281
  if (event.widget != this) {
296
282
  return;
297
283
  }
298
- this.currentAction = null;
284
+ this.#currentAction = null;
299
285
  // Clean up event subscriptions when widget is deleted
300
286
  this.destroy();
301
287
  }
302
- /**
303
- * Creates the reactive state container when the widget component mounts.
304
- * This should be called from the component's onMount lifecycle.
305
- */
306
- createReactiveState() {
307
- // Create the reactive state with current backing state
308
- this.reactiveState = new WidgetReactiveState(this, this.backingState);
309
- // Transfer current grabber/resizer counts to reactive state
310
- for (let i = 0; i < this.#grabbersCount; i++) {
311
- this.reactiveState.addGrabber();
312
- }
313
- for (let i = 0; i < this.#resizersCount; i++) {
314
- this.reactiveState.addResizer();
315
- }
316
- }
317
288
  /**
318
289
  * Cleanup method to be called when the widget is destroyed
319
290
  */
320
291
  destroy() {
321
- // Clean up reactive state first
322
- this.destroyReactiveState();
323
292
  // Clean up event subscriptions
324
293
  this.#unsubscribers.forEach((unsubscribe) => unsubscribe());
325
294
  this.#unsubscribers = [];
295
+ // Reset counters
296
+ this.#grabbersCount = 0;
297
+ this.#resizersCount = 0;
326
298
  }
327
- /**
328
- * Destroys the reactive state container when the widget component unmounts.
329
- * This should be called from the component's onDestroy lifecycle.
330
- */
331
- destroyReactiveState() {
332
- if (this.reactiveState) {
333
- this.reactiveState.destroy();
334
- this.reactiveState = undefined;
335
- }
299
+ // Implement abstract getters from base class
300
+ get currentAction() {
301
+ return this.#currentAction;
336
302
  }
337
- /**
338
- * The width in units of the widget.
339
- */
340
303
  get width() {
341
- if (this.reactiveState) {
342
- return this.reactiveState.width;
343
- }
344
- return this.backingState.width;
304
+ return this.#width;
345
305
  }
346
- /**
347
- * The height in units of the widget.
348
- */
349
306
  get height() {
350
- if (this.reactiveState) {
351
- return this.reactiveState.height;
352
- }
353
- return this.backingState.height;
354
- }
355
- set width(value) {
356
- if (this.reactiveState) {
357
- this.reactiveState.width = value;
358
- }
359
- this.backingState.width = value;
307
+ return this.#height;
360
308
  }
361
- set height(value) {
362
- if (this.reactiveState) {
363
- this.reactiveState.height = value;
364
- }
365
- this.backingState.height = value;
366
- }
367
- /**
368
- * Gets the column (x-coordinate) of the widget. This value is readonly and is managed by the target.
369
- */
370
309
  get x() {
371
- if (this.reactiveState) {
372
- return this.reactiveState.x;
373
- }
374
- return this.backingState.x;
310
+ return this.#x;
375
311
  }
376
- /**
377
- * Gets the row (y-coordinate) of the widget. This value is readonly and is managed by the target.
378
- */
379
312
  get y() {
380
- if (this.reactiveState) {
381
- return this.reactiveState.y;
382
- }
383
- return this.backingState.y;
313
+ return this.#y;
384
314
  }
385
- set x(value) {
386
- if (this.reactiveState) {
387
- this.reactiveState.x = value;
388
- }
389
- this.backingState.x = value;
315
+ get isBeingDropped() {
316
+ return this.#isBeingDropped;
390
317
  }
391
- set y(value) {
392
- if (this.reactiveState) {
393
- this.reactiveState.y = value;
394
- }
395
- this.backingState.y = value;
318
+ get hasGrabbers() {
319
+ return this.#hasGrabbers;
396
320
  }
397
- /**
398
- * Gets the widget's interpolator for transitions.
399
- */
400
- get interpolator() {
401
- return this.reactiveState?.interpolator;
321
+ get hasResizers() {
322
+ return this.#hasResizers;
323
+ }
324
+ get isShadow() {
325
+ return this.#isShadow;
326
+ }
327
+ get isGrabbed() {
328
+ return this.#currentAction?.action == 'grab';
329
+ }
330
+ get isResizing() {
331
+ return this.#currentAction?.action == 'resize';
332
+ }
333
+ // Internal setters for these properties
334
+ set currentAction(value) {
335
+ this.#currentAction = value;
336
+ }
337
+ set isBeingDropped(value) {
338
+ this.#isBeingDropped = value;
402
339
  }
403
340
  /**
404
341
  * Whether the widget should draw a placeholder widget in the DOM.
405
342
  */
406
343
  get shouldDrawPlaceholder() {
407
- if (this.reactiveState) {
408
- return this.reactiveState.interpolator?.active ?? false;
409
- }
410
344
  return this.interpolator?.active ?? false;
411
345
  }
412
- set isBeingDropped(value) {
413
- if (this.reactiveState) {
414
- this.reactiveState.isBeingDropped = value;
415
- }
416
- this.backingState.isBeingDropped = value;
417
- }
418
346
  }
@@ -5,14 +5,15 @@ import { WidgetPointerEventWatcher } from './triggers.svelte.js';
5
5
  export function widgetEvents(widget) {
6
6
  const eventBus = getFlexiEventBusCtx();
7
7
  const board = getInternalFlexiboardCtx();
8
- const eventWatcher = new WidgetPointerEventWatcher(widget, 'grab');
8
+ const grabWatcher = new WidgetPointerEventWatcher(widget, 'grab');
9
9
  return {
10
10
  onpointerdown: (event) => {
11
11
  // Grabbing the widget directly only works if the widget does not have grabbers.
12
12
  if (widget.hasGrabbers) {
13
13
  return;
14
14
  }
15
- dispatchPointerDownGrab(eventBus, widget, board, event);
15
+ // Invoke the trigger watcher which respects trigger configuration (immediate vs long press).
16
+ grabWatcher.onstartpointerdown(event);
16
17
  },
17
18
  onkeydown: (event) => {
18
19
  // Grabbing the widget directly only works if the widget does not have grabbers.
@@ -26,39 +27,27 @@ export function widgetEvents(widget) {
26
27
  export function widgetGrabberEvents(widget) {
27
28
  const eventBus = getFlexiEventBusCtx();
28
29
  const board = getInternalFlexiboardCtx();
30
+ const grabWatcher = new WidgetPointerEventWatcher(widget, 'grab');
29
31
  return {
30
- onpointerdown: (event) => dispatchPointerDownGrab(eventBus, widget, board, event),
32
+ onpointerdown: (event) => {
33
+ // Use the trigger watcher which respects trigger configuration (immediate vs long press)
34
+ grabWatcher.onstartpointerdown(event);
35
+ },
31
36
  onkeydown: (event) => dispatchKeyDownGrab(eventBus, widget, board, event)
32
37
  };
33
38
  }
34
39
  export function widgetResizerEvents(widget) {
35
40
  const eventBus = getFlexiEventBusCtx();
36
41
  const board = getInternalFlexiboardCtx();
42
+ const resizeWatcher = new WidgetPointerEventWatcher(widget, 'resize');
37
43
  return {
38
- onpointerdown: (event) => dispatchPointerDownResize(eventBus, widget, board, event),
44
+ onpointerdown: (event) => {
45
+ // Invoke the trigger watcher which respects trigger configuration (immediate vs long press).
46
+ resizeWatcher.onstartpointerdown(event);
47
+ },
39
48
  onkeydown: (event) => dispatchKeyDownResize(eventBus, widget, board, event)
40
49
  };
41
50
  }
42
- /**
43
- * Releases the pointer capture on the event target, before dispatching a 'widget:grabbed' event.
44
- * @param eventBus The event bus to dispatch the event to.
45
- * @param widget The widget that was grabbed.
46
- * @param event The pointer event.
47
- */
48
- function dispatchPointerDownGrab(eventBus, widget, board, event) {
49
- if (!widget.draggable || !widget.ref || !isGrabPointerEvent(event)) {
50
- return;
51
- }
52
- // TODO: this MIGHT not be necessary anymore, due to our simulated pointer watcher.
53
- // Don't implicitly keep the pointer capture, as then mobile can't move the widget in and out of targets.
54
- // (event.target as HTMLElement).releasePointerCapture(event.pointerId);
55
- // event.stopPropagation();
56
- // event.preventDefault();
57
- dispatchGrab(eventBus, widget, board, {
58
- clientX: event.clientX,
59
- clientY: event.clientY
60
- });
61
- }
62
51
  /**
63
52
  * Resolves clientX/clientY coordinates from a keyboard event, before dispatching a 'widget:grabbed' event.
64
53
  * @param eventBus The event bus to dispatch the event to.
@@ -110,25 +99,6 @@ function dispatchGrab(eventBus, widget, board, { clientX, clientY }) {
110
99
  yOffset: clientY - rect.top
111
100
  });
112
101
  }
113
- /**
114
- * Releases the pointer capture on the event target, before dispatching a 'widget:resizing' event.
115
- * @param eventBus The event bus to dispatch the event to.
116
- * @param widget The widget that was resized.
117
- * @param event The pointer event.
118
- */
119
- function dispatchPointerDownResize(eventBus, widget, board, event) {
120
- if (!widget.resizable || !widget.ref) {
121
- return;
122
- }
123
- // Don't implicitly keep the pointer capture, as then mobile can't move the widget in and out of targets.
124
- // (event.target as HTMLElement).releasePointerCapture(event.pointerId);
125
- // event.stopPropagation();
126
- // event.preventDefault();
127
- dispatchResize(eventBus, widget, board, {
128
- clientX: event.clientX,
129
- clientY: event.clientY
130
- });
131
- }
132
102
  /**
133
103
  * Resolves clientX/clientY coordinates from a keyboard event, before dispatching a 'widget:resizing' event.
134
104
  * @param eventBus The event bus to dispatch the event to.
@@ -146,24 +116,7 @@ function dispatchKeyDownResize(eventBus, widget, board, event) {
146
116
  }
147
117
  event.stopPropagation();
148
118
  event.preventDefault();
149
- const { x, y } = getElementMidpoint(event.target);
150
- dispatchResize(eventBus, widget, board, {
151
- clientX: x,
152
- clientY: y
153
- });
154
- }
155
- /**
156
- * Dispatches a 'widget:resizing' event to the event bus.
157
- * @param eventBus The event bus to dispatch the event to.
158
- * @param widget The widget that was resized.
159
- * @param clientX The x-coordinate of the pointer event.
160
- * @param clientY The y-coordinate of the pointer event.
161
- */
162
- function dispatchResize(eventBus, widget, board, { clientX, clientY }) {
163
- if (!widget.resizable || !widget.ref) {
164
- return;
165
- }
166
- const rect = widget.ref?.getBoundingClientRect();
119
+ const rect = widget.ref.getBoundingClientRect();
167
120
  if (!rect) {
168
121
  return;
169
122
  }
@@ -171,17 +124,18 @@ function dispatchResize(eventBus, widget, board, { clientX, clientY }) {
171
124
  if (!boardRect) {
172
125
  return;
173
126
  }
127
+ const { x, y } = getElementMidpoint(event.target);
128
+ // Calculate position relative to board container (same as resize trigger logic)
174
129
  const left = rect.left - boardRect.left + board.ref.scrollLeft;
175
130
  const top = rect.top - boardRect.top + board.ref.scrollTop;
176
- // TODO: resizing event schema
177
131
  eventBus.dispatch('widget:resizing', {
178
132
  widget,
179
133
  board,
180
134
  target: widget.internalTarget,
181
- offsetX: clientX - left,
182
- offsetY: clientY - top,
183
- clientX,
184
- clientY,
135
+ offsetX: x - left,
136
+ offsetY: y - top,
137
+ clientX: x,
138
+ clientY: y,
185
139
  left,
186
140
  top,
187
141
  capturedHeightPx: rect.height,
@@ -13,7 +13,6 @@ export function flexiwidget(config) {
13
13
  throw new Error("Failed to create widget. Check that the widget's x and y coordinates do not lead to an unresolvable collision.");
14
14
  }
15
15
  setContext(contextKey, widget);
16
- widget.createReactiveState();
17
16
  return {
18
17
  widget
19
18
  };
@@ -27,11 +26,7 @@ export function renderedflexiwidget(widget) {
27
26
  widget.mounted = true;
28
27
  });
29
28
  const events = widgetEvents(widget);
30
- // Only create reactive state if it doesn't already exist
31
- // (it may have been created earlier when widget was added to target)
32
- if (!widget.interpolator) {
33
- widget.createReactiveState();
34
- }
29
+ // Widget is now fully reactive by default - no additional setup needed
35
30
  return {
36
31
  widget,
37
32
  ...events
@@ -90,16 +90,23 @@ export class WidgetPointerEventWatcher {
90
90
  if (!this.#widget.resizable) {
91
91
  return;
92
92
  }
93
+ // For resize, calculate position relative to board container.
94
+ const boardRect = this.#board.ref?.getBoundingClientRect();
95
+ if (!boardRect) {
96
+ return;
97
+ }
98
+ const left = rect.left - boardRect.left + this.#board.ref.scrollLeft;
99
+ const top = rect.top - boardRect.top + this.#board.ref.scrollTop;
93
100
  this.#eventBus.dispatch('widget:resizing', {
94
101
  widget: this.#widget,
95
102
  board: this.#board,
96
103
  target: this.#widget.target,
97
- offsetX: event.clientX - rect.left,
98
- offsetY: event.clientY - rect.top,
104
+ offsetX: event.clientX - left,
105
+ offsetY: event.clientY - top,
99
106
  clientX: event.clientX,
100
107
  clientY: event.clientY,
101
- left: rect.left,
102
- top: rect.top,
108
+ left,
109
+ top,
103
110
  capturedHeightPx: rect.height,
104
111
  capturedWidthPx: rect.width
105
112
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-flexiboards",
3
3
  "licence": "MIT",
4
- "version": "0.3.0",
4
+ "version": "0.3.2-alpha.0",
5
5
  "description": "The headless drag-and-drop toolkit for Svelte.",
6
6
  "scripts": {
7
7
  "dev": "pnpm watch",
@@ -1,59 +0,0 @@
1
- import type { WidgetAction } from '../types.js';
2
- import type { WidgetStateData } from './base.svelte.js';
3
- import type { InternalFlexiWidgetController } from './controller.svelte.js';
4
- import { WidgetMoveInterpolator } from './interpolator.svelte.js';
5
- /**
6
- * Reactive state container for FlexiWidgets.
7
- * Constructed separately from the widget controller, as each state container
8
- * is bound to the lifecycle of a widget component.
9
- */
10
- export declare class WidgetReactiveState {
11
- #private;
12
- currentAction: WidgetAction | null;
13
- width: number;
14
- height: number;
15
- x: number;
16
- y: number;
17
- isBeingDropped: boolean;
18
- hasGrabbers: boolean;
19
- hasResizers: boolean;
20
- interpolator: WidgetMoveInterpolator;
21
- constructor(widget: InternalFlexiWidgetController, initialState: WidgetStateData);
22
- /**
23
- * Adds a grabber and returns the current count
24
- */
25
- addGrabber(): number;
26
- /**
27
- * Removes a grabber and returns the current count
28
- */
29
- removeGrabber(): number;
30
- /**
31
- * Adds a resizer and returns the current count
32
- */
33
- addResizer(): number;
34
- /**
35
- * Removes a resizer and returns the current count
36
- */
37
- removeResizer(): number;
38
- /**
39
- * Gets the current grabber count
40
- */
41
- get grabberCount(): number;
42
- /**
43
- * Gets the current resizer count
44
- */
45
- get resizerCount(): number;
46
- /**
47
- * Syncs this reactive state back to the widget's backing state.
48
- */
49
- syncToBackingState(): void;
50
- /**
51
- * Clean up resources when state is destroyed
52
- */
53
- destroy(): void;
54
- }
55
- /**
56
- * Legacy export for backwards compatibility
57
- * @deprecated Use WidgetReactiveState instead
58
- */
59
- export declare const WidgetState: typeof WidgetReactiveState;
@@ -1,102 +0,0 @@
1
- import { WidgetMoveInterpolator } from './interpolator.svelte.js';
2
- /**
3
- * Reactive state container for FlexiWidgets.
4
- * Constructed separately from the widget controller, as each state container
5
- * is bound to the lifecycle of a widget component.
6
- */
7
- export class WidgetReactiveState {
8
- // Core state data
9
- currentAction = $state(null);
10
- width = $state(1);
11
- height = $state(1);
12
- x = $state(0);
13
- y = $state(0);
14
- isBeingDropped = $state(false);
15
- // Grabber and resizer tracking
16
- #grabbers = $state(0);
17
- #resizers = $state(0);
18
- // Derived reactive properties
19
- hasGrabbers = $derived(this.#grabbers > 0);
20
- hasResizers = $derived(this.#resizers > 0);
21
- // Movement interpolation
22
- interpolator;
23
- // Reference to the controller (for callbacks, not state)
24
- #widget;
25
- constructor(widget, initialState) {
26
- this.#widget = widget;
27
- // Initialize with backing state values
28
- this.currentAction = initialState.currentAction;
29
- this.width = initialState.width;
30
- this.height = initialState.height;
31
- this.x = initialState.x;
32
- this.y = initialState.y;
33
- this.isBeingDropped = initialState.isBeingDropped;
34
- // Create the widget's interpolator.
35
- this.interpolator = new WidgetMoveInterpolator(widget.provider, widget);
36
- }
37
- /**
38
- * Adds a grabber and returns the current count
39
- */
40
- addGrabber() {
41
- return ++this.#grabbers;
42
- }
43
- /**
44
- * Removes a grabber and returns the current count
45
- */
46
- removeGrabber() {
47
- return --this.#grabbers;
48
- }
49
- /**
50
- * Adds a resizer and returns the current count
51
- */
52
- addResizer() {
53
- return ++this.#resizers;
54
- }
55
- /**
56
- * Removes a resizer and returns the current count
57
- */
58
- removeResizer() {
59
- return --this.#resizers;
60
- }
61
- /**
62
- * Gets the current grabber count
63
- */
64
- get grabberCount() {
65
- return this.#grabbers;
66
- }
67
- /**
68
- * Gets the current resizer count
69
- */
70
- get resizerCount() {
71
- return this.#resizers;
72
- }
73
- /**
74
- * Syncs this reactive state back to the widget's backing state.
75
- */
76
- syncToBackingState() {
77
- const backing = this.#widget.backingState;
78
- backing.currentAction = this.currentAction;
79
- backing.width = this.width;
80
- backing.height = this.height;
81
- backing.x = this.x;
82
- backing.y = this.y;
83
- backing.isBeingDropped = this.isBeingDropped;
84
- backing.hasGrabbers = this.hasGrabbers;
85
- backing.hasResizers = this.hasResizers;
86
- }
87
- /**
88
- * Clean up resources when state is destroyed
89
- */
90
- destroy() {
91
- // Sync final state back to backing store
92
- this.syncToBackingState();
93
- // Reset counters
94
- this.#grabbers = 0;
95
- this.#resizers = 0;
96
- }
97
- }
98
- /**
99
- * Legacy export for backwards compatibility
100
- * @deprecated Use WidgetReactiveState instead
101
- */
102
- export const WidgetState = WidgetReactiveState;