@window-splitter/interface 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tshy/build.json +8 -0
- package/.tshy/commonjs.json +18 -0
- package/.tshy/esm.json +17 -0
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint.log +5 -0
- package/CHANGELOG.md +44 -0
- package/README.md +3 -0
- package/dist/commonjs/index.d.ts +4 -0
- package/dist/commonjs/index.d.ts.map +1 -0
- package/dist/commonjs/index.js +20 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/interface.d.ts +128 -0
- package/dist/commonjs/interface.d.ts.map +1 -0
- package/dist/commonjs/interface.js +67 -0
- package/dist/commonjs/interface.js.map +1 -0
- package/dist/commonjs/mergeAttributes.d.ts +19 -0
- package/dist/commonjs/mergeAttributes.d.ts.map +1 -0
- package/dist/commonjs/mergeAttributes.js +63 -0
- package/dist/commonjs/mergeAttributes.js.map +1 -0
- package/dist/commonjs/move.d.ts +52 -0
- package/dist/commonjs/move.d.ts.map +1 -0
- package/dist/commonjs/move.js +124 -0
- package/dist/commonjs/move.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/test.d.ts +18 -0
- package/dist/commonjs/test.d.ts.map +1 -0
- package/dist/commonjs/test.js +85 -0
- package/dist/commonjs/test.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interface.d.ts +128 -0
- package/dist/esm/interface.d.ts.map +1 -0
- package/dist/esm/interface.js +62 -0
- package/dist/esm/interface.js.map +1 -0
- package/dist/esm/mergeAttributes.d.ts +19 -0
- package/dist/esm/mergeAttributes.d.ts.map +1 -0
- package/dist/esm/mergeAttributes.js +60 -0
- package/dist/esm/mergeAttributes.js.map +1 -0
- package/dist/esm/move.d.ts +52 -0
- package/dist/esm/move.d.ts.map +1 -0
- package/dist/esm/move.js +121 -0
- package/dist/esm/move.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/test.d.ts +18 -0
- package/dist/esm/test.d.ts.map +1 -0
- package/dist/esm/test.js +81 -0
- package/dist/esm/test.js.map +1 -0
- package/eslint.config.js +3 -0
- package/package.json +83 -0
- package/src/index.ts +3 -0
- package/src/interface.ts +214 -0
- package/src/mergeAttributes.ts +84 -0
- package/src/move.ts +210 -0
- package/src/test.ts +133 -0
- package/tsconfig.json +6 -0
package/src/move.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// import {disableTextSelection, restoreTextSelection} from './textSelection';
|
|
2
|
+
|
|
3
|
+
export interface MoveResult {
|
|
4
|
+
/** Props to spread on the target element. */
|
|
5
|
+
moveProps: {
|
|
6
|
+
onPointerDown: (e: PointerEvent) => void;
|
|
7
|
+
onKeyDown: (e: KeyboardEvent) => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface EventBase {
|
|
12
|
+
shiftKey: boolean;
|
|
13
|
+
ctrlKey: boolean;
|
|
14
|
+
metaKey: boolean;
|
|
15
|
+
altKey: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual";
|
|
19
|
+
|
|
20
|
+
interface BaseMoveEvent {
|
|
21
|
+
/** The pointer type that triggered the move event. */
|
|
22
|
+
pointerType: PointerType;
|
|
23
|
+
/** Whether the shift keyboard modifier was held during the move event. */
|
|
24
|
+
shiftKey: boolean;
|
|
25
|
+
/** Whether the ctrl keyboard modifier was held during the move event. */
|
|
26
|
+
ctrlKey: boolean;
|
|
27
|
+
/** Whether the meta keyboard modifier was held during the move event. */
|
|
28
|
+
metaKey: boolean;
|
|
29
|
+
/** Whether the alt keyboard modifier was held during the move event. */
|
|
30
|
+
altKey: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface MoveStartEvent extends BaseMoveEvent {
|
|
34
|
+
/** The type of move event being fired. */
|
|
35
|
+
type: "movestart";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MoveMoveEvent extends BaseMoveEvent {
|
|
39
|
+
/** The type of move event being fired. */
|
|
40
|
+
type: "move";
|
|
41
|
+
/** The amount moved in the X direction since the last event. */
|
|
42
|
+
deltaX: number;
|
|
43
|
+
/** The amount moved in the Y direction since the last event. */
|
|
44
|
+
deltaY: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface MoveEndEvent extends BaseMoveEvent {
|
|
48
|
+
/** The type of move event being fired. */
|
|
49
|
+
type: "moveend";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MoveEvents {
|
|
53
|
+
/** Handler that is called when a move interaction starts. */
|
|
54
|
+
onMoveStart?: (e: MoveStartEvent) => void;
|
|
55
|
+
/** Handler that is called when the element is moved. */
|
|
56
|
+
onMove?: (e: MoveMoveEvent) => void;
|
|
57
|
+
/** Handler that is called when a move interaction ends. */
|
|
58
|
+
onMoveEnd?: (e: MoveEndEvent) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handles move interactions across mouse, touch, and keyboard, including dragging with
|
|
63
|
+
* the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and
|
|
64
|
+
* platforms, and ignores emulated mouse events on touch devices.
|
|
65
|
+
*/
|
|
66
|
+
export function move(props: MoveEvents): MoveResult {
|
|
67
|
+
const { onMoveStart, onMove: onMoveProp, onMoveEnd } = props;
|
|
68
|
+
|
|
69
|
+
const state = { didMove: false, lastPosition: null, id: null } as {
|
|
70
|
+
didMove: boolean;
|
|
71
|
+
lastPosition: { pageX: number; pageY: number } | null;
|
|
72
|
+
id: number | null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const onMove = (
|
|
76
|
+
originalEvent: EventBase,
|
|
77
|
+
pointerType: PointerType,
|
|
78
|
+
deltaX: number,
|
|
79
|
+
deltaY: number
|
|
80
|
+
) => {
|
|
81
|
+
if (deltaX === 0 && deltaY === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!state.didMove) {
|
|
86
|
+
state.didMove = true;
|
|
87
|
+
onMoveStart?.({
|
|
88
|
+
type: "movestart",
|
|
89
|
+
pointerType,
|
|
90
|
+
shiftKey: originalEvent.shiftKey,
|
|
91
|
+
metaKey: originalEvent.metaKey,
|
|
92
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
93
|
+
altKey: originalEvent.altKey,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
onMoveProp?.({
|
|
97
|
+
type: "move",
|
|
98
|
+
pointerType,
|
|
99
|
+
deltaX: deltaX,
|
|
100
|
+
deltaY: deltaY,
|
|
101
|
+
shiftKey: originalEvent.shiftKey,
|
|
102
|
+
metaKey: originalEvent.metaKey,
|
|
103
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
104
|
+
altKey: originalEvent.altKey,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const end = (originalEvent: EventBase, pointerType: PointerType) => {
|
|
109
|
+
// restoreTextSelection();
|
|
110
|
+
if (state.didMove) {
|
|
111
|
+
onMoveEnd?.({
|
|
112
|
+
type: "moveend",
|
|
113
|
+
pointerType,
|
|
114
|
+
shiftKey: originalEvent.shiftKey,
|
|
115
|
+
metaKey: originalEvent.metaKey,
|
|
116
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
117
|
+
altKey: originalEvent.altKey,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const moveProps: Partial<MoveResult["moveProps"]> = {};
|
|
123
|
+
|
|
124
|
+
const start = () => {
|
|
125
|
+
// disableTextSelection();
|
|
126
|
+
state.didMove = false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
130
|
+
if (e.pointerId === state.id) {
|
|
131
|
+
const pointerType = (e.pointerType || "mouse") as PointerType;
|
|
132
|
+
|
|
133
|
+
// Problems with PointerEvent#movementX/movementY:
|
|
134
|
+
// 1. it is always 0 on macOS Safari.
|
|
135
|
+
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
|
|
136
|
+
onMove(
|
|
137
|
+
e,
|
|
138
|
+
pointerType,
|
|
139
|
+
e.pageX - (state.lastPosition?.pageX ?? 0),
|
|
140
|
+
e.pageY - (state.lastPosition?.pageY ?? 0)
|
|
141
|
+
);
|
|
142
|
+
state.lastPosition = { pageX: e.pageX, pageY: e.pageY };
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
147
|
+
if (e.pointerId === state.id) {
|
|
148
|
+
const pointerType = (e.pointerType || "mouse") as PointerType;
|
|
149
|
+
end(e, pointerType);
|
|
150
|
+
state.id = null;
|
|
151
|
+
window.removeEventListener("pointermove", onPointerMove, false);
|
|
152
|
+
window.removeEventListener("pointerup", onPointerUp, false);
|
|
153
|
+
window.removeEventListener("pointercancel", onPointerUp, false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
moveProps.onPointerDown = (e) => {
|
|
158
|
+
if (e.button === 0 && state.id == null) {
|
|
159
|
+
start();
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
state.lastPosition = { pageX: e.pageX, pageY: e.pageY };
|
|
163
|
+
state.id = e.pointerId;
|
|
164
|
+
window.addEventListener("pointermove", onPointerMove, false);
|
|
165
|
+
window.addEventListener("pointerup", onPointerUp, false);
|
|
166
|
+
window.addEventListener("pointercancel", onPointerUp, false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const triggerKeyboardMove = (
|
|
171
|
+
e: EventBase,
|
|
172
|
+
deltaX: number,
|
|
173
|
+
deltaY: number
|
|
174
|
+
) => {
|
|
175
|
+
start();
|
|
176
|
+
onMove(e, "keyboard", deltaX, deltaY);
|
|
177
|
+
end(e, "keyboard");
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
moveProps.onKeyDown = (e) => {
|
|
181
|
+
switch (e.key) {
|
|
182
|
+
case "Left":
|
|
183
|
+
case "ArrowLeft":
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
triggerKeyboardMove(e, -1, 0);
|
|
187
|
+
break;
|
|
188
|
+
case "Right":
|
|
189
|
+
case "ArrowRight":
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
triggerKeyboardMove(e, 1, 0);
|
|
193
|
+
break;
|
|
194
|
+
case "Up":
|
|
195
|
+
case "ArrowUp":
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
triggerKeyboardMove(e, 0, -1);
|
|
199
|
+
break;
|
|
200
|
+
case "Down":
|
|
201
|
+
case "ArrowDown":
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
triggerKeyboardMove(e, 0, 1);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return { moveProps: moveProps as MoveResult["moveProps"] };
|
|
210
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
import { PanelGroupHandle } from "./interface.js";
|
|
3
|
+
|
|
4
|
+
export async function dragHandle(options: {
|
|
5
|
+
handleId?: string;
|
|
6
|
+
delta: number;
|
|
7
|
+
orientation?: "horizontal" | "vertical";
|
|
8
|
+
}) {
|
|
9
|
+
const orientation = options.orientation || "horizontal";
|
|
10
|
+
const handle = document.querySelector(
|
|
11
|
+
options.handleId
|
|
12
|
+
? `[data-splitter-id="${options.handleId}"]`
|
|
13
|
+
: '[data-splitter-type="handle"]'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!handle) {
|
|
17
|
+
throw new Error(`Handle not found: ${options.handleId}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rect = handle.getBoundingClientRect();
|
|
21
|
+
const x = rect.x + rect.width / 2;
|
|
22
|
+
const y = rect.y + rect.height / 2;
|
|
23
|
+
const step = options.delta > 0 ? 1 : -1;
|
|
24
|
+
const pointerId = 1;
|
|
25
|
+
|
|
26
|
+
handle.dispatchEvent(
|
|
27
|
+
new PointerEvent("pointerdown", {
|
|
28
|
+
bubbles: true,
|
|
29
|
+
clientX: x,
|
|
30
|
+
clientY: y,
|
|
31
|
+
pointerId,
|
|
32
|
+
button: 0,
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < Math.abs(options.delta - 1); i++) {
|
|
37
|
+
handle.dispatchEvent(
|
|
38
|
+
new PointerEvent("pointermove", {
|
|
39
|
+
bubbles: true,
|
|
40
|
+
pointerId,
|
|
41
|
+
clientX: orientation === "horizontal" ? x + i * step : x,
|
|
42
|
+
clientY: orientation === "vertical" ? y + i * step : y,
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
handle.dispatchEvent(
|
|
49
|
+
new PointerEvent("pointerup", {
|
|
50
|
+
bubbles: true,
|
|
51
|
+
pointerId,
|
|
52
|
+
clientX: orientation === "horizontal" ? x + options.delta : x,
|
|
53
|
+
clientY: orientation === "vertical" ? y + options.delta : y,
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type WaitForFn = <T>(
|
|
59
|
+
callback: () => T | Promise<T>,
|
|
60
|
+
options?: {
|
|
61
|
+
timeout?: number;
|
|
62
|
+
}
|
|
63
|
+
) => Promise<T>;
|
|
64
|
+
|
|
65
|
+
function waitForCondition(waitFor: WaitForFn, condition: () => boolean) {
|
|
66
|
+
const stack = new Error().stack;
|
|
67
|
+
|
|
68
|
+
return waitFor(
|
|
69
|
+
() => {
|
|
70
|
+
if (!condition()) {
|
|
71
|
+
const error = new Error("Not ready");
|
|
72
|
+
error.stack = stack;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
timeout: 10_000,
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function waitForMeasurement(
|
|
83
|
+
waitFor: WaitForFn,
|
|
84
|
+
handle: PanelGroupHandle
|
|
85
|
+
) {
|
|
86
|
+
await waitForCondition(
|
|
87
|
+
waitFor,
|
|
88
|
+
() => !handle.getTemplate().includes("minmax")
|
|
89
|
+
);
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function expectTemplate(
|
|
94
|
+
waitFor: WaitForFn,
|
|
95
|
+
handle: PanelGroupHandle,
|
|
96
|
+
resolvedTemplate: string
|
|
97
|
+
) {
|
|
98
|
+
const stack = new Error().stack;
|
|
99
|
+
let template;
|
|
100
|
+
|
|
101
|
+
await waitFor(
|
|
102
|
+
() => {
|
|
103
|
+
template = handle.getTemplate();
|
|
104
|
+
|
|
105
|
+
if (!template.includes(resolvedTemplate)) {
|
|
106
|
+
const e = new Error(
|
|
107
|
+
`\nExpected: ${resolvedTemplate}\nGot : ${template}`
|
|
108
|
+
);
|
|
109
|
+
e.stack = stack;
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (handle.getState() !== "idle") {
|
|
114
|
+
const e = new Error(`\nExpected: idle\nGot : ${handle.getState()}`);
|
|
115
|
+
e.stack = stack;
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
timeout: 2_000,
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(template).toBe(resolvedTemplate);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createTestUtils(options: { waitFor: WaitForFn }) {
|
|
128
|
+
return {
|
|
129
|
+
waitForCondition: waitForCondition.bind(null, options.waitFor),
|
|
130
|
+
waitForMeasurement: waitForMeasurement.bind(null, options.waitFor),
|
|
131
|
+
expectTemplate: expectTemplate.bind(null, options.waitFor),
|
|
132
|
+
};
|
|
133
|
+
}
|