gesturx 0.0.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/.github/workflows/publish.yml +17 -0
- package/.prettierrc.json +1 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/eslint.config.js +9 -0
- package/index.html +13 -0
- package/package.json +32 -0
- package/public/vite.svg +1 -0
- package/src/core/manager.ts +45 -0
- package/src/core/tapRecognizer.test.ts +542 -0
- package/src/core/tapRecognizer.ts +143 -0
- package/src/core/types.ts +3 -0
- package/src/core/utils.ts +211 -0
- package/src/main.ts +19 -0
- package/src/style.css +29 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
branches: main
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
publish:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: actions/setup-node@v3
|
|
11
|
+
with:
|
|
12
|
+
node-version: "20"
|
|
13
|
+
- run: npm ci
|
|
14
|
+
- run: npm test
|
|
15
|
+
- uses: JS-DevTools/npm-publish@v3
|
|
16
|
+
with:
|
|
17
|
+
token: ${{ secrets.NPM_TOKEN }}
|
package/.prettierrc.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Maxime
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/eslint.config.js
ADDED
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>gesturx</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gesturx",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "prettier --write . && eslint --fix .",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"coverage": "vitest run --coverage"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@eslint/js": "^9.0.0",
|
|
16
|
+
"@types/node": "^20.12.7",
|
|
17
|
+
"@vitest/coverage-v8": "^1.5.0",
|
|
18
|
+
"eslint": "^8.57.0",
|
|
19
|
+
"happy-dom": "^14.7.1",
|
|
20
|
+
"prettier": "3.2.5",
|
|
21
|
+
"rollup-plugin-node-externals": "^7.1.1",
|
|
22
|
+
"typescript": "^5.4.5",
|
|
23
|
+
"typescript-eslint": "^7.7.0",
|
|
24
|
+
"vite": "^5.2.0",
|
|
25
|
+
"vite-plugin-dts": "^3.8.3",
|
|
26
|
+
"vite-plugin-eslint": "^1.8.1",
|
|
27
|
+
"vitest": "^1.5.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"rxjs": "^7.8.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { map, merge } from "rxjs";
|
|
2
|
+
import { createTapRecognizer } from "./tapRecognizer";
|
|
3
|
+
import { getCenterFromEvent } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const createManager = ({
|
|
6
|
+
container,
|
|
7
|
+
afterEventReceived = (event) => event,
|
|
8
|
+
recognizers,
|
|
9
|
+
}: {
|
|
10
|
+
container: HTMLElement;
|
|
11
|
+
recognizers: ReturnType<typeof createTapRecognizer>[];
|
|
12
|
+
afterEventReceived?: (event: PointerEvent) => PointerEvent;
|
|
13
|
+
}) => {
|
|
14
|
+
// user api
|
|
15
|
+
// const dragRecognizer = createPanRecognizer();
|
|
16
|
+
// const singleTapRecognizer = createTapRecognizer({
|
|
17
|
+
// maxTaps: 2,
|
|
18
|
+
// failWith: [dragRecognizer],
|
|
19
|
+
// });
|
|
20
|
+
// const pinchRecognizer = createPinchRecognizer();
|
|
21
|
+
|
|
22
|
+
// internal api
|
|
23
|
+
// dragRecognizer.initialize({ container, afterEventReceived });
|
|
24
|
+
|
|
25
|
+
// const recognizers = [
|
|
26
|
+
// singleTapRecognizer({ container, afterEventReceived }),
|
|
27
|
+
// // pinchRecognizer({ container, afterEventReceived }),
|
|
28
|
+
// // dragRecognizer,
|
|
29
|
+
// ]
|
|
30
|
+
|
|
31
|
+
const recognizerInstances = recognizers.map((recognizer) =>
|
|
32
|
+
recognizer({ container, afterEventReceived }),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const events$ = merge(
|
|
36
|
+
...recognizerInstances.map((recognizer) => recognizer.events$),
|
|
37
|
+
).pipe(
|
|
38
|
+
map((event) => ({
|
|
39
|
+
...event,
|
|
40
|
+
center: getCenterFromEvent(event.srcEvent),
|
|
41
|
+
})),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return { events$ };
|
|
45
|
+
};
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTapRecognizer } from "./tapRecognizer";
|
|
3
|
+
import { buffer, first, lastValueFrom, tap, timer } from "rxjs";
|
|
4
|
+
|
|
5
|
+
const waitFor = (time: number) =>
|
|
6
|
+
new Promise((resolve) => setTimeout(resolve, time));
|
|
7
|
+
|
|
8
|
+
describe("TapGestureRecognizer", () => {
|
|
9
|
+
let container = document.createElement("div");
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
container = document.createElement("div");
|
|
13
|
+
document.body.appendChild(container);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function createPointerEvent({
|
|
17
|
+
type,
|
|
18
|
+
x,
|
|
19
|
+
y,
|
|
20
|
+
identifier,
|
|
21
|
+
}: {
|
|
22
|
+
type: string;
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
identifier: number;
|
|
26
|
+
}) {
|
|
27
|
+
const event = new PointerEvent(type, {
|
|
28
|
+
clientX: x,
|
|
29
|
+
clientY: y,
|
|
30
|
+
pointerId: identifier,
|
|
31
|
+
pointerType: "touch",
|
|
32
|
+
bubbles: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return event;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sendPointerEvent({
|
|
39
|
+
type,
|
|
40
|
+
x,
|
|
41
|
+
y,
|
|
42
|
+
container,
|
|
43
|
+
identifier,
|
|
44
|
+
}: {
|
|
45
|
+
container: HTMLElement;
|
|
46
|
+
type: string;
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
identifier: number;
|
|
50
|
+
}) {
|
|
51
|
+
const event = createPointerEvent({
|
|
52
|
+
type,
|
|
53
|
+
x,
|
|
54
|
+
y,
|
|
55
|
+
identifier,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
sendPointerEventFromEvent({ event, container });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sendPointerEventFromEvent({
|
|
62
|
+
event,
|
|
63
|
+
container,
|
|
64
|
+
}: {
|
|
65
|
+
event: PointerEvent;
|
|
66
|
+
container: HTMLElement;
|
|
67
|
+
}) {
|
|
68
|
+
container.dispatchEvent(event);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
it("should detect a single tap", async () => {
|
|
72
|
+
const multiTapThreshold = 5;
|
|
73
|
+
const waitLongEnough$ = timer(multiTapThreshold * 2);
|
|
74
|
+
const recognizer = createTapRecognizer({ multiTapThreshold })({
|
|
75
|
+
container,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const values = await lastValueFrom(
|
|
79
|
+
recognizer.events$.pipe(
|
|
80
|
+
tap({
|
|
81
|
+
subscribe: async () => {
|
|
82
|
+
await waitFor(1);
|
|
83
|
+
|
|
84
|
+
sendPointerEvent({
|
|
85
|
+
container,
|
|
86
|
+
identifier: 1,
|
|
87
|
+
type: "pointerdown",
|
|
88
|
+
x: 0,
|
|
89
|
+
y: 0,
|
|
90
|
+
});
|
|
91
|
+
sendPointerEvent({
|
|
92
|
+
container,
|
|
93
|
+
identifier: 1,
|
|
94
|
+
type: "pointerup",
|
|
95
|
+
x: 0,
|
|
96
|
+
y: 0,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
buffer(waitLongEnough$),
|
|
101
|
+
first(),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(values).toMatchObject([{ type: "tap", taps: 1 }]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should detect a double tap", async () => {
|
|
109
|
+
const multiTapThreshold = 5;
|
|
110
|
+
const waitLongEnough$ = timer(multiTapThreshold * 2);
|
|
111
|
+
const recognizer = createTapRecognizer({ multiTapThreshold, maxTaps: 2 })({
|
|
112
|
+
container,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const values = await lastValueFrom(
|
|
116
|
+
recognizer.events$.pipe(
|
|
117
|
+
tap({
|
|
118
|
+
subscribe: async () => {
|
|
119
|
+
await waitFor(1);
|
|
120
|
+
|
|
121
|
+
sendPointerEvent({
|
|
122
|
+
container,
|
|
123
|
+
identifier: 1,
|
|
124
|
+
type: "pointerdown",
|
|
125
|
+
x: 0,
|
|
126
|
+
y: 0,
|
|
127
|
+
});
|
|
128
|
+
sendPointerEvent({
|
|
129
|
+
container,
|
|
130
|
+
identifier: 1,
|
|
131
|
+
type: "pointerup",
|
|
132
|
+
x: 0,
|
|
133
|
+
y: 0,
|
|
134
|
+
});
|
|
135
|
+
sendPointerEvent({
|
|
136
|
+
container,
|
|
137
|
+
identifier: 2,
|
|
138
|
+
type: "pointerdown",
|
|
139
|
+
x: 0,
|
|
140
|
+
y: 0,
|
|
141
|
+
});
|
|
142
|
+
sendPointerEvent({
|
|
143
|
+
container,
|
|
144
|
+
identifier: 2,
|
|
145
|
+
type: "pointerup",
|
|
146
|
+
x: 0,
|
|
147
|
+
y: 0,
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
buffer(waitLongEnough$),
|
|
152
|
+
first(),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(values).toMatchObject([{ type: "tap", taps: 2 }]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should detect a triple tap", async () => {
|
|
160
|
+
const multiTapThreshold = 5;
|
|
161
|
+
const recognizer = createTapRecognizer({ multiTapThreshold, maxTaps: 3 })({
|
|
162
|
+
container,
|
|
163
|
+
});
|
|
164
|
+
const waitLongEnough$ = timer(100);
|
|
165
|
+
|
|
166
|
+
const values = await lastValueFrom(
|
|
167
|
+
recognizer.events$.pipe(
|
|
168
|
+
tap({
|
|
169
|
+
subscribe: () => {
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
sendPointerEvent({
|
|
172
|
+
container,
|
|
173
|
+
identifier: 1,
|
|
174
|
+
type: "pointerdown",
|
|
175
|
+
x: 0,
|
|
176
|
+
y: 0,
|
|
177
|
+
});
|
|
178
|
+
sendPointerEvent({
|
|
179
|
+
container,
|
|
180
|
+
identifier: 1,
|
|
181
|
+
type: "pointerup",
|
|
182
|
+
x: 0,
|
|
183
|
+
y: 0,
|
|
184
|
+
});
|
|
185
|
+
sendPointerEvent({
|
|
186
|
+
container,
|
|
187
|
+
identifier: 1,
|
|
188
|
+
type: "pointerdown",
|
|
189
|
+
x: 0,
|
|
190
|
+
y: 0,
|
|
191
|
+
});
|
|
192
|
+
sendPointerEvent({
|
|
193
|
+
container,
|
|
194
|
+
identifier: 1,
|
|
195
|
+
type: "pointerup",
|
|
196
|
+
x: 0,
|
|
197
|
+
y: 0,
|
|
198
|
+
});
|
|
199
|
+
sendPointerEvent({
|
|
200
|
+
container,
|
|
201
|
+
identifier: 1,
|
|
202
|
+
type: "pointerdown",
|
|
203
|
+
x: 0,
|
|
204
|
+
y: 0,
|
|
205
|
+
});
|
|
206
|
+
sendPointerEvent({
|
|
207
|
+
container,
|
|
208
|
+
identifier: 1,
|
|
209
|
+
type: "pointerup",
|
|
210
|
+
x: 0,
|
|
211
|
+
y: 0,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
buffer(waitLongEnough$),
|
|
217
|
+
first(),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(values).toMatchObject([{ type: "tap", taps: 3 }]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should not detect a double tap if the second tap is after threshold", async () => {
|
|
225
|
+
const multiTapThreshold = 5;
|
|
226
|
+
const waitLongEnough$ = timer(100);
|
|
227
|
+
const recognizer = createTapRecognizer({ multiTapThreshold, maxTaps: 2 })({
|
|
228
|
+
container,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const values = await lastValueFrom(
|
|
232
|
+
recognizer.events$.pipe(
|
|
233
|
+
tap({
|
|
234
|
+
subscribe: async () => {
|
|
235
|
+
await waitFor(1);
|
|
236
|
+
|
|
237
|
+
sendPointerEvent({
|
|
238
|
+
container,
|
|
239
|
+
identifier: 1,
|
|
240
|
+
type: "pointerdown",
|
|
241
|
+
x: 0,
|
|
242
|
+
y: 0,
|
|
243
|
+
});
|
|
244
|
+
sendPointerEvent({
|
|
245
|
+
container,
|
|
246
|
+
identifier: 1,
|
|
247
|
+
type: "pointerup",
|
|
248
|
+
x: 0,
|
|
249
|
+
y: 0,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await waitFor(multiTapThreshold);
|
|
253
|
+
|
|
254
|
+
sendPointerEvent({
|
|
255
|
+
container,
|
|
256
|
+
identifier: 2,
|
|
257
|
+
type: "pointerdown",
|
|
258
|
+
x: 0,
|
|
259
|
+
y: 0,
|
|
260
|
+
});
|
|
261
|
+
sendPointerEvent({
|
|
262
|
+
container,
|
|
263
|
+
identifier: 2,
|
|
264
|
+
type: "pointerup",
|
|
265
|
+
x: 0,
|
|
266
|
+
y: 0,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
buffer(waitLongEnough$),
|
|
271
|
+
first(),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(values.length).toBe(2);
|
|
276
|
+
expect(values).toMatchObject([
|
|
277
|
+
{ type: "tap", taps: 1, srcEvent: { pointerId: 1 } },
|
|
278
|
+
{ type: "tap", taps: 1, srcEvent: { pointerId: 2 } },
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should not trigger tap if moved too much between down and up", async () => {
|
|
283
|
+
const multiTapThreshold = 5;
|
|
284
|
+
const waitLongEnough$ = timer(100);
|
|
285
|
+
const recognizer = createTapRecognizer({
|
|
286
|
+
multiTapThreshold,
|
|
287
|
+
posThreshold: 5,
|
|
288
|
+
})({ container });
|
|
289
|
+
|
|
290
|
+
const values = await lastValueFrom(
|
|
291
|
+
recognizer.events$.pipe(
|
|
292
|
+
tap({
|
|
293
|
+
subscribe: async () => {
|
|
294
|
+
await waitFor(1);
|
|
295
|
+
|
|
296
|
+
sendPointerEvent({
|
|
297
|
+
container,
|
|
298
|
+
identifier: 1,
|
|
299
|
+
type: "pointerdown",
|
|
300
|
+
x: 0,
|
|
301
|
+
y: 0,
|
|
302
|
+
});
|
|
303
|
+
sendPointerEvent({
|
|
304
|
+
container,
|
|
305
|
+
identifier: 1,
|
|
306
|
+
type: "pointerup",
|
|
307
|
+
x: 50,
|
|
308
|
+
y: 50,
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
buffer(waitLongEnough$),
|
|
313
|
+
first(),
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(values.length).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should not detect a tap if the pointer is held down too long", async () => {
|
|
321
|
+
const multiTapThreshold = 5;
|
|
322
|
+
const maximumPressTime = 10;
|
|
323
|
+
const waitLongEnough$ = timer(100);
|
|
324
|
+
const recognizer = createTapRecognizer({
|
|
325
|
+
multiTapThreshold,
|
|
326
|
+
posThreshold: 5,
|
|
327
|
+
maximumPressTime,
|
|
328
|
+
})({ container });
|
|
329
|
+
|
|
330
|
+
const values = await lastValueFrom(
|
|
331
|
+
recognizer.events$.pipe(
|
|
332
|
+
tap({
|
|
333
|
+
subscribe: async () => {
|
|
334
|
+
await waitFor(1);
|
|
335
|
+
|
|
336
|
+
sendPointerEvent({
|
|
337
|
+
container,
|
|
338
|
+
identifier: 1,
|
|
339
|
+
type: "pointerdown",
|
|
340
|
+
x: 0,
|
|
341
|
+
y: 0,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await waitFor(maximumPressTime - 5);
|
|
345
|
+
|
|
346
|
+
sendPointerEvent({
|
|
347
|
+
container,
|
|
348
|
+
identifier: 1,
|
|
349
|
+
type: "pointerup",
|
|
350
|
+
x: 0,
|
|
351
|
+
y: 0,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await waitFor(multiTapThreshold * 2);
|
|
355
|
+
|
|
356
|
+
sendPointerEvent({
|
|
357
|
+
container,
|
|
358
|
+
identifier: 2,
|
|
359
|
+
type: "pointerdown",
|
|
360
|
+
x: 0,
|
|
361
|
+
y: 0,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await waitFor(maximumPressTime);
|
|
365
|
+
|
|
366
|
+
sendPointerEvent({
|
|
367
|
+
container,
|
|
368
|
+
identifier: 2,
|
|
369
|
+
type: "pointerup",
|
|
370
|
+
x: 0,
|
|
371
|
+
y: 0,
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
buffer(waitLongEnough$),
|
|
376
|
+
first(),
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(values.length).toBe(1);
|
|
381
|
+
expect(values).toMatchObject([
|
|
382
|
+
{ type: "tap", taps: 1, srcEvent: { pointerId: 1 } },
|
|
383
|
+
]);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should ignore taps when there are simultaneous pointers active", async () => {
|
|
387
|
+
const recognizer = createTapRecognizer({})({ container });
|
|
388
|
+
const waitLongEnough$ = timer(100);
|
|
389
|
+
|
|
390
|
+
const values = await lastValueFrom(
|
|
391
|
+
recognizer.events$.pipe(
|
|
392
|
+
tap({
|
|
393
|
+
subscribe: () => {
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
sendPointerEvent({
|
|
396
|
+
container,
|
|
397
|
+
identifier: 1,
|
|
398
|
+
type: "pointerdown",
|
|
399
|
+
x: 0,
|
|
400
|
+
y: 0,
|
|
401
|
+
});
|
|
402
|
+
sendPointerEvent({
|
|
403
|
+
container,
|
|
404
|
+
identifier: 2,
|
|
405
|
+
type: "pointerdown",
|
|
406
|
+
x: 10,
|
|
407
|
+
y: 10,
|
|
408
|
+
});
|
|
409
|
+
sendPointerEvent({
|
|
410
|
+
container,
|
|
411
|
+
identifier: 1,
|
|
412
|
+
type: "pointerup",
|
|
413
|
+
x: 0,
|
|
414
|
+
y: 0,
|
|
415
|
+
});
|
|
416
|
+
sendPointerEvent({
|
|
417
|
+
container,
|
|
418
|
+
identifier: 2,
|
|
419
|
+
type: "pointerup",
|
|
420
|
+
x: 10,
|
|
421
|
+
y: 10,
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
buffer(waitLongEnough$),
|
|
427
|
+
first(),
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(values.length).toBe(0); // Assuming the recognizer should ignore simultaneous taps
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("should ignore one tap if there is more than one tap", async () => {
|
|
435
|
+
const recognizer = createTapRecognizer({
|
|
436
|
+
multiTapThreshold: 10,
|
|
437
|
+
maxTaps: 1,
|
|
438
|
+
})({ container });
|
|
439
|
+
const waitLongEnough$ = timer(100);
|
|
440
|
+
|
|
441
|
+
const values = await lastValueFrom(
|
|
442
|
+
recognizer.events$.pipe(
|
|
443
|
+
tap({
|
|
444
|
+
subscribe: async () => {
|
|
445
|
+
await waitFor(1);
|
|
446
|
+
|
|
447
|
+
sendPointerEvent({
|
|
448
|
+
container,
|
|
449
|
+
identifier: 1,
|
|
450
|
+
type: "pointerdown",
|
|
451
|
+
x: 0,
|
|
452
|
+
y: 0,
|
|
453
|
+
});
|
|
454
|
+
sendPointerEvent({
|
|
455
|
+
container,
|
|
456
|
+
identifier: 1,
|
|
457
|
+
type: "pointerup",
|
|
458
|
+
x: 0,
|
|
459
|
+
y: 0,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
await waitFor(1);
|
|
463
|
+
|
|
464
|
+
sendPointerEvent({
|
|
465
|
+
container,
|
|
466
|
+
identifier: 2,
|
|
467
|
+
type: "pointerdown",
|
|
468
|
+
x: 0,
|
|
469
|
+
y: 0,
|
|
470
|
+
});
|
|
471
|
+
sendPointerEvent({
|
|
472
|
+
container,
|
|
473
|
+
identifier: 2,
|
|
474
|
+
type: "pointerup",
|
|
475
|
+
x: 0,
|
|
476
|
+
y: 0,
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
buffer(waitLongEnough$),
|
|
481
|
+
first(),
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(values.length).toBe(0);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should ignore taps when a second tap is outside of position threshold", async () => {
|
|
489
|
+
const recognizer = createTapRecognizer({
|
|
490
|
+
posThreshold: 5,
|
|
491
|
+
multiTapThreshold: 10,
|
|
492
|
+
maxTaps: Infinity,
|
|
493
|
+
})({ container });
|
|
494
|
+
const waitLongEnough$ = timer(100);
|
|
495
|
+
|
|
496
|
+
const values = await lastValueFrom(
|
|
497
|
+
recognizer.events$.pipe(
|
|
498
|
+
tap({
|
|
499
|
+
subscribe: async () => {
|
|
500
|
+
// await waitFor(1)
|
|
501
|
+
|
|
502
|
+
sendPointerEvent({
|
|
503
|
+
container,
|
|
504
|
+
identifier: 1,
|
|
505
|
+
type: "pointerdown",
|
|
506
|
+
x: 0,
|
|
507
|
+
y: 0,
|
|
508
|
+
});
|
|
509
|
+
sendPointerEvent({
|
|
510
|
+
container,
|
|
511
|
+
identifier: 1,
|
|
512
|
+
type: "pointerup",
|
|
513
|
+
x: 0,
|
|
514
|
+
y: 0,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// await waitFor(1)
|
|
518
|
+
|
|
519
|
+
sendPointerEvent({
|
|
520
|
+
container,
|
|
521
|
+
identifier: 2,
|
|
522
|
+
type: "pointerdown",
|
|
523
|
+
x: 10,
|
|
524
|
+
y: 10,
|
|
525
|
+
});
|
|
526
|
+
sendPointerEvent({
|
|
527
|
+
container,
|
|
528
|
+
identifier: 2,
|
|
529
|
+
type: "pointerup",
|
|
530
|
+
x: 10,
|
|
531
|
+
y: 10,
|
|
532
|
+
});
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
buffer(waitLongEnough$),
|
|
536
|
+
first(),
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(values.length).toBe(0);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Observable,
|
|
3
|
+
buffer,
|
|
4
|
+
debounceTime,
|
|
5
|
+
exhaustMap,
|
|
6
|
+
filter,
|
|
7
|
+
first,
|
|
8
|
+
ignoreElements,
|
|
9
|
+
map,
|
|
10
|
+
merge,
|
|
11
|
+
of,
|
|
12
|
+
share,
|
|
13
|
+
shareReplay,
|
|
14
|
+
switchMap,
|
|
15
|
+
takeUntil,
|
|
16
|
+
timer,
|
|
17
|
+
withLatestFrom,
|
|
18
|
+
} from "rxjs";
|
|
19
|
+
import {
|
|
20
|
+
fromFailWith,
|
|
21
|
+
fromPointerDown,
|
|
22
|
+
getPointerEvents,
|
|
23
|
+
hasAtLeastOneItem,
|
|
24
|
+
isOUtsidePosThreshold,
|
|
25
|
+
trackActivePointers,
|
|
26
|
+
} from "./utils";
|
|
27
|
+
import { GestureEvent } from "./types";
|
|
28
|
+
|
|
29
|
+
export type TapEvent = GestureEvent & { type: "tap"; taps: number };
|
|
30
|
+
|
|
31
|
+
type Options = {
|
|
32
|
+
// Maximum time in ms between multiple taps.
|
|
33
|
+
multiTapThreshold?: number;
|
|
34
|
+
// Maximum press time in ms.
|
|
35
|
+
maximumPressTime?: number;
|
|
36
|
+
maxTaps?: number;
|
|
37
|
+
posThreshold?: number;
|
|
38
|
+
failWith?: { start$: Observable<unknown> }[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mapToTapEvent = (
|
|
42
|
+
stream: Observable<[PointerEvent, ...PointerEvent[]]>,
|
|
43
|
+
): Observable<TapEvent> =>
|
|
44
|
+
stream.pipe(
|
|
45
|
+
map((events) => ({
|
|
46
|
+
type: "tap",
|
|
47
|
+
srcEvent: events[0],
|
|
48
|
+
taps: events.length,
|
|
49
|
+
})),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const createTapRecognizer = ({
|
|
53
|
+
multiTapThreshold = 200,
|
|
54
|
+
maximumPressTime = 250,
|
|
55
|
+
maxTaps = 1,
|
|
56
|
+
posThreshold = 5,
|
|
57
|
+
failWith,
|
|
58
|
+
}: Options = {}) => {
|
|
59
|
+
return ({
|
|
60
|
+
container,
|
|
61
|
+
afterEventReceived = (event) => event,
|
|
62
|
+
}: {
|
|
63
|
+
container: HTMLElement;
|
|
64
|
+
afterEventReceived?: (event: PointerEvent) => PointerEvent;
|
|
65
|
+
}) => {
|
|
66
|
+
const { pointerUp$, pointerLeave$, pointerCancel$, pointerMove$ } =
|
|
67
|
+
getPointerEvents({
|
|
68
|
+
container,
|
|
69
|
+
afterEventReceived,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const pointerDown$ = fromPointerDown({ container, afterEventReceived });
|
|
73
|
+
const activePointers$ = trackActivePointers({ container }).pipe(
|
|
74
|
+
shareReplay(1),
|
|
75
|
+
);
|
|
76
|
+
const hasMoreThanOneActivePointer$ = activePointers$.pipe(
|
|
77
|
+
filter((pointers) => pointers.length > 1),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const onlyOnePointer = <T>(stream: Observable<T>) =>
|
|
81
|
+
stream.pipe(
|
|
82
|
+
withLatestFrom(activePointers$),
|
|
83
|
+
filter(([, pointers]) => pointers.length === 1),
|
|
84
|
+
map(([pointer]) => pointer),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const bufferPointerDowns = (stream: Observable<PointerEvent>) =>
|
|
88
|
+
stream.pipe(
|
|
89
|
+
buffer(pointerUp$.pipe(debounceTime(multiTapThreshold))),
|
|
90
|
+
first(),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const failWith$ = fromFailWith(failWith);
|
|
94
|
+
|
|
95
|
+
const tap$ = merge(
|
|
96
|
+
pointerDown$,
|
|
97
|
+
activePointers$.pipe(ignoreElements()),
|
|
98
|
+
).pipe(
|
|
99
|
+
onlyOnePointer,
|
|
100
|
+
exhaustMap((initialPointerEvent) => {
|
|
101
|
+
const pointerDowns$ = merge(of(initialPointerEvent), pointerDown$);
|
|
102
|
+
const subsequentPointerEvents$ = merge(
|
|
103
|
+
pointerUp$,
|
|
104
|
+
pointerLeave$,
|
|
105
|
+
pointerCancel$,
|
|
106
|
+
pointerMove$,
|
|
107
|
+
);
|
|
108
|
+
const pointerDownsBuffered$ = pointerDowns$.pipe(bufferPointerDowns);
|
|
109
|
+
const subsequentPointersOutOfPositionThreshold$ =
|
|
110
|
+
subsequentPointerEvents$.pipe(
|
|
111
|
+
filter((event) =>
|
|
112
|
+
isOUtsidePosThreshold(initialPointerEvent, event, posThreshold),
|
|
113
|
+
),
|
|
114
|
+
first(),
|
|
115
|
+
);
|
|
116
|
+
const waitedTooLong$ = activePointers$.pipe(
|
|
117
|
+
switchMap(() => timer(maximumPressTime)),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const takeUntil$ = merge(
|
|
121
|
+
failWith$,
|
|
122
|
+
pointerCancel$,
|
|
123
|
+
pointerLeave$,
|
|
124
|
+
hasMoreThanOneActivePointer$,
|
|
125
|
+
subsequentPointersOutOfPositionThreshold$,
|
|
126
|
+
waitedTooLong$,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const tap$ = pointerDownsBuffered$.pipe(
|
|
130
|
+
filter(hasAtLeastOneItem),
|
|
131
|
+
filter((events) => events.length <= maxTaps),
|
|
132
|
+
mapToTapEvent,
|
|
133
|
+
takeUntil(takeUntil$),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return tap$;
|
|
137
|
+
}),
|
|
138
|
+
share(),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return { events$: tap$ };
|
|
142
|
+
};
|
|
143
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NEVER,
|
|
3
|
+
Observable,
|
|
4
|
+
defer,
|
|
5
|
+
endWith,
|
|
6
|
+
filter,
|
|
7
|
+
fromEvent,
|
|
8
|
+
map,
|
|
9
|
+
merge,
|
|
10
|
+
mergeMap,
|
|
11
|
+
of,
|
|
12
|
+
scan,
|
|
13
|
+
takeUntil,
|
|
14
|
+
} from "rxjs";
|
|
15
|
+
|
|
16
|
+
export function isDefined<T>(
|
|
17
|
+
arg: T | null | undefined,
|
|
18
|
+
): arg is T extends null | undefined ? never : T {
|
|
19
|
+
return arg !== null && arg !== undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const hasAtLeastOneItem = <T>(events: T[]): events is [T, ...T[]] =>
|
|
23
|
+
events.length > 0;
|
|
24
|
+
|
|
25
|
+
export const getCenterFromEvent = (
|
|
26
|
+
event: MouseEvent | TouchEvent | PointerEvent,
|
|
27
|
+
) => ({
|
|
28
|
+
x:
|
|
29
|
+
"changedTouches" in event
|
|
30
|
+
? event.changedTouches[0]?.pageX ?? 0
|
|
31
|
+
: event.clientX,
|
|
32
|
+
y:
|
|
33
|
+
"changedTouches" in event
|
|
34
|
+
? event.changedTouches[0]?.pageY ?? 0
|
|
35
|
+
: event.clientY,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function isOUtsidePosThreshold(
|
|
39
|
+
startEvent: PointerEvent,
|
|
40
|
+
endEvent: PointerEvent,
|
|
41
|
+
posThreshold: number,
|
|
42
|
+
) {
|
|
43
|
+
const start = getCenterFromEvent(startEvent);
|
|
44
|
+
const end = getCenterFromEvent(endEvent);
|
|
45
|
+
|
|
46
|
+
// Determines if the movement qualifies as a drag
|
|
47
|
+
return (
|
|
48
|
+
Math.abs(end.x - start.x) > posThreshold ||
|
|
49
|
+
Math.abs(end.y - start.y) > posThreshold
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function calculateDistance(pointA: PointerEvent, pointB: PointerEvent) {
|
|
54
|
+
const dx = pointA.clientX - pointB.clientX;
|
|
55
|
+
const dy = pointA.clientY - pointB.clientY;
|
|
56
|
+
|
|
57
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const fromFailWith = (failWith?: { start$: Observable<unknown> }[]) =>
|
|
61
|
+
!failWith?.length
|
|
62
|
+
? NEVER
|
|
63
|
+
: merge(...(failWith?.map(({ start$ }) => start$) ?? []));
|
|
64
|
+
|
|
65
|
+
export function matchPointer(pointer: PointerEvent) {
|
|
66
|
+
return (stream: Observable<PointerEvent>) =>
|
|
67
|
+
stream.pipe(
|
|
68
|
+
filter((newPointer) => newPointer.pointerId === pointer.pointerId),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const getPointerEvents = ({
|
|
73
|
+
container,
|
|
74
|
+
afterEventReceived = (event) => event,
|
|
75
|
+
}: {
|
|
76
|
+
container: HTMLElement;
|
|
77
|
+
afterEventReceived?: (event: PointerEvent) => PointerEvent;
|
|
78
|
+
}) => {
|
|
79
|
+
const pointerDown$ = fromPointerDown({ afterEventReceived, container });
|
|
80
|
+
const pointerUp$ = fromEvent<PointerEvent>(container, "pointerup").pipe(
|
|
81
|
+
map(afterEventReceived),
|
|
82
|
+
);
|
|
83
|
+
const pointerMove$ = fromEvent<PointerEvent>(container, "pointermove").pipe(
|
|
84
|
+
map(afterEventReceived),
|
|
85
|
+
);
|
|
86
|
+
/**
|
|
87
|
+
* Mostly used to handle cursor leaving window on mouse pointers.
|
|
88
|
+
*/
|
|
89
|
+
const pointerLeave$ = fromEvent<PointerEvent>(container, "pointerleave").pipe(
|
|
90
|
+
map(afterEventReceived),
|
|
91
|
+
);
|
|
92
|
+
/**
|
|
93
|
+
* Handle things such as contextual menu popup or
|
|
94
|
+
* native swipe navigation conflict.
|
|
95
|
+
*/
|
|
96
|
+
const pointerCancel$ = fromEvent<PointerEvent>(
|
|
97
|
+
container,
|
|
98
|
+
"pointercancel",
|
|
99
|
+
).pipe(map(afterEventReceived));
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pointerDown$,
|
|
103
|
+
pointerCancel$,
|
|
104
|
+
pointerLeave$,
|
|
105
|
+
pointerUp$,
|
|
106
|
+
pointerMove$,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const fromPointerDown = ({
|
|
111
|
+
container,
|
|
112
|
+
afterEventReceived = (event) => event,
|
|
113
|
+
}: {
|
|
114
|
+
container: HTMLElement;
|
|
115
|
+
afterEventReceived?: (event: PointerEvent) => PointerEvent;
|
|
116
|
+
}) =>
|
|
117
|
+
fromEvent<PointerEvent>(container, "pointerdown").pipe(
|
|
118
|
+
map(afterEventReceived),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export const enrichEvent = ({
|
|
122
|
+
event,
|
|
123
|
+
startTime,
|
|
124
|
+
startEvent,
|
|
125
|
+
}: {
|
|
126
|
+
event: PointerEvent;
|
|
127
|
+
startTime: number;
|
|
128
|
+
startEvent: PointerEvent;
|
|
129
|
+
}) => {
|
|
130
|
+
const startX = startEvent.clientX;
|
|
131
|
+
const startY = startEvent.clientY;
|
|
132
|
+
const deltaX = event.clientX - startX;
|
|
133
|
+
const deltaY = event.clientY - startY;
|
|
134
|
+
|
|
135
|
+
// Calculate the change in time
|
|
136
|
+
const dt = Date.now() - startTime;
|
|
137
|
+
|
|
138
|
+
// Avoid division by zero
|
|
139
|
+
// Calculate velocity in pixels per second
|
|
140
|
+
const velocityX = dt > 0 ? deltaX / dt : 0;
|
|
141
|
+
const velocityY = dt > 0 ? deltaY / dt : 0;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
srcEvent: event,
|
|
145
|
+
deltaX,
|
|
146
|
+
deltaY,
|
|
147
|
+
velocityX,
|
|
148
|
+
velocityY,
|
|
149
|
+
delay: new Date().getTime() - startTime,
|
|
150
|
+
center: {
|
|
151
|
+
x: event.x,
|
|
152
|
+
y: event.y,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @todo test the strict stability
|
|
159
|
+
* @important
|
|
160
|
+
* Stable function, will not emit if the pointers don't change
|
|
161
|
+
*/
|
|
162
|
+
export const trackActivePointers = ({
|
|
163
|
+
container,
|
|
164
|
+
}: {
|
|
165
|
+
container: HTMLElement;
|
|
166
|
+
}) => {
|
|
167
|
+
const pointerUp$ = fromEvent<PointerEvent>(container, "pointerup");
|
|
168
|
+
// const pointerMove$ = fromEvent<PointerEvent>(container, "pointermove")
|
|
169
|
+
const pointerLeave$ = fromEvent<PointerEvent>(container, "pointerleave");
|
|
170
|
+
const pointerCancel$ = fromEvent<PointerEvent>(container, "pointercancel");
|
|
171
|
+
|
|
172
|
+
return fromEvent<PointerEvent>(container, "pointerdown").pipe(
|
|
173
|
+
mergeMap((pointerDown) => {
|
|
174
|
+
const pointerDownRelease$ = merge(
|
|
175
|
+
pointerUp$,
|
|
176
|
+
pointerLeave$,
|
|
177
|
+
pointerCancel$,
|
|
178
|
+
).pipe(matchPointer(pointerDown));
|
|
179
|
+
const pointerDown$ = defer(() => of(pointerDown));
|
|
180
|
+
// const currentPointerMove$ = pointerMove$.pipe(
|
|
181
|
+
// filter((pointerMoveEvent) => matchPointer(pointerMoveEvent, pointerDown)),
|
|
182
|
+
// throttleTime(100, undefined, { trailing: true }),
|
|
183
|
+
// )
|
|
184
|
+
|
|
185
|
+
return pointerDown$.pipe(
|
|
186
|
+
// mergeMap(() => merge(pointerDown$, currentPointerMove$)),
|
|
187
|
+
mergeMap(() => merge(pointerDown$, NEVER)),
|
|
188
|
+
map((event) => ({ id: event.pointerId, event })),
|
|
189
|
+
takeUntil(pointerDownRelease$),
|
|
190
|
+
endWith({ id: pointerDown.pointerId, event: undefined }),
|
|
191
|
+
);
|
|
192
|
+
}),
|
|
193
|
+
scan(
|
|
194
|
+
(acc, { event, id }) => {
|
|
195
|
+
if (!event) {
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
197
|
+
const { [id]: _deleted, ...rest } = acc;
|
|
198
|
+
|
|
199
|
+
return rest;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
...acc,
|
|
204
|
+
[id]: event,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
{} as Record<number, PointerEvent | undefined>,
|
|
208
|
+
),
|
|
209
|
+
map((events) => Object.values(events).filter(isDefined)),
|
|
210
|
+
);
|
|
211
|
+
};
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createManager } from "./core/manager";
|
|
2
|
+
import { createTapRecognizer } from "./core/tapRecognizer";
|
|
3
|
+
import "./style.css";
|
|
4
|
+
|
|
5
|
+
const container = document.querySelector<HTMLDivElement>("#app")!;
|
|
6
|
+
|
|
7
|
+
const tapRecognizer = createTapRecognizer({ maxTaps: 3 });
|
|
8
|
+
|
|
9
|
+
const manager = createManager({
|
|
10
|
+
container,
|
|
11
|
+
recognizers: [tapRecognizer],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
manager.events$.subscribe(console.warn);
|
|
15
|
+
|
|
16
|
+
container.innerHTML = `
|
|
17
|
+
<div class="container">
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
package/src/style.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
color: rgba(255, 255, 255, 0.87);
|
|
8
|
+
background-color: #242424;
|
|
9
|
+
|
|
10
|
+
font-synthesis: none;
|
|
11
|
+
text-rendering: optimizeLegibility;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
-moz-osx-font-smoothing: grayscale;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
display: flex;
|
|
19
|
+
min-width: 100vw;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#app {
|
|
24
|
+
display: flex;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
align-items: center;
|
|
28
|
+
flex: 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import dts from "vite-plugin-dts";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { name } from "../prose-reader/packages/enhancer-gesture/package.json";
|
|
5
|
+
import externals from "rollup-plugin-node-externals";
|
|
6
|
+
|
|
7
|
+
const libName = name.replace(`@`, ``).replace(`/`, `-`);
|
|
8
|
+
|
|
9
|
+
export default defineConfig(({ mode }) => ({
|
|
10
|
+
build: {
|
|
11
|
+
minify: false,
|
|
12
|
+
lib: {
|
|
13
|
+
entry: resolve(__dirname, `src/index.ts`),
|
|
14
|
+
name: libName,
|
|
15
|
+
fileName: "index",
|
|
16
|
+
},
|
|
17
|
+
emptyOutDir: mode !== `development`,
|
|
18
|
+
sourcemap: true,
|
|
19
|
+
},
|
|
20
|
+
plugins: [
|
|
21
|
+
{
|
|
22
|
+
enforce: `pre`,
|
|
23
|
+
...externals({
|
|
24
|
+
peerDeps: true,
|
|
25
|
+
deps: true,
|
|
26
|
+
devDeps: true,
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
dts({
|
|
30
|
+
entryRoot: "src",
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
}));
|