@tremolo-ui/dom 0.5.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 +459 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -39
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +234 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +457 -102
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/canvas/animation.ts +49 -31
- package/src/canvas/context.ts +32 -5
- package/src/index.ts +13 -2
- package/src/midi/access.ts +83 -13
- package/src/midi/input.ts +97 -26
- package/src/midi/message.ts +37 -8
- package/src/piano/{input.ts → index.ts} +23 -3
- package/src/pointer/{dragValue.ts → drag-value.ts} +155 -19
- package/src/pointer/drag.ts +215 -20
- package/src/pointer/wheel.ts +4 -2
- package/src/selection/box.ts +136 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
linearScale,
|
|
4
4
|
normalizeValue,
|
|
5
5
|
stepValue,
|
|
6
|
+
toPrecision,
|
|
6
7
|
type ValueRange,
|
|
7
8
|
} from '@tremolo-ui/functions'
|
|
8
9
|
|
|
@@ -59,7 +60,32 @@ export interface DragValueMapping {
|
|
|
59
60
|
*/
|
|
60
61
|
export function elementMapping(
|
|
61
62
|
getElement: () => Element | null | undefined,
|
|
63
|
+
{
|
|
64
|
+
sensitivity,
|
|
65
|
+
}: {
|
|
66
|
+
/**
|
|
67
|
+
* How much the movement counts, read on every move. `1` is the pointer
|
|
68
|
+
* position itself; `0.1` makes the same movement cover a tenth of the
|
|
69
|
+
* travel, which is what a fine-adjustment modifier wants.
|
|
70
|
+
*
|
|
71
|
+
* Anything but `1` turns the mapping relative: the value stops being the
|
|
72
|
+
* position pointed at and starts being where it stood when the sensitivity
|
|
73
|
+
* changed, plus the movement since. **The pointer and the value stay apart
|
|
74
|
+
* for the rest of the drag** rather than snapping back together when the
|
|
75
|
+
* key is released, since snapping would move the value nobody asked to
|
|
76
|
+
* move.
|
|
77
|
+
*/
|
|
78
|
+
sensitivity?: (state: DragState) => number
|
|
79
|
+
} = {},
|
|
62
80
|
): DragValueMapping {
|
|
81
|
+
/** The reported position when the sensitivity last changed. */
|
|
82
|
+
let origin: XY<number> = [0, 0]
|
|
83
|
+
/** The raw position at that same moment. */
|
|
84
|
+
let anchor: XY<number> = [0, 0]
|
|
85
|
+
/** The previous event, so a sensitivity change can be dated back to it. */
|
|
86
|
+
let previous: XY<number> = [0, 0]
|
|
87
|
+
let factor = 1
|
|
88
|
+
|
|
63
89
|
function positionIn(state: DragState): XY<number> | null {
|
|
64
90
|
const element = getElement()
|
|
65
91
|
if (!element) return null
|
|
@@ -72,7 +98,48 @@ export function elementMapping(
|
|
|
72
98
|
]
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
|
|
101
|
+
const reported = (raw: XY<number>, at: number): XY<number> => [
|
|
102
|
+
origin[0] + (raw[0] - anchor[0]) * at,
|
|
103
|
+
origin[1] + (raw[1] - anchor[1]) * at,
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
start: (state) => {
|
|
108
|
+
const raw = positionIn(state)
|
|
109
|
+
if (!raw) return null
|
|
110
|
+
// origin and anchor together, so the value is the position pointed at:
|
|
111
|
+
// a plain click still lands where it was aimed.
|
|
112
|
+
origin = raw
|
|
113
|
+
anchor = raw
|
|
114
|
+
previous = raw
|
|
115
|
+
// Read here too, so a modifier already held when the pointer went down
|
|
116
|
+
// applies from the first pixel rather than from the first change.
|
|
117
|
+
factor = sensitivity ? sensitivity(state) : 1
|
|
118
|
+
return raw
|
|
119
|
+
},
|
|
120
|
+
move: (state) => {
|
|
121
|
+
const raw = positionIn(state)
|
|
122
|
+
if (!raw) return null
|
|
123
|
+
|
|
124
|
+
const next = sensitivity ? sensitivity(state) : 1
|
|
125
|
+
if (next !== factor) {
|
|
126
|
+
// Fold the travel so far into the origin, or the new sensitivity would
|
|
127
|
+
// apply to the whole drag and the value would jump.
|
|
128
|
+
//
|
|
129
|
+
// Dated to the previous event rather than this one: a key produces no
|
|
130
|
+
// pointer event of its own, so the change is only seen on the next
|
|
131
|
+
// move, and that move's own distance belongs to the new sensitivity.
|
|
132
|
+
origin = reported(previous, factor)
|
|
133
|
+
anchor = previous
|
|
134
|
+
factor = next
|
|
135
|
+
}
|
|
136
|
+
previous = raw
|
|
137
|
+
|
|
138
|
+
// With no sensitivity given, origin and anchor never move apart and this
|
|
139
|
+
// is the raw position, exactly as before.
|
|
140
|
+
return reported(raw, factor)
|
|
141
|
+
},
|
|
142
|
+
}
|
|
76
143
|
}
|
|
77
144
|
|
|
78
145
|
/**
|
|
@@ -82,27 +149,65 @@ export function elementMapping(
|
|
|
82
149
|
*/
|
|
83
150
|
export function relativeMapping({
|
|
84
151
|
pixelRange = 100,
|
|
152
|
+
sensitivity,
|
|
85
153
|
}: {
|
|
86
154
|
/**
|
|
87
155
|
* Pixels of movement that span the whole range.
|
|
88
156
|
* @default 100
|
|
89
157
|
*/
|
|
90
158
|
pixelRange?: XYInput<number>
|
|
159
|
+
/**
|
|
160
|
+
* How much the movement counts, read on every move. `1` is `pixelRange` as
|
|
161
|
+
* given; `0.1` makes the same movement cover a tenth of the range, which is
|
|
162
|
+
* what a fine-adjustment modifier wants.
|
|
163
|
+
*
|
|
164
|
+
* Changing it mid-drag does not disturb the value: the travel so far is
|
|
165
|
+
* folded into the origin and measuring starts again from there.
|
|
166
|
+
*/
|
|
167
|
+
sensitivity?: (state: DragState) => number
|
|
91
168
|
} = {}): DragValueMapping {
|
|
92
|
-
const [
|
|
169
|
+
const [baseX, baseY] = toXY(pixelRange)
|
|
93
170
|
let origin: XY<number> = [0, 0]
|
|
171
|
+
/** Where the current sensitivity took over, in drag coordinates. */
|
|
172
|
+
let anchor: XY<number> = [0, 0]
|
|
173
|
+
/** The previous event, so a sensitivity change can be dated back to it. */
|
|
174
|
+
let previous: XY<number> = [0, 0]
|
|
175
|
+
let factor = 1
|
|
176
|
+
|
|
177
|
+
const travelled = (to: XY<number>, at: number): XY<number> => [
|
|
178
|
+
baseX === 0 ? origin[0] : origin[0] + ((to[0] - anchor[0]) * at) / baseX,
|
|
179
|
+
baseY === 0 ? origin[1] : origin[1] + ((to[1] - anchor[1]) * at) / baseY,
|
|
180
|
+
]
|
|
94
181
|
|
|
95
182
|
return {
|
|
96
|
-
start: (
|
|
183
|
+
start: (state, context) => {
|
|
97
184
|
origin = context.position()
|
|
185
|
+
anchor = [0, 0]
|
|
186
|
+
previous = [0, 0]
|
|
187
|
+
// Read here too, so a modifier already held when the pointer went down
|
|
188
|
+
// applies from the first pixel rather than from the first change.
|
|
189
|
+
factor = sensitivity ? sensitivity(state) : 1
|
|
98
190
|
return origin
|
|
99
191
|
},
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
// Measured from the anchor rather than accumulated per event, so the
|
|
193
|
+
// position picks up no rounding error of its own. The anchor only moves
|
|
194
|
+
// when the sensitivity does, which is a handful of times per drag at most.
|
|
195
|
+
move: (state) => {
|
|
196
|
+
const next = sensitivity ? sensitivity(state) : 1
|
|
197
|
+
if (next !== factor) {
|
|
198
|
+
// Fold the travel so far into the origin, or the new sensitivity would
|
|
199
|
+
// apply to the whole drag and the value would jump.
|
|
200
|
+
//
|
|
201
|
+
// Dated to the previous event rather than this one: a key produces no
|
|
202
|
+
// pointer event of its own, so the change is only seen on the next
|
|
203
|
+
// move, and that move's own distance belongs to the new sensitivity.
|
|
204
|
+
origin = travelled(previous, factor)
|
|
205
|
+
anchor = previous
|
|
206
|
+
factor = next
|
|
207
|
+
}
|
|
208
|
+
previous = [state.x, state.y]
|
|
209
|
+
return travelled([state.x, state.y], factor)
|
|
210
|
+
},
|
|
106
211
|
}
|
|
107
212
|
}
|
|
108
213
|
|
|
@@ -135,6 +240,16 @@ export interface DragValueOptions {
|
|
|
135
240
|
threshold?: number
|
|
136
241
|
/** @see DragOptions.cursor */
|
|
137
242
|
cursor?: string
|
|
243
|
+
/**
|
|
244
|
+
* @see DragOptions.pointerLock
|
|
245
|
+
*
|
|
246
|
+
* Only for a mapping that moves the value relative to where it stood, such
|
|
247
|
+
* as {@link relativeMapping}. {@link elementMapping} reads the pointer
|
|
248
|
+
* position, and there is none while it is locked.
|
|
249
|
+
*/
|
|
250
|
+
pointerLock?: boolean
|
|
251
|
+
/** @see DragOptions.shouldStart */
|
|
252
|
+
shouldStart?: (event: PointerEvent) => boolean
|
|
138
253
|
|
|
139
254
|
onChange?: (value: XY<number>, state: DragState) => void
|
|
140
255
|
onDragStart?: (value: XY<number>, state: DragState) => void
|
|
@@ -165,6 +280,7 @@ export function createDragValue(
|
|
|
165
280
|
): DragValueInstance {
|
|
166
281
|
let opts = options
|
|
167
282
|
let lastValue: XY<number> = [0, 0]
|
|
283
|
+
let active = false
|
|
168
284
|
|
|
169
285
|
const axes = () => toXY(opts.axis)
|
|
170
286
|
|
|
@@ -175,8 +291,9 @@ export function createDragValue(
|
|
|
175
291
|
const scale = axis.scale ?? linearScale
|
|
176
292
|
const value = scale.denormalize(p, axis.min, axis.max)
|
|
177
293
|
const stepped = axis.step ? stepValue(value, axis.step) : value
|
|
178
|
-
// Rounding to the step can leave the range
|
|
179
|
-
|
|
294
|
+
// Rounding to the step can leave the range, and so can dropping the
|
|
295
|
+
// binary artefact, so the clamp comes last.
|
|
296
|
+
return clamp(toPrecision(stepped), axis.min, axis.max)
|
|
180
297
|
}) as XY<number>
|
|
181
298
|
}
|
|
182
299
|
|
|
@@ -200,29 +317,48 @@ export function createDragValue(
|
|
|
200
317
|
const drag = createDrag(element, {
|
|
201
318
|
threshold: opts.threshold,
|
|
202
319
|
cursor: opts.cursor,
|
|
320
|
+
pointerLock: opts.pointerLock,
|
|
321
|
+
shouldStart: (event) => opts.shouldStart?.(event) ?? true,
|
|
203
322
|
onDragStart: (state) => {
|
|
204
323
|
const position = opts.mapping.start(state, context)
|
|
205
|
-
if (position)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
324
|
+
if (!position) return
|
|
325
|
+
active = true
|
|
326
|
+
lastValue = valueOf(position)
|
|
327
|
+
if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state)
|
|
209
328
|
opts.onDragStart?.(lastValue, state)
|
|
210
329
|
},
|
|
211
330
|
onDrag: (state) => {
|
|
331
|
+
if (!active) return
|
|
212
332
|
const position = opts.mapping.move(state, context)
|
|
213
333
|
if (!position) return
|
|
214
334
|
lastValue = valueOf(position)
|
|
215
335
|
opts.onChange?.(lastValue, state)
|
|
216
336
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
337
|
+
onDragEnd: (state) => {
|
|
338
|
+
if (!active) return
|
|
339
|
+
if (
|
|
340
|
+
state.event.type === 'pointerup' &&
|
|
341
|
+
(state.deltaX !== 0 || state.deltaY !== 0)
|
|
342
|
+
) {
|
|
343
|
+
const position = opts.mapping.move(state, context)
|
|
344
|
+
if (position) {
|
|
345
|
+
lastValue = valueOf(position)
|
|
346
|
+
opts.onChange?.(lastValue, state)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
active = false
|
|
350
|
+
opts.onDragEnd?.(lastValue, state)
|
|
351
|
+
},
|
|
220
352
|
})
|
|
221
353
|
|
|
222
354
|
return {
|
|
223
355
|
update: (next) => {
|
|
224
356
|
opts = { ...opts, ...next, mapping: opts.mapping }
|
|
225
|
-
drag.update({
|
|
357
|
+
drag.update({
|
|
358
|
+
threshold: opts.threshold,
|
|
359
|
+
cursor: opts.cursor,
|
|
360
|
+
pointerLock: opts.pointerLock,
|
|
361
|
+
})
|
|
226
362
|
},
|
|
227
363
|
destroy: () => drag.destroy(),
|
|
228
364
|
}
|
package/src/pointer/drag.ts
CHANGED
|
@@ -38,6 +38,42 @@ export type DragOptions = {
|
|
|
38
38
|
*/
|
|
39
39
|
cursor?: string
|
|
40
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
|
+
|
|
41
77
|
/**
|
|
42
78
|
* Track every pointer that goes down on the element, rather than only the
|
|
43
79
|
* first. Each one gets its own `onDragStart` / `onDrag` / `onDragEnd` and
|
|
@@ -52,6 +88,10 @@ export type DragOptions = {
|
|
|
52
88
|
|
|
53
89
|
onDragStart?: (state: DragState) => void
|
|
54
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
|
+
*/
|
|
55
95
|
onDragEnd?: (state: DragState) => void
|
|
56
96
|
}
|
|
57
97
|
|
|
@@ -64,6 +104,7 @@ export interface DragInstance {
|
|
|
64
104
|
* here.
|
|
65
105
|
*/
|
|
66
106
|
update: (options: DragOptions) => void
|
|
107
|
+
/** End any active drags before removing the instance. */
|
|
67
108
|
destroy: () => void
|
|
68
109
|
}
|
|
69
110
|
|
|
@@ -81,7 +122,15 @@ const MANAGED_STYLES = [
|
|
|
81
122
|
['-webkit-touch-callout', 'none'],
|
|
82
123
|
] as const
|
|
83
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
|
+
|
|
84
132
|
type CaptureTarget = {
|
|
133
|
+
requestPointerLock?: () => unknown
|
|
85
134
|
setPointerCapture?: (pointerId: number) => void
|
|
86
135
|
releasePointerCapture?: (pointerId: number) => void
|
|
87
136
|
hasPointerCapture?: (pointerId: number) => boolean
|
|
@@ -95,6 +144,21 @@ type PointerState = {
|
|
|
95
144
|
startY: number
|
|
96
145
|
lastX: number
|
|
97
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
|
|
98
162
|
}
|
|
99
163
|
|
|
100
164
|
/**
|
|
@@ -117,11 +181,21 @@ export function createDrag(
|
|
|
117
181
|
const capture = element as CaptureTarget
|
|
118
182
|
const style = (element as Partial<HTMLElement>).style
|
|
119
183
|
|
|
120
|
-
|
|
184
|
+
let managedStyles = managedStyleStates.get(element)
|
|
121
185
|
if (style) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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)
|
|
125
199
|
}
|
|
126
200
|
}
|
|
127
201
|
|
|
@@ -133,6 +207,10 @@ export function createDrag(
|
|
|
133
207
|
*/
|
|
134
208
|
const targets = new Map<EventTarget, number>()
|
|
135
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
|
|
136
214
|
|
|
137
215
|
function state(
|
|
138
216
|
event: PointerEvent,
|
|
@@ -141,8 +219,14 @@ export function createDrag(
|
|
|
141
219
|
deltaY: number,
|
|
142
220
|
): DragState {
|
|
143
221
|
return {
|
|
144
|
-
x:
|
|
145
|
-
|
|
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,
|
|
146
230
|
deltaX,
|
|
147
231
|
deltaY,
|
|
148
232
|
clientX: event.clientX,
|
|
@@ -167,6 +251,9 @@ export function createDrag(
|
|
|
167
251
|
target.addEventListener('pointermove', handlePointerMove)
|
|
168
252
|
target.addEventListener('pointerup', handlePointerUp)
|
|
169
253
|
target.addEventListener('pointercancel', handlePointerUp)
|
|
254
|
+
if (target === element) {
|
|
255
|
+
target.addEventListener('lostpointercapture', handleLostPointerCapture)
|
|
256
|
+
}
|
|
170
257
|
}
|
|
171
258
|
targets.set(target, count + 1)
|
|
172
259
|
}
|
|
@@ -180,16 +267,22 @@ export function createDrag(
|
|
|
180
267
|
target.removeEventListener('pointermove', handlePointerMove)
|
|
181
268
|
target.removeEventListener('pointerup', handlePointerUp)
|
|
182
269
|
target.removeEventListener('pointercancel', handlePointerUp)
|
|
270
|
+
if (target === element) {
|
|
271
|
+
target.removeEventListener('lostpointercapture', handleLostPointerCapture)
|
|
272
|
+
}
|
|
183
273
|
targets.delete(target)
|
|
184
274
|
}
|
|
185
275
|
|
|
186
276
|
function handlePointerDown(event: Event) {
|
|
187
277
|
const pointerEvent = event as PointerEvent
|
|
278
|
+
if (pointerEvent.button !== 0) return
|
|
188
279
|
const pointerId = pointerEvent.pointerId
|
|
189
280
|
// Without multiPointer only one pointer drives the drag; ignore the rest.
|
|
190
281
|
if (pointers.has(pointerId)) return
|
|
191
282
|
if (!multiPointer && pointers.size > 0) return
|
|
192
283
|
|
|
284
|
+
if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return
|
|
285
|
+
|
|
193
286
|
const isFirst = pointers.size === 0
|
|
194
287
|
if (isFirst && opts.cursor && style) {
|
|
195
288
|
previousCursor = style.cursor
|
|
@@ -210,6 +303,9 @@ export function createDrag(
|
|
|
210
303
|
startY: pointerEvent.screenY,
|
|
211
304
|
lastX: pointerEvent.screenX,
|
|
212
305
|
lastY: pointerEvent.screenY,
|
|
306
|
+
lockMoveX: 0,
|
|
307
|
+
lockMoveY: 0,
|
|
308
|
+
lastEvent: pointerEvent,
|
|
213
309
|
}
|
|
214
310
|
pointers.set(pointerId, pointer)
|
|
215
311
|
|
|
@@ -218,6 +314,38 @@ export function createDrag(
|
|
|
218
314
|
globalThis.document?.addEventListener('selectstart', preventSelectStart)
|
|
219
315
|
}
|
|
220
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
|
+
|
|
221
349
|
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0))
|
|
222
350
|
}
|
|
223
351
|
|
|
@@ -226,8 +354,15 @@ export function createDrag(
|
|
|
226
354
|
const pointer = pointers.get(pointerEvent.pointerId)
|
|
227
355
|
if (!pointer) return
|
|
228
356
|
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
231
366
|
|
|
232
367
|
// Movement below the threshold accumulates until it crosses it. Dropping it
|
|
233
368
|
// instead would swallow a slow drag entirely: pointer coordinates are
|
|
@@ -237,20 +372,62 @@ export function createDrag(
|
|
|
237
372
|
|
|
238
373
|
pointer.lastX = pointerEvent.screenX
|
|
239
374
|
pointer.lastY = pointerEvent.screenY
|
|
375
|
+
pointer.lastEvent = pointerEvent
|
|
376
|
+
if (locked) {
|
|
377
|
+
pointer.lockMoveX += deltaX
|
|
378
|
+
pointer.lockMoveY += deltaY
|
|
379
|
+
}
|
|
240
380
|
|
|
241
381
|
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY))
|
|
242
382
|
}
|
|
243
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)
|
|
405
|
+
}
|
|
406
|
+
|
|
244
407
|
function handlePointerUp(event: Event) {
|
|
245
408
|
const pointerEvent = event as PointerEvent
|
|
246
|
-
|
|
409
|
+
finishDrag(pointerEvent.pointerId, pointerEvent)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function handleLostPointerCapture(event: Event) {
|
|
413
|
+
const pointerEvent = event as PointerEvent
|
|
414
|
+
finishDrag(pointerEvent.pointerId)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function finishDrag(pointerId: number, event?: PointerEvent) {
|
|
418
|
+
const pointer = pointers.get(pointerId)
|
|
247
419
|
if (!pointer) return
|
|
248
420
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
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)
|
|
252
429
|
|
|
253
|
-
stopTracking(
|
|
430
|
+
stopTracking(pointerId)
|
|
254
431
|
opts.onDragEnd?.(finalState)
|
|
255
432
|
}
|
|
256
433
|
|
|
@@ -258,9 +435,24 @@ export function createDrag(
|
|
|
258
435
|
const pointer = pointers.get(pointerId)
|
|
259
436
|
if (!pointer) return
|
|
260
437
|
|
|
261
|
-
capture.releasePointerCapture?.(pointerId)
|
|
262
|
-
releaseTarget(pointer.moveTarget)
|
|
263
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
|
+
}
|
|
264
456
|
|
|
265
457
|
if (pointers.size > 0) return
|
|
266
458
|
|
|
@@ -278,17 +470,20 @@ export function createDrag(
|
|
|
278
470
|
opts = { ...opts, ...next, multiPointer }
|
|
279
471
|
},
|
|
280
472
|
destroy: () => {
|
|
281
|
-
|
|
473
|
+
if (destroyed) return
|
|
474
|
+
destroyed = true
|
|
282
475
|
element.removeEventListener('pointerdown', handlePointerDown)
|
|
283
|
-
|
|
476
|
+
for (const pointerId of [...pointers.keys()]) finishDrag(pointerId)
|
|
477
|
+
if (style && managedStyles && --managedStyles.count === 0) {
|
|
284
478
|
for (const [property] of MANAGED_STYLES) {
|
|
285
|
-
const previous =
|
|
286
|
-
if (previous) {
|
|
287
|
-
style.setProperty(property, previous)
|
|
479
|
+
const previous = managedStyles.previous.get(property)
|
|
480
|
+
if (previous?.value) {
|
|
481
|
+
style.setProperty(property, previous.value, previous.priority)
|
|
288
482
|
} else {
|
|
289
483
|
style.removeProperty(property)
|
|
290
484
|
}
|
|
291
485
|
}
|
|
486
|
+
managedStyleStates.delete(element)
|
|
292
487
|
}
|
|
293
488
|
},
|
|
294
489
|
}
|
package/src/pointer/wheel.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export interface WheelOptions {
|
|
2
|
+
/** Replace the callback through {@link WheelInstance.update}. */
|
|
3
|
+
onWheel?: (event: WheelEvent) => void
|
|
2
4
|
/**
|
|
3
5
|
* Only report events while the focus is inside the element.
|
|
4
6
|
*
|
|
@@ -32,7 +34,7 @@ export function createWheel(
|
|
|
32
34
|
onWheel: (event: WheelEvent) => void,
|
|
33
35
|
options: WheelOptions = {},
|
|
34
36
|
): WheelInstance {
|
|
35
|
-
let opts = options
|
|
37
|
+
let opts = { ...options, onWheel }
|
|
36
38
|
|
|
37
39
|
function hasFocus() {
|
|
38
40
|
const active = element.ownerDocument?.activeElement
|
|
@@ -41,7 +43,7 @@ export function createWheel(
|
|
|
41
43
|
|
|
42
44
|
const handler = (event: Event) => {
|
|
43
45
|
if (opts.requireFocus && !hasFocus()) return
|
|
44
|
-
onWheel(event as WheelEvent)
|
|
46
|
+
opts.onWheel?.(event as WheelEvent)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
element.addEventListener('wheel', handler, { passive: false })
|