@tremolo-ui/dom 0.4.0 → 0.6.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/dist/index.cjs +934 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +580 -31
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +580 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +923 -90
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/canvas/animation.ts +322 -0
- package/src/canvas/context.ts +108 -0
- package/src/index.ts +48 -1
- package/src/midi/access.ts +83 -13
- package/src/midi/input.ts +97 -26
- package/src/midi/message.ts +37 -8
- package/src/piano/index.ts +205 -0
- package/src/pointer/drag-value.ts +365 -0
- package/src/pointer/drag.ts +340 -65
- package/src/pointer/wheel.ts +36 -1
- package/src/selection/box.ts +136 -0
- package/src/xy.ts +38 -0
package/src/pointer/drag.ts
CHANGED
|
@@ -8,16 +8,23 @@ export type DragState = {
|
|
|
8
8
|
/** Pointer position in viewport coordinates. */
|
|
9
9
|
clientX: number
|
|
10
10
|
clientY: number
|
|
11
|
+
/**
|
|
12
|
+
* Which pointer this is. Always the same value for one drag; with
|
|
13
|
+
* {@link DragOptions.multiPointer} it tells concurrent drags apart.
|
|
14
|
+
*/
|
|
15
|
+
pointerId: number
|
|
11
16
|
event: PointerEvent
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export type DragOptions = {
|
|
15
20
|
/**
|
|
16
|
-
* Minimum movement in pixels before `onDrag` fires.
|
|
21
|
+
* Minimum movement in pixels before `onDrag` fires. Movement below it is
|
|
22
|
+
* carried over to the next event rather than discarded, so a slow drag still
|
|
23
|
+
* reports once it adds up.
|
|
24
|
+
*
|
|
17
25
|
* Prevents `onDrag` from firing on, for example, a double click.
|
|
18
|
-
* Values below 1 are clamped to 1.
|
|
19
26
|
*
|
|
20
|
-
* @default
|
|
27
|
+
* @default 0
|
|
21
28
|
*/
|
|
22
29
|
threshold?: number
|
|
23
30
|
|
|
@@ -25,15 +32,79 @@ export type DragOptions = {
|
|
|
25
32
|
* CSS cursor to show while dragging. Applied to the element itself: pointer
|
|
26
33
|
* capture keeps it in effect even once the pointer leaves the element, so
|
|
27
34
|
* there is no need to touch the document.
|
|
35
|
+
*
|
|
36
|
+
* With {@link DragOptions.multiPointer} it is set for the first pointer and
|
|
37
|
+
* restored once the last one is up.
|
|
28
38
|
*/
|
|
29
39
|
cursor?: string
|
|
30
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Decide whether a pointerdown starts a drag at all.
|
|
43
|
+
*
|
|
44
|
+
* Checked before anything else — **before the pointer is captured** — so
|
|
45
|
+
* declining here leaves the whole gesture to whatever else is listening.
|
|
46
|
+
* Deciding later would be too late: the capture has already been taken from
|
|
47
|
+
* the element that was going to handle it.
|
|
48
|
+
*
|
|
49
|
+
* The use for it is a drag on a container that also holds draggable things
|
|
50
|
+
* of its own, such as a rubber-band selection that must not begin on top of
|
|
51
|
+
* one of the objects it would select.
|
|
52
|
+
*/
|
|
53
|
+
shouldStart?: (event: PointerEvent) => boolean
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hide the pointer and read its movement directly, instead of following it
|
|
57
|
+
* around the screen.
|
|
58
|
+
*
|
|
59
|
+
* A relative drag — a knob, a stepper — does not care where the pointer is,
|
|
60
|
+
* only how far it moved, and letting it wander has two costs: the cursor
|
|
61
|
+
* ends up far from what it is holding, and **the drag stops at the edge of
|
|
62
|
+
* the screen**, where the operating system pins the pointer and the
|
|
63
|
+
* coordinates stop changing. A fine drag reaches that edge quickly.
|
|
64
|
+
*
|
|
65
|
+
* **Not for a drag whose value is the position pointed at** — anything on
|
|
66
|
+
* `elementMapping`. `clientX` / `clientY` freeze while the pointer is
|
|
67
|
+
* locked, so there is no position left to read.
|
|
68
|
+
*
|
|
69
|
+
* The request needs a user gesture, which a pointerdown is, but it can still
|
|
70
|
+
* be refused; the drag then carries on as an ordinary one. Read on
|
|
71
|
+
* pointerdown, so `update()` reaches the next drag rather than the current.
|
|
72
|
+
*
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
pointerLock?: boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Track every pointer that goes down on the element, rather than only the
|
|
79
|
+
* first. Each one gets its own `onDragStart` / `onDrag` / `onDragEnd` and
|
|
80
|
+
* carries its own totals; {@link DragState.pointerId} says which is which.
|
|
81
|
+
*
|
|
82
|
+
* Fixed for the lifetime of the instance: switching part way through a drag
|
|
83
|
+
* has no meaning, so `update()` ignores it.
|
|
84
|
+
*
|
|
85
|
+
* @default false
|
|
86
|
+
*/
|
|
87
|
+
multiPointer?: boolean
|
|
88
|
+
|
|
31
89
|
onDragStart?: (state: DragState) => void
|
|
32
90
|
onDrag?: (state: DragState) => void
|
|
91
|
+
/**
|
|
92
|
+
* Called exactly once for every drag that starts, whether tracking ends by
|
|
93
|
+
* pointer release, cancellation, capture or lock loss, or destruction.
|
|
94
|
+
*/
|
|
33
95
|
onDragEnd?: (state: DragState) => void
|
|
34
96
|
}
|
|
35
97
|
|
|
36
98
|
export interface DragInstance {
|
|
99
|
+
/**
|
|
100
|
+
* Replace the given options. Lets a wrapper feed fresh handlers in without
|
|
101
|
+
* tearing down the listeners, which would abort a drag in progress.
|
|
102
|
+
*
|
|
103
|
+
* `multiPointer` is fixed for the lifetime of the instance and is ignored
|
|
104
|
+
* here.
|
|
105
|
+
*/
|
|
106
|
+
update: (options: DragOptions) => void
|
|
107
|
+
/** End any active drags before removing the instance. */
|
|
37
108
|
destroy: () => void
|
|
38
109
|
}
|
|
39
110
|
|
|
@@ -51,12 +122,45 @@ const MANAGED_STYLES = [
|
|
|
51
122
|
['-webkit-touch-callout', 'none'],
|
|
52
123
|
] as const
|
|
53
124
|
|
|
125
|
+
type ManagedStyleState = {
|
|
126
|
+
count: number
|
|
127
|
+
previous: Map<string, { value: string; priority: string }>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const managedStyleStates = new WeakMap<Element, ManagedStyleState>()
|
|
131
|
+
|
|
54
132
|
type CaptureTarget = {
|
|
133
|
+
requestPointerLock?: () => unknown
|
|
55
134
|
setPointerCapture?: (pointerId: number) => void
|
|
56
135
|
releasePointerCapture?: (pointerId: number) => void
|
|
57
136
|
hasPointerCapture?: (pointerId: number) => boolean
|
|
58
137
|
}
|
|
59
138
|
|
|
139
|
+
/** What one pointer needs to report its own movement. */
|
|
140
|
+
type PointerState = {
|
|
141
|
+
/** Where the listeners for this pointer live. */
|
|
142
|
+
moveTarget: EventTarget
|
|
143
|
+
startX: number
|
|
144
|
+
startY: number
|
|
145
|
+
lastX: number
|
|
146
|
+
lastY: number
|
|
147
|
+
/**
|
|
148
|
+
* Where the travel stood when the pointer lock took effect, and how far it
|
|
149
|
+
* has moved since. Screen coordinates stop changing under the lock, so from
|
|
150
|
+
* that point the movement of each event is added up instead.
|
|
151
|
+
*
|
|
152
|
+
* The base is taken when the lock engages rather than on pointerdown: the
|
|
153
|
+
* request is asynchronous, and whatever movement happened while it was in
|
|
154
|
+
* flight was measured the ordinary way.
|
|
155
|
+
*/
|
|
156
|
+
lockBaseX?: number
|
|
157
|
+
lockBaseY?: number
|
|
158
|
+
lockMoveX: number
|
|
159
|
+
lockMoveY: number
|
|
160
|
+
/** The most recent event, to end the drag with when the lock is lost. */
|
|
161
|
+
lastEvent: PointerEvent
|
|
162
|
+
}
|
|
163
|
+
|
|
60
164
|
/**
|
|
61
165
|
* Track a pointer drag on an element.
|
|
62
166
|
*
|
|
@@ -65,50 +169,69 @@ type CaptureTarget = {
|
|
|
65
169
|
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
66
170
|
* page, plus `user-select: none` so that a long press does not start a text
|
|
67
171
|
* selection instead.
|
|
172
|
+
*
|
|
173
|
+
* One pointer at a time by default; see {@link DragOptions.multiPointer}.
|
|
68
174
|
*/
|
|
69
175
|
export function createDrag(
|
|
70
176
|
element: Element,
|
|
71
|
-
{
|
|
72
|
-
threshold: _threshold = 1,
|
|
73
|
-
cursor,
|
|
74
|
-
onDragStart,
|
|
75
|
-
onDrag,
|
|
76
|
-
onDragEnd,
|
|
77
|
-
}: DragOptions = {},
|
|
177
|
+
options: DragOptions = {},
|
|
78
178
|
): DragInstance {
|
|
79
|
-
|
|
179
|
+
let opts = options
|
|
180
|
+
const multiPointer = options.multiPointer ?? false
|
|
80
181
|
const capture = element as CaptureTarget
|
|
81
182
|
const style = (element as Partial<HTMLElement>).style
|
|
82
183
|
|
|
83
|
-
|
|
184
|
+
let managedStyles = managedStyleStates.get(element)
|
|
84
185
|
if (style) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
186
|
+
if (managedStyles) {
|
|
187
|
+
managedStyles.count += 1
|
|
188
|
+
} else {
|
|
189
|
+
const previous = new Map<string, { value: string; priority: string }>()
|
|
190
|
+
for (const [property, value] of MANAGED_STYLES) {
|
|
191
|
+
previous.set(property, {
|
|
192
|
+
value: style.getPropertyValue(property),
|
|
193
|
+
priority: style.getPropertyPriority(property),
|
|
194
|
+
})
|
|
195
|
+
style.setProperty(property, value)
|
|
196
|
+
}
|
|
197
|
+
managedStyles = { count: 1, previous }
|
|
198
|
+
managedStyleStates.set(element, managedStyles)
|
|
88
199
|
}
|
|
89
200
|
}
|
|
90
201
|
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
202
|
+
const pointers = new Map<number, PointerState>()
|
|
203
|
+
/**
|
|
204
|
+
* How many pointers each target carries. Adding the same listener twice is a
|
|
205
|
+
* no-op and removing it once removes it for good, so a target is subscribed
|
|
206
|
+
* to while at least one pointer is on it and no longer.
|
|
207
|
+
*/
|
|
208
|
+
const targets = new Map<EventTarget, number>()
|
|
98
209
|
let previousCursor: string | undefined
|
|
210
|
+
/** The pointer that asked for the lock, while it is still down. */
|
|
211
|
+
let lockedPointerId: number | null = null
|
|
212
|
+
let lockRequest = 0
|
|
213
|
+
let destroyed = false
|
|
99
214
|
|
|
100
215
|
function state(
|
|
101
216
|
event: PointerEvent,
|
|
217
|
+
pointer: PointerState,
|
|
102
218
|
deltaX: number,
|
|
103
219
|
deltaY: number,
|
|
104
220
|
): DragState {
|
|
105
221
|
return {
|
|
106
|
-
x:
|
|
107
|
-
|
|
222
|
+
x:
|
|
223
|
+
pointer.lockBaseX !== undefined
|
|
224
|
+
? pointer.lockBaseX + pointer.lockMoveX
|
|
225
|
+
: event.screenX - pointer.startX,
|
|
226
|
+
y:
|
|
227
|
+
pointer.lockBaseY !== undefined
|
|
228
|
+
? pointer.lockBaseY + pointer.lockMoveY
|
|
229
|
+
: event.screenY - pointer.startY,
|
|
108
230
|
deltaX,
|
|
109
231
|
deltaY,
|
|
110
232
|
clientX: event.clientX,
|
|
111
233
|
clientY: event.clientY,
|
|
234
|
+
pointerId: event.pointerId,
|
|
112
235
|
event,
|
|
113
236
|
}
|
|
114
237
|
}
|
|
@@ -122,93 +245,245 @@ export function createDrag(
|
|
|
122
245
|
event.preventDefault()
|
|
123
246
|
}
|
|
124
247
|
|
|
248
|
+
function retainTarget(target: EventTarget) {
|
|
249
|
+
const count = targets.get(target) ?? 0
|
|
250
|
+
if (count === 0) {
|
|
251
|
+
target.addEventListener('pointermove', handlePointerMove)
|
|
252
|
+
target.addEventListener('pointerup', handlePointerUp)
|
|
253
|
+
target.addEventListener('pointercancel', handlePointerUp)
|
|
254
|
+
if (target === element) {
|
|
255
|
+
target.addEventListener('lostpointercapture', handleLostPointerCapture)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
targets.set(target, count + 1)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function releaseTarget(target: EventTarget) {
|
|
262
|
+
const count = targets.get(target) ?? 0
|
|
263
|
+
if (count > 1) {
|
|
264
|
+
targets.set(target, count - 1)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
target.removeEventListener('pointermove', handlePointerMove)
|
|
268
|
+
target.removeEventListener('pointerup', handlePointerUp)
|
|
269
|
+
target.removeEventListener('pointercancel', handlePointerUp)
|
|
270
|
+
if (target === element) {
|
|
271
|
+
target.removeEventListener('lostpointercapture', handleLostPointerCapture)
|
|
272
|
+
}
|
|
273
|
+
targets.delete(target)
|
|
274
|
+
}
|
|
275
|
+
|
|
125
276
|
function handlePointerDown(event: Event) {
|
|
126
277
|
const pointerEvent = event as PointerEvent
|
|
127
|
-
|
|
128
|
-
|
|
278
|
+
if (pointerEvent.button !== 0) return
|
|
279
|
+
const pointerId = pointerEvent.pointerId
|
|
280
|
+
// Without multiPointer only one pointer drives the drag; ignore the rest.
|
|
281
|
+
if (pointers.has(pointerId)) return
|
|
282
|
+
if (!multiPointer && pointers.size > 0) return
|
|
129
283
|
|
|
130
|
-
|
|
131
|
-
startX = lastX = pointerEvent.screenX
|
|
132
|
-
startY = lastY = pointerEvent.screenY
|
|
284
|
+
if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return
|
|
133
285
|
|
|
134
|
-
|
|
286
|
+
const isFirst = pointers.size === 0
|
|
287
|
+
if (isFirst && opts.cursor && style) {
|
|
135
288
|
previousCursor = style.cursor
|
|
136
|
-
style.cursor = cursor
|
|
289
|
+
style.cursor = opts.cursor
|
|
137
290
|
}
|
|
138
291
|
|
|
139
292
|
capture.setPointerCapture?.(pointerId)
|
|
140
293
|
// With pointer capture the element receives the rest of the gesture.
|
|
141
294
|
// Without it (older engines, jsdom) fall back to the window.
|
|
142
|
-
moveTarget =
|
|
295
|
+
const moveTarget =
|
|
143
296
|
capture.hasPointerCapture?.(pointerId) === true
|
|
144
297
|
? element
|
|
145
298
|
: (globalThis.window ?? element)
|
|
146
299
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
300
|
+
const pointer: PointerState = {
|
|
301
|
+
moveTarget,
|
|
302
|
+
startX: pointerEvent.screenX,
|
|
303
|
+
startY: pointerEvent.screenY,
|
|
304
|
+
lastX: pointerEvent.screenX,
|
|
305
|
+
lastY: pointerEvent.screenY,
|
|
306
|
+
lockMoveX: 0,
|
|
307
|
+
lockMoveY: 0,
|
|
308
|
+
lastEvent: pointerEvent,
|
|
309
|
+
}
|
|
310
|
+
pointers.set(pointerId, pointer)
|
|
151
311
|
|
|
152
|
-
|
|
312
|
+
retainTarget(moveTarget)
|
|
313
|
+
if (isFirst) {
|
|
314
|
+
globalThis.document?.addEventListener('selectstart', preventSelectStart)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// One pointer can be locked, so the first one takes it.
|
|
318
|
+
if (isFirst && opts.pointerLock) {
|
|
319
|
+
const requestId = ++lockRequest
|
|
320
|
+
lockedPointerId = pointerId
|
|
321
|
+
globalThis.document?.addEventListener(
|
|
322
|
+
'pointerlockchange',
|
|
323
|
+
handleLockChange,
|
|
324
|
+
)
|
|
325
|
+
try {
|
|
326
|
+
// Newer engines return a promise that rejects; older ones fire
|
|
327
|
+
// `pointerlockerror` instead. Either way a refusal only means the drag
|
|
328
|
+
// stays an ordinary one, so nothing here has to act on it.
|
|
329
|
+
const request = capture.requestPointerLock?.() as
|
|
330
|
+
| Promise<void>
|
|
331
|
+
| undefined
|
|
332
|
+
request?.then?.(
|
|
333
|
+
() => {
|
|
334
|
+
if (
|
|
335
|
+
requestId === lockRequest &&
|
|
336
|
+
(!pointers.has(pointerId) || destroyed) &&
|
|
337
|
+
globalThis.document?.pointerLockElement === element
|
|
338
|
+
) {
|
|
339
|
+
globalThis.document.exitPointerLock?.()
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
() => {},
|
|
343
|
+
)
|
|
344
|
+
} catch {
|
|
345
|
+
// requestPointerLock threw synchronously; same story.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0))
|
|
153
350
|
}
|
|
154
351
|
|
|
155
352
|
function handlePointerMove(event: Event) {
|
|
156
353
|
const pointerEvent = event as PointerEvent
|
|
157
|
-
|
|
354
|
+
const pointer = pointers.get(pointerEvent.pointerId)
|
|
355
|
+
if (!pointer) return
|
|
158
356
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
357
|
+
// Under the lock the screen position no longer moves, so the movement the
|
|
358
|
+
// event reports is the only thing left to read.
|
|
359
|
+
const locked = pointer.lockBaseX !== undefined
|
|
360
|
+
const deltaX = locked
|
|
361
|
+
? (pointerEvent.movementX ?? 0)
|
|
362
|
+
: pointerEvent.screenX - pointer.lastX
|
|
363
|
+
const deltaY = locked
|
|
364
|
+
? (pointerEvent.movementY ?? 0)
|
|
365
|
+
: pointerEvent.screenY - pointer.lastY
|
|
163
366
|
|
|
164
|
-
// Movement below the threshold
|
|
367
|
+
// Movement below the threshold accumulates until it crosses it. Dropping it
|
|
368
|
+
// instead would swallow a slow drag entirely: pointer coordinates are
|
|
369
|
+
// fractional, so each event can move less than a pixel and never report.
|
|
370
|
+
const threshold = opts.threshold ?? 0
|
|
165
371
|
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return
|
|
166
372
|
|
|
167
|
-
|
|
373
|
+
pointer.lastX = pointerEvent.screenX
|
|
374
|
+
pointer.lastY = pointerEvent.screenY
|
|
375
|
+
pointer.lastEvent = pointerEvent
|
|
376
|
+
if (locked) {
|
|
377
|
+
pointer.lockMoveX += deltaX
|
|
378
|
+
pointer.lockMoveY += deltaY
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleLockChange() {
|
|
385
|
+
if (lockedPointerId === null) return
|
|
386
|
+
const pointer = pointers.get(lockedPointerId)
|
|
387
|
+
if (!pointer) return
|
|
388
|
+
|
|
389
|
+
if (globalThis.document?.pointerLockElement === element) {
|
|
390
|
+
// Engaged. Whatever moved while the request was in flight was measured
|
|
391
|
+
// the ordinary way, so the travel so far becomes the base and the
|
|
392
|
+
// per-event movement is added to it from here.
|
|
393
|
+
pointer.lockBaseX = pointer.lastX - pointer.startX
|
|
394
|
+
pointer.lockBaseY = pointer.lastY - pointer.startY
|
|
395
|
+
pointer.lockMoveX = 0
|
|
396
|
+
pointer.lockMoveY = 0
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Lost without the pointer coming up: Esc, a tab switch, leaving
|
|
401
|
+
// fullscreen. No pointerup is coming, so the drag ends here rather than
|
|
402
|
+
// hanging on with a pointer nobody can see.
|
|
403
|
+
if (pointer.lockBaseX === undefined) return
|
|
404
|
+
finishDrag(lockedPointerId)
|
|
168
405
|
}
|
|
169
406
|
|
|
170
407
|
function handlePointerUp(event: Event) {
|
|
171
408
|
const pointerEvent = event as PointerEvent
|
|
172
|
-
|
|
409
|
+
finishDrag(pointerEvent.pointerId, pointerEvent)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function handleLostPointerCapture(event: Event) {
|
|
413
|
+
const pointerEvent = event as PointerEvent
|
|
414
|
+
finishDrag(pointerEvent.pointerId)
|
|
415
|
+
}
|
|
173
416
|
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
417
|
+
function finishDrag(pointerId: number, event?: PointerEvent) {
|
|
418
|
+
const pointer = pointers.get(pointerId)
|
|
419
|
+
if (!pointer) return
|
|
177
420
|
|
|
178
|
-
|
|
179
|
-
|
|
421
|
+
const finalState = event
|
|
422
|
+
? state(
|
|
423
|
+
event,
|
|
424
|
+
pointer,
|
|
425
|
+
event.screenX - pointer.lastX,
|
|
426
|
+
event.screenY - pointer.lastY,
|
|
427
|
+
)
|
|
428
|
+
: state(pointer.lastEvent, pointer, 0, 0)
|
|
429
|
+
|
|
430
|
+
stopTracking(pointerId)
|
|
431
|
+
opts.onDragEnd?.(finalState)
|
|
180
432
|
}
|
|
181
433
|
|
|
182
|
-
function stopTracking() {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
moveTarget
|
|
434
|
+
function stopTracking(pointerId: number) {
|
|
435
|
+
const pointer = pointers.get(pointerId)
|
|
436
|
+
if (!pointer) return
|
|
437
|
+
|
|
438
|
+
pointers.delete(pointerId)
|
|
439
|
+
releaseTarget(pointer.moveTarget)
|
|
440
|
+
try {
|
|
441
|
+
if (capture.hasPointerCapture?.(pointerId) === true) {
|
|
442
|
+
capture.releasePointerCapture?.(pointerId)
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
// The capture may disappear between checking and releasing it. Tracking
|
|
446
|
+
// is already cleared, so the drag still ends normally.
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (lockedPointerId === pointerId) {
|
|
450
|
+
lockedPointerId = null
|
|
451
|
+
const document = globalThis.document
|
|
452
|
+
document?.removeEventListener('pointerlockchange', handleLockChange)
|
|
453
|
+
// Already gone when the lock is what ended the drag.
|
|
454
|
+
if (document?.pointerLockElement === element) document.exitPointerLock?.()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (pointers.size > 0) return
|
|
458
|
+
|
|
188
459
|
globalThis.document?.removeEventListener('selectstart', preventSelectStart)
|
|
189
|
-
if (
|
|
190
|
-
style.cursor = previousCursor
|
|
460
|
+
if (previousCursor !== undefined && style) {
|
|
461
|
+
style.cursor = previousCursor
|
|
191
462
|
previousCursor = undefined
|
|
192
463
|
}
|
|
193
|
-
moveTarget = null
|
|
194
|
-
pointerId = null
|
|
195
464
|
}
|
|
196
465
|
|
|
197
466
|
element.addEventListener('pointerdown', handlePointerDown)
|
|
198
467
|
|
|
199
468
|
return {
|
|
469
|
+
update: (next) => {
|
|
470
|
+
opts = { ...opts, ...next, multiPointer }
|
|
471
|
+
},
|
|
200
472
|
destroy: () => {
|
|
201
|
-
|
|
473
|
+
if (destroyed) return
|
|
474
|
+
destroyed = true
|
|
202
475
|
element.removeEventListener('pointerdown', handlePointerDown)
|
|
203
|
-
|
|
476
|
+
for (const pointerId of [...pointers.keys()]) finishDrag(pointerId)
|
|
477
|
+
if (style && managedStyles && --managedStyles.count === 0) {
|
|
204
478
|
for (const [property] of MANAGED_STYLES) {
|
|
205
|
-
const previous =
|
|
206
|
-
if (previous) {
|
|
207
|
-
style.setProperty(property, previous)
|
|
479
|
+
const previous = managedStyles.previous.get(property)
|
|
480
|
+
if (previous?.value) {
|
|
481
|
+
style.setProperty(property, previous.value, previous.priority)
|
|
208
482
|
} else {
|
|
209
483
|
style.removeProperty(property)
|
|
210
484
|
}
|
|
211
485
|
}
|
|
486
|
+
managedStyleStates.delete(element)
|
|
212
487
|
}
|
|
213
488
|
},
|
|
214
489
|
}
|
package/src/pointer/wheel.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
+
export interface WheelOptions {
|
|
2
|
+
/** Replace the callback through {@link WheelInstance.update}. */
|
|
3
|
+
onWheel?: (event: WheelEvent) => void
|
|
4
|
+
/**
|
|
5
|
+
* Only report events while the focus is inside the element.
|
|
6
|
+
*
|
|
7
|
+
* A control that reacts to the wheel on hover alone takes the scroll away
|
|
8
|
+
* from the page, so passing over one in a long form silently changes its
|
|
9
|
+
* value. Requiring focus makes that an explicit act.
|
|
10
|
+
*
|
|
11
|
+
* The check is `contains`, not an identity test: the element that actually
|
|
12
|
+
* takes focus is usually a descendant, such as a thumb or an `<input>`, and
|
|
13
|
+
* a caller may have replaced it with markup of their own.
|
|
14
|
+
*
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
requireFocus?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export interface WheelInstance {
|
|
21
|
+
/** Replace the given options, keeping the listener in place. */
|
|
22
|
+
update: (options: WheelOptions) => void
|
|
2
23
|
destroy: () => void
|
|
3
24
|
}
|
|
4
25
|
|
|
@@ -11,12 +32,26 @@ export interface WheelInstance {
|
|
|
11
32
|
export function createWheel(
|
|
12
33
|
element: Element,
|
|
13
34
|
onWheel: (event: WheelEvent) => void,
|
|
35
|
+
options: WheelOptions = {},
|
|
14
36
|
): WheelInstance {
|
|
15
|
-
|
|
37
|
+
let opts = { ...options, onWheel }
|
|
38
|
+
|
|
39
|
+
function hasFocus() {
|
|
40
|
+
const active = element.ownerDocument?.activeElement
|
|
41
|
+
return !!active && element.contains(active)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handler = (event: Event) => {
|
|
45
|
+
if (opts.requireFocus && !hasFocus()) return
|
|
46
|
+
opts.onWheel?.(event as WheelEvent)
|
|
47
|
+
}
|
|
16
48
|
|
|
17
49
|
element.addEventListener('wheel', handler, { passive: false })
|
|
18
50
|
|
|
19
51
|
return {
|
|
52
|
+
update: (next) => {
|
|
53
|
+
opts = { ...opts, ...next }
|
|
54
|
+
},
|
|
20
55
|
destroy: () => {
|
|
21
56
|
// Only `capture` matters when removing, and it is false here.
|
|
22
57
|
element.removeEventListener('wheel', handler)
|