@tremolo-ui/dom 0.4.0 → 0.5.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 +541 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +366 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +366 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +534 -56
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/canvas/animation.ts +304 -0
- package/src/canvas/context.ts +81 -0
- package/src/index.ts +37 -1
- package/src/piano/input.ts +185 -0
- package/src/pointer/drag.ts +137 -57
- package/src/pointer/dragValue.ts +229 -0
- package/src/pointer/wheel.ts +34 -1
- package/src/xy.ts +38 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { noteAt, type PianoLayout } from '@tremolo-ui/functions'
|
|
2
|
+
|
|
3
|
+
import { createDrag } from '../pointer/drag'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What asked for a note to sound.
|
|
7
|
+
*
|
|
8
|
+
* A note stops only once everything that asked for it has let go, so two
|
|
9
|
+
* fingers on one key, or a key held by both the mouse and a MIDI keyboard,
|
|
10
|
+
* behave the way they look.
|
|
11
|
+
*
|
|
12
|
+
* Pointers use `pointer:<pointerId>`; everything else names its own source.
|
|
13
|
+
*/
|
|
14
|
+
export type NoteSource = string
|
|
15
|
+
|
|
16
|
+
export interface PianoInputOptions {
|
|
17
|
+
/** Geometry of the drawn keyboard, used to find the note under a pointer. */
|
|
18
|
+
layout: PianoLayout
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Let a pointer slide from one key to the next while it is down. With it off,
|
|
22
|
+
* the key that was pressed keeps sounding until the pointer is released.
|
|
23
|
+
*
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
glissando?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Highest note that can sound. Above it a note is refused, whichever source
|
|
30
|
+
* asks for it.
|
|
31
|
+
*
|
|
32
|
+
* @default 127
|
|
33
|
+
*/
|
|
34
|
+
midiMax?: number
|
|
35
|
+
|
|
36
|
+
/** Called when a note starts sounding, not for each source that asks. */
|
|
37
|
+
onPlayNote?: (note: number, velocity?: number) => void
|
|
38
|
+
/** Called once the last source holding a note has let go. */
|
|
39
|
+
onStopNote?: (note: number) => void
|
|
40
|
+
/** Called whenever {@link PianoInputInstance.activeNotes} would change. */
|
|
41
|
+
onActiveNotesChange?: (notes: number[]) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PianoInputInstance {
|
|
45
|
+
/**
|
|
46
|
+
* Replace the given options. Lets a wrapper feed a fresh layout in without
|
|
47
|
+
* tearing down the listeners, which would abort a drag in progress.
|
|
48
|
+
*/
|
|
49
|
+
update: (options: Partial<PianoInputOptions>) => void
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Start a note from something other than a pointer: a keyboard shortcut, a
|
|
53
|
+
* MIDI message, an imperative call.
|
|
54
|
+
*/
|
|
55
|
+
noteOn: (
|
|
56
|
+
note: number,
|
|
57
|
+
options?: { source?: NoteSource; velocity?: number },
|
|
58
|
+
) => void
|
|
59
|
+
/** Release a note held by `source`. */
|
|
60
|
+
noteOff: (note: number, options?: { source?: NoteSource }) => void
|
|
61
|
+
|
|
62
|
+
/** The notes currently sounding, ascending. */
|
|
63
|
+
activeNotes: () => number[]
|
|
64
|
+
|
|
65
|
+
destroy: () => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DEFAULT_SOURCE: NoteSource = 'api'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drive a piano keyboard with pointers, and own what is sounding.
|
|
72
|
+
*
|
|
73
|
+
* Every way of playing a note goes through the one instance — pointers here,
|
|
74
|
+
* keyboard shortcuts and MIDI through {@link PianoInputInstance.noteOn} — so
|
|
75
|
+
* there is a single answer to what is currently held, and drawing the keys is
|
|
76
|
+
* left entirely to the wrapper.
|
|
77
|
+
*
|
|
78
|
+
* Tracks every pointer at once, so a chord can be played with several fingers.
|
|
79
|
+
*/
|
|
80
|
+
export function createPianoInput(
|
|
81
|
+
element: Element,
|
|
82
|
+
options: PianoInputOptions,
|
|
83
|
+
): PianoInputInstance {
|
|
84
|
+
let opts = options
|
|
85
|
+
|
|
86
|
+
/** Which sources are holding each sounding note. */
|
|
87
|
+
const held = new Map<number, Set<NoteSource>>()
|
|
88
|
+
/** The note each pointer is currently on. */
|
|
89
|
+
const pointerNotes = new Map<number, number>()
|
|
90
|
+
|
|
91
|
+
function activeNotes(): number[] {
|
|
92
|
+
return [...held.keys()].sort((a, b) => a - b)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function noteOn(
|
|
96
|
+
note: number,
|
|
97
|
+
{
|
|
98
|
+
source = DEFAULT_SOURCE,
|
|
99
|
+
velocity,
|
|
100
|
+
}: { source?: NoteSource; velocity?: number } = {},
|
|
101
|
+
) {
|
|
102
|
+
if (note > (opts.midiMax ?? 127)) return
|
|
103
|
+
|
|
104
|
+
const sources = held.get(note)
|
|
105
|
+
if (sources) {
|
|
106
|
+
// Already sounding: remember the extra holder and leave it alone.
|
|
107
|
+
sources.add(source)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
held.set(note, new Set([source]))
|
|
112
|
+
opts.onPlayNote?.(note, velocity)
|
|
113
|
+
opts.onActiveNotesChange?.(activeNotes())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function noteOff(
|
|
117
|
+
note: number,
|
|
118
|
+
{ source = DEFAULT_SOURCE }: { source?: NoteSource } = {},
|
|
119
|
+
) {
|
|
120
|
+
const sources = held.get(note)
|
|
121
|
+
if (!sources) return
|
|
122
|
+
|
|
123
|
+
sources.delete(source)
|
|
124
|
+
if (sources.size > 0) return
|
|
125
|
+
|
|
126
|
+
held.delete(note)
|
|
127
|
+
opts.onStopNote?.(note)
|
|
128
|
+
opts.onActiveNotesChange?.(activeNotes())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The note under a pointer, or null where the pointer is off the keys. */
|
|
132
|
+
function noteUnder(clientX: number, clientY: number): number | null {
|
|
133
|
+
const { left, top, height } = element.getBoundingClientRect()
|
|
134
|
+
return noteAt(clientX - left, clientY - top, height, opts.layout)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Move a pointer onto a note, releasing whatever it held before. */
|
|
138
|
+
function movePointer(pointerId: number, note: number | null) {
|
|
139
|
+
const source = `pointer:${pointerId}`
|
|
140
|
+
const previous = pointerNotes.get(pointerId)
|
|
141
|
+
if (previous === note) return
|
|
142
|
+
|
|
143
|
+
if (previous !== undefined) noteOff(previous, { source })
|
|
144
|
+
|
|
145
|
+
if (note === null) {
|
|
146
|
+
pointerNotes.delete(pointerId)
|
|
147
|
+
} else {
|
|
148
|
+
pointerNotes.set(pointerId, note)
|
|
149
|
+
noteOn(note, { source })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const drag = createDrag(element, {
|
|
154
|
+
multiPointer: true,
|
|
155
|
+
onDragStart: (state) =>
|
|
156
|
+
movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),
|
|
157
|
+
onDrag: (state) => {
|
|
158
|
+
// Without glissando the pressed key holds until the pointer is released,
|
|
159
|
+
// so where the pointer wanders to does not matter.
|
|
160
|
+
if (opts.glissando === false) return
|
|
161
|
+
movePointer(state.pointerId, noteUnder(state.clientX, state.clientY))
|
|
162
|
+
},
|
|
163
|
+
onDragEnd: (state) => movePointer(state.pointerId, null),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
update: (next) => {
|
|
168
|
+
opts = { ...opts, ...next }
|
|
169
|
+
},
|
|
170
|
+
noteOn,
|
|
171
|
+
noteOff,
|
|
172
|
+
activeNotes,
|
|
173
|
+
destroy: () => {
|
|
174
|
+
drag.destroy()
|
|
175
|
+
// Anything still held is released, so a caller that mirrors these
|
|
176
|
+
// callbacks into a synth is not left with a stuck note.
|
|
177
|
+
for (const note of activeNotes()) {
|
|
178
|
+
held.delete(note)
|
|
179
|
+
opts.onStopNote?.(note)
|
|
180
|
+
}
|
|
181
|
+
pointerNotes.clear()
|
|
182
|
+
opts.onActiveNotesChange?.([])
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
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,38 @@ 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
|
+
* Track every pointer that goes down on the element, rather than only the
|
|
43
|
+
* first. Each one gets its own `onDragStart` / `onDrag` / `onDragEnd` and
|
|
44
|
+
* carries its own totals; {@link DragState.pointerId} says which is which.
|
|
45
|
+
*
|
|
46
|
+
* Fixed for the lifetime of the instance: switching part way through a drag
|
|
47
|
+
* has no meaning, so `update()` ignores it.
|
|
48
|
+
*
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
multiPointer?: boolean
|
|
52
|
+
|
|
31
53
|
onDragStart?: (state: DragState) => void
|
|
32
54
|
onDrag?: (state: DragState) => void
|
|
33
55
|
onDragEnd?: (state: DragState) => void
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
export interface DragInstance {
|
|
59
|
+
/**
|
|
60
|
+
* Replace the given options. Lets a wrapper feed fresh handlers in without
|
|
61
|
+
* tearing down the listeners, which would abort a drag in progress.
|
|
62
|
+
*
|
|
63
|
+
* `multiPointer` is fixed for the lifetime of the instance and is ignored
|
|
64
|
+
* here.
|
|
65
|
+
*/
|
|
66
|
+
update: (options: DragOptions) => void
|
|
37
67
|
destroy: () => void
|
|
38
68
|
}
|
|
39
69
|
|
|
@@ -57,6 +87,16 @@ type CaptureTarget = {
|
|
|
57
87
|
hasPointerCapture?: (pointerId: number) => boolean
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
/** What one pointer needs to report its own movement. */
|
|
91
|
+
type PointerState = {
|
|
92
|
+
/** Where the listeners for this pointer live. */
|
|
93
|
+
moveTarget: EventTarget
|
|
94
|
+
startX: number
|
|
95
|
+
startY: number
|
|
96
|
+
lastX: number
|
|
97
|
+
lastY: number
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
/**
|
|
61
101
|
* Track a pointer drag on an element.
|
|
62
102
|
*
|
|
@@ -65,18 +105,15 @@ type CaptureTarget = {
|
|
|
65
105
|
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
66
106
|
* page, plus `user-select: none` so that a long press does not start a text
|
|
67
107
|
* selection instead.
|
|
108
|
+
*
|
|
109
|
+
* One pointer at a time by default; see {@link DragOptions.multiPointer}.
|
|
68
110
|
*/
|
|
69
111
|
export function createDrag(
|
|
70
112
|
element: Element,
|
|
71
|
-
{
|
|
72
|
-
threshold: _threshold = 1,
|
|
73
|
-
cursor,
|
|
74
|
-
onDragStart,
|
|
75
|
-
onDrag,
|
|
76
|
-
onDragEnd,
|
|
77
|
-
}: DragOptions = {},
|
|
113
|
+
options: DragOptions = {},
|
|
78
114
|
): DragInstance {
|
|
79
|
-
|
|
115
|
+
let opts = options
|
|
116
|
+
const multiPointer = options.multiPointer ?? false
|
|
80
117
|
const capture = element as CaptureTarget
|
|
81
118
|
const style = (element as Partial<HTMLElement>).style
|
|
82
119
|
|
|
@@ -88,27 +125,29 @@ export function createDrag(
|
|
|
88
125
|
}
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
const pointers = new Map<number, PointerState>()
|
|
129
|
+
/**
|
|
130
|
+
* How many pointers each target carries. Adding the same listener twice is a
|
|
131
|
+
* no-op and removing it once removes it for good, so a target is subscribed
|
|
132
|
+
* to while at least one pointer is on it and no longer.
|
|
133
|
+
*/
|
|
134
|
+
const targets = new Map<EventTarget, number>()
|
|
98
135
|
let previousCursor: string | undefined
|
|
99
136
|
|
|
100
137
|
function state(
|
|
101
138
|
event: PointerEvent,
|
|
139
|
+
pointer: PointerState,
|
|
102
140
|
deltaX: number,
|
|
103
141
|
deltaY: number,
|
|
104
142
|
): DragState {
|
|
105
143
|
return {
|
|
106
|
-
x: event.screenX - startX,
|
|
107
|
-
y: event.screenY - startY,
|
|
144
|
+
x: event.screenX - pointer.startX,
|
|
145
|
+
y: event.screenY - pointer.startY,
|
|
108
146
|
deltaX,
|
|
109
147
|
deltaY,
|
|
110
148
|
clientX: event.clientX,
|
|
111
149
|
clientY: event.clientY,
|
|
150
|
+
pointerId: event.pointerId,
|
|
112
151
|
event,
|
|
113
152
|
}
|
|
114
153
|
}
|
|
@@ -122,83 +161,124 @@ export function createDrag(
|
|
|
122
161
|
event.preventDefault()
|
|
123
162
|
}
|
|
124
163
|
|
|
164
|
+
function retainTarget(target: EventTarget) {
|
|
165
|
+
const count = targets.get(target) ?? 0
|
|
166
|
+
if (count === 0) {
|
|
167
|
+
target.addEventListener('pointermove', handlePointerMove)
|
|
168
|
+
target.addEventListener('pointerup', handlePointerUp)
|
|
169
|
+
target.addEventListener('pointercancel', handlePointerUp)
|
|
170
|
+
}
|
|
171
|
+
targets.set(target, count + 1)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function releaseTarget(target: EventTarget) {
|
|
175
|
+
const count = targets.get(target) ?? 0
|
|
176
|
+
if (count > 1) {
|
|
177
|
+
targets.set(target, count - 1)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
target.removeEventListener('pointermove', handlePointerMove)
|
|
181
|
+
target.removeEventListener('pointerup', handlePointerUp)
|
|
182
|
+
target.removeEventListener('pointercancel', handlePointerUp)
|
|
183
|
+
targets.delete(target)
|
|
184
|
+
}
|
|
185
|
+
|
|
125
186
|
function handlePointerDown(event: Event) {
|
|
126
187
|
const pointerEvent = event as PointerEvent
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
startX = lastX = pointerEvent.screenX
|
|
132
|
-
startY = lastY = pointerEvent.screenY
|
|
188
|
+
const pointerId = pointerEvent.pointerId
|
|
189
|
+
// Without multiPointer only one pointer drives the drag; ignore the rest.
|
|
190
|
+
if (pointers.has(pointerId)) return
|
|
191
|
+
if (!multiPointer && pointers.size > 0) return
|
|
133
192
|
|
|
134
|
-
|
|
193
|
+
const isFirst = pointers.size === 0
|
|
194
|
+
if (isFirst && opts.cursor && style) {
|
|
135
195
|
previousCursor = style.cursor
|
|
136
|
-
style.cursor = cursor
|
|
196
|
+
style.cursor = opts.cursor
|
|
137
197
|
}
|
|
138
198
|
|
|
139
199
|
capture.setPointerCapture?.(pointerId)
|
|
140
200
|
// With pointer capture the element receives the rest of the gesture.
|
|
141
201
|
// Without it (older engines, jsdom) fall back to the window.
|
|
142
|
-
moveTarget =
|
|
202
|
+
const moveTarget =
|
|
143
203
|
capture.hasPointerCapture?.(pointerId) === true
|
|
144
204
|
? element
|
|
145
205
|
: (globalThis.window ?? element)
|
|
146
206
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
207
|
+
const pointer: PointerState = {
|
|
208
|
+
moveTarget,
|
|
209
|
+
startX: pointerEvent.screenX,
|
|
210
|
+
startY: pointerEvent.screenY,
|
|
211
|
+
lastX: pointerEvent.screenX,
|
|
212
|
+
lastY: pointerEvent.screenY,
|
|
213
|
+
}
|
|
214
|
+
pointers.set(pointerId, pointer)
|
|
215
|
+
|
|
216
|
+
retainTarget(moveTarget)
|
|
217
|
+
if (isFirst) {
|
|
218
|
+
globalThis.document?.addEventListener('selectstart', preventSelectStart)
|
|
219
|
+
}
|
|
151
220
|
|
|
152
|
-
onDragStart?.(state(pointerEvent, 0, 0))
|
|
221
|
+
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0))
|
|
153
222
|
}
|
|
154
223
|
|
|
155
224
|
function handlePointerMove(event: Event) {
|
|
156
225
|
const pointerEvent = event as PointerEvent
|
|
157
|
-
|
|
226
|
+
const pointer = pointers.get(pointerEvent.pointerId)
|
|
227
|
+
if (!pointer) return
|
|
158
228
|
|
|
159
|
-
const deltaX = pointerEvent.screenX - lastX
|
|
160
|
-
const deltaY = pointerEvent.screenY - lastY
|
|
161
|
-
lastX = pointerEvent.screenX
|
|
162
|
-
lastY = pointerEvent.screenY
|
|
229
|
+
const deltaX = pointerEvent.screenX - pointer.lastX
|
|
230
|
+
const deltaY = pointerEvent.screenY - pointer.lastY
|
|
163
231
|
|
|
164
|
-
// Movement below the threshold
|
|
232
|
+
// Movement below the threshold accumulates until it crosses it. Dropping it
|
|
233
|
+
// instead would swallow a slow drag entirely: pointer coordinates are
|
|
234
|
+
// fractional, so each event can move less than a pixel and never report.
|
|
235
|
+
const threshold = opts.threshold ?? 0
|
|
165
236
|
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return
|
|
166
237
|
|
|
167
|
-
|
|
238
|
+
pointer.lastX = pointerEvent.screenX
|
|
239
|
+
pointer.lastY = pointerEvent.screenY
|
|
240
|
+
|
|
241
|
+
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY))
|
|
168
242
|
}
|
|
169
243
|
|
|
170
244
|
function handlePointerUp(event: Event) {
|
|
171
245
|
const pointerEvent = event as PointerEvent
|
|
172
|
-
|
|
246
|
+
const pointer = pointers.get(pointerEvent.pointerId)
|
|
247
|
+
if (!pointer) return
|
|
173
248
|
|
|
174
|
-
const deltaX = pointerEvent.screenX - lastX
|
|
175
|
-
const deltaY = pointerEvent.screenY - lastY
|
|
176
|
-
const finalState = state(pointerEvent, deltaX, deltaY)
|
|
249
|
+
const deltaX = pointerEvent.screenX - pointer.lastX
|
|
250
|
+
const deltaY = pointerEvent.screenY - pointer.lastY
|
|
251
|
+
const finalState = state(pointerEvent, pointer, deltaX, deltaY)
|
|
177
252
|
|
|
178
|
-
stopTracking()
|
|
179
|
-
onDragEnd?.(finalState)
|
|
253
|
+
stopTracking(pointerEvent.pointerId)
|
|
254
|
+
opts.onDragEnd?.(finalState)
|
|
180
255
|
}
|
|
181
256
|
|
|
182
|
-
function stopTracking() {
|
|
183
|
-
|
|
257
|
+
function stopTracking(pointerId: number) {
|
|
258
|
+
const pointer = pointers.get(pointerId)
|
|
259
|
+
if (!pointer) return
|
|
260
|
+
|
|
184
261
|
capture.releasePointerCapture?.(pointerId)
|
|
185
|
-
moveTarget
|
|
186
|
-
|
|
187
|
-
|
|
262
|
+
releaseTarget(pointer.moveTarget)
|
|
263
|
+
pointers.delete(pointerId)
|
|
264
|
+
|
|
265
|
+
if (pointers.size > 0) return
|
|
266
|
+
|
|
188
267
|
globalThis.document?.removeEventListener('selectstart', preventSelectStart)
|
|
189
|
-
if (
|
|
190
|
-
style.cursor = previousCursor
|
|
268
|
+
if (previousCursor !== undefined && style) {
|
|
269
|
+
style.cursor = previousCursor
|
|
191
270
|
previousCursor = undefined
|
|
192
271
|
}
|
|
193
|
-
moveTarget = null
|
|
194
|
-
pointerId = null
|
|
195
272
|
}
|
|
196
273
|
|
|
197
274
|
element.addEventListener('pointerdown', handlePointerDown)
|
|
198
275
|
|
|
199
276
|
return {
|
|
277
|
+
update: (next) => {
|
|
278
|
+
opts = { ...opts, ...next, multiPointer }
|
|
279
|
+
},
|
|
200
280
|
destroy: () => {
|
|
201
|
-
stopTracking()
|
|
281
|
+
for (const pointerId of [...pointers.keys()]) stopTracking(pointerId)
|
|
202
282
|
element.removeEventListener('pointerdown', handlePointerDown)
|
|
203
283
|
if (style) {
|
|
204
284
|
for (const [property] of MANAGED_STYLES) {
|