@witchcraft/layout 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/LayoutDragEdgeGrabbed.vue +1 -1
  3. package/dist/runtime/components/LayoutDragEdgeHandle.vue +1 -2
  4. package/dist/runtime/components/LayoutDragEdges.d.vue.ts +2 -2
  5. package/dist/runtime/components/LayoutDragEdges.vue +1 -1
  6. package/dist/runtime/components/LayoutDragEdges.vue.d.ts +2 -2
  7. package/dist/runtime/components/LayoutIntersections.vue +1 -0
  8. package/dist/runtime/components/LayoutWindow.d.vue.ts +3 -0
  9. package/dist/runtime/components/LayoutWindow.vue +3 -2
  10. package/dist/runtime/components/LayoutWindow.vue.d.ts +3 -0
  11. package/dist/runtime/composables/useFrames.d.ts +1 -0
  12. package/dist/runtime/composables/useFrames.js +12 -0
  13. package/dist/runtime/drag/CloseAction.d.ts +6 -1
  14. package/dist/runtime/drag/CloseAction.js +6 -1
  15. package/dist/runtime/drag/FrameDragAction.d.ts +10 -5
  16. package/dist/runtime/drag/FrameDragAction.js +6 -2
  17. package/dist/runtime/drag/SplitAction.d.ts +5 -1
  18. package/dist/runtime/drag/SplitAction.js +11 -6
  19. package/dist/runtime/settings.js +1 -1
  20. package/dist/runtime/types/index.d.ts +2 -0
  21. package/package.json +1 -1
  22. package/src/runtime/components/LayoutDragEdgeGrabbed.vue +1 -1
  23. package/src/runtime/components/LayoutDragEdgeHandle.vue +1 -2
  24. package/src/runtime/components/LayoutDragEdges.vue +1 -1
  25. package/src/runtime/components/LayoutIntersections.vue +1 -0
  26. package/src/runtime/components/LayoutWindow.vue +6 -4
  27. package/src/runtime/composables/useFrames.ts +15 -0
  28. package/src/runtime/drag/CloseAction.ts +11 -2
  29. package/src/runtime/drag/FrameDragAction.ts +15 -3
  30. package/src/runtime/drag/SplitAction.ts +20 -7
  31. package/src/runtime/settings.ts +1 -1
  32. package/src/runtime/types/index.ts +2 -0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/layout",
3
3
  "configKey": "witchcraftLayout",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -7,7 +7,7 @@
7
7
  z-20
8
8
  bg-accent-500
9
9
  cursor-pointer
10
- [&.request-split]:hidden
10
+ [.request-split_&]:hidden
11
11
  `,
12
12
  $attrs.class
13
13
  )"
@@ -7,8 +7,7 @@
7
7
  drag-edge
8
8
  z-20
9
9
  hover:cursor-pointer
10
-
11
- [&.request-split]:hidden
10
+ [.request-split_&]:hidden
12
11
  `,
13
12
  edge.error && `
14
13
  drag-edge-errored
@@ -2,14 +2,14 @@ declare var __VLS_1: {
2
2
  css: any;
3
3
  edge: any;
4
4
  onPointerDown: any;
5
- }, __VLS_10: {
5
+ }, __VLS_6: {
6
6
  css: any;
7
7
  edge: any;
8
8
  };
9
9
  type __VLS_Slots = {} & {
10
10
  handle?: (props: typeof __VLS_1) => any;
11
11
  } & {
12
- indicator?: (props: typeof __VLS_10) => any;
12
+ indicator?: (props: typeof __VLS_6) => any;
13
13
  };
14
14
  declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
15
15
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
@@ -11,7 +11,7 @@
11
11
  <LayoutDragEdgeHandle
12
12
  :css="css.handle"
13
13
  :edge="visualEdges[i]"
14
- @pointerdown="dragCtx.dragStart"
14
+ :on-pointer-down="dragCtx.dragStart"
15
15
  />
16
16
  </slot>
17
17
  <slot name="indicator" v-bind="{
@@ -2,14 +2,14 @@ declare var __VLS_1: {
2
2
  css: any;
3
3
  edge: any;
4
4
  onPointerDown: any;
5
- }, __VLS_10: {
5
+ }, __VLS_6: {
6
6
  css: any;
7
7
  edge: any;
8
8
  };
9
9
  type __VLS_Slots = {} & {
10
10
  handle?: (props: typeof __VLS_1) => any;
11
11
  } & {
12
- indicator?: (props: typeof __VLS_10) => any;
12
+ indicator?: (props: typeof __VLS_6) => any;
13
13
  };
14
14
  declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
15
15
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
@@ -11,6 +11,7 @@
11
11
  <LayoutIntersectionHandle
12
12
  :css="css.handle"
13
13
  :intersection="draggableIntersections[i]"
14
+ :on-pointer-down="dragCtx.dragStart"
14
15
  />
15
16
  </slot>
16
17
  <slot name="indicator" v-bind="{ css: css.visible, intersection: draggableIntersections[i] }">
@@ -3,6 +3,8 @@ import { type DragState, type IDragAction } from "../types/index.js";
3
3
  import { getUpdateWindowSizeInfo } from "../layout/getUpdateWindowSizeInfo.js";
4
4
  import { type LayoutWindow } from "../types/index.js";
5
5
  type __VLS_Props = {
6
+ /** If you really need it, you can provide your own drag action handler, note the actionHandler's prop will no longer have any effect.*/
7
+ dragActionHandler?: DragActionHandler<any, any>;
6
8
  /** Custom drag action handlers. Falls back to default split/close/frame handlers if not provided. */
7
9
  actionHandlers?: IDragAction[];
8
10
  textHints?: {
@@ -41,6 +43,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_PublicProps,
41
43
  x: number;
42
44
  y: number;
43
45
  } | undefined;
46
+ dragDistance: number;
44
47
  isDragging: false | "edge" | "frame";
45
48
  showDragging: boolean;
46
49
  draggingFrameId: string | undefined;
@@ -7,7 +7,7 @@
7
7
  flex
8
8
  flex-col
9
9
  `,
10
- isDragging && `dragging cursor-pointer`,
10
+ isDragging && `dragging cursor-pointer user-select-none`,
11
11
  requestType && `request-${requestType}`,
12
12
  $attrs.class
13
13
  )"
@@ -82,6 +82,7 @@ import { applyFrameChanges } from "../layout/applyFrameChanges.js";
82
82
  const $attrs = useAttrs();
83
83
  const win = defineModel("win", { type: Object, ...{ required: true } });
84
84
  const props = defineProps({
85
+ dragActionHandler: { type: Object, required: false },
85
86
  actionHandlers: { type: Array, required: false, default: void 0 },
86
87
  textHints: { type: Array, required: false, default: () => [] },
87
88
  textHintsTeleportTo: { type: null, required: true, default: void 0 },
@@ -91,7 +92,7 @@ const props = defineProps({
91
92
  const emit = defineEmits(["isShowingDrag", "dragState"]);
92
93
  const windowEl = ref(null);
93
94
  const requestType = ref();
94
- const dragActionHandler = new DragActionHandler(
95
+ const dragActionHandler = props.dragActionHandler ?? new DragActionHandler(
95
96
  [
96
97
  ...props.actionHandlers ?? createDefaultHandlers()
97
98
  ],
@@ -3,6 +3,8 @@ import { type DragState, type IDragAction } from "../types/index.js";
3
3
  import { getUpdateWindowSizeInfo } from "../layout/getUpdateWindowSizeInfo.js";
4
4
  import { type LayoutWindow } from "../types/index.js";
5
5
  type __VLS_Props = {
6
+ /** If you really need it, you can provide your own drag action handler, note the actionHandler's prop will no longer have any effect.*/
7
+ dragActionHandler?: DragActionHandler<any, any>;
6
8
  /** Custom drag action handlers. Falls back to default split/close/frame handlers if not provided. */
7
9
  actionHandlers?: IDragAction[];
8
10
  textHints?: {
@@ -41,6 +43,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_PublicProps,
41
43
  x: number;
42
44
  y: number;
43
45
  } | undefined;
46
+ dragDistance: number;
44
47
  isDragging: false | "edge" | "frame";
45
48
  showDragging: boolean;
46
49
  draggingFrameId: string | undefined;
@@ -744,6 +744,7 @@ export declare function useFrames(win: Ref<LayoutWindow>, handler: {
744
744
  x: number;
745
745
  y: number;
746
746
  } | undefined;
747
+ dragDistance: number;
747
748
  isDragging: false | "edge" | "frame";
748
749
  showDragging: boolean;
749
750
  draggingFrameId: string | undefined;
@@ -27,6 +27,8 @@ export function useFrames(win, handler) {
27
27
  const showDragging = ref(false);
28
28
  const dragPoint = ref();
29
29
  const dragDirections = ref({});
30
+ let dragStartPoint;
31
+ const dragDistance = ref(-1);
30
32
  const draggingIntersection = ref(void 0);
31
33
  const isDraggingFromWindowEdge = ref(false);
32
34
  const frameDragFrameId = ref();
@@ -64,6 +66,7 @@ export function useFrames(win, handler) {
64
66
  const state = computed(() => ({
65
67
  dragDirections: dragDirections.value,
66
68
  dragPoint: dragPoint.value,
69
+ dragDistance: dragDistance.value,
67
70
  isDragging: isDragging.value,
68
71
  showDragging: showDragging.value,
69
72
  draggingFrameId: frameDragFrameId.value,
@@ -80,10 +83,12 @@ export function useFrames(win, handler) {
80
83
  }));
81
84
  let controller;
82
85
  function resetState() {
86
+ dragStartPoint = void 0;
83
87
  draggingEdges.value = [];
84
88
  draggingIntersection.value = void 0;
85
89
  isDragging.value = false;
86
90
  dragPoint.value = void 0;
91
+ dragDistance.value = -1;
87
92
  touchingFrames.value = [];
88
93
  frameDragFrameId.value = void 0;
89
94
  dragDirStore.reset();
@@ -98,6 +103,7 @@ export function useFrames(win, handler) {
98
103
  window.addEventListener("pointerup", dragEnd, { signal: controller.signal });
99
104
  const point = toWindowCoord(win.value, e);
100
105
  dragPoint.value = point;
106
+ dragStartPoint = { ...point };
101
107
  dragDirStore.update(point);
102
108
  isDragging.value = type;
103
109
  showDragging.value = true;
@@ -136,6 +142,12 @@ export function useFrames(win, handler) {
136
142
  const point = toWindowCoord(win.value, e);
137
143
  const didChange = dragDirStore.update(point);
138
144
  dragPoint.value = point;
145
+ dragDistance.value = 0;
146
+ if (dragStartPoint) {
147
+ dragDistance.value = Math.sqrt(
148
+ Math.pow(point.x - dragStartPoint.x, 2) + Math.pow(point.y - dragStartPoint.y, 2)
149
+ );
150
+ }
139
151
  if (!didChange) return;
140
152
  const res = handler.onDragChange("move", e, state.value, forceRecalculateEdges, cancel);
141
153
  showDragging.value = res.showDragging ?? true;
@@ -4,6 +4,7 @@ import type { KnownError } from "../utils/KnownError.js";
4
4
  export type CloseInfo = Exclude<ReturnType<typeof getCloseFrameInfo>, KnownError>;
5
5
  export declare class CloseAction implements IDragAction {
6
6
  name: "close";
7
+ minDragDistance: number;
7
8
  state: {
8
9
  allowed: true;
9
10
  force: boolean;
@@ -23,7 +24,9 @@ export declare class CloseAction implements IDragAction {
23
24
  errors: string[];
24
25
  };
25
26
  closeHints: {
26
- actions: string[];
27
+ actions: string[] | ((state: {
28
+ force: boolean;
29
+ }) => string[]);
27
30
  transformError: (e: KnownError) => string;
28
31
  };
29
32
  handleEvent: (e: PointerEvent | KeyboardEvent, state: DragState) => boolean | "force";
@@ -38,6 +41,8 @@ export declare class CloseAction implements IDragAction {
38
41
  modifyDecos?: CloseAction["modifyDecos"], hooks?: CloseAction["hooks"], config?: {
39
42
  debug?: boolean | string;
40
43
  closeHints?: Partial<CloseAction["closeHints"]>;
44
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
45
+ minDragDistance?: number;
41
46
  });
42
47
  reset(): void;
43
48
  private _getDecos;
@@ -7,6 +7,7 @@ import { findFramesTouchingEdge } from "../layout/findFramesTouchingEdge.js";
7
7
  import { getCloseFrameInfo } from "../layout/getCloseFrameInfo.js";
8
8
  export class CloseAction {
9
9
  name = "close";
10
+ minDragDistance = 5;
10
11
  state = {};
11
12
  // this is initialized by this.reset()
12
13
  debug = false;
@@ -30,6 +31,7 @@ export class CloseAction {
30
31
  if (modifyDecos !== void 0) this.modifyDecos = modifyDecos;
31
32
  this.hooks = hooks;
32
33
  if (config?.debug) this.debug = true;
34
+ if (config?.minDragDistance !== void 0) this.minDragDistance = config.minDragDistance;
33
35
  this.reset();
34
36
  }
35
37
  reset() {
@@ -73,7 +75,7 @@ export class CloseAction {
73
75
  this.textHints.actions = [];
74
76
  this.textHints.errors = [this.closeHints.transformError(result)];
75
77
  } else {
76
- this.textHints.actions = this.closeHints.actions;
78
+ this.textHints.actions = typeof this.closeHints.actions === "function" ? this.closeHints.actions({ force: this.state.force }) : this.closeHints.actions;
77
79
  this.textHints.errors = [];
78
80
  }
79
81
  }
@@ -97,6 +99,9 @@ export class CloseAction {
97
99
  return false;
98
100
  }
99
101
  onDragChange(_type, _e, state) {
102
+ if (state.dragDistance <= this.minDragDistance) {
103
+ return { updateEdges: false, shapes: [] };
104
+ }
100
105
  const {
101
106
  touchingFramesArrays,
102
107
  dragDirections,
@@ -2,6 +2,7 @@ import type { ActionDragChangeResult, DragState, DragZone, FrameDragDeco, IDragA
2
2
  import type { KnownError } from "../utils/KnownError.js";
3
3
  export declare class FrameDragAction implements IDragAction {
4
4
  name: "frameDrag";
5
+ minDragDistance: number;
5
6
  state: {
6
7
  lastReturn?: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError;
7
8
  };
@@ -13,11 +14,13 @@ export declare class FrameDragAction implements IDragAction {
13
14
  handleEvent: (e: PointerEvent | KeyboardEvent, state: DragState) => boolean;
14
15
  dragHints: {
15
16
  actions: {
16
- split: string;
17
- swap: string;
18
- rearrange: string;
19
- dock: string;
20
- };
17
+ split?: string;
18
+ swap?: string;
19
+ rearrange?: string;
20
+ dock?: string;
21
+ } | ((state: {
22
+ type: "split" | "swap" | "rearrange" | "dock";
23
+ }) => string[]);
21
24
  transformError: (e: KnownError) => string;
22
25
  };
23
26
  modifyDecos: (shapes: FrameDragDeco[]) => void;
@@ -30,6 +33,8 @@ export declare class FrameDragAction implements IDragAction {
30
33
  /** Modify the created decos before they are rendered. */
31
34
  modifyDecos?: FrameDragAction["modifyDecos"], hooks?: FrameDragAction["hooks"], config?: {
32
35
  debug?: boolean | string;
36
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
37
+ minDragDistance?: number;
33
38
  });
34
39
  reset(): void;
35
40
  setTextHints(result: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError | undefined): void;
@@ -8,6 +8,7 @@ import { getFrameSwapInfo } from "../layout/getFrameSwapInfo.js";
8
8
  import { settings } from "../settings.js";
9
9
  export class FrameDragAction {
10
10
  name = "frameDrag";
11
+ minDragDistance = 5;
11
12
  state = {
12
13
  lastReturn: void 0
13
14
  };
@@ -31,6 +32,7 @@ export class FrameDragAction {
31
32
  if (modifyDecos !== void 0) this.modifyDecos = modifyDecos;
32
33
  this.hooks = hooks;
33
34
  if (config?.debug) this.debug = true;
35
+ if (config?.minDragDistance !== void 0) this.minDragDistance = config.minDragDistance;
34
36
  this.reset();
35
37
  }
36
38
  reset() {
@@ -46,8 +48,7 @@ export class FrameDragAction {
46
48
  this.textHints.actions = [];
47
49
  this.textHints.errors = [this.dragHints.transformError(result)];
48
50
  } else {
49
- const text = this.dragHints.actions[result.info];
50
- this.textHints.actions = [text];
51
+ this.textHints.actions = typeof this.dragHints.actions === "function" ? this.dragHints.actions({ type: result.info }) : this.dragHints.actions[result.info] !== void 0 ? [this.dragHints.actions[result.info]] : [];
51
52
  this.textHints.errors = [];
52
53
  }
53
54
  }
@@ -93,6 +94,9 @@ export class FrameDragAction {
93
94
  return false;
94
95
  }
95
96
  onDragChange(_type, _e, state) {
97
+ if (state.dragDistance <= this.minDragDistance) {
98
+ return { updateEdges: false, shapes: [], showDragging: false };
99
+ }
96
100
  const { win, draggingFrameId, dragHoveredFrame } = state;
97
101
  const matchedZone = getDragZones(state, settings.zoneSizes);
98
102
  if (!matchedZone) {
@@ -2,8 +2,10 @@ import { getFrameSplitInfo } from "../layout/getFrameSplitInfo.js";
2
2
  import type { ActionDragChangeResult, DragState, IDragAction, 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
+ export type DragChangeType = "start" | "move" | "end";
5
6
  export declare class SplitAction implements IDragAction {
6
7
  name: "split";
8
+ minDragDistance: number;
7
9
  state: {
8
10
  allowed: true;
9
11
  res: SplitInfo;
@@ -21,7 +23,7 @@ export declare class SplitAction implements IDragAction {
21
23
  errors: string[];
22
24
  };
23
25
  splitHints: {
24
- action: string;
26
+ actions: string[] | ((type: DragChangeType) => string[]);
25
27
  transformError: (e: KnownError) => string;
26
28
  };
27
29
  handleEvent: (e: PointerEvent | KeyboardEvent, state: DragState) => boolean;
@@ -36,6 +38,8 @@ export declare class SplitAction implements IDragAction {
36
38
  modifyDecos?: SplitAction["modifyDecos"], hooks?: SplitAction["hooks"], config?: {
37
39
  debug?: boolean | string;
38
40
  splitHints?: Partial<SplitAction["splitHints"]>;
41
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
42
+ minDragDistance?: number;
39
43
  });
40
44
  reset(): void;
41
45
  getDecos(state: DragState): SplitDeco[];
@@ -6,12 +6,13 @@ import { createSplitDecoFromDrag } from "../layout/createSplitDecoFromDrag.js";
6
6
  import { getFrameSplitInfo } from "../layout/getFrameSplitInfo.js";
7
7
  export class SplitAction {
8
8
  name = "split";
9
+ minDragDistance = 5;
9
10
  state = {};
10
11
  // this is initialized by `this.reset()`
11
12
  debug = false;
12
13
  textHints = { actions: [], errors: [] };
13
14
  splitHints = {
14
- action: "Hold Alt to Split",
15
+ actions: ["Hold Alt to Split"],
15
16
  transformError: (e) => e.message
16
17
  };
17
18
  handleEvent = (e, state) => e.altKey || state.isDraggingFromWindowEdge;
@@ -24,7 +25,8 @@ export class SplitAction {
24
25
  this.hooks = hooks;
25
26
  this.reset();
26
27
  if (config?.debug) this.debug = true;
27
- if (config?.splitHints?.action) this.splitHints.action = config.splitHints.action;
28
+ if (config?.minDragDistance !== void 0) this.minDragDistance = config.minDragDistance;
29
+ if (config?.splitHints?.actions) this.splitHints.actions = config.splitHints.actions;
28
30
  if (config?.splitHints?.transformError) this.splitHints.transformError = config.splitHints.transformError;
29
31
  }
30
32
  reset() {
@@ -62,7 +64,7 @@ export class SplitAction {
62
64
  canHandleRequest(e, state) {
63
65
  const { draggingEdges } = state;
64
66
  if (draggingEdges.length !== 1) return false;
65
- this.setTextHints(state.isDragging === "edge" ? true : void 0);
67
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? true : void 0);
66
68
  if (this.handleEvent(e, state)) {
67
69
  this.hooks.onStart?.();
68
70
  return true;
@@ -78,7 +80,7 @@ export class SplitAction {
78
80
  this.textHints.actions = [];
79
81
  this.textHints.errors = [this.splitHints.transformError(result)];
80
82
  } else {
81
- this.textHints.actions = [this.splitHints.action];
83
+ this.textHints.actions = typeof this.splitHints.actions === "function" ? this.splitHints.actions("move") : this.splitHints.actions;
82
84
  this.textHints.errors = [];
83
85
  }
84
86
  }
@@ -103,7 +105,7 @@ export class SplitAction {
103
105
  dragDirections[oppositeOrientation],
104
106
  dragPoint
105
107
  );
106
- this.setTextHints(canSplit);
108
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? canSplit : void 0);
107
109
  if (!(canSplit instanceof Error)) {
108
110
  this.state.allowed = true;
109
111
  this.state.res = canSplit;
@@ -117,7 +119,10 @@ export class SplitAction {
117
119
  }
118
120
  }
119
121
  onDragChange(_type, _e, state) {
120
- const { dragHoveredFrame } = state;
122
+ const { dragHoveredFrame, dragDistance } = state;
123
+ if (dragDistance <= this.minDragDistance) {
124
+ return { updateEdges: false, shapes: [], showDragging: false };
125
+ }
121
126
  let ok = false;
122
127
  if (dragHoveredFrame) {
123
128
  if (this.state.lastPoint?.x === state.dragPoint?.x && this.state.lastPoint?.y === state.dragPoint?.y && this.state.lastReturn) {
@@ -8,7 +8,7 @@ export class Settings {
8
8
  resetToDefaults() {
9
9
  this.scale = 3;
10
10
  this.snapPoint = 0.5;
11
- this.minSize = 5;
11
+ this.minSize = 3;
12
12
  this.collapseSizePx = 15;
13
13
  this.maxPerpendicularLength = 20;
14
14
  this.zoneSizes = { frameEdgePx: 40, windowEdgePx: 20 };
@@ -1808,6 +1808,8 @@ export type DragState = {
1808
1808
  dragDirections: Record<Orientation, Direction | undefined>;
1809
1809
  /** The curren point (in scaled window coordinates) the user is dragging at. */
1810
1810
  dragPoint?: Point;
1811
+ /** Cumulative pixel distance the user has dragged from the initial click point. 0 on start, increases on each move. */
1812
+ dragDistance: number;
1811
1813
  /** Whether the user is currently dragging and what type of drag. Is truthy string during all drag events. */
1812
1814
  isDragging: boolean | "frame" | "edge";
1813
1815
  /** Whether to show the moved frames while dragging. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/layout",
3
3
  "description": "Headless layout manager.",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "main": "./dist/runtime/index.js",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -6,7 +6,7 @@
6
6
  z-20
7
7
  bg-accent-500
8
8
  cursor-pointer
9
- [&.request-split]:hidden
9
+ [.request-split_&]:hidden
10
10
  `,
11
11
  ($attrs as any).class
12
12
  )"
@@ -7,8 +7,7 @@
7
7
  drag-edge
8
8
  z-20
9
9
  hover:cursor-pointer
10
-
11
- [&.request-split]:hidden
10
+ [.request-split_&]:hidden
12
11
  `,
13
12
  edge.error && `
14
13
  drag-edge-errored
@@ -11,7 +11,7 @@
11
11
  <LayoutDragEdgeHandle
12
12
  :css="css.handle"
13
13
  :edge="visualEdges[i]"
14
- @pointerdown="dragCtx.dragStart"
14
+ :on-pointer-down="dragCtx.dragStart"
15
15
  />
16
16
  </slot>
17
17
  <slot name="indicator" v-bind="{
@@ -11,6 +11,7 @@
11
11
  <LayoutIntersectionHandle
12
12
  :css="css.handle"
13
13
  :intersection="draggableIntersections[i]"
14
+ :on-pointer-down="dragCtx.dragStart"
14
15
  />
15
16
  </slot>
16
17
  <slot name="indicator" v-bind="{ css: css.visible, intersection: draggableIntersections[i] }">
@@ -7,7 +7,7 @@
7
7
  flex
8
8
  flex-col
9
9
  `,
10
- isDragging && `dragging cursor-pointer`,
10
+ isDragging && `dragging cursor-pointer user-select-none`,
11
11
  requestType && `request-${requestType}`,
12
12
  ($attrs as any).class
13
13
  )"
@@ -90,6 +90,8 @@ const $attrs = useAttrs()
90
90
  const win = defineModel<LayoutWindow>("win", { required: true })
91
91
 
92
92
  const props = withDefaults(defineProps<{
93
+ /** If you really need it, you can provide your own drag action handler, note the actionHandler's prop will no longer have any effect.*/
94
+ dragActionHandler?: DragActionHandler<any, any>
93
95
  /** Custom drag action handlers. Falls back to default split/close/frame handlers if not provided. */
94
96
  actionHandlers?: IDragAction[]
95
97
  textHints?: {text:string, classes?:string}[]
@@ -120,9 +122,9 @@ const windowEl = ref<HTMLElement | null>(null)
120
122
 
121
123
  const requestType = ref<string | undefined | string>()
122
124
 
123
- const dragActionHandler = new DragActionHandler(
125
+ const dragActionHandler = props.dragActionHandler ?? new DragActionHandler(
124
126
  [
125
- ...(props.actionHandlers ?? createDefaultHandlers()),
127
+ ...(props.actionHandlers ?? createDefaultHandlers())
126
128
  ],
127
129
  {
128
130
  onEvent: (e, cancel) => {
@@ -132,7 +134,7 @@ const dragActionHandler = new DragActionHandler(
132
134
  }
133
135
  },
134
136
  onRequestChange: type => {
135
- requestType.value = type
137
+ requestType.value = type as any
136
138
  },
137
139
  onEnd: () => {
138
140
  requestType.value = undefined
@@ -59,6 +59,9 @@ export function useFrames(
59
59
 
60
60
  const dragDirections = ref<Record<Orientation, Direction | undefined>>({} as any)
61
61
 
62
+ let dragStartPoint: Point | undefined
63
+ const dragDistance = ref(-1)
64
+
62
65
  const draggingIntersection = ref<IntersectionEntry | undefined>(undefined)
63
66
  const isDraggingFromWindowEdge = ref<boolean>(false)
64
67
 
@@ -110,6 +113,7 @@ export function useFrames(
110
113
  const state = computed(() => ({
111
114
  dragDirections: dragDirections.value,
112
115
  dragPoint: dragPoint.value,
116
+ dragDistance: dragDistance.value,
113
117
  isDragging: isDragging.value,
114
118
  showDragging: showDragging.value,
115
119
  draggingFrameId: frameDragFrameId.value,
@@ -128,10 +132,13 @@ export function useFrames(
128
132
  let controller: AbortController
129
133
 
130
134
  function resetState(): void {
135
+ dragStartPoint = undefined
136
+
131
137
  draggingEdges.value = []
132
138
  draggingIntersection.value = undefined
133
139
  isDragging.value = false
134
140
  dragPoint.value = undefined
141
+ dragDistance.value = -1
135
142
  touchingFrames.value = []
136
143
  frameDragFrameId.value = undefined
137
144
  dragDirStore.reset()
@@ -154,6 +161,7 @@ export function useFrames(
154
161
 
155
162
  const point = toWindowCoord(win.value, e)
156
163
  dragPoint.value = point
164
+ dragStartPoint = { ...point }
157
165
  dragDirStore.update(point)
158
166
 
159
167
  isDragging.value = type
@@ -205,6 +213,13 @@ export function useFrames(
205
213
  const point = toWindowCoord(win.value, e)
206
214
  const didChange = dragDirStore.update(point)
207
215
  dragPoint.value = point
216
+ // at least one drag move has happened
217
+ dragDistance.value = 0
218
+ if (dragStartPoint) {
219
+ dragDistance.value = Math.sqrt(
220
+ Math.pow(point.x - dragStartPoint.x, 2) + Math.pow(point.y - dragStartPoint.y, 2)
221
+ )
222
+ }
208
223
  if (!didChange) return
209
224
  const res = handler.onDragChange("move", e, state.value, forceRecalculateEdges, cancel)
210
225
 
@@ -13,6 +13,7 @@ export type CloseInfo = Exclude<ReturnType<typeof getCloseFrameInfo>, KnownError
13
13
 
14
14
  export class CloseAction implements IDragAction {
15
15
  name = "close" as const
16
+ minDragDistance = 5
16
17
 
17
18
  state: {
18
19
  allowed: true
@@ -32,7 +33,7 @@ export class CloseAction implements IDragAction {
32
33
  textHints: { actions: string[], errors: string[] } = { actions: [], errors: [] }
33
34
 
34
35
  closeHints: {
35
- actions: string[]
36
+ actions: string[] | ((state: { force: boolean }) => string[])
36
37
  transformError: (e: KnownError) => string
37
38
  } = {
38
39
  actions: ["Hold Shift to Close", "Hold Ctrl+Shift to Force Close"],
@@ -62,12 +63,15 @@ export class CloseAction implements IDragAction {
62
63
  config?: {
63
64
  debug?: boolean | string
64
65
  closeHints?: Partial<CloseAction["closeHints"]>
66
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
67
+ minDragDistance?: number
65
68
  }
66
69
  ) {
67
70
  if (handleEvent !== undefined) this.handleEvent = handleEvent
68
71
  if (modifyDecos !== undefined) this.modifyDecos = modifyDecos
69
72
  this.hooks = hooks
70
73
  if (config?.debug) this.debug = true
74
+ if (config?.minDragDistance !== undefined) this.minDragDistance = config.minDragDistance
71
75
  this.reset()
72
76
  }
73
77
 
@@ -116,7 +120,9 @@ export class CloseAction implements IDragAction {
116
120
  this.textHints.actions = []
117
121
  this.textHints.errors = [this.closeHints.transformError(result)]
118
122
  } else {
119
- this.textHints.actions = this.closeHints.actions
123
+ this.textHints.actions = typeof this.closeHints.actions === "function"
124
+ ? this.closeHints.actions({ force: this.state.force })
125
+ : this.closeHints.actions
120
126
  this.textHints.errors = []
121
127
  }
122
128
  }
@@ -149,6 +155,9 @@ export class CloseAction implements IDragAction {
149
155
  _e: PointerEvent | undefined,
150
156
  state: DragState
151
157
  ): ActionDragChangeResult {
158
+ if (state.dragDistance <= this.minDragDistance) {
159
+ return { updateEdges: false, shapes: [] }
160
+ }
152
161
  const {
153
162
  touchingFramesArrays,
154
163
  dragDirections,
@@ -13,6 +13,9 @@ import type { KnownError } from "../utils/KnownError.js"
13
13
 
14
14
  export class FrameDragAction implements IDragAction {
15
15
  name = "frameDrag" as const
16
+
17
+ minDragDistance = 5
18
+
16
19
  state: {
17
20
  lastReturn?: LayoutChange<"split" | "swap" | "rearrange" | "dock"> | KnownError
18
21
  } = {
@@ -26,7 +29,7 @@ export class FrameDragAction implements IDragAction {
26
29
  = (_e: PointerEvent | KeyboardEvent, state: DragState) => state.isDragging === "frame"
27
30
 
28
31
  dragHints: {
29
- actions: { split: string, swap: string, rearrange: string, dock: string }
32
+ actions: { split?: string, swap?: string, rearrange?: string, dock?: string } | ((state: { type: "split" | "swap" | "rearrange" | "dock" }) => string[])
30
33
  transformError: (e: KnownError) => string
31
34
  } = {
32
35
  actions: {
@@ -52,12 +55,15 @@ export class FrameDragAction implements IDragAction {
52
55
  hooks: FrameDragAction["hooks"] = {},
53
56
  config?: {
54
57
  debug?: boolean | string
58
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
59
+ minDragDistance?: number
55
60
  }
56
61
  ) {
57
62
  if (handleEvent !== undefined) this.handleEvent = handleEvent
58
63
  if (modifyDecos !== undefined) this.modifyDecos = modifyDecos
59
64
  this.hooks = hooks
60
65
  if (config?.debug) this.debug = true
66
+ if (config?.minDragDistance !== undefined) this.minDragDistance = config.minDragDistance
61
67
 
62
68
  this.reset()
63
69
  }
@@ -76,8 +82,11 @@ export class FrameDragAction implements IDragAction {
76
82
  this.textHints.actions = []
77
83
  this.textHints.errors = [this.dragHints.transformError(result)]
78
84
  } else {
79
- const text = this.dragHints.actions[result.info]
80
- this.textHints.actions = [text]
85
+ this.textHints.actions = typeof this.dragHints.actions === "function"
86
+ ? this.dragHints.actions({ type: result.info })
87
+ : this.dragHints.actions[result.info] !== undefined
88
+ ? [this.dragHints.actions[result.info]!]
89
+ : []
81
90
  this.textHints.errors = []
82
91
  }
83
92
  }
@@ -146,6 +155,9 @@ export class FrameDragAction implements IDragAction {
146
155
  _e: PointerEvent | undefined,
147
156
  state: DragState
148
157
  ): ActionDragChangeResult {
158
+ if (state.dragDistance <= this.minDragDistance) {
159
+ return { updateEdges: false, shapes: [], showDragging: false }
160
+ }
149
161
  const { win, draggingFrameId, dragHoveredFrame } = state
150
162
  const matchedZone = getDragZones(state, settings.zoneSizes)
151
163
 
@@ -12,8 +12,11 @@ export type SplitInfo = Exclude<ReturnType<typeof getFrameSplitInfo>, KnownError
12
12
 
13
13
  // note that we dont annotateEdges because is possible to forexample drag from a window edge to a frame that can be split, moving over a collapsed frame that was sharing the edge but can't be split
14
14
 
15
+ export type DragChangeType = "start" | "move" | "end"
16
+
15
17
  export class SplitAction implements IDragAction {
16
18
  name = "split" as const
19
+ minDragDistance = 5
17
20
 
18
21
  state: {
19
22
  allowed: true
@@ -30,10 +33,10 @@ export class SplitAction implements IDragAction {
30
33
  debug: boolean | string = false
31
34
  textHints: { actions: string[], errors: string[] } = { actions: [], errors: [] }
32
35
  splitHints: {
33
- action: string
36
+ actions: string[] | ((type: DragChangeType) => string[])
34
37
  transformError: (e: KnownError) => string
35
38
  } = {
36
- action: "Hold Alt to Split",
39
+ actions: ["Hold Alt to Split"],
37
40
  transformError: e => e.message
38
41
  }
39
42
 
@@ -55,6 +58,8 @@ export class SplitAction implements IDragAction {
55
58
  config?: {
56
59
  debug?: boolean | string
57
60
  splitHints?: Partial<SplitAction["splitHints"]>
61
+ /** Minimum pixel distance the user must drag before the action is allowed (decos shown and action applied). Defaults to 5. */
62
+ minDragDistance?: number
58
63
  }
59
64
  ) {
60
65
  if (handleEvent !== undefined) this.handleEvent = handleEvent
@@ -63,7 +68,8 @@ export class SplitAction implements IDragAction {
63
68
  this.hooks = hooks
64
69
  this.reset()
65
70
  if (config?.debug) this.debug = true
66
- if (config?.splitHints?.action) this.splitHints.action = config.splitHints.action
71
+ if (config?.minDragDistance !== undefined) this.minDragDistance = config.minDragDistance
72
+ if (config?.splitHints?.actions) this.splitHints.actions = config.splitHints.actions
67
73
  if (config?.splitHints?.transformError) this.splitHints.transformError = config.splitHints.transformError
68
74
  }
69
75
 
@@ -104,7 +110,8 @@ export class SplitAction implements IDragAction {
104
110
  canHandleRequest(e: PointerEvent | KeyboardEvent, state: DragState): boolean {
105
111
  const { draggingEdges } = state
106
112
  if (draggingEdges.length !== 1) return false
107
- this.setTextHints(state.isDragging === "edge" ? true : undefined)
113
+ // hint should not be shown when dragging from window edge but we should still handle the event
114
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? true : undefined)
108
115
  if (this.handleEvent(e, state)) {
109
116
  this.hooks.onStart?.()
110
117
  return true
@@ -121,7 +128,10 @@ export class SplitAction implements IDragAction {
121
128
  this.textHints.actions = []
122
129
  this.textHints.errors = [this.splitHints.transformError(result)]
123
130
  } else {
124
- this.textHints.actions = [this.splitHints.action]
131
+ this.textHints.actions = typeof this.splitHints.actions === "function"
132
+ ? this.splitHints.actions("move")
133
+ : this.splitHints.actions
134
+
125
135
  this.textHints.errors = []
126
136
  }
127
137
  }
@@ -149,7 +159,7 @@ export class SplitAction implements IDragAction {
149
159
  dragDirections[oppositeOrientation]!,
150
160
  dragPoint!
151
161
  )
152
- this.setTextHints(canSplit)
162
+ this.setTextHints(state.isDragging === "edge" && !state.isDraggingFromWindowEdge ? canSplit : undefined)
153
163
  if (!(canSplit instanceof Error)) {
154
164
  this.state.allowed = true
155
165
  this.state.res = canSplit
@@ -168,7 +178,10 @@ export class SplitAction implements IDragAction {
168
178
  _e: PointerEvent | undefined,
169
179
  state: DragState
170
180
  ): ActionDragChangeResult {
171
- const { dragHoveredFrame } = state
181
+ const { dragHoveredFrame, dragDistance } = state
182
+ if (dragDistance <= this.minDragDistance) {
183
+ return { updateEdges: false, shapes: [], showDragging: false }
184
+ }
172
185
  let ok = false
173
186
  if (dragHoveredFrame) {
174
187
  if (this.state.lastPoint?.x === state.dragPoint?.x && this.state.lastPoint?.y === state.dragPoint?.y && this.state.lastReturn) {
@@ -11,7 +11,7 @@ export class Settings {
11
11
  resetToDefaults() {
12
12
  this.scale = 3
13
13
  this.snapPoint = 0.5
14
- this.minSize = 5
14
+ this.minSize = 3
15
15
  this.collapseSizePx = 15
16
16
  this.maxPerpendicularLength = 20
17
17
  this.zoneSizes = { frameEdgePx: 40, windowEdgePx: 20 }
@@ -442,6 +442,8 @@ export type DragState = {
442
442
  dragDirections: Record<Orientation, Direction | undefined>
443
443
  /** The curren point (in scaled window coordinates) the user is dragging at. */
444
444
  dragPoint?: Point
445
+ /** Cumulative pixel distance the user has dragged from the initial click point. 0 on start, increases on each move. */
446
+ dragDistance: number
445
447
  /** Whether the user is currently dragging and what type of drag. Is truthy string during all drag events. */
446
448
  isDragging: boolean | "frame" | "edge"
447
449
  /** Whether to show the moved frames while dragging. */