@witchcraft/layout 0.5.0 → 0.5.2

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/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/layout",
3
3
  "configKey": "witchcraftLayout",
4
- "version": "0.5.0",
4
+ "version": "0.5.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -20,7 +20,7 @@ const module$1 = defineNuxtModule({
20
20
  getContents: () => [
21
21
  `@source "${resolve("runtime/components")}";`,
22
22
  // the drag actions handlers and utils they use add some styles
23
- `@source "${resolve("runtime/drag")}";`,
23
+ `@source "${resolve("runtime/move")}";`,
24
24
  `@source "${resolve("runtime/utils/createSplitDecoShapes")}";`
25
25
  ].join("\n")
26
26
  });
@@ -1,12 +1,14 @@
1
1
  import type { Ref } from "vue";
2
- import type { ActionHandler, ActionHandlerApplyResult, EdgeMoveStartData, FrameMoveStartData, IntersectionEntry, LayoutFrame, LayoutWindow } from "../types/index.js";
3
- export declare function useFrames(win: Ref<LayoutWindow>, handler: ActionHandler): {
2
+ import type { EdgeMoveStartData, FrameMoveStartData, IActionHandler, IntersectionEntry, LayoutFrame, LayoutWindow } from "../types/index.js";
3
+ export declare function useFrames(win: Ref<LayoutWindow>, handler: IActionHandler): {
4
4
  moveStart: <T extends "edge" | "frame">(e: PointerEvent, type: T, data: T extends "edge" ? EdgeMoveStartData : FrameMoveStartData, { moveEvent, endEvent, context }?: {
5
5
  moveEvent?: string;
6
6
  endEvent?: string;
7
7
  context?: Record<string, unknown>;
8
8
  }) => Promise<any>;
9
- moveEnd: (e?: PointerEvent, { apply }?: Partial<Pick<ActionHandlerApplyResult, "apply">>) => void;
9
+ moveEnd: (e?: PointerEvent, { apply }?: {
10
+ apply?: boolean;
11
+ }) => void;
10
12
  cancel: () => void;
11
13
  onMove: (e: PointerEvent) => void;
12
14
  moveDirections: Ref<{
@@ -1049,5 +1051,5 @@ export declare function useFrames(win: Ref<LayoutWindow>, handler: ActionHandler
1049
1051
  }>;
1050
1052
  showMoving: Ref<boolean, boolean>;
1051
1053
  movingFrameId: Ref<string | undefined, string | undefined>;
1052
- actionHandler: ActionHandler;
1054
+ actionHandler: IActionHandler;
1053
1055
  };
@@ -102,6 +102,7 @@ export function useFrames(win, handler) {
102
102
  moveDirStore.reset();
103
103
  showMoving.value = false;
104
104
  forceRecalculateEdges();
105
+ handler.onMoveEnded();
105
106
  }
106
107
  function moveStart(e, type, data, {
107
108
  moveEvent = "pointermove",
@@ -169,10 +170,13 @@ export function useFrames(win, handler) {
169
170
  );
170
171
  }
171
172
  if (!didChange) return;
172
- const res = handler.onMoveChange("move", e, state.value, forceRecalculateEdges, cancel, resolve);
173
+ onMoveChange(e, "move", point);
174
+ }
175
+ function onMoveChange(e, type, point) {
176
+ const res = handler.onMoveChange(type, e, state.value, forceRecalculateEdges, cancel, type === "end" ? void 0 : resolve);
173
177
  showMoving.value = res.showMoving ?? true;
174
178
  if (!res.updateEdges) return;
175
- if (isMoving.value === "edge") {
179
+ if (isMoving.value === "edge" && point) {
176
180
  requestAnimationFrame(() => {
177
181
  for (let i = 0; i < movingEdges.value.length; i++) {
178
182
  const movingEdge = movingEdges.value[i];
@@ -189,16 +193,16 @@ export function useFrames(win, handler) {
189
193
  const point = toWindowCoord(win.value, e);
190
194
  movePoint.value = point;
191
195
  }
196
+ onMoveChange(e, "end", movePoint.value);
192
197
  if (apply) {
193
198
  const applyResult = handler.onMoveApply(state.value, forceRecalculateEdges);
194
199
  moveResult = applyResult.result;
195
- if (applyResult.apply) {
200
+ if (applyResult.updateEdges) {
196
201
  for (const frame of touchingFramesArrays.value.flat()) {
197
202
  win.value.frames[frame.id] = frame;
198
203
  }
199
204
  }
200
205
  }
201
- handler.onMoveChange("end", e, state.value, forceRecalculateEdges, void 0, void 0);
202
206
  controller?.abort();
203
207
  }
204
208
  function cancel() {
@@ -1,16 +1,16 @@
1
1
  import { type RecordFromArray } from "@alanscodelog/utils";
2
- import type { ActionHandlerApplyResult, Edge, IAction, LayoutFrame, LayoutShape, MoveChangeHandler, MoveChangeResult, MoveState } from "../types/index.js";
2
+ import type { ActionHandlerApplyResult, ActionResolve, Edge, IAction, IActionHandler, LayoutFrame, LayoutShape, MoveChangeResult, MoveState } from "../types/index.js";
3
3
  /**
4
4
  * Handles the lifecycle of a drag actions {@link IAction} and provides additional hooks.
5
5
  *
6
6
  * The first action instance that can handle the request is passed control of the event handlers until the request changes.
7
7
  */
8
- export declare class ActionHandler<TRawActions extends IAction[], TActions extends RecordFromArray<TRawActions, "name"> = RecordFromArray<TRawActions, "name">> {
8
+ export declare class ActionHandler<TRawActions extends IAction[], TActions extends RecordFromArray<TRawActions, "name"> = RecordFromArray<TRawActions, "name">> implements IActionHandler {
9
9
  activeAction?: keyof TActions;
10
10
  actions: TActions;
11
11
  eventCanceller: ((e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void) | undefined;
12
12
  boundCancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void;
13
- defaultOnMoveChange: MoveChangeHandler;
13
+ defaultOnMoveChange: IAction["onMoveChange"];
14
14
  hooks: {
15
15
  /** Called while dragging during dragChange events. You can use this to update the dragging edges. */
16
16
  onRecalculate?: () => void;
@@ -25,7 +25,8 @@ export declare class ActionHandler<TRawActions extends IAction[], TActions exten
25
25
  /** Called when the drag action ends either because it was completed or cancelled. */
26
26
  onEnd?: (context: {
27
27
  cancelled: boolean;
28
- applied: boolean;
28
+ wasApplied: boolean;
29
+ result?: any;
29
30
  }) => void;
30
31
  };
31
32
  /** All action shapes merged into a single array. If using vue you can set this to a reactive array for reactivity. */
@@ -37,19 +38,18 @@ export declare class ActionHandler<TRawActions extends IAction[], TActions exten
37
38
  };
38
39
  constructor(actions: TRawActions, hooks?: ActionHandler<TRawActions, TActions>["hooks"],
39
40
  /**
40
- * Default onMoveChange handler for when no action can handle the request.
41
- *
42
- * Should return true to allow the edges to be moved, or false to prevent it.
41
+ * Default onMoveChange handler for when no action can handle the request. See {@link IAction.onMoveChange}.
43
42
  *
44
43
  * The default prevents movement when the edge is a window edge and when the edge is touching a collapsed frame.
45
44
  */
46
- defaultOnMoveChange?: MoveChangeHandler);
45
+ defaultOnMoveChange?: IAction["onMoveChange"]);
47
46
  eventHandler(e: KeyboardEvent | PointerEvent, state: MoveState, forceRecalculateEdges: () => void): undefined;
48
- onMoveChange<T extends "start" | "move" | "end">(type: T, e: T extends "end" ? PointerEvent | undefined : PointerEvent, state: MoveState, forceRecalculateEdges: () => void, cancel: T extends "end" ? undefined : (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void, resolve: T extends "end" ? undefined : (opts: ActionHandlerApplyResult) => void): MoveChangeResult;
49
- setTextHints(type: "start" | "move" | "end"): void;
50
- annotateEdges(edges: Edge[], frames: LayoutFrame[]): void;
47
+ onMoveChange<T extends "start" | "move" | "end">(type: T, e: T extends "end" ? PointerEvent | undefined : PointerEvent, state: MoveState, forceRecalculateEdges: () => void, cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void, resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)): MoveChangeResult;
51
48
  onMoveApply(state: MoveState, forceRecalculateEdges: () => void): ActionHandlerApplyResult;
49
+ onMoveEnded(): void;
52
50
  cancel(e: PointerEvent | KeyboardEvent | undefined, state: MoveState): void;
51
+ setTextHints(type: "start" | "move" | "end"): void;
52
+ annotateEdges(edges: Edge[], frames: LayoutFrame[]): void;
53
53
  static debugState(pluginName: string, type: "before" | "after" | string, state: MoveState, pluginState?: Record<string, any>,
54
54
  /** Object key to filter the state by, e.g. state.win.frames. If false is ignored. The idea is you pass this.debug and users can set this.debug to a string to filter. */
55
55
  key?: string | boolean): void;
@@ -82,11 +82,31 @@ export class ActionHandler {
82
82
  this.setTextHints(type);
83
83
  const res = this.defaultOnMoveChange(type, e, state, forceRecalculateEdges, this.boundCancel, resolve);
84
84
  this.shapes.splice(0, this.shapes.length, ...res.shapes);
85
- if (type === "end") {
86
- this.activeAction = void 0;
87
- }
88
85
  return res;
89
86
  }
87
+ onMoveApply(state, forceRecalculateEdges) {
88
+ if (this.activeAction) {
89
+ const res = this.actions[this.activeAction].onMoveApply(state, forceRecalculateEdges);
90
+ this.hooks.onEnd?.({ cancelled: false, wasApplied: res.wasApplied, result: res.result });
91
+ return res;
92
+ }
93
+ this.hooks.onEnd?.({ cancelled: false, wasApplied: false });
94
+ return { updateEdges: true, result: void 0 };
95
+ }
96
+ onMoveEnded() {
97
+ if (this.activeAction) {
98
+ this.actions[this.activeAction].onMoveEnded();
99
+ }
100
+ this.activeAction = void 0;
101
+ }
102
+ cancel(e, state) {
103
+ if (this.activeAction) {
104
+ this.actions[this.activeAction].cancel(e, state);
105
+ }
106
+ this.activeAction = void 0;
107
+ this.eventCanceller?.(e, state);
108
+ this.hooks.onEnd?.({ cancelled: true, wasApplied: false });
109
+ }
90
110
  setTextHints(type) {
91
111
  this.textHints.actions = [];
92
112
  this.textHints.errors = [];
@@ -118,23 +138,6 @@ export class ActionHandler {
118
138
  }
119
139
  }
120
140
  }
121
- onMoveApply(state, forceRecalculateEdges) {
122
- if (this.activeAction) {
123
- const res = this.actions[this.activeAction].onMoveApply(state, forceRecalculateEdges);
124
- this.hooks.onEnd?.({ cancelled: false, applied: res });
125
- return { apply: res, result: void 0 };
126
- }
127
- this.hooks.onEnd?.({ cancelled: false, applied: false });
128
- return { apply: true, result: void 0 };
129
- }
130
- cancel(e, state) {
131
- if (this.activeAction) {
132
- this.actions[this.activeAction].cancel(e, state);
133
- }
134
- this.activeAction = void 0;
135
- this.eventCanceller?.(e, state);
136
- this.hooks.onEnd?.({ cancelled: true, applied: false });
137
- }
138
141
  static debugState(pluginName, type, state, pluginState = {}, key) {
139
142
  if (key === false) return;
140
143
  let res = { state, pluginState };
@@ -1,5 +1,5 @@
1
1
  import { getCloseFrameInfo } from "../layout/getCloseFrameInfo.js";
2
- import type { ActionChangeResult, CloseDeco, IAction, MoveState } from "../types/index.js";
2
+ import type { ActionApplyResult, ActionChangeResult, CloseDeco, IAction, MoveState } from "../types/index.js";
3
3
  import type { KnownError } from "../utils/KnownError.js";
4
4
  export type CloseInfo = Exclude<ReturnType<typeof getCloseFrameInfo>, KnownError>;
5
5
  export declare class CloseAction implements IAction {
@@ -53,6 +53,7 @@ export declare class CloseAction implements IAction {
53
53
  };
54
54
  canHandleRequest(e: PointerEvent | KeyboardEvent, state: MoveState): boolean;
55
55
  onMoveChange(type: "start" | "end" | "move", _e: PointerEvent | undefined, state: MoveState): ActionChangeResult;
56
- onMoveApply(state: MoveState): boolean;
56
+ onMoveApply(state: MoveState): ActionApplyResult;
57
57
  cancel(): void;
58
+ onMoveEnded(): void;
58
59
  }
@@ -99,10 +99,6 @@ export class CloseAction {
99
99
  return false;
100
100
  }
101
101
  onMoveChange(type, _e, state) {
102
- if (type === "end") {
103
- this.reset();
104
- return { shapes: [] };
105
- }
106
102
  if (state.moveDistance <= this.minDragDistance) {
107
103
  return { updateEdges: false, shapes: [] };
108
104
  }
@@ -152,7 +148,10 @@ export class CloseAction {
152
148
  }
153
149
  }
154
150
  const decos = this._getDecos(state);
155
- this.state.lastReturn = { updateEdges: !isMovingFromWindowEdge, shapes: decos.flatMap((_) => _.shapes) };
151
+ this.state.lastReturn = {
152
+ updateEdges: !isMovingFromWindowEdge,
153
+ shapes: type === "end" ? [] : decos.flatMap((_) => _.shapes)
154
+ };
156
155
  return this.state.lastReturn;
157
156
  }
158
157
  onMoveApply(state) {
@@ -165,12 +164,14 @@ export class CloseAction {
165
164
  if (this.debug) {
166
165
  ActionHandler.debugState(this.name, "after", state, this.state, this.debug);
167
166
  }
168
- return true;
167
+ return { updateEdges: false, wasApplied: true };
169
168
  }
170
- return false;
169
+ return { updateEdges: true, wasApplied: false };
171
170
  }
172
171
  cancel() {
173
- this.reset();
174
172
  this.hooks.onCancel?.();
175
173
  }
174
+ onMoveEnded() {
175
+ this.reset();
176
+ }
176
177
  }
@@ -1,4 +1,4 @@
1
- import type { ActionChangeResult, FrameDeco, IAction, LayoutChange, MoveState, Zone } from "../types/index.js";
1
+ import type { ActionApplyResult, ActionChangeResult, FrameDeco, IAction, LayoutChange, MoveState, Zone } from "../types/index.js";
2
2
  import type { KnownError } from "../utils/KnownError.js";
3
3
  export declare class FrameDragAction implements IAction {
4
4
  name: "frameDrag";
@@ -45,6 +45,7 @@ export declare class FrameDragAction implements IAction {
45
45
  getDecos(matchedZone: Zone | undefined, state: MoveState, result: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError | undefined): FrameDeco[];
46
46
  canHandleRequest(e: PointerEvent | KeyboardEvent, state: MoveState): boolean;
47
47
  onMoveChange<T extends "start" | "move" | "end">(type: T, _e: PointerEvent | undefined, state: MoveState): ActionChangeResult;
48
- onMoveApply(state: MoveState): boolean;
48
+ onMoveApply(state: MoveState): ActionApplyResult;
49
+ onMoveEnded(): void;
49
50
  cancel(): void;
50
51
  }
@@ -94,10 +94,6 @@ export class FrameDragAction {
94
94
  return false;
95
95
  }
96
96
  onMoveChange(type, _e, state) {
97
- if (type === "end") {
98
- this.reset();
99
- return { shapes: [] };
100
- }
101
97
  if (state.moveDistance <= this.minDragDistance) {
102
98
  return { updateEdges: false, shapes: [], showMoving: false };
103
99
  }
@@ -119,7 +115,7 @@ export class FrameDragAction {
119
115
  this.setTextHints(this.state.lastReturn);
120
116
  const decos = this.getDecos(matchedZone, state, this.state.lastReturn);
121
117
  return {
122
- shapes: decos.flatMap((_) => _.shapes),
118
+ shapes: type === "end" ? [] : decos.flatMap((_) => _.shapes),
123
119
  updateEdges: false,
124
120
  showMoving: false
125
121
  };
@@ -127,7 +123,7 @@ export class FrameDragAction {
127
123
  onMoveApply(state) {
128
124
  const result = this.state.lastReturn;
129
125
  if (!result || !state.moveHoveredFrame || !state.movingFrameId) {
130
- return true;
126
+ return { updateEdges: false, wasApplied: false };
131
127
  }
132
128
  if (result instanceof Error) {
133
129
  this.hooks.onError?.(result);
@@ -140,7 +136,10 @@ export class FrameDragAction {
140
136
  ActionHandler.debugState(this.name, "after", state, this.state, this.debug);
141
137
  }
142
138
  }
143
- return true;
139
+ return { updateEdges: false, wasApplied: true };
140
+ }
141
+ onMoveEnded() {
142
+ this.reset();
144
143
  }
145
144
  cancel() {
146
145
  this.reset();
@@ -1,5 +1,5 @@
1
1
  import { getFrameSplitInfo } from "../layout/getFrameSplitInfo.js";
2
- import type { ActionChangeResult, IAction, MoveState, Point, SplitDeco } from "../types/index.js";
2
+ import type { ActionApplyResult, ActionChangeResult, IAction, MoveState, Point, SplitDeco } from "../types/index.js";
3
3
  import type { KnownError } from "../utils/KnownError.js";
4
4
  export type SplitInfo = Exclude<ReturnType<typeof getFrameSplitInfo>, KnownError>;
5
5
  export type DragChangeType = "start" | "move" | "end";
@@ -51,6 +51,7 @@ export declare class SplitAction implements IAction {
51
51
  };
52
52
  calculateSplitRequest(state: MoveState): boolean;
53
53
  onMoveChange(type: "start" | "end" | "move", _e: PointerEvent | undefined, state: MoveState): ActionChangeResult;
54
- onMoveApply(state: MoveState): boolean;
54
+ onMoveApply(state: MoveState): ActionApplyResult;
55
+ onMoveEnded(): void;
55
56
  cancel(): void;
56
57
  }
@@ -119,10 +119,6 @@ export class SplitAction {
119
119
  }
120
120
  }
121
121
  onMoveChange(type, _e, state) {
122
- if (type === "end") {
123
- this.reset();
124
- return { shapes: [] };
125
- }
126
122
  const { moveHoveredFrame, moveDistance } = state;
127
123
  if (moveDistance <= this.minDragDistance) {
128
124
  return { updateEdges: false, shapes: [], showMoving: false };
@@ -147,11 +143,19 @@ export class SplitAction {
147
143
  },
148
144
  attrs: { class: "deco-split-error bg-red-500/50" }
149
145
  };
150
- this.state.lastReturn = { updateEdges: true, shapes: [errorDeco], showMoving: false };
146
+ this.state.lastReturn = {
147
+ updateEdges: true,
148
+ shapes: type === "end" ? [] : [errorDeco],
149
+ showMoving: false
150
+ };
151
151
  return this.state.lastReturn;
152
152
  }
153
153
  }
154
- this.state.lastReturn = { updateEdges: false, shapes: decos.flatMap((_) => _.shapes), showMoving: false };
154
+ this.state.lastReturn = {
155
+ updateEdges: false,
156
+ shapes: type === "end" ? [] : decos.flatMap((_) => _.shapes),
157
+ showMoving: false
158
+ };
155
159
  return this.state.lastReturn;
156
160
  }
157
161
  onMoveApply(state) {
@@ -167,10 +171,12 @@ export class SplitAction {
167
171
  }
168
172
  }
169
173
  }
170
- return true;
174
+ return { updateEdges: false, wasApplied: true };
171
175
  }
172
- cancel() {
176
+ onMoveEnded() {
173
177
  this.reset();
178
+ }
179
+ cancel() {
174
180
  this.hooks.onCancel?.();
175
181
  }
176
182
  }
@@ -1889,7 +1889,7 @@ export type MoveState = {
1889
1889
  */
1890
1890
  intersections: IntersectionEntry[];
1891
1891
  /**
1892
- * Whether the drag was initiated from a point along the window edge.
1892
+ * Whether the move was initiated from a point along the window edge.
1893
1893
  */
1894
1894
  isMovingFromWindowEdge: boolean;
1895
1895
  /** Custom context passed to moveStart, available to action handlers via state.eventContext. */
@@ -1897,7 +1897,7 @@ export type MoveState = {
1897
1897
  win: LayoutWindow;
1898
1898
  };
1899
1899
  export interface ActionChangeResult {
1900
- /** Whether the drag should update the edges. Defaults to false. */
1900
+ /** Whether the move should update the edges. Defaults to false. */
1901
1901
  updateEdges?: boolean;
1902
1902
  /** Deco shapes produced by this action during this drag step. */
1903
1903
  shapes: LayoutShape[];
@@ -1905,25 +1905,6 @@ export interface ActionChangeResult {
1905
1905
  showMoving?: boolean;
1906
1906
  }
1907
1907
  export type MoveChangeResult = Omit<ActionChangeResult, "shapes">;
1908
- /**
1909
- * Called when the drag coordinates change (during any event).
1910
- *
1911
- * Should return `{ allowed: true/false, shapes: LayoutShape[] }` to control whether the action is allowed and edges update and what deco shapes to render.
1912
- *
1913
- * Note that the allowed return type only affect the `move` event but is also typed as `boolean` for other events for ease of use.
1914
- *
1915
- * Use also to cleanup your action when type is "end".
1916
- *
1917
- * See also {@link ActionHandler.onMoveChange} to understand it's lifecycle as it is the extended version of.
1918
- */
1919
- export type MoveChangeHandler = <T extends "start" | "move" | "end">(type: T, e: T extends "end" ? PointerEvent | undefined : PointerEvent, state: MoveState, forceRecalculateEdges: () => void,
1920
- /** Calls moveEnd with apply: false */
1921
- cancel: T extends "end" ? undefined : (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
1922
- /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. */
1923
- resolve: T extends "end" ? undefined : ({ apply, result }: {
1924
- apply: boolean;
1925
- result: any;
1926
- }) => void) => ActionChangeResult;
1927
1908
  /**
1928
1909
  * A drag action describes when and how to handle a drag event.
1929
1910
  *
@@ -1934,18 +1915,30 @@ resolve: T extends "end" ? undefined : ({ apply, result }: {
1934
1915
  export interface IAction {
1935
1916
  /** A unique name for your action. */
1936
1917
  name: string;
1937
- onMoveChange: MoveChangeHandler;
1938
1918
  /**
1919
+ * Called when the drag coordinates change (during any event).
1939
1920
  *
1940
- * Is called before `onMoveChange("end")` with the same event. Might not be called if the request was cancelled.
1921
+ * Should return `{ allowed: true/false, shapes: LayoutShape[] }` to control whether the action is allowed and edges update and what deco shapes to render.
1941
1922
  *
1942
- * You should apply your action if possible and return whether it was applied.
1923
+ * Note that the allowed return type only affect the `move` event but is also typed as `boolean` for other events for ease of use.
1943
1924
  *
1944
- * Do not reset state here, use onMoveChange ("end").
1925
+ * Use also to cleanup your action when type is "end".
1926
+ *
1927
+ * See also {@link ActionHandler.onMoveChange} to understand it's lifecycle as it is the extended version of.
1928
+ */
1929
+ onMoveChange: <T extends "start" | "move" | "end">(type: T, e: T extends "end" ? PointerEvent | undefined : PointerEvent, state: MoveState, forceRecalculateEdges: () => void,
1930
+ /** Calls moveEnd with updateEdges: false. This can technically be called from "end", it should still work. */
1931
+ cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
1932
+ /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. Not available during "end" event. It's designed for resolving from other external evente (e.g. key events). */
1933
+ resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)) => ActionChangeResult;
1934
+ /**
1935
+ * Is called after `onMoveChange("end")` with the same event. Will not be called if the request was cancelled.
1936
+ *
1937
+ * You should apply your action if possible and return whether it was applied `wasApplied` as well as `updateEdges` and `result` (optional), see {@link ActionHandler.onMoveApply}, which this is the extended version of.
1945
1938
  *
1946
- * See also {@link ActionHandler.onMoveApply} which this is the extended version of.
1939
+ * Do not reset state here, use onMoveChange ("end").
1947
1940
  */
1948
- onMoveApply: (state: MoveState, forceRecalculateEdges: () => void) => boolean;
1941
+ onMoveApply: (state: MoveState, forceRecalculateEdges: () => void) => ActionApplyResult;
1949
1942
  /**
1950
1943
  * Should return true if it should handle the "request"/event (e.g. some modifier is being pressed => user is requesting x action).
1951
1944
  *
@@ -1977,6 +1970,10 @@ export interface IAction {
1977
1970
  /** Error texts/hints to display when the action produces an error. */
1978
1971
  errors?: string[];
1979
1972
  };
1973
+ /**
1974
+ * Called when an action is cancelled. Call action specific onCancel hooks here.
1975
+ */
1976
+ cancel(e: PointerEvent | KeyboardEvent | undefined, state: MoveState): void;
1980
1977
  /**
1981
1978
  * Called after visual edges are recalculated, once per edge. Action handlers can annotate edges with error info here ({@link Edge.error}).
1982
1979
  *
@@ -2006,35 +2003,51 @@ export type FrameMoveStartData = {
2006
2003
  frameId: FrameId;
2007
2004
  };
2008
2005
  export type ActionHandlerApplyResult = {
2009
- /** Whether to apply the regular drag end changes. Return false to reset to the position before dragging. */
2006
+ updateEdges: boolean;
2007
+ /** Value to resolve the drag promise with. Ignored if `updateEdges` is false. */
2008
+ result?: any;
2009
+ };
2010
+ export type ActionApplyResult = ActionHandlerApplyResult & {
2011
+ wasApplied: boolean;
2012
+ };
2013
+ export type ActionResolve = {
2010
2014
  apply: boolean;
2011
- /** Value to resolve the drag promise with. Ignored if `apply` is false. */
2012
- result: any;
2015
+ result?: any;
2013
2016
  };
2014
2017
  /**
2015
2018
  * Handler interface for drag actions.
2016
2019
  */
2017
- export interface ActionHandler {
2020
+ export interface IActionHandler {
2018
2021
  eventHandler: (e: KeyboardEvent, state: MoveState, forceRecalculateEdges: () => void) => void;
2019
2022
  /**
2020
- * Called when the drag coordinates change (during any event). Should return true to allow the edges to be updated/moved, or false to prevent it. Note that the return only affects the move event but it's typed like this for ease of use. See also {@link MoveChangeHandler}.
2023
+ * Called when the drag coordinates change (during any event). Should return updateEdges true to allow the edges to be updated/moved, or false to prevent it. See also {@link MoveChangeHandler} for the built in action handler.
2021
2024
  *
2022
2025
  * Can be used to save some context/info to later apply safely during onMoveApply.
2023
2026
  *
2024
2027
  * The call order is:
2025
2028
  * - onMoveChange("start", ...)
2026
2029
  * - onMoveChange("move", ...)
2030
+ * - onMoveChange("end", ...)
2027
2031
  * - onMoveApply(...) (IF moveEnd was called with apply: true, otherwise this is skipped)
2028
- * - onMoveChange("end", ...) // always called last, do cleanup here
2032
+ * - If anything calls cancel or resolve they call onMoveApply (cancel with apply: false, resolve with whatever apply value you gave it).
2033
+ * - onMoveEnded() // do cleanup here
2034
+ *
2035
+ * Note also that resolve just resolves the promise value (after onMoveApply("end") and before onMoveEnded()). Depending on what you're doing you might still have to apply the result, remember onMoveApply will still be called if you do `resolve({ apply:true })`.
2029
2036
  */
2030
- onMoveChange: (...args: Parameters<MoveChangeHandler>) => MoveChangeResult;
2037
+ onMoveChange<T extends "start" | "move" | "end">(type: T, e: T extends "end" ? PointerEvent | undefined : PointerEvent, state: MoveState, forceRecalculateEdges: () => void,
2038
+ /** Calls moveEnd with apply: false. This can technically be called from "end", it should still work. */
2039
+ cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
2040
+ /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. Not available during "end" event. It's designed for resolving from other external evente (e.g. key events). */
2041
+ resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)): MoveChangeResult;
2031
2042
  /**
2032
2043
  * Called when drag will be applied. If moveEnd was called with apply false, it will not be called.
2033
- * Return false to not apply the regular drag end changes (i.e. return false to reset to the position before dragging).
2044
+ * Return `{updateEdges: false`} to not apply the regular drag end changes (i.e. return false to reset to the position before dragging). Optionally return a `result` value to resolve the promise with.
2034
2045
  *
2035
- * Do not use for resetting handler state, use onMoveChange("end", ...) for that.
2046
+ * Do not use for resetting handler state, use onMoveEnded for that.
2036
2047
  */
2037
2048
  onMoveApply: (state: MoveState, forceRecalculateEdges: () => void) => ActionHandlerApplyResult;
2049
+ /** For doing cleanup */
2050
+ onMoveEnded: () => void;
2038
2051
  /**
2039
2052
  * Called after visual edges are recalculated. Action handlers can annotate edges with error info.
2040
2053
  */
@@ -1,5 +1,5 @@
1
1
  import type { ComputedRef, InjectionKey, Ref } from "vue";
2
- import type { ActionHandler, ActionHandlerApplyResult, Direction, Edge, FrameId, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, MoveState, Orientation, Point } from "./index.js";
2
+ import type { Direction, Edge, FrameId, IActionHandler, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, MoveState, Orientation, Point } from "./index.js";
3
3
  export type LayoutContext = ComputedRef<{
4
4
  /** The owning window, needed so we can correctly scale coordinates. */
5
5
  win: LayoutWindow;
@@ -8,7 +8,7 @@ export type LayoutContext = ComputedRef<{
8
8
  }>;
9
9
  export declare const layoutContextInjectionKey: InjectionKey<LayoutContext>;
10
10
  export interface UseFramesContext {
11
- actionHandler: ActionHandler;
11
+ actionHandler: IActionHandler;
12
12
  moveStart: {
13
13
  (e: PointerEvent, type: "edge", data: {
14
14
  edge?: Edge;
@@ -27,7 +27,9 @@ export interface UseFramesContext {
27
27
  }): Promise<any>;
28
28
  };
29
29
  onMove: (e: PointerEvent) => void;
30
- moveEnd: (e?: PointerEvent, options?: Partial<Pick<ActionHandlerApplyResult, "apply">>) => void;
30
+ moveEnd: (e?: PointerEvent, options?: {
31
+ apply?: boolean;
32
+ }) => void;
31
33
  cancel: () => void;
32
34
  moveDirections: Ref<Record<Orientation, Direction | undefined>>;
33
35
  movePoint: Ref<Point | undefined>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/layout",
3
3
  "description": "Headless layout manager.",
4
- "version": "0.5.0",
4
+ "version": "0.5.2",
5
5
  "main": "./dist/runtime/index.js",
6
6
  "type": "module",
7
7
  "sideEffects": false,
package/src/module.ts CHANGED
@@ -38,7 +38,7 @@ export default defineNuxtModule<ModuleOptions>({
38
38
  getContents: () => [
39
39
  `@source "${resolve("runtime/components")}";`,
40
40
  // the drag actions handlers and utils they use add some styles
41
- `@source "${resolve("runtime/drag")}";`,
41
+ `@source "${resolve("runtime/move")}";`,
42
42
  `@source "${resolve("runtime/utils/createSplitDecoShapes")}";`
43
43
  ].join("\n")
44
44
  })
@@ -12,11 +12,11 @@ import { toWindowCoord } from "../helpers/toWindowCoord.js"
12
12
  import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
13
13
  import { isPointInRect } from "../layout/isPointInRect.js"
14
14
  import { MoveDirectionStore } from "../move/MoveDirectionStore.js"
15
- import type { ActionHandler, ActionHandlerApplyResult, Direction, Edge, EdgeMoveStartData, FrameId, FrameMoveStartData, IntersectionEntry, LayoutFrame, LayoutWindow, MoveState, Orientation, Point } from "../types/index.js"
15
+ import type { ActionResolve, Direction, Edge, EdgeMoveStartData, FrameId, FrameMoveStartData, IActionHandler, IntersectionEntry, LayoutFrame, LayoutWindow, MoveState, Orientation, Point } from "../types/index.js"
16
16
 
17
17
  export function useFrames(
18
18
  win: Ref<LayoutWindow>,
19
- handler: ActionHandler
19
+ handler: IActionHandler
20
20
  ) {
21
21
  const movingEdges = ref<Edge[]>([])
22
22
 
@@ -134,6 +134,7 @@ export function useFrames(
134
134
  moveDirStore.reset()
135
135
  showMoving.value = false
136
136
  forceRecalculateEdges()
137
+ handler.onMoveEnded()
137
138
  }
138
139
 
139
140
  function moveStart<T extends "edge" | "frame">(
@@ -231,13 +232,17 @@ export function useFrames(
231
232
  )
232
233
  }
233
234
  if (!didChange) return
234
- const res = handler.onMoveChange("move", e, state.value, forceRecalculateEdges, cancel, resolve)
235
+ onMoveChange(e, "move", point)
236
+ }
237
+
238
+ function onMoveChange(e: PointerEvent | undefined, type: "move" | "end", point: Point | undefined) {
239
+ const res = handler.onMoveChange(type, e, state.value, forceRecalculateEdges, cancel, type === "end" ? undefined : resolve)
235
240
 
236
241
 
237
242
  showMoving.value = res.showMoving ?? true
238
243
  if (!res.updateEdges) return
239
244
 
240
- if (isMoving.value === "edge") {
245
+ if (isMoving.value === "edge" && point) {
241
246
  requestAnimationFrame(() => {
242
247
  for (let i = 0; i < movingEdges.value.length; i++) {
243
248
  const movingEdge = movingEdges.value[i]
@@ -250,33 +255,34 @@ export function useFrames(
250
255
  }
251
256
  }
252
257
 
253
- function moveEnd(e?: PointerEvent, { apply = true }: Partial<Pick<ActionHandlerApplyResult, "apply">> = {}): void {
258
+ function moveEnd(e?: PointerEvent, { apply = true }: { apply?: boolean } = {}) {
254
259
  if (e) {
255
260
  const point = toWindowCoord(win.value, e)
256
261
  movePoint.value = point
257
262
  }
258
263
 
264
+ onMoveChange(e, "end", movePoint.value)
265
+
259
266
  if (apply) {
260
267
  const applyResult = handler.onMoveApply(state.value, forceRecalculateEdges)
261
268
  moveResult = applyResult.result
262
- if (applyResult.apply) {
269
+ if (applyResult.updateEdges) {
263
270
  for (const frame of touchingFramesArrays.value.flat()) {
264
271
  win!.value.frames[frame.id] = frame
265
272
  }
266
273
  }
267
274
  }
268
275
 
269
- handler.onMoveChange("end", e, state.value, forceRecalculateEdges, undefined, undefined)
270
-
271
276
  // this can get called from elsewhere
272
- // also takes care of cleanup nd resolving promise
277
+ // also takes care of cleanup, resolving promise, and calling onMoveEnded
273
278
  controller?.abort()
274
279
  }
275
280
 
276
281
  function cancel(): void {
277
282
  moveEnd(undefined, { apply: false })
278
283
  }
279
- function resolve({ apply, result: value }: ActionHandlerApplyResult): void {
284
+ // we use apply not updateEdges, updateEdges needs to be returned by onMoveApply
285
+ function resolve({ apply, result: value }: ActionResolve): void {
280
286
  moveResult = value
281
287
  moveEnd(undefined, { apply })
282
288
  }
@@ -2,7 +2,7 @@ import { get, type RecordFromArray } from "@alanscodelog/utils"
2
2
 
3
3
  import { isWindowEdge } from "../helpers/isWindowEdge.js"
4
4
  import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
5
- import type { ActionHandlerApplyResult, Edge, IAction, LayoutFrame, LayoutShape, MoveChangeHandler, MoveChangeResult, MoveState } from "../types/index.js"
5
+ import type { ActionHandlerApplyResult, ActionResolve, Edge, IAction, IActionHandler, LayoutFrame, LayoutShape, MoveChangeResult, MoveState } from "../types/index.js"
6
6
  import { LAYOUT_ERROR } from "../types/index.js"
7
7
  import { KnownError } from "../utils/KnownError.js"
8
8
 
@@ -14,7 +14,7 @@ import { KnownError } from "../utils/KnownError.js"
14
14
  export class ActionHandler<
15
15
  TRawActions extends IAction[],
16
16
  TActions extends RecordFromArray<TRawActions, "name"> = RecordFromArray<TRawActions, "name">
17
- > {
17
+ > implements IActionHandler {
18
18
  activeAction?: keyof TActions
19
19
 
20
20
  actions: TActions
@@ -23,7 +23,7 @@ export class ActionHandler<
23
23
 
24
24
  boundCancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void
25
25
 
26
- defaultOnMoveChange: MoveChangeHandler = (type, _e, state, _forceRecalculateEdges, _cancel) => {
26
+ defaultOnMoveChange: IAction["onMoveChange"] = (type, _e, state, _forceRecalculateEdges, _cancel) => {
27
27
  const isTouchingCollapsedFrameEdge = type === "move"
28
28
  && state.isMoving === "edge"
29
29
  && state.touchingFramesArrays.some(frames =>
@@ -48,7 +48,7 @@ export class ActionHandler<
48
48
  /** Called when the action requested changes. */
49
49
  onRequestChange?: (type: keyof TActions | undefined) => void
50
50
  /** Called when the drag action ends either because it was completed or cancelled. */
51
- onEnd?: (context: { cancelled: boolean, applied: boolean }) => void
51
+ onEnd?: (context: { cancelled: boolean, wasApplied: boolean, result?: any }) => void
52
52
  }
53
53
 
54
54
  /** All action shapes merged into a single array. If using vue you can set this to a reactive array for reactivity. */
@@ -61,13 +61,11 @@ export class ActionHandler<
61
61
  actions: TRawActions,
62
62
  hooks: ActionHandler<TRawActions, TActions>["hooks"] = {},
63
63
  /**
64
- * Default onMoveChange handler for when no action can handle the request.
65
- *
66
- * Should return true to allow the edges to be moved, or false to prevent it.
64
+ * Default onMoveChange handler for when no action can handle the request. See {@link IAction.onMoveChange}.
67
65
  *
68
66
  * The default prevents movement when the edge is a window edge and when the edge is touching a collapsed frame.
69
67
  */
70
- defaultOnMoveChange?: MoveChangeHandler
68
+ defaultOnMoveChange?: IAction["onMoveChange"]
71
69
  ) {
72
70
  if (defaultOnMoveChange) this.defaultOnMoveChange = defaultOnMoveChange
73
71
  this.hooks = hooks
@@ -128,8 +126,8 @@ export class ActionHandler<
128
126
  e: T extends "end" ? PointerEvent | undefined : PointerEvent,
129
127
  state: MoveState,
130
128
  forceRecalculateEdges: () => void,
131
- cancel: T extends "end" ? undefined : (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
132
- resolve: T extends "end" ? undefined : (opts: ActionHandlerApplyResult) => void
129
+ cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
130
+ resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)
133
131
  ): MoveChangeResult {
134
132
  if (type === "start") {
135
133
  this.eventCanceller = cancel
@@ -137,7 +135,7 @@ export class ActionHandler<
137
135
  }
138
136
 
139
137
  if (this.activeAction) {
140
- const res = this.actions[this.activeAction]!.onMoveChange(type, e, state, forceRecalculateEdges, this.boundCancel as any, resolve as any)
138
+ const res = this.actions[this.activeAction]!.onMoveChange(type, e, state, forceRecalculateEdges, this.boundCancel as any, resolve)
141
139
  // in case it's a vue reactive array
142
140
  this.shapes.splice(0, this.shapes.length, ...res.shapes)
143
141
  this.setTextHints(type)
@@ -147,12 +145,40 @@ export class ActionHandler<
147
145
  const res = this.defaultOnMoveChange(type, e, state, forceRecalculateEdges, this.boundCancel as any, resolve as any)
148
146
  this.shapes.splice(0, this.shapes.length, ...res.shapes)
149
147
 
150
- if (type === "end") {
151
- this.activeAction = undefined
152
- }
153
148
  return res
154
149
  }
155
150
 
151
+ onMoveApply(
152
+ state: MoveState,
153
+ forceRecalculateEdges: () => void
154
+ ): ActionHandlerApplyResult {
155
+ if (this.activeAction) {
156
+ const res = this.actions[this.activeAction]!.onMoveApply(state, forceRecalculateEdges)
157
+ this.hooks.onEnd?.({ cancelled: false, wasApplied: res.wasApplied, result: res.result })
158
+ return res
159
+ }
160
+ this.hooks.onEnd?.({ cancelled: false, wasApplied: false })
161
+ return { updateEdges: true, result: undefined }
162
+ }
163
+
164
+ onMoveEnded() {
165
+ if (this.activeAction) {
166
+ this.actions[this.activeAction]!.onMoveEnded()
167
+ }
168
+
169
+ this.activeAction = undefined
170
+ }
171
+
172
+ cancel(e: PointerEvent | KeyboardEvent | undefined, state: MoveState): void {
173
+ if (this.activeAction) {
174
+ this.actions[this.activeAction].cancel(e, state)
175
+ }
176
+ this.activeAction = undefined
177
+ this.eventCanceller?.(e, state)
178
+ this.hooks.onEnd?.({ cancelled: true, wasApplied: false })
179
+ }
180
+
181
+
156
182
  setTextHints(type: "start" | "move" | "end") {
157
183
  // again in case it's a vue reactive object
158
184
  this.textHints.actions = []
@@ -190,28 +216,6 @@ export class ActionHandler<
190
216
  }
191
217
  }
192
218
 
193
- onMoveApply(
194
- state: MoveState,
195
- forceRecalculateEdges: () => void
196
- ): ActionHandlerApplyResult {
197
- if (this.activeAction) {
198
- const res = this.actions[this.activeAction]!.onMoveApply(state, forceRecalculateEdges)
199
- this.hooks.onEnd?.({ cancelled: false, applied: res })
200
- return { apply: res, result: undefined }
201
- }
202
- this.hooks.onEnd?.({ cancelled: false, applied: false })
203
- return { apply: true, result: undefined }
204
- }
205
-
206
- cancel(e: PointerEvent | KeyboardEvent | undefined, state: MoveState): void {
207
- if (this.activeAction) {
208
- this.actions[this.activeAction].cancel(e, state)
209
- }
210
- this.activeAction = undefined
211
- this.eventCanceller?.(e, state)
212
- this.hooks.onEnd?.({ cancelled: true, applied: false })
213
- }
214
-
215
219
  static debugState(
216
220
  pluginName: string,
217
221
  type: "before" | "after" | string,
@@ -6,7 +6,7 @@ import { oppositeSide } from "../helpers/oppositeSide.js"
6
6
  import { applyFrameChanges } from "../layout/applyFrameChanges.js"
7
7
  import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js"
8
8
  import { getCloseFrameInfo } from "../layout/getCloseFrameInfo.js"
9
- import type { ActionChangeResult, CloseDeco, IAction, MoveState } from "../types/index.js"
9
+ import type { ActionApplyResult, ActionChangeResult, CloseDeco, IAction, MoveState } from "../types/index.js"
10
10
  import type { KnownError } from "../utils/KnownError.js"
11
11
 
12
12
  export type CloseInfo = Exclude<ReturnType<typeof getCloseFrameInfo>, KnownError>
@@ -155,10 +155,6 @@ export class CloseAction implements IAction {
155
155
  _e: PointerEvent | undefined,
156
156
  state: MoveState
157
157
  ): ActionChangeResult {
158
- if (type === "end") {
159
- this.reset()
160
- return { shapes: [] }
161
- }
162
158
  if (state.moveDistance <= this.minDragDistance) {
163
159
  return { updateEdges: false, shapes: [] }
164
160
  }
@@ -209,23 +205,29 @@ export class CloseAction implements IAction {
209
205
  }
210
206
  }
211
207
  const decos = this._getDecos(state)
212
- this.state.lastReturn = { updateEdges: !isMovingFromWindowEdge, shapes: decos.flatMap(_ => _.shapes) }
208
+ this.state.lastReturn = {
209
+ updateEdges: !isMovingFromWindowEdge,
210
+ shapes: type === "end" ? [] : decos.flatMap(_ => _.shapes)
211
+ }
213
212
  return this.state.lastReturn
214
213
  }
215
214
 
216
- onMoveApply(state: MoveState): boolean {
215
+ onMoveApply(state: MoveState): ActionApplyResult {
217
216
  if (this.state.res) {
218
217
  const win = state.win
219
218
  if (this.debug) { ActionHandler.debugState(this.name, "before", state, this.state, this.debug) }
220
219
  applyFrameChanges(win, this.state.res)
221
220
  if (this.debug) { ActionHandler.debugState(this.name, "after", state, this.state, this.debug) }
222
- return true
221
+ return { updateEdges: false, wasApplied: true }
223
222
  }
224
- return false
223
+ return { updateEdges: true, wasApplied: false }
225
224
  }
226
225
 
227
226
  cancel(): void {
228
- this.reset()
229
227
  this.hooks.onCancel?.()
230
228
  }
229
+
230
+ onMoveEnded() {
231
+ this.reset()
232
+ }
231
233
  }
@@ -7,7 +7,7 @@ import { getFrameRearrangeInfo } from "../layout/getFrameRearrangeInfo.js"
7
7
  import { getFrameSwapInfo } from "../layout/getFrameSwapInfo.js"
8
8
  import { getZones } from "../layout/getZones.js"
9
9
  import { settings } from "../settings.js"
10
- import type { ActionChangeResult, FrameDeco, IAction, LayoutChange, MoveState, Zone } from "../types/index.js"
10
+ import type { ActionApplyResult, ActionChangeResult, FrameDeco, IAction, LayoutChange, MoveState, Zone } from "../types/index.js"
11
11
  import type { KnownError } from "../utils/KnownError.js"
12
12
 
13
13
 
@@ -155,10 +155,6 @@ export class FrameDragAction implements IAction {
155
155
  _e: PointerEvent | undefined,
156
156
  state: MoveState
157
157
  ): ActionChangeResult {
158
- if (type === "end") {
159
- this.reset()
160
- return { shapes: [] }
161
- }
162
158
  if (state.moveDistance <= this.minDragDistance) {
163
159
  return { updateEdges: false, shapes: [], showMoving: false }
164
160
  }
@@ -185,16 +181,16 @@ export class FrameDragAction implements IAction {
185
181
  const decos = this.getDecos(matchedZone, state, this.state.lastReturn)
186
182
 
187
183
  return {
188
- shapes: decos.flatMap(_ => _.shapes),
184
+ shapes: type === "end" ? [] : decos.flatMap(_ => _.shapes),
189
185
  updateEdges: false,
190
186
  showMoving: false
191
187
  }
192
188
  }
193
189
 
194
- onMoveApply(state: MoveState): boolean {
190
+ onMoveApply(state: MoveState): ActionApplyResult {
195
191
  const result = this.state.lastReturn
196
192
  if (!result || !state.moveHoveredFrame || !state.movingFrameId) {
197
- return true
193
+ return { updateEdges: false, wasApplied: false }
198
194
  }
199
195
 
200
196
  if (result instanceof Error) {
@@ -205,7 +201,11 @@ export class FrameDragAction implements IAction {
205
201
  if (this.debug) { ActionHandler.debugState(this.name, "after", state, this.state, this.debug) }
206
202
  }
207
203
 
208
- return true
204
+ return { updateEdges: false, wasApplied: true }
205
+ }
206
+
207
+ onMoveEnded() {
208
+ this.reset()
209
209
  }
210
210
 
211
211
  cancel(): void {
@@ -5,7 +5,7 @@ import { oppositeSide } from "../helpers/oppositeSide.js"
5
5
  import { applyFrameChanges } from "../layout/applyFrameChanges.js"
6
6
  import { createSplitDecoFromDrag } from "../layout/createSplitDecoFromDrag.js"
7
7
  import { getFrameSplitInfo } from "../layout/getFrameSplitInfo.js"
8
- import type { ActionChangeResult, IAction, LayoutShape, MoveState, Point, SplitDeco } from "../types/index.js"
8
+ import type { ActionApplyResult, ActionChangeResult, IAction, LayoutShape, MoveState, Point, SplitDeco } from "../types/index.js"
9
9
  import type { KnownError } from "../utils/KnownError.js"
10
10
 
11
11
  export type SplitInfo = Exclude<ReturnType<typeof getFrameSplitInfo>, KnownError>
@@ -178,10 +178,6 @@ export class SplitAction implements IAction {
178
178
  _e: PointerEvent | undefined,
179
179
  state: MoveState
180
180
  ): ActionChangeResult {
181
- if (type === "end") {
182
- this.reset()
183
- return { shapes: [] }
184
- }
185
181
  const { moveHoveredFrame, moveDistance } = state
186
182
  if (moveDistance <= this.minDragDistance) {
187
183
  return { updateEdges: false, shapes: [], showMoving: false }
@@ -207,15 +203,23 @@ export class SplitAction implements IAction {
207
203
  },
208
204
  attrs: { class: "deco-split-error bg-red-500/50" }
209
205
  }
210
- this.state.lastReturn = { updateEdges: true, shapes: [errorDeco], showMoving: false }
206
+ this.state.lastReturn = {
207
+ updateEdges: true,
208
+ shapes: type === "end" ? [] : [errorDeco],
209
+ showMoving: false
210
+ }
211
211
  return this.state.lastReturn
212
212
  }
213
213
  }
214
- this.state.lastReturn = { updateEdges: false, shapes: decos.flatMap(_ => _.shapes), showMoving: false }
214
+ this.state.lastReturn = {
215
+ updateEdges: false,
216
+ shapes: type === "end" ? [] : decos.flatMap(_ => _.shapes),
217
+ showMoving: false
218
+ }
215
219
  return this.state.lastReturn
216
220
  }
217
221
 
218
- onMoveApply(state: MoveState): boolean {
222
+ onMoveApply(state: MoveState): ActionApplyResult {
219
223
  if (this.state.res && state.moveHoveredFrame) {
220
224
  // this only caches once per frame hovered over
221
225
  // so the drag position is outdated, we must recalculate
@@ -226,11 +230,14 @@ export class SplitAction implements IAction {
226
230
  if (this.debug) { ActionHandler.debugState(this.name, "after", state, this.state, this.debug) }
227
231
  }
228
232
  }
229
- return true
233
+ return { updateEdges: false, wasApplied: true }
230
234
  }
231
235
 
232
- cancel(): void {
236
+ onMoveEnded() {
233
237
  this.reset()
238
+ }
239
+
240
+ cancel(): void {
234
241
  this.hooks.onCancel?.()
235
242
  }
236
243
  }
@@ -487,7 +487,7 @@ export type MoveState = {
487
487
  */
488
488
  intersections: IntersectionEntry[]
489
489
  /**
490
- * Whether the drag was initiated from a point along the window edge.
490
+ * Whether the move was initiated from a point along the window edge.
491
491
  */
492
492
  isMovingFromWindowEdge: boolean
493
493
  /** Custom context passed to moveStart, available to action handlers via state.eventContext. */
@@ -497,7 +497,7 @@ export type MoveState = {
497
497
 
498
498
 
499
499
  export interface ActionChangeResult {
500
- /** Whether the drag should update the edges. Defaults to false. */
500
+ /** Whether the move should update the edges. Defaults to false. */
501
501
  updateEdges?: boolean
502
502
  /** Deco shapes produced by this action during this drag step. */
503
503
  shapes: LayoutShape[]
@@ -506,27 +506,6 @@ export interface ActionChangeResult {
506
506
  }
507
507
  export type MoveChangeResult = Omit<ActionChangeResult, "shapes">
508
508
 
509
- /**
510
- * Called when the drag coordinates change (during any event).
511
- *
512
- * Should return `{ allowed: true/false, shapes: LayoutShape[] }` to control whether the action is allowed and edges update and what deco shapes to render.
513
- *
514
- * Note that the allowed return type only affect the `move` event but is also typed as `boolean` for other events for ease of use.
515
- *
516
- * Use also to cleanup your action when type is "end".
517
- *
518
- * See also {@link ActionHandler.onMoveChange} to understand it's lifecycle as it is the extended version of.
519
- */
520
- export type MoveChangeHandler = <T extends "start" | "move" | "end">(
521
- type: T,
522
- e: T extends "end" ? PointerEvent | undefined : PointerEvent,
523
- state: MoveState,
524
- forceRecalculateEdges: () => void,
525
- /** Calls moveEnd with apply: false */
526
- cancel: T extends "end" ? undefined : (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
527
- /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. */
528
- resolve: T extends "end" ? undefined : ({ apply, result }: { apply: boolean, result: any }) => void
529
- ) => ActionChangeResult
530
509
 
531
510
  /**
532
511
  * A drag action describes when and how to handle a drag event.
@@ -539,18 +518,35 @@ export type MoveChangeHandler = <T extends "start" | "move" | "end">(
539
518
  export interface IAction {
540
519
  /** A unique name for your action. */
541
520
  name: string
542
- onMoveChange: MoveChangeHandler
543
521
  /**
522
+ * Called when the drag coordinates change (during any event).
544
523
  *
545
- * Is called before `onMoveChange("end")` with the same event. Might not be called if the request was cancelled.
524
+ * Should return `{ allowed: true/false, shapes: LayoutShape[] }` to control whether the action is allowed and edges update and what deco shapes to render.
546
525
  *
547
- * You should apply your action if possible and return whether it was applied.
526
+ * Note that the allowed return type only affect the `move` event but is also typed as `boolean` for other events for ease of use.
548
527
  *
549
- * Do not reset state here, use onMoveChange ("end").
528
+ * Use also to cleanup your action when type is "end".
550
529
  *
551
- * See also {@link ActionHandler.onMoveApply} which this is the extended version of.
530
+ * See also {@link ActionHandler.onMoveChange} to understand it's lifecycle as it is the extended version of.
552
531
  */
553
- onMoveApply: (state: MoveState, forceRecalculateEdges: () => void) => boolean
532
+ onMoveChange: <T extends "start" | "move" | "end">(
533
+ type: T,
534
+ e: T extends "end" ? PointerEvent | undefined : PointerEvent,
535
+ state: MoveState,
536
+ forceRecalculateEdges: () => void,
537
+ /** Calls moveEnd with updateEdges: false. This can technically be called from "end", it should still work. */
538
+ cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
539
+ /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. Not available during "end" event. It's designed for resolving from other external evente (e.g. key events). */
540
+ resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)
541
+ ) => ActionChangeResult
542
+ /**
543
+ * Is called after `onMoveChange("end")` with the same event. Will not be called if the request was cancelled.
544
+ *
545
+ * You should apply your action if possible and return whether it was applied `wasApplied` as well as `updateEdges` and `result` (optional), see {@link ActionHandler.onMoveApply}, which this is the extended version of.
546
+ *
547
+ * Do not reset state here, use onMoveChange ("end").
548
+ */
549
+ onMoveApply: (state: MoveState, forceRecalculateEdges: () => void) => ActionApplyResult
554
550
  /**
555
551
  * Should return true if it should handle the "request"/event (e.g. some modifier is being pressed => user is requesting x action).
556
552
  *
@@ -587,6 +583,11 @@ export interface IAction {
587
583
  /** Error texts/hints to display when the action produces an error. */
588
584
  errors?: string[]
589
585
  }
586
+ /**
587
+ * Called when an action is cancelled. Call action specific onCancel hooks here.
588
+ */
589
+ // note: internally this is done by the actionhandler wrapping the given cancel function for an event
590
+ cancel(e: PointerEvent | KeyboardEvent | undefined, state: MoveState): void
590
591
  /**
591
592
  * Called after visual edges are recalculated, once per edge. Action handlers can annotate edges with error info here ({@link Edge.error}).
592
593
  *
@@ -612,39 +613,62 @@ export type EdgeMoveStartData = { edge?: Edge, intersection?: IntersectionEntry
612
613
  export type FrameMoveStartData = { frameId: FrameId }
613
614
 
614
615
  export type ActionHandlerApplyResult = {
615
- /** Whether to apply the regular drag end changes. Return false to reset to the position before dragging. */
616
- apply: boolean
617
- /** Value to resolve the drag promise with. Ignored if `apply` is false. */
618
- result: any
616
+ // /** Whether the move should update the edges. Defaults to false which resets to the position before moving started.*/
617
+ updateEdges: boolean
618
+ /** Value to resolve the drag promise with. Ignored if `updateEdges` is false. */
619
+ result?: any
619
620
  }
620
621
 
622
+ export type ActionApplyResult = ActionHandlerApplyResult & { wasApplied: boolean }
623
+
624
+ export type ActionResolve = { apply: boolean, result?: any }
625
+
621
626
  /**
622
627
  * Handler interface for drag actions.
623
628
  */
624
- export interface ActionHandler {
629
+ // eslint-disable-next-line @typescript-eslint/naming-convention
630
+ export interface IActionHandler {
625
631
  eventHandler: (e: KeyboardEvent, state: MoveState, forceRecalculateEdges: () => void) => void
632
+
626
633
  /**
627
- * Called when the drag coordinates change (during any event). Should return true to allow the edges to be updated/moved, or false to prevent it. Note that the return only affects the move event but it's typed like this for ease of use. See also {@link MoveChangeHandler}.
634
+ * Called when the drag coordinates change (during any event). Should return updateEdges true to allow the edges to be updated/moved, or false to prevent it. See also {@link MoveChangeHandler} for the built in action handler.
628
635
  *
629
636
  * Can be used to save some context/info to later apply safely during onMoveApply.
630
637
  *
631
638
  * The call order is:
632
639
  * - onMoveChange("start", ...)
633
640
  * - onMoveChange("move", ...)
641
+ * - onMoveChange("end", ...)
634
642
  * - onMoveApply(...) (IF moveEnd was called with apply: true, otherwise this is skipped)
635
- * - onMoveChange("end", ...) // always called last, do cleanup here
643
+ * - If anything calls cancel or resolve they call onMoveApply (cancel with apply: false, resolve with whatever apply value you gave it).
644
+ * - onMoveEnded() // do cleanup here
645
+ *
646
+ * Note also that resolve just resolves the promise value (after onMoveApply("end") and before onMoveEnded()). Depending on what you're doing you might still have to apply the result, remember onMoveApply will still be called if you do `resolve({ apply:true })`.
636
647
  */
637
- onMoveChange: (...args: Parameters<MoveChangeHandler>) => MoveChangeResult
648
+ onMoveChange<T extends "start" | "move" | "end">(
649
+ type: T,
650
+ e: T extends "end" ? PointerEvent | undefined : PointerEvent,
651
+ state: MoveState,
652
+ forceRecalculateEdges: () => void,
653
+ /** Calls moveEnd with apply: false. This can technically be called from "end", it should still work. */
654
+ cancel: (e: PointerEvent | KeyboardEvent | undefined, state: MoveState) => void,
655
+ /** Saves result to resolve moveStart promise with then calls moveEnd with given apply. Not available during "end" event. It's designed for resolving from other external evente (e.g. key events). */
656
+ resolve: T extends "end" ? undefined : ((opts: ActionResolve) => void)
657
+ ): MoveChangeResult
638
658
  /**
639
659
  * Called when drag will be applied. If moveEnd was called with apply false, it will not be called.
640
- * Return false to not apply the regular drag end changes (i.e. return false to reset to the position before dragging).
660
+ * Return `{updateEdges: false`} to not apply the regular drag end changes (i.e. return false to reset to the position before dragging). Optionally return a `result` value to resolve the promise with.
641
661
  *
642
- * Do not use for resetting handler state, use onMoveChange("end", ...) for that.
662
+ * Do not use for resetting handler state, use onMoveEnded for that.
643
663
  */
644
664
  onMoveApply: (
645
665
  state: MoveState,
646
666
  forceRecalculateEdges: () => void
647
667
  ) => ActionHandlerApplyResult
668
+
669
+ /** For doing cleanup */
670
+ onMoveEnded: () => void
671
+
648
672
  /**
649
673
  * Called after visual edges are recalculated. Action handlers can annotate edges with error info.
650
674
  */
@@ -1,7 +1,7 @@
1
1
  import type { ComputedRef, InjectionKey, Ref } from "vue"
2
2
 
3
3
  // eslint-disable-next-line no-restricted-imports
4
- import type { ActionHandler, ActionHandlerApplyResult, Direction, Edge, FrameId, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, MoveState, Orientation, Point } from "./index.js"
4
+ import type { Direction, Edge, FrameId, IActionHandler, IntersectionEntry, LayoutFrame, LayoutShape, LayoutWindow, MoveState, Orientation, Point } from "./index.js"
5
5
 
6
6
  export type LayoutContext = ComputedRef<
7
7
  & {
@@ -15,13 +15,13 @@ export type LayoutContext = ComputedRef<
15
15
  export const layoutContextInjectionKey = Symbol.for("@witchcraft/layout:context") as InjectionKey<LayoutContext>
16
16
 
17
17
  export interface UseFramesContext {
18
- actionHandler: ActionHandler
18
+ actionHandler: IActionHandler
19
19
  moveStart: {
20
20
  (e: PointerEvent, type: "edge", data: { edge?: Edge, intersection?: IntersectionEntry }, opts?: { moveEvent?: string, endEvent?: string, context?: Record<string, unknown> }): Promise<any>
21
21
  (e: PointerEvent, type: "frame", data: { frameId: FrameId }, opts?: { moveEvent?: string, endEvent?: string, context?: Record<string, unknown> }): Promise<any>
22
22
  }
23
23
  onMove: (e: PointerEvent) => void
24
- moveEnd: (e?: PointerEvent, options?: Partial<Pick<ActionHandlerApplyResult, "apply">>) => void
24
+ moveEnd: (e?: PointerEvent, options?: { apply?: boolean }) => void
25
25
  cancel: () => void
26
26
  moveDirections: Ref<Record<Orientation, Direction | undefined>>
27
27
  movePoint: Ref<Point | undefined>