@tremolo-ui/dom 0.3.0 → 0.4.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 +137 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/pointer/drag.ts +215 -0
- package/src/pointer/wheel.ts +25 -0
package/dist/index.cjs
CHANGED
|
@@ -97,10 +97,147 @@ function createMIDIInput(midiAccess, { onNoteOnEvent, onNoteOffEvent, onPitchBen
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
//#endregion
|
|
100
|
+
//#region src/pointer/drag.ts
|
|
101
|
+
/**
|
|
102
|
+
* Applied to the element for the lifetime of the instance.
|
|
103
|
+
*
|
|
104
|
+
* `touch-action` stops touch dragging from scrolling the page. That alone makes
|
|
105
|
+
* the browser treat a long press as the start of a text selection, so selection
|
|
106
|
+
* and the iOS callout are suppressed here too.
|
|
107
|
+
*/
|
|
108
|
+
const MANAGED_STYLES = [
|
|
109
|
+
["touch-action", "none"],
|
|
110
|
+
["user-select", "none"],
|
|
111
|
+
["-webkit-user-select", "none"],
|
|
112
|
+
["-webkit-touch-callout", "none"]
|
|
113
|
+
];
|
|
114
|
+
/**
|
|
115
|
+
* Track a pointer drag on an element.
|
|
116
|
+
*
|
|
117
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
118
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
119
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
120
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
121
|
+
* selection instead.
|
|
122
|
+
*/
|
|
123
|
+
function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, onDrag, onDragEnd } = {}) {
|
|
124
|
+
const threshold = Math.max(_threshold, 1);
|
|
125
|
+
const capture = element;
|
|
126
|
+
const style = element.style;
|
|
127
|
+
const previousStyles = /* @__PURE__ */ new Map();
|
|
128
|
+
if (style) for (const [property, value] of MANAGED_STYLES) {
|
|
129
|
+
previousStyles.set(property, style.getPropertyValue(property));
|
|
130
|
+
style.setProperty(property, value);
|
|
131
|
+
}
|
|
132
|
+
let pointerId = null;
|
|
133
|
+
/** Where the listeners for the current drag live. */
|
|
134
|
+
let moveTarget = null;
|
|
135
|
+
let startX = 0;
|
|
136
|
+
let startY = 0;
|
|
137
|
+
let lastX = 0;
|
|
138
|
+
let lastY = 0;
|
|
139
|
+
let previousCursor;
|
|
140
|
+
function state(event, deltaX, deltaY) {
|
|
141
|
+
return {
|
|
142
|
+
x: event.screenX - startX,
|
|
143
|
+
y: event.screenY - startY,
|
|
144
|
+
deltaX,
|
|
145
|
+
deltaY,
|
|
146
|
+
clientX: event.clientX,
|
|
147
|
+
clientY: event.clientY,
|
|
148
|
+
event
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `user-select: none` only makes the element's own text unselectable; the
|
|
153
|
+
* browser still starts a selection on long press and grabs whatever text it
|
|
154
|
+
* finds nearby. Cancelling the selection outright is what actually stops it.
|
|
155
|
+
*/
|
|
156
|
+
function preventSelectStart(event) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
}
|
|
159
|
+
function handlePointerDown(event) {
|
|
160
|
+
const pointerEvent = event;
|
|
161
|
+
if (pointerId !== null) return;
|
|
162
|
+
pointerId = pointerEvent.pointerId;
|
|
163
|
+
startX = lastX = pointerEvent.screenX;
|
|
164
|
+
startY = lastY = pointerEvent.screenY;
|
|
165
|
+
if (cursor && style) {
|
|
166
|
+
previousCursor = style.cursor;
|
|
167
|
+
style.cursor = cursor;
|
|
168
|
+
}
|
|
169
|
+
capture.setPointerCapture?.(pointerId);
|
|
170
|
+
moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
|
|
171
|
+
moveTarget.addEventListener("pointermove", handlePointerMove);
|
|
172
|
+
moveTarget.addEventListener("pointerup", handlePointerUp);
|
|
173
|
+
moveTarget.addEventListener("pointercancel", handlePointerUp);
|
|
174
|
+
globalThis.document?.addEventListener("selectstart", preventSelectStart);
|
|
175
|
+
onDragStart?.(state(pointerEvent, 0, 0));
|
|
176
|
+
}
|
|
177
|
+
function handlePointerMove(event) {
|
|
178
|
+
const pointerEvent = event;
|
|
179
|
+
if (pointerEvent.pointerId !== pointerId) return;
|
|
180
|
+
const deltaX = pointerEvent.screenX - lastX;
|
|
181
|
+
const deltaY = pointerEvent.screenY - lastY;
|
|
182
|
+
lastX = pointerEvent.screenX;
|
|
183
|
+
lastY = pointerEvent.screenY;
|
|
184
|
+
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
185
|
+
onDrag?.(state(pointerEvent, deltaX, deltaY));
|
|
186
|
+
}
|
|
187
|
+
function handlePointerUp(event) {
|
|
188
|
+
const pointerEvent = event;
|
|
189
|
+
if (pointerEvent.pointerId !== pointerId) return;
|
|
190
|
+
const finalState = state(pointerEvent, pointerEvent.screenX - lastX, pointerEvent.screenY - lastY);
|
|
191
|
+
stopTracking();
|
|
192
|
+
onDragEnd?.(finalState);
|
|
193
|
+
}
|
|
194
|
+
function stopTracking() {
|
|
195
|
+
if (pointerId === null) return;
|
|
196
|
+
capture.releasePointerCapture?.(pointerId);
|
|
197
|
+
moveTarget?.removeEventListener("pointermove", handlePointerMove);
|
|
198
|
+
moveTarget?.removeEventListener("pointerup", handlePointerUp);
|
|
199
|
+
moveTarget?.removeEventListener("pointercancel", handlePointerUp);
|
|
200
|
+
globalThis.document?.removeEventListener("selectstart", preventSelectStart);
|
|
201
|
+
if (cursor && style) {
|
|
202
|
+
style.cursor = previousCursor ?? "";
|
|
203
|
+
previousCursor = void 0;
|
|
204
|
+
}
|
|
205
|
+
moveTarget = null;
|
|
206
|
+
pointerId = null;
|
|
207
|
+
}
|
|
208
|
+
element.addEventListener("pointerdown", handlePointerDown);
|
|
209
|
+
return { destroy: () => {
|
|
210
|
+
stopTracking();
|
|
211
|
+
element.removeEventListener("pointerdown", handlePointerDown);
|
|
212
|
+
if (style) for (const [property] of MANAGED_STYLES) {
|
|
213
|
+
const previous = previousStyles.get(property);
|
|
214
|
+
if (previous) style.setProperty(property, previous);
|
|
215
|
+
else style.removeProperty(property);
|
|
216
|
+
}
|
|
217
|
+
} };
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/pointer/wheel.ts
|
|
221
|
+
/**
|
|
222
|
+
* Listen to wheel events on an element.
|
|
223
|
+
*
|
|
224
|
+
* The listener is registered with `passive: false` so that the handler can call
|
|
225
|
+
* `preventDefault()` to stop the page from scrolling.
|
|
226
|
+
*/
|
|
227
|
+
function createWheel(element, onWheel) {
|
|
228
|
+
const handler = (event) => onWheel(event);
|
|
229
|
+
element.addEventListener("wheel", handler, { passive: false });
|
|
230
|
+
return { destroy: () => {
|
|
231
|
+
element.removeEventListener("wheel", handler);
|
|
232
|
+
} };
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
100
235
|
exports.NOT_SUPPORTED = NOT_SUPPORTED;
|
|
101
236
|
exports.PERMISSION_DENIED = PERMISSION_DENIED;
|
|
237
|
+
exports.createDrag = createDrag;
|
|
102
238
|
exports.createMIDIAccess = createMIDIAccess;
|
|
103
239
|
exports.createMIDIInput = createMIDIInput;
|
|
104
240
|
exports.createMIDIMessage = createMIDIMessage;
|
|
241
|
+
exports.createWheel = createWheel;
|
|
105
242
|
|
|
106
243
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts"],"sourcesContent":["/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n\n/** @private */\nexport type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: () => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n function request() {\n if (destroyed) return\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator\n .requestMIDIAccess()\n .then((access) => {\n if (destroyed) return\n setState({ midiAccess: access, error: null })\n })\n .catch(() => {\n if (destroyed) return\n setState({ ...state, error: PERMISSION_DENIED })\n })\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n if (!midiAccess) {\n return { destroy: () => {} }\n }\n\n const inputs = [...midiAccess.inputs.values()]\n for (const input of inputs) {\n input.addEventListener('midimessage', onMIDIMessage)\n }\n\n return {\n destroy: () => {\n for (const input of inputs) {\n input.removeEventListener('midimessage', onMIDIMessage)\n }\n },\n }\n}\n","import { createMIDIMessage, type MIDIMessageInstance } from './message'\n\nconst MIDI_EVENT_TO_NUMBER = {\n NOTE_ON: 0x90,\n NOTE_OFF: 0x80,\n PITCH_BEND: 0xe0,\n}\n\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number) => void\n onNoteOffEvent?: (note: number) => void\n onPitchBendEvent?: (msb: number, lsb: number) => void\n}\n\nexport type MIDIInputInstance = MIDIMessageInstance\n\n/**\n * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.\n * Internally uses {@link createMIDIMessage}.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }: MIDIInputHandlers,\n): MIDIInputInstance {\n return createMIDIMessage(midiAccess, (event) => {\n if (!event.data) return\n // event.data[0] ... command\n // event.data[1] ... note, MSB (Most Significant Byte)\n // event.data[2] ... velocity, LSB (Least Significant Byte)\n const kind = event.data[0] & 0xf0\n\n if (\n kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||\n (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)\n ) {\n onNoteOffEvent?.(event.data[1])\n } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {\n onNoteOnEvent?.(event.data[1], event.data[2])\n } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {\n onPitchBendEvent?.(event.data[1], event.data[2])\n }\n })\n}\n"],"mappings":";;;AACA,MAAa,oBAAoB;;AAEjC,MAAa,gBAAgB;AAU7B,MAAM,gBAAiC;CACrC,YAAY;CACZ,OAAO;CACR;;;;;;;AAmBD,SAAgB,mBAAuC;CACrD,IAAI,QAAQ;CACZ,IAAI,YAAY;CAChB,MAAM,4BAAY,IAAI,KAAiB;CAEvC,SAAS,SAAS,MAAuB;AACvC,UAAQ;AACR,OAAK,MAAM,YAAY,UAAW,WAAU;;CAG9C,SAAS,UAAU;AACjB,MAAI,UAAW;AACf,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,mBAAmB;AACpE,YAAS;IAAE,GAAG;IAAO,OAAO;IAAe,CAAC;AAC5C;;AAEF,YACG,mBAAmB,CACnB,MAAM,WAAW;AAChB,OAAI,UAAW;AACf,YAAS;IAAE,YAAY;IAAQ,OAAO;IAAM,CAAC;IAC7C,CACD,YAAY;AACX,OAAI,UAAW;AACf,YAAS;IAAE,GAAG;IAAO,OAAO;IAAmB,CAAC;IAChD;;AAGN,QAAO;EACL;EACA,gBAAgB;EAChB,sBAAsB;EACtB,YAAY,aAAa;AACvB,aAAU,IAAI,SAAS;AACvB,gBAAa;AACX,cAAU,OAAO,SAAS;;;EAG9B,eAAe;AACb,eAAY;AACZ,aAAU,OAAO;;EAEpB;;;;;;;;;ACpEH,SAAgB,kBACd,YACA,eACqB;AACrB,KAAI,CAAC,WACH,QAAO,EAAE,eAAe,IAAI;CAG9B,MAAM,SAAS,CAAC,GAAG,WAAW,OAAO,QAAQ,CAAC;AAC9C,MAAK,MAAM,SAAS,OAClB,OAAM,iBAAiB,eAAe,cAAc;AAGtD,QAAO,EACL,eAAe;AACb,OAAK,MAAM,SAAS,OAClB,OAAM,oBAAoB,eAAe,cAAc;IAG5D;;;;AC1BH,MAAM,uBAAuB;CAC3B,SAAS;CACT,UAAU;CACV,YAAY;CACb;;;;;AAcD,SAAgB,gBACd,YACA,EAAE,eAAe,gBAAgB,oBACd;AACnB,QAAO,kBAAkB,aAAa,UAAU;AAC9C,MAAI,CAAC,MAAM,KAAM;EAIjB,MAAM,OAAO,MAAM,KAAK,KAAK;AAE7B,MACE,QAAQ,qBAAqB,YAC5B,QAAQ,qBAAqB,WAAW,MAAM,KAAK,MAAM,EAE1D,kBAAiB,MAAM,KAAK,GAAG;WACtB,QAAQ,qBAAqB,QACtC,iBAAgB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;WACpC,QAAQ,qBAAqB,WACtC,oBAAmB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;GAElD"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/pointer/wheel.ts"],"sourcesContent":["/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n\n/** @private */\nexport type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: () => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n function request() {\n if (destroyed) return\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator\n .requestMIDIAccess()\n .then((access) => {\n if (destroyed) return\n setState({ midiAccess: access, error: null })\n })\n .catch(() => {\n if (destroyed) return\n setState({ ...state, error: PERMISSION_DENIED })\n })\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n if (!midiAccess) {\n return { destroy: () => {} }\n }\n\n const inputs = [...midiAccess.inputs.values()]\n for (const input of inputs) {\n input.addEventListener('midimessage', onMIDIMessage)\n }\n\n return {\n destroy: () => {\n for (const input of inputs) {\n input.removeEventListener('midimessage', onMIDIMessage)\n }\n },\n }\n}\n","import { createMIDIMessage, type MIDIMessageInstance } from './message'\n\nconst MIDI_EVENT_TO_NUMBER = {\n NOTE_ON: 0x90,\n NOTE_OFF: 0x80,\n PITCH_BEND: 0xe0,\n}\n\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number) => void\n onNoteOffEvent?: (note: number) => void\n onPitchBendEvent?: (msb: number, lsb: number) => void\n}\n\nexport type MIDIInputInstance = MIDIMessageInstance\n\n/**\n * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.\n * Internally uses {@link createMIDIMessage}.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }: MIDIInputHandlers,\n): MIDIInputInstance {\n return createMIDIMessage(midiAccess, (event) => {\n if (!event.data) return\n // event.data[0] ... command\n // event.data[1] ... note, MSB (Most Significant Byte)\n // event.data[2] ... velocity, LSB (Least Significant Byte)\n const kind = event.data[0] & 0xf0\n\n if (\n kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||\n (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)\n ) {\n onNoteOffEvent?.(event.data[1])\n } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {\n onNoteOnEvent?.(event.data[1], event.data[2])\n } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {\n onPitchBendEvent?.(event.data[1], event.data[2])\n }\n })\n}\n","export type DragState = {\n /** Total movement from the drag start, in screen coordinates. */\n x: number\n y: number\n /** Movement since the previous event, in screen coordinates. */\n deltaX: number\n deltaY: number\n /** Pointer position in viewport coordinates. */\n clientX: number\n clientY: number\n event: PointerEvent\n}\n\nexport type DragOptions = {\n /**\n * Minimum movement in pixels before `onDrag` fires.\n * Prevents `onDrag` from firing on, for example, a double click.\n * Values below 1 are clamped to 1.\n *\n * @default 1\n */\n threshold?: number\n\n /**\n * CSS cursor to show while dragging. Applied to the element itself: pointer\n * capture keeps it in effect even once the pointer leaves the element, so\n * there is no need to touch the document.\n */\n cursor?: string\n\n onDragStart?: (state: DragState) => void\n onDrag?: (state: DragState) => void\n onDragEnd?: (state: DragState) => void\n}\n\nexport interface DragInstance {\n destroy: () => void\n}\n\n/**\n * Applied to the element for the lifetime of the instance.\n *\n * `touch-action` stops touch dragging from scrolling the page. That alone makes\n * the browser treat a long press as the start of a text selection, so selection\n * and the iOS callout are suppressed here too.\n */\nconst MANAGED_STYLES = [\n ['touch-action', 'none'],\n ['user-select', 'none'],\n ['-webkit-user-select', 'none'],\n ['-webkit-touch-callout', 'none'],\n] as const\n\ntype CaptureTarget = {\n setPointerCapture?: (pointerId: number) => void\n releasePointerCapture?: (pointerId: number) => void\n hasPointerCapture?: (pointerId: number) => boolean\n}\n\n/**\n * Track a pointer drag on an element.\n *\n * Uses Pointer Events only, and pointer capture so that the drag keeps working\n * once the pointer leaves the element. For the lifetime of the instance the\n * element gets `touch-action: none` so that touch dragging does not scroll the\n * page, plus `user-select: none` so that a long press does not start a text\n * selection instead.\n */\nexport function createDrag(\n element: Element,\n {\n threshold: _threshold = 1,\n cursor,\n onDragStart,\n onDrag,\n onDragEnd,\n }: DragOptions = {},\n): DragInstance {\n const threshold = Math.max(_threshold, 1)\n const capture = element as CaptureTarget\n const style = (element as Partial<HTMLElement>).style\n\n const previousStyles = new Map<string, string>()\n if (style) {\n for (const [property, value] of MANAGED_STYLES) {\n previousStyles.set(property, style.getPropertyValue(property))\n style.setProperty(property, value)\n }\n }\n\n let pointerId: number | null = null\n /** Where the listeners for the current drag live. */\n let moveTarget: EventTarget | null = null\n let startX = 0\n let startY = 0\n let lastX = 0\n let lastY = 0\n let previousCursor: string | undefined\n\n function state(\n event: PointerEvent,\n deltaX: number,\n deltaY: number,\n ): DragState {\n return {\n x: event.screenX - startX,\n y: event.screenY - startY,\n deltaX,\n deltaY,\n clientX: event.clientX,\n clientY: event.clientY,\n event,\n }\n }\n\n /**\n * `user-select: none` only makes the element's own text unselectable; the\n * browser still starts a selection on long press and grabs whatever text it\n * finds nearby. Cancelling the selection outright is what actually stops it.\n */\n function preventSelectStart(event: Event) {\n event.preventDefault()\n }\n\n function handlePointerDown(event: Event) {\n const pointerEvent = event as PointerEvent\n // Only one pointer drives the drag; ignore additional touches.\n if (pointerId !== null) return\n\n pointerId = pointerEvent.pointerId\n startX = lastX = pointerEvent.screenX\n startY = lastY = pointerEvent.screenY\n\n if (cursor && style) {\n previousCursor = style.cursor\n style.cursor = cursor\n }\n\n capture.setPointerCapture?.(pointerId)\n // With pointer capture the element receives the rest of the gesture.\n // Without it (older engines, jsdom) fall back to the window.\n moveTarget =\n capture.hasPointerCapture?.(pointerId) === true\n ? element\n : (globalThis.window ?? element)\n\n moveTarget.addEventListener('pointermove', handlePointerMove)\n moveTarget.addEventListener('pointerup', handlePointerUp)\n moveTarget.addEventListener('pointercancel', handlePointerUp)\n globalThis.document?.addEventListener('selectstart', preventSelectStart)\n\n onDragStart?.(state(pointerEvent, 0, 0))\n }\n\n function handlePointerMove(event: Event) {\n const pointerEvent = event as PointerEvent\n if (pointerEvent.pointerId !== pointerId) return\n\n const deltaX = pointerEvent.screenX - lastX\n const deltaY = pointerEvent.screenY - lastY\n lastX = pointerEvent.screenX\n lastY = pointerEvent.screenY\n\n // Movement below the threshold is dropped rather than accumulated.\n if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return\n\n onDrag?.(state(pointerEvent, deltaX, deltaY))\n }\n\n function handlePointerUp(event: Event) {\n const pointerEvent = event as PointerEvent\n if (pointerEvent.pointerId !== pointerId) return\n\n const deltaX = pointerEvent.screenX - lastX\n const deltaY = pointerEvent.screenY - lastY\n const finalState = state(pointerEvent, deltaX, deltaY)\n\n stopTracking()\n onDragEnd?.(finalState)\n }\n\n function stopTracking() {\n if (pointerId === null) return\n capture.releasePointerCapture?.(pointerId)\n moveTarget?.removeEventListener('pointermove', handlePointerMove)\n moveTarget?.removeEventListener('pointerup', handlePointerUp)\n moveTarget?.removeEventListener('pointercancel', handlePointerUp)\n globalThis.document?.removeEventListener('selectstart', preventSelectStart)\n if (cursor && style) {\n style.cursor = previousCursor ?? ''\n previousCursor = undefined\n }\n moveTarget = null\n pointerId = null\n }\n\n element.addEventListener('pointerdown', handlePointerDown)\n\n return {\n destroy: () => {\n stopTracking()\n element.removeEventListener('pointerdown', handlePointerDown)\n if (style) {\n for (const [property] of MANAGED_STYLES) {\n const previous = previousStyles.get(property)\n if (previous) {\n style.setProperty(property, previous)\n } else {\n style.removeProperty(property)\n }\n }\n }\n },\n }\n}\n","export interface WheelInstance {\n destroy: () => void\n}\n\n/**\n * Listen to wheel events on an element.\n *\n * The listener is registered with `passive: false` so that the handler can call\n * `preventDefault()` to stop the page from scrolling.\n */\nexport function createWheel(\n element: Element,\n onWheel: (event: WheelEvent) => void,\n): WheelInstance {\n const handler = (event: Event) => onWheel(event as WheelEvent)\n\n element.addEventListener('wheel', handler, { passive: false })\n\n return {\n destroy: () => {\n // Only `capture` matters when removing, and it is false here.\n element.removeEventListener('wheel', handler)\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -55,5 +55,66 @@ declare function createMIDIInput(midiAccess: MIDIAccess | null, {
|
|
|
55
55
|
onPitchBendEvent
|
|
56
56
|
}: MIDIInputHandlers): MIDIInputInstance;
|
|
57
57
|
//#endregion
|
|
58
|
-
|
|
58
|
+
//#region src/pointer/drag.d.ts
|
|
59
|
+
type DragState = {
|
|
60
|
+
/** Total movement from the drag start, in screen coordinates. */x: number;
|
|
61
|
+
y: number; /** Movement since the previous event, in screen coordinates. */
|
|
62
|
+
deltaX: number;
|
|
63
|
+
deltaY: number; /** Pointer position in viewport coordinates. */
|
|
64
|
+
clientX: number;
|
|
65
|
+
clientY: number;
|
|
66
|
+
event: PointerEvent;
|
|
67
|
+
};
|
|
68
|
+
type DragOptions = {
|
|
69
|
+
/**
|
|
70
|
+
* Minimum movement in pixels before `onDrag` fires.
|
|
71
|
+
* Prevents `onDrag` from firing on, for example, a double click.
|
|
72
|
+
* Values below 1 are clamped to 1.
|
|
73
|
+
*
|
|
74
|
+
* @default 1
|
|
75
|
+
*/
|
|
76
|
+
threshold?: number;
|
|
77
|
+
/**
|
|
78
|
+
* CSS cursor to show while dragging. Applied to the element itself: pointer
|
|
79
|
+
* capture keeps it in effect even once the pointer leaves the element, so
|
|
80
|
+
* there is no need to touch the document.
|
|
81
|
+
*/
|
|
82
|
+
cursor?: string;
|
|
83
|
+
onDragStart?: (state: DragState) => void;
|
|
84
|
+
onDrag?: (state: DragState) => void;
|
|
85
|
+
onDragEnd?: (state: DragState) => void;
|
|
86
|
+
};
|
|
87
|
+
interface DragInstance {
|
|
88
|
+
destroy: () => void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Track a pointer drag on an element.
|
|
92
|
+
*
|
|
93
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
94
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
95
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
96
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
97
|
+
* selection instead.
|
|
98
|
+
*/
|
|
99
|
+
declare function createDrag(element: Element, {
|
|
100
|
+
threshold: _threshold,
|
|
101
|
+
cursor,
|
|
102
|
+
onDragStart,
|
|
103
|
+
onDrag,
|
|
104
|
+
onDragEnd
|
|
105
|
+
}?: DragOptions): DragInstance;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/pointer/wheel.d.ts
|
|
108
|
+
interface WheelInstance {
|
|
109
|
+
destroy: () => void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Listen to wheel events on an element.
|
|
113
|
+
*
|
|
114
|
+
* The listener is registered with `passive: false` so that the handler can call
|
|
115
|
+
* `preventDefault()` to stop the page from scrolling.
|
|
116
|
+
*/
|
|
117
|
+
declare function createWheel(element: Element, onWheel: (event: WheelEvent) => void): WheelInstance;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { type DragInstance, type DragOptions, type DragState, type MIDIAccessError, type MIDIAccessInstance, type MIDIAccessState, type MIDIInputHandlers, type MIDIInputInstance, type MIDIMessageInstance, NOT_SUPPORTED, PERMISSION_DENIED, type WheelInstance, createDrag, createMIDIAccess, createMIDIInput, createMIDIMessage, createWheel };
|
|
59
120
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts"],"mappings":";;cACa,iBAAA;;cAEA,aAAA;;KAGD,eAAA,UAAyB,iBAAA,UAA2B,aAAA;AAAA,KAEpD,eAAA;EAAA,SACD,UAAA,EAAY,UAAA;EAAA,SACZ,KAAA,EAAO,eAAA;AAAA;AAAA,UAQD,kBAAA;EAZL;EAcV,OAAA;EACA,QAAA,QAAgB,eAAA;EAfmB;EAiBnC,cAAA,QAAsB,eAAA;EAfZ;EAiBV,SAAA,GAAY,QAAA;EACZ,OAAA;AAAA;;;;;;;iBASc,gBAAA,CAAA,GAAoB,kBAAA;;;UCnCnB,mBAAA;EACf,OAAA;AAAA;;;;ADEF;;iBCMgB,iBAAA,CACd,UAAA,EAAY,UAAA,SACZ,aAAA,GAAgB,KAAA,EAAO,gBAAA,YACtB,mBAAA;;;KCJS,iBAAA;EACV,aAAA,IAAiB,IAAA,UAAc,QAAA;EAC/B,cAAA,IAAkB,IAAA;EAClB,gBAAA,IAAoB,GAAA,UAAa,GAAA;AAAA;AAAA,KAGvB,iBAAA,GAAoB,mBAAA;;;;;iBAMhB,eAAA,CACd,UAAA,EAAY,UAAA;EACV,aAAA;EAAe,cAAA;EAAgB;AAAA,GAAoB,iBAAA,GACpD,iBAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/pointer/wheel.ts"],"mappings":";;cACa,iBAAA;;cAEA,aAAA;;KAGD,eAAA,UAAyB,iBAAA,UAA2B,aAAA;AAAA,KAEpD,eAAA;EAAA,SACD,UAAA,EAAY,UAAA;EAAA,SACZ,KAAA,EAAO,eAAA;AAAA;AAAA,UAQD,kBAAA;EAZL;EAcV,OAAA;EACA,QAAA,QAAgB,eAAA;EAfmB;EAiBnC,cAAA,QAAsB,eAAA;EAfZ;EAiBV,SAAA,GAAY,QAAA;EACZ,OAAA;AAAA;;;;;;;iBASc,gBAAA,CAAA,GAAoB,kBAAA;;;UCnCnB,mBAAA;EACf,OAAA;AAAA;;;;ADEF;;iBCMgB,iBAAA,CACd,UAAA,EAAY,UAAA,SACZ,aAAA,GAAgB,KAAA,EAAO,gBAAA,YACtB,mBAAA;;;KCJS,iBAAA;EACV,aAAA,IAAiB,IAAA,UAAc,QAAA;EAC/B,cAAA,IAAkB,IAAA;EAClB,gBAAA,IAAoB,GAAA,UAAa,GAAA;AAAA;AAAA,KAGvB,iBAAA,GAAoB,mBAAA;;;;;iBAMhB,eAAA,CACd,UAAA,EAAY,UAAA;EACV,aAAA;EAAe,cAAA;EAAgB;AAAA,GAAoB,iBAAA,GACpD,iBAAA;;;KCvBS,SAAA;EHCC,iEGCX,CAAA;EACA,CAAA,UHF4B;EGI5B,MAAA;EACA,MAAA,UHHwB;EGKxB,OAAA;EACA,OAAA;EACA,KAAA,EAAO,YAAA;AAAA;AAAA,KAGG,WAAA;;;;AHLZ;;;;EGaE,SAAA;EHZqB;;;;;EGmBrB,MAAA;EAEA,WAAA,IAAe,KAAA,EAAO,SAAA;EACtB,MAAA,IAAU,KAAA,EAAO,SAAA;EACjB,SAAA,IAAa,KAAA,EAAO,SAAA;AAAA;AAAA,UAGL,YAAA;EACf,OAAA;AAAA;;;;;;;AHDF;;;iBGiCgB,UAAA,CACd,OAAA,EAAS,OAAA;EAEP,SAAA,EAAW,UAAA;EACX,MAAA;EACA,WAAA;EACA,MAAA;EACA;AAAA,IACC,WAAA,GACF,YAAA;;;UC7Ec,aAAA;EACf,OAAA;AAAA;;;;AJEF;;;iBIOgB,WAAA,CACd,OAAA,EAAS,OAAA,EACT,OAAA,GAAU,KAAA,EAAO,UAAA,YAChB,aAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -55,5 +55,66 @@ declare function createMIDIInput(midiAccess: MIDIAccess | null, {
|
|
|
55
55
|
onPitchBendEvent
|
|
56
56
|
}: MIDIInputHandlers): MIDIInputInstance;
|
|
57
57
|
//#endregion
|
|
58
|
-
|
|
58
|
+
//#region src/pointer/drag.d.ts
|
|
59
|
+
type DragState = {
|
|
60
|
+
/** Total movement from the drag start, in screen coordinates. */x: number;
|
|
61
|
+
y: number; /** Movement since the previous event, in screen coordinates. */
|
|
62
|
+
deltaX: number;
|
|
63
|
+
deltaY: number; /** Pointer position in viewport coordinates. */
|
|
64
|
+
clientX: number;
|
|
65
|
+
clientY: number;
|
|
66
|
+
event: PointerEvent;
|
|
67
|
+
};
|
|
68
|
+
type DragOptions = {
|
|
69
|
+
/**
|
|
70
|
+
* Minimum movement in pixels before `onDrag` fires.
|
|
71
|
+
* Prevents `onDrag` from firing on, for example, a double click.
|
|
72
|
+
* Values below 1 are clamped to 1.
|
|
73
|
+
*
|
|
74
|
+
* @default 1
|
|
75
|
+
*/
|
|
76
|
+
threshold?: number;
|
|
77
|
+
/**
|
|
78
|
+
* CSS cursor to show while dragging. Applied to the element itself: pointer
|
|
79
|
+
* capture keeps it in effect even once the pointer leaves the element, so
|
|
80
|
+
* there is no need to touch the document.
|
|
81
|
+
*/
|
|
82
|
+
cursor?: string;
|
|
83
|
+
onDragStart?: (state: DragState) => void;
|
|
84
|
+
onDrag?: (state: DragState) => void;
|
|
85
|
+
onDragEnd?: (state: DragState) => void;
|
|
86
|
+
};
|
|
87
|
+
interface DragInstance {
|
|
88
|
+
destroy: () => void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Track a pointer drag on an element.
|
|
92
|
+
*
|
|
93
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
94
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
95
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
96
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
97
|
+
* selection instead.
|
|
98
|
+
*/
|
|
99
|
+
declare function createDrag(element: Element, {
|
|
100
|
+
threshold: _threshold,
|
|
101
|
+
cursor,
|
|
102
|
+
onDragStart,
|
|
103
|
+
onDrag,
|
|
104
|
+
onDragEnd
|
|
105
|
+
}?: DragOptions): DragInstance;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/pointer/wheel.d.ts
|
|
108
|
+
interface WheelInstance {
|
|
109
|
+
destroy: () => void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Listen to wheel events on an element.
|
|
113
|
+
*
|
|
114
|
+
* The listener is registered with `passive: false` so that the handler can call
|
|
115
|
+
* `preventDefault()` to stop the page from scrolling.
|
|
116
|
+
*/
|
|
117
|
+
declare function createWheel(element: Element, onWheel: (event: WheelEvent) => void): WheelInstance;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { type DragInstance, type DragOptions, type DragState, type MIDIAccessError, type MIDIAccessInstance, type MIDIAccessState, type MIDIInputHandlers, type MIDIInputInstance, type MIDIMessageInstance, NOT_SUPPORTED, PERMISSION_DENIED, type WheelInstance, createDrag, createMIDIAccess, createMIDIInput, createMIDIMessage, createWheel };
|
|
59
120
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts"],"mappings":";;cACa,iBAAA;;cAEA,aAAA;;KAGD,eAAA,UAAyB,iBAAA,UAA2B,aAAA;AAAA,KAEpD,eAAA;EAAA,SACD,UAAA,EAAY,UAAA;EAAA,SACZ,KAAA,EAAO,eAAA;AAAA;AAAA,UAQD,kBAAA;EAZL;EAcV,OAAA;EACA,QAAA,QAAgB,eAAA;EAfmB;EAiBnC,cAAA,QAAsB,eAAA;EAfZ;EAiBV,SAAA,GAAY,QAAA;EACZ,OAAA;AAAA;;;;;;;iBASc,gBAAA,CAAA,GAAoB,kBAAA;;;UCnCnB,mBAAA;EACf,OAAA;AAAA;;;;ADEF;;iBCMgB,iBAAA,CACd,UAAA,EAAY,UAAA,SACZ,aAAA,GAAgB,KAAA,EAAO,gBAAA,YACtB,mBAAA;;;KCJS,iBAAA;EACV,aAAA,IAAiB,IAAA,UAAc,QAAA;EAC/B,cAAA,IAAkB,IAAA;EAClB,gBAAA,IAAoB,GAAA,UAAa,GAAA;AAAA;AAAA,KAGvB,iBAAA,GAAoB,mBAAA;;;;;iBAMhB,eAAA,CACd,UAAA,EAAY,UAAA;EACV,aAAA;EAAe,cAAA;EAAgB;AAAA,GAAoB,iBAAA,GACpD,iBAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/pointer/wheel.ts"],"mappings":";;cACa,iBAAA;;cAEA,aAAA;;KAGD,eAAA,UAAyB,iBAAA,UAA2B,aAAA;AAAA,KAEpD,eAAA;EAAA,SACD,UAAA,EAAY,UAAA;EAAA,SACZ,KAAA,EAAO,eAAA;AAAA;AAAA,UAQD,kBAAA;EAZL;EAcV,OAAA;EACA,QAAA,QAAgB,eAAA;EAfmB;EAiBnC,cAAA,QAAsB,eAAA;EAfZ;EAiBV,SAAA,GAAY,QAAA;EACZ,OAAA;AAAA;;;;;;;iBASc,gBAAA,CAAA,GAAoB,kBAAA;;;UCnCnB,mBAAA;EACf,OAAA;AAAA;;;;ADEF;;iBCMgB,iBAAA,CACd,UAAA,EAAY,UAAA,SACZ,aAAA,GAAgB,KAAA,EAAO,gBAAA,YACtB,mBAAA;;;KCJS,iBAAA;EACV,aAAA,IAAiB,IAAA,UAAc,QAAA;EAC/B,cAAA,IAAkB,IAAA;EAClB,gBAAA,IAAoB,GAAA,UAAa,GAAA;AAAA;AAAA,KAGvB,iBAAA,GAAoB,mBAAA;;;;;iBAMhB,eAAA,CACd,UAAA,EAAY,UAAA;EACV,aAAA;EAAe,cAAA;EAAgB;AAAA,GAAoB,iBAAA,GACpD,iBAAA;;;KCvBS,SAAA;EHCC,iEGCX,CAAA;EACA,CAAA,UHF4B;EGI5B,MAAA;EACA,MAAA,UHHwB;EGKxB,OAAA;EACA,OAAA;EACA,KAAA,EAAO,YAAA;AAAA;AAAA,KAGG,WAAA;;;;AHLZ;;;;EGaE,SAAA;EHZqB;;;;;EGmBrB,MAAA;EAEA,WAAA,IAAe,KAAA,EAAO,SAAA;EACtB,MAAA,IAAU,KAAA,EAAO,SAAA;EACjB,SAAA,IAAa,KAAA,EAAO,SAAA;AAAA;AAAA,UAGL,YAAA;EACf,OAAA;AAAA;;;;;;;AHDF;;;iBGiCgB,UAAA,CACd,OAAA,EAAS,OAAA;EAEP,SAAA,EAAW,UAAA;EACX,MAAA;EACA,WAAA;EACA,MAAA;EACA;AAAA,IACC,WAAA,GACF,YAAA;;;UC7Ec,aAAA;EACf,OAAA;AAAA;;;;AJEF;;;iBIOgB,WAAA,CACd,OAAA,EAAS,OAAA,EACT,OAAA,GAAU,KAAA,EAAO,UAAA,YAChB,aAAA"}
|
package/dist/index.js
CHANGED
|
@@ -96,6 +96,141 @@ function createMIDIInput(midiAccess, { onNoteOnEvent, onNoteOffEvent, onPitchBen
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
//#endregion
|
|
99
|
-
|
|
99
|
+
//#region src/pointer/drag.ts
|
|
100
|
+
/**
|
|
101
|
+
* Applied to the element for the lifetime of the instance.
|
|
102
|
+
*
|
|
103
|
+
* `touch-action` stops touch dragging from scrolling the page. That alone makes
|
|
104
|
+
* the browser treat a long press as the start of a text selection, so selection
|
|
105
|
+
* and the iOS callout are suppressed here too.
|
|
106
|
+
*/
|
|
107
|
+
const MANAGED_STYLES = [
|
|
108
|
+
["touch-action", "none"],
|
|
109
|
+
["user-select", "none"],
|
|
110
|
+
["-webkit-user-select", "none"],
|
|
111
|
+
["-webkit-touch-callout", "none"]
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Track a pointer drag on an element.
|
|
115
|
+
*
|
|
116
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
117
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
118
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
119
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
120
|
+
* selection instead.
|
|
121
|
+
*/
|
|
122
|
+
function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, onDrag, onDragEnd } = {}) {
|
|
123
|
+
const threshold = Math.max(_threshold, 1);
|
|
124
|
+
const capture = element;
|
|
125
|
+
const style = element.style;
|
|
126
|
+
const previousStyles = /* @__PURE__ */ new Map();
|
|
127
|
+
if (style) for (const [property, value] of MANAGED_STYLES) {
|
|
128
|
+
previousStyles.set(property, style.getPropertyValue(property));
|
|
129
|
+
style.setProperty(property, value);
|
|
130
|
+
}
|
|
131
|
+
let pointerId = null;
|
|
132
|
+
/** Where the listeners for the current drag live. */
|
|
133
|
+
let moveTarget = null;
|
|
134
|
+
let startX = 0;
|
|
135
|
+
let startY = 0;
|
|
136
|
+
let lastX = 0;
|
|
137
|
+
let lastY = 0;
|
|
138
|
+
let previousCursor;
|
|
139
|
+
function state(event, deltaX, deltaY) {
|
|
140
|
+
return {
|
|
141
|
+
x: event.screenX - startX,
|
|
142
|
+
y: event.screenY - startY,
|
|
143
|
+
deltaX,
|
|
144
|
+
deltaY,
|
|
145
|
+
clientX: event.clientX,
|
|
146
|
+
clientY: event.clientY,
|
|
147
|
+
event
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* `user-select: none` only makes the element's own text unselectable; the
|
|
152
|
+
* browser still starts a selection on long press and grabs whatever text it
|
|
153
|
+
* finds nearby. Cancelling the selection outright is what actually stops it.
|
|
154
|
+
*/
|
|
155
|
+
function preventSelectStart(event) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
}
|
|
158
|
+
function handlePointerDown(event) {
|
|
159
|
+
const pointerEvent = event;
|
|
160
|
+
if (pointerId !== null) return;
|
|
161
|
+
pointerId = pointerEvent.pointerId;
|
|
162
|
+
startX = lastX = pointerEvent.screenX;
|
|
163
|
+
startY = lastY = pointerEvent.screenY;
|
|
164
|
+
if (cursor && style) {
|
|
165
|
+
previousCursor = style.cursor;
|
|
166
|
+
style.cursor = cursor;
|
|
167
|
+
}
|
|
168
|
+
capture.setPointerCapture?.(pointerId);
|
|
169
|
+
moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
|
|
170
|
+
moveTarget.addEventListener("pointermove", handlePointerMove);
|
|
171
|
+
moveTarget.addEventListener("pointerup", handlePointerUp);
|
|
172
|
+
moveTarget.addEventListener("pointercancel", handlePointerUp);
|
|
173
|
+
globalThis.document?.addEventListener("selectstart", preventSelectStart);
|
|
174
|
+
onDragStart?.(state(pointerEvent, 0, 0));
|
|
175
|
+
}
|
|
176
|
+
function handlePointerMove(event) {
|
|
177
|
+
const pointerEvent = event;
|
|
178
|
+
if (pointerEvent.pointerId !== pointerId) return;
|
|
179
|
+
const deltaX = pointerEvent.screenX - lastX;
|
|
180
|
+
const deltaY = pointerEvent.screenY - lastY;
|
|
181
|
+
lastX = pointerEvent.screenX;
|
|
182
|
+
lastY = pointerEvent.screenY;
|
|
183
|
+
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
184
|
+
onDrag?.(state(pointerEvent, deltaX, deltaY));
|
|
185
|
+
}
|
|
186
|
+
function handlePointerUp(event) {
|
|
187
|
+
const pointerEvent = event;
|
|
188
|
+
if (pointerEvent.pointerId !== pointerId) return;
|
|
189
|
+
const finalState = state(pointerEvent, pointerEvent.screenX - lastX, pointerEvent.screenY - lastY);
|
|
190
|
+
stopTracking();
|
|
191
|
+
onDragEnd?.(finalState);
|
|
192
|
+
}
|
|
193
|
+
function stopTracking() {
|
|
194
|
+
if (pointerId === null) return;
|
|
195
|
+
capture.releasePointerCapture?.(pointerId);
|
|
196
|
+
moveTarget?.removeEventListener("pointermove", handlePointerMove);
|
|
197
|
+
moveTarget?.removeEventListener("pointerup", handlePointerUp);
|
|
198
|
+
moveTarget?.removeEventListener("pointercancel", handlePointerUp);
|
|
199
|
+
globalThis.document?.removeEventListener("selectstart", preventSelectStart);
|
|
200
|
+
if (cursor && style) {
|
|
201
|
+
style.cursor = previousCursor ?? "";
|
|
202
|
+
previousCursor = void 0;
|
|
203
|
+
}
|
|
204
|
+
moveTarget = null;
|
|
205
|
+
pointerId = null;
|
|
206
|
+
}
|
|
207
|
+
element.addEventListener("pointerdown", handlePointerDown);
|
|
208
|
+
return { destroy: () => {
|
|
209
|
+
stopTracking();
|
|
210
|
+
element.removeEventListener("pointerdown", handlePointerDown);
|
|
211
|
+
if (style) for (const [property] of MANAGED_STYLES) {
|
|
212
|
+
const previous = previousStyles.get(property);
|
|
213
|
+
if (previous) style.setProperty(property, previous);
|
|
214
|
+
else style.removeProperty(property);
|
|
215
|
+
}
|
|
216
|
+
} };
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/pointer/wheel.ts
|
|
220
|
+
/**
|
|
221
|
+
* Listen to wheel events on an element.
|
|
222
|
+
*
|
|
223
|
+
* The listener is registered with `passive: false` so that the handler can call
|
|
224
|
+
* `preventDefault()` to stop the page from scrolling.
|
|
225
|
+
*/
|
|
226
|
+
function createWheel(element, onWheel) {
|
|
227
|
+
const handler = (event) => onWheel(event);
|
|
228
|
+
element.addEventListener("wheel", handler, { passive: false });
|
|
229
|
+
return { destroy: () => {
|
|
230
|
+
element.removeEventListener("wheel", handler);
|
|
231
|
+
} };
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
export { NOT_SUPPORTED, PERMISSION_DENIED, createDrag, createMIDIAccess, createMIDIInput, createMIDIMessage, createWheel };
|
|
100
235
|
|
|
101
236
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts"],"sourcesContent":["/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n\n/** @private */\nexport type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: () => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n function request() {\n if (destroyed) return\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator\n .requestMIDIAccess()\n .then((access) => {\n if (destroyed) return\n setState({ midiAccess: access, error: null })\n })\n .catch(() => {\n if (destroyed) return\n setState({ ...state, error: PERMISSION_DENIED })\n })\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n if (!midiAccess) {\n return { destroy: () => {} }\n }\n\n const inputs = [...midiAccess.inputs.values()]\n for (const input of inputs) {\n input.addEventListener('midimessage', onMIDIMessage)\n }\n\n return {\n destroy: () => {\n for (const input of inputs) {\n input.removeEventListener('midimessage', onMIDIMessage)\n }\n },\n }\n}\n","import { createMIDIMessage, type MIDIMessageInstance } from './message'\n\nconst MIDI_EVENT_TO_NUMBER = {\n NOTE_ON: 0x90,\n NOTE_OFF: 0x80,\n PITCH_BEND: 0xe0,\n}\n\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number) => void\n onNoteOffEvent?: (note: number) => void\n onPitchBendEvent?: (msb: number, lsb: number) => void\n}\n\nexport type MIDIInputInstance = MIDIMessageInstance\n\n/**\n * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.\n * Internally uses {@link createMIDIMessage}.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }: MIDIInputHandlers,\n): MIDIInputInstance {\n return createMIDIMessage(midiAccess, (event) => {\n if (!event.data) return\n // event.data[0] ... command\n // event.data[1] ... note, MSB (Most Significant Byte)\n // event.data[2] ... velocity, LSB (Least Significant Byte)\n const kind = event.data[0] & 0xf0\n\n if (\n kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||\n (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)\n ) {\n onNoteOffEvent?.(event.data[1])\n } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {\n onNoteOnEvent?.(event.data[1], event.data[2])\n } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {\n onPitchBendEvent?.(event.data[1], event.data[2])\n }\n })\n}\n"],"mappings":";;AACA,MAAa,oBAAoB;;AAEjC,MAAa,gBAAgB;AAU7B,MAAM,gBAAiC;CACrC,YAAY;CACZ,OAAO;CACR;;;;;;;AAmBD,SAAgB,mBAAuC;CACrD,IAAI,QAAQ;CACZ,IAAI,YAAY;CAChB,MAAM,4BAAY,IAAI,KAAiB;CAEvC,SAAS,SAAS,MAAuB;AACvC,UAAQ;AACR,OAAK,MAAM,YAAY,UAAW,WAAU;;CAG9C,SAAS,UAAU;AACjB,MAAI,UAAW;AACf,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,mBAAmB;AACpE,YAAS;IAAE,GAAG;IAAO,OAAO;IAAe,CAAC;AAC5C;;AAEF,YACG,mBAAmB,CACnB,MAAM,WAAW;AAChB,OAAI,UAAW;AACf,YAAS;IAAE,YAAY;IAAQ,OAAO;IAAM,CAAC;IAC7C,CACD,YAAY;AACX,OAAI,UAAW;AACf,YAAS;IAAE,GAAG;IAAO,OAAO;IAAmB,CAAC;IAChD;;AAGN,QAAO;EACL;EACA,gBAAgB;EAChB,sBAAsB;EACtB,YAAY,aAAa;AACvB,aAAU,IAAI,SAAS;AACvB,gBAAa;AACX,cAAU,OAAO,SAAS;;;EAG9B,eAAe;AACb,eAAY;AACZ,aAAU,OAAO;;EAEpB;;;;;;;;;ACpEH,SAAgB,kBACd,YACA,eACqB;AACrB,KAAI,CAAC,WACH,QAAO,EAAE,eAAe,IAAI;CAG9B,MAAM,SAAS,CAAC,GAAG,WAAW,OAAO,QAAQ,CAAC;AAC9C,MAAK,MAAM,SAAS,OAClB,OAAM,iBAAiB,eAAe,cAAc;AAGtD,QAAO,EACL,eAAe;AACb,OAAK,MAAM,SAAS,OAClB,OAAM,oBAAoB,eAAe,cAAc;IAG5D;;;;AC1BH,MAAM,uBAAuB;CAC3B,SAAS;CACT,UAAU;CACV,YAAY;CACb;;;;;AAcD,SAAgB,gBACd,YACA,EAAE,eAAe,gBAAgB,oBACd;AACnB,QAAO,kBAAkB,aAAa,UAAU;AAC9C,MAAI,CAAC,MAAM,KAAM;EAIjB,MAAM,OAAO,MAAM,KAAK,KAAK;AAE7B,MACE,QAAQ,qBAAqB,YAC5B,QAAQ,qBAAqB,WAAW,MAAM,KAAK,MAAM,EAE1D,kBAAiB,MAAM,KAAK,GAAG;WACtB,QAAQ,qBAAqB,QACtC,iBAAgB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;WACpC,QAAQ,qBAAqB,WACtC,oBAAmB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;GAElD"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/pointer/wheel.ts"],"sourcesContent":["/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n\n/** @private */\nexport type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: () => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n function request() {\n if (destroyed) return\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator\n .requestMIDIAccess()\n .then((access) => {\n if (destroyed) return\n setState({ midiAccess: access, error: null })\n })\n .catch(() => {\n if (destroyed) return\n setState({ ...state, error: PERMISSION_DENIED })\n })\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n if (!midiAccess) {\n return { destroy: () => {} }\n }\n\n const inputs = [...midiAccess.inputs.values()]\n for (const input of inputs) {\n input.addEventListener('midimessage', onMIDIMessage)\n }\n\n return {\n destroy: () => {\n for (const input of inputs) {\n input.removeEventListener('midimessage', onMIDIMessage)\n }\n },\n }\n}\n","import { createMIDIMessage, type MIDIMessageInstance } from './message'\n\nconst MIDI_EVENT_TO_NUMBER = {\n NOTE_ON: 0x90,\n NOTE_OFF: 0x80,\n PITCH_BEND: 0xe0,\n}\n\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number) => void\n onNoteOffEvent?: (note: number) => void\n onPitchBendEvent?: (msb: number, lsb: number) => void\n}\n\nexport type MIDIInputInstance = MIDIMessageInstance\n\n/**\n * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.\n * Internally uses {@link createMIDIMessage}.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }: MIDIInputHandlers,\n): MIDIInputInstance {\n return createMIDIMessage(midiAccess, (event) => {\n if (!event.data) return\n // event.data[0] ... command\n // event.data[1] ... note, MSB (Most Significant Byte)\n // event.data[2] ... velocity, LSB (Least Significant Byte)\n const kind = event.data[0] & 0xf0\n\n if (\n kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||\n (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)\n ) {\n onNoteOffEvent?.(event.data[1])\n } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {\n onNoteOnEvent?.(event.data[1], event.data[2])\n } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {\n onPitchBendEvent?.(event.data[1], event.data[2])\n }\n })\n}\n","export type DragState = {\n /** Total movement from the drag start, in screen coordinates. */\n x: number\n y: number\n /** Movement since the previous event, in screen coordinates. */\n deltaX: number\n deltaY: number\n /** Pointer position in viewport coordinates. */\n clientX: number\n clientY: number\n event: PointerEvent\n}\n\nexport type DragOptions = {\n /**\n * Minimum movement in pixels before `onDrag` fires.\n * Prevents `onDrag` from firing on, for example, a double click.\n * Values below 1 are clamped to 1.\n *\n * @default 1\n */\n threshold?: number\n\n /**\n * CSS cursor to show while dragging. Applied to the element itself: pointer\n * capture keeps it in effect even once the pointer leaves the element, so\n * there is no need to touch the document.\n */\n cursor?: string\n\n onDragStart?: (state: DragState) => void\n onDrag?: (state: DragState) => void\n onDragEnd?: (state: DragState) => void\n}\n\nexport interface DragInstance {\n destroy: () => void\n}\n\n/**\n * Applied to the element for the lifetime of the instance.\n *\n * `touch-action` stops touch dragging from scrolling the page. That alone makes\n * the browser treat a long press as the start of a text selection, so selection\n * and the iOS callout are suppressed here too.\n */\nconst MANAGED_STYLES = [\n ['touch-action', 'none'],\n ['user-select', 'none'],\n ['-webkit-user-select', 'none'],\n ['-webkit-touch-callout', 'none'],\n] as const\n\ntype CaptureTarget = {\n setPointerCapture?: (pointerId: number) => void\n releasePointerCapture?: (pointerId: number) => void\n hasPointerCapture?: (pointerId: number) => boolean\n}\n\n/**\n * Track a pointer drag on an element.\n *\n * Uses Pointer Events only, and pointer capture so that the drag keeps working\n * once the pointer leaves the element. For the lifetime of the instance the\n * element gets `touch-action: none` so that touch dragging does not scroll the\n * page, plus `user-select: none` so that a long press does not start a text\n * selection instead.\n */\nexport function createDrag(\n element: Element,\n {\n threshold: _threshold = 1,\n cursor,\n onDragStart,\n onDrag,\n onDragEnd,\n }: DragOptions = {},\n): DragInstance {\n const threshold = Math.max(_threshold, 1)\n const capture = element as CaptureTarget\n const style = (element as Partial<HTMLElement>).style\n\n const previousStyles = new Map<string, string>()\n if (style) {\n for (const [property, value] of MANAGED_STYLES) {\n previousStyles.set(property, style.getPropertyValue(property))\n style.setProperty(property, value)\n }\n }\n\n let pointerId: number | null = null\n /** Where the listeners for the current drag live. */\n let moveTarget: EventTarget | null = null\n let startX = 0\n let startY = 0\n let lastX = 0\n let lastY = 0\n let previousCursor: string | undefined\n\n function state(\n event: PointerEvent,\n deltaX: number,\n deltaY: number,\n ): DragState {\n return {\n x: event.screenX - startX,\n y: event.screenY - startY,\n deltaX,\n deltaY,\n clientX: event.clientX,\n clientY: event.clientY,\n event,\n }\n }\n\n /**\n * `user-select: none` only makes the element's own text unselectable; the\n * browser still starts a selection on long press and grabs whatever text it\n * finds nearby. Cancelling the selection outright is what actually stops it.\n */\n function preventSelectStart(event: Event) {\n event.preventDefault()\n }\n\n function handlePointerDown(event: Event) {\n const pointerEvent = event as PointerEvent\n // Only one pointer drives the drag; ignore additional touches.\n if (pointerId !== null) return\n\n pointerId = pointerEvent.pointerId\n startX = lastX = pointerEvent.screenX\n startY = lastY = pointerEvent.screenY\n\n if (cursor && style) {\n previousCursor = style.cursor\n style.cursor = cursor\n }\n\n capture.setPointerCapture?.(pointerId)\n // With pointer capture the element receives the rest of the gesture.\n // Without it (older engines, jsdom) fall back to the window.\n moveTarget =\n capture.hasPointerCapture?.(pointerId) === true\n ? element\n : (globalThis.window ?? element)\n\n moveTarget.addEventListener('pointermove', handlePointerMove)\n moveTarget.addEventListener('pointerup', handlePointerUp)\n moveTarget.addEventListener('pointercancel', handlePointerUp)\n globalThis.document?.addEventListener('selectstart', preventSelectStart)\n\n onDragStart?.(state(pointerEvent, 0, 0))\n }\n\n function handlePointerMove(event: Event) {\n const pointerEvent = event as PointerEvent\n if (pointerEvent.pointerId !== pointerId) return\n\n const deltaX = pointerEvent.screenX - lastX\n const deltaY = pointerEvent.screenY - lastY\n lastX = pointerEvent.screenX\n lastY = pointerEvent.screenY\n\n // Movement below the threshold is dropped rather than accumulated.\n if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return\n\n onDrag?.(state(pointerEvent, deltaX, deltaY))\n }\n\n function handlePointerUp(event: Event) {\n const pointerEvent = event as PointerEvent\n if (pointerEvent.pointerId !== pointerId) return\n\n const deltaX = pointerEvent.screenX - lastX\n const deltaY = pointerEvent.screenY - lastY\n const finalState = state(pointerEvent, deltaX, deltaY)\n\n stopTracking()\n onDragEnd?.(finalState)\n }\n\n function stopTracking() {\n if (pointerId === null) return\n capture.releasePointerCapture?.(pointerId)\n moveTarget?.removeEventListener('pointermove', handlePointerMove)\n moveTarget?.removeEventListener('pointerup', handlePointerUp)\n moveTarget?.removeEventListener('pointercancel', handlePointerUp)\n globalThis.document?.removeEventListener('selectstart', preventSelectStart)\n if (cursor && style) {\n style.cursor = previousCursor ?? ''\n previousCursor = undefined\n }\n moveTarget = null\n pointerId = null\n }\n\n element.addEventListener('pointerdown', handlePointerDown)\n\n return {\n destroy: () => {\n stopTracking()\n element.removeEventListener('pointerdown', handlePointerDown)\n if (style) {\n for (const [property] of MANAGED_STYLES) {\n const previous = previousStyles.get(property)\n if (previous) {\n style.setProperty(property, previous)\n } else {\n style.removeProperty(property)\n }\n }\n }\n },\n }\n}\n","export interface WheelInstance {\n destroy: () => void\n}\n\n/**\n * Listen to wheel events on an element.\n *\n * The listener is registered with `passive: false` so that the handler can call\n * `preventDefault()` to stop the page from scrolling.\n */\nexport function createWheel(\n element: Element,\n onWheel: (event: WheelEvent) => void,\n): WheelInstance {\n const handler = (event: Event) => onWheel(event as WheelEvent)\n\n element.addEventListener('wheel', handler, { passive: false })\n\n return {\n destroy: () => {\n // Only `capture` matters when removing, and it is false here.\n element.removeEventListener('wheel', handler)\n },\n }\n}\n"],"mappings":";;AACA,MAAa,oBAAoB;;AAEjC,MAAa,gBAAgB;AAU7B,MAAM,gBAAiC;CACrC,YAAY;CACZ,OAAO;CACR;;;;;;;AAmBD,SAAgB,mBAAuC;CACrD,IAAI,QAAQ;CACZ,IAAI,YAAY;CAChB,MAAM,4BAAY,IAAI,KAAiB;CAEvC,SAAS,SAAS,MAAuB;AACvC,UAAQ;AACR,OAAK,MAAM,YAAY,UAAW,WAAU;;CAG9C,SAAS,UAAU;AACjB,MAAI,UAAW;AACf,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,mBAAmB;AACpE,YAAS;IAAE,GAAG;IAAO,OAAO;IAAe,CAAC;AAC5C;;AAEF,YACG,mBAAmB,CACnB,MAAM,WAAW;AAChB,OAAI,UAAW;AACf,YAAS;IAAE,YAAY;IAAQ,OAAO;IAAM,CAAC;IAC7C,CACD,YAAY;AACX,OAAI,UAAW;AACf,YAAS;IAAE,GAAG;IAAO,OAAO;IAAmB,CAAC;IAChD;;AAGN,QAAO;EACL;EACA,gBAAgB;EAChB,sBAAsB;EACtB,YAAY,aAAa;AACvB,aAAU,IAAI,SAAS;AACvB,gBAAa;AACX,cAAU,OAAO,SAAS;;;EAG9B,eAAe;AACb,eAAY;AACZ,aAAU,OAAO;;EAEpB;;;;;;;;;ACpEH,SAAgB,kBACd,YACA,eACqB;AACrB,KAAI,CAAC,WACH,QAAO,EAAE,eAAe,IAAI;CAG9B,MAAM,SAAS,CAAC,GAAG,WAAW,OAAO,QAAQ,CAAC;AAC9C,MAAK,MAAM,SAAS,OAClB,OAAM,iBAAiB,eAAe,cAAc;AAGtD,QAAO,EACL,eAAe;AACb,OAAK,MAAM,SAAS,OAClB,OAAM,oBAAoB,eAAe,cAAc;IAG5D;;;;AC1BH,MAAM,uBAAuB;CAC3B,SAAS;CACT,UAAU;CACV,YAAY;CACb;;;;;AAcD,SAAgB,gBACd,YACA,EAAE,eAAe,gBAAgB,oBACd;AACnB,QAAO,kBAAkB,aAAa,UAAU;AAC9C,MAAI,CAAC,MAAM,KAAM;EAIjB,MAAM,OAAO,MAAM,KAAK,KAAK;AAE7B,MACE,QAAQ,qBAAqB,YAC5B,QAAQ,qBAAqB,WAAW,MAAM,KAAK,MAAM,EAE1D,kBAAiB,MAAM,KAAK,GAAG;WACtB,QAAQ,qBAAqB,QACtC,iBAAgB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;WACpC,QAAQ,qBAAqB,WACtC,oBAAmB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;GAElD;;;;;;;;;;;ACKJ,MAAM,iBAAiB;CACrB,CAAC,gBAAgB,OAAO;CACxB,CAAC,eAAe,OAAO;CACvB,CAAC,uBAAuB,OAAO;CAC/B,CAAC,yBAAyB,OAAO;CAClC;;;;;;;;;;AAiBD,SAAgB,WACd,SACA,EACE,WAAW,aAAa,GACxB,QACA,aACA,QACA,cACe,EAAE,EACL;CACd,MAAM,YAAY,KAAK,IAAI,YAAY,EAAE;CACzC,MAAM,UAAU;CAChB,MAAM,QAAS,QAAiC;CAEhD,MAAM,iCAAiB,IAAI,KAAqB;AAChD,KAAI,MACF,MAAK,MAAM,CAAC,UAAU,UAAU,gBAAgB;AAC9C,iBAAe,IAAI,UAAU,MAAM,iBAAiB,SAAS,CAAC;AAC9D,QAAM,YAAY,UAAU,MAAM;;CAItC,IAAI,YAA2B;;CAE/B,IAAI,aAAiC;CACrC,IAAI,SAAS;CACb,IAAI,SAAS;CACb,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI;CAEJ,SAAS,MACP,OACA,QACA,QACW;AACX,SAAO;GACL,GAAG,MAAM,UAAU;GACnB,GAAG,MAAM,UAAU;GACnB;GACA;GACA,SAAS,MAAM;GACf,SAAS,MAAM;GACf;GACD;;;;;;;CAQH,SAAS,mBAAmB,OAAc;AACxC,QAAM,gBAAgB;;CAGxB,SAAS,kBAAkB,OAAc;EACvC,MAAM,eAAe;AAErB,MAAI,cAAc,KAAM;AAExB,cAAY,aAAa;AACzB,WAAS,QAAQ,aAAa;AAC9B,WAAS,QAAQ,aAAa;AAE9B,MAAI,UAAU,OAAO;AACnB,oBAAiB,MAAM;AACvB,SAAM,SAAS;;AAGjB,UAAQ,oBAAoB,UAAU;AAGtC,eACE,QAAQ,oBAAoB,UAAU,KAAK,OACvC,UACC,WAAW,UAAU;AAE5B,aAAW,iBAAiB,eAAe,kBAAkB;AAC7D,aAAW,iBAAiB,aAAa,gBAAgB;AACzD,aAAW,iBAAiB,iBAAiB,gBAAgB;AAC7D,aAAW,UAAU,iBAAiB,eAAe,mBAAmB;AAExE,gBAAc,MAAM,cAAc,GAAG,EAAE,CAAC;;CAG1C,SAAS,kBAAkB,OAAc;EACvC,MAAM,eAAe;AACrB,MAAI,aAAa,cAAc,UAAW;EAE1C,MAAM,SAAS,aAAa,UAAU;EACtC,MAAM,SAAS,aAAa,UAAU;AACtC,UAAQ,aAAa;AACrB,UAAQ,aAAa;AAGrB,MAAI,KAAK,IAAI,OAAO,GAAG,aAAa,KAAK,IAAI,OAAO,GAAG,UAAW;AAElE,WAAS,MAAM,cAAc,QAAQ,OAAO,CAAC;;CAG/C,SAAS,gBAAgB,OAAc;EACrC,MAAM,eAAe;AACrB,MAAI,aAAa,cAAc,UAAW;EAI1C,MAAM,aAAa,MAAM,cAFV,aAAa,UAAU,OACvB,aAAa,UAAU,MACgB;AAEtD,gBAAc;AACd,cAAY,WAAW;;CAGzB,SAAS,eAAe;AACtB,MAAI,cAAc,KAAM;AACxB,UAAQ,wBAAwB,UAAU;AAC1C,cAAY,oBAAoB,eAAe,kBAAkB;AACjE,cAAY,oBAAoB,aAAa,gBAAgB;AAC7D,cAAY,oBAAoB,iBAAiB,gBAAgB;AACjE,aAAW,UAAU,oBAAoB,eAAe,mBAAmB;AAC3E,MAAI,UAAU,OAAO;AACnB,SAAM,SAAS,kBAAkB;AACjC,oBAAiB,KAAA;;AAEnB,eAAa;AACb,cAAY;;AAGd,SAAQ,iBAAiB,eAAe,kBAAkB;AAE1D,QAAO,EACL,eAAe;AACb,gBAAc;AACd,UAAQ,oBAAoB,eAAe,kBAAkB;AAC7D,MAAI,MACF,MAAK,MAAM,CAAC,aAAa,gBAAgB;GACvC,MAAM,WAAW,eAAe,IAAI,SAAS;AAC7C,OAAI,SACF,OAAM,YAAY,UAAU,SAAS;OAErC,OAAM,eAAe,SAAS;;IAKvC;;;;;;;;;;AC3MH,SAAgB,YACd,SACA,SACe;CACf,MAAM,WAAW,UAAiB,QAAQ,MAAoB;AAE9D,SAAQ,iBAAiB,SAAS,SAAS,EAAE,SAAS,OAAO,CAAC;AAE9D,QAAO,EACL,eAAe;AAEb,UAAQ,oBAAoB,SAAS,QAAQ;IAEhD"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,3 +12,10 @@ export {
|
|
|
12
12
|
type MIDIInputInstance,
|
|
13
13
|
} from './midi/input'
|
|
14
14
|
export { createMIDIMessage, type MIDIMessageInstance } from './midi/message'
|
|
15
|
+
export {
|
|
16
|
+
createDrag,
|
|
17
|
+
type DragInstance,
|
|
18
|
+
type DragOptions,
|
|
19
|
+
type DragState,
|
|
20
|
+
} from './pointer/drag'
|
|
21
|
+
export { createWheel, type WheelInstance } from './pointer/wheel'
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export type DragState = {
|
|
2
|
+
/** Total movement from the drag start, in screen coordinates. */
|
|
3
|
+
x: number
|
|
4
|
+
y: number
|
|
5
|
+
/** Movement since the previous event, in screen coordinates. */
|
|
6
|
+
deltaX: number
|
|
7
|
+
deltaY: number
|
|
8
|
+
/** Pointer position in viewport coordinates. */
|
|
9
|
+
clientX: number
|
|
10
|
+
clientY: number
|
|
11
|
+
event: PointerEvent
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type DragOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Minimum movement in pixels before `onDrag` fires.
|
|
17
|
+
* Prevents `onDrag` from firing on, for example, a double click.
|
|
18
|
+
* Values below 1 are clamped to 1.
|
|
19
|
+
*
|
|
20
|
+
* @default 1
|
|
21
|
+
*/
|
|
22
|
+
threshold?: number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* CSS cursor to show while dragging. Applied to the element itself: pointer
|
|
26
|
+
* capture keeps it in effect even once the pointer leaves the element, so
|
|
27
|
+
* there is no need to touch the document.
|
|
28
|
+
*/
|
|
29
|
+
cursor?: string
|
|
30
|
+
|
|
31
|
+
onDragStart?: (state: DragState) => void
|
|
32
|
+
onDrag?: (state: DragState) => void
|
|
33
|
+
onDragEnd?: (state: DragState) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DragInstance {
|
|
37
|
+
destroy: () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Applied to the element for the lifetime of the instance.
|
|
42
|
+
*
|
|
43
|
+
* `touch-action` stops touch dragging from scrolling the page. That alone makes
|
|
44
|
+
* the browser treat a long press as the start of a text selection, so selection
|
|
45
|
+
* and the iOS callout are suppressed here too.
|
|
46
|
+
*/
|
|
47
|
+
const MANAGED_STYLES = [
|
|
48
|
+
['touch-action', 'none'],
|
|
49
|
+
['user-select', 'none'],
|
|
50
|
+
['-webkit-user-select', 'none'],
|
|
51
|
+
['-webkit-touch-callout', 'none'],
|
|
52
|
+
] as const
|
|
53
|
+
|
|
54
|
+
type CaptureTarget = {
|
|
55
|
+
setPointerCapture?: (pointerId: number) => void
|
|
56
|
+
releasePointerCapture?: (pointerId: number) => void
|
|
57
|
+
hasPointerCapture?: (pointerId: number) => boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Track a pointer drag on an element.
|
|
62
|
+
*
|
|
63
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
64
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
65
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
66
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
67
|
+
* selection instead.
|
|
68
|
+
*/
|
|
69
|
+
export function createDrag(
|
|
70
|
+
element: Element,
|
|
71
|
+
{
|
|
72
|
+
threshold: _threshold = 1,
|
|
73
|
+
cursor,
|
|
74
|
+
onDragStart,
|
|
75
|
+
onDrag,
|
|
76
|
+
onDragEnd,
|
|
77
|
+
}: DragOptions = {},
|
|
78
|
+
): DragInstance {
|
|
79
|
+
const threshold = Math.max(_threshold, 1)
|
|
80
|
+
const capture = element as CaptureTarget
|
|
81
|
+
const style = (element as Partial<HTMLElement>).style
|
|
82
|
+
|
|
83
|
+
const previousStyles = new Map<string, string>()
|
|
84
|
+
if (style) {
|
|
85
|
+
for (const [property, value] of MANAGED_STYLES) {
|
|
86
|
+
previousStyles.set(property, style.getPropertyValue(property))
|
|
87
|
+
style.setProperty(property, value)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let pointerId: number | null = null
|
|
92
|
+
/** Where the listeners for the current drag live. */
|
|
93
|
+
let moveTarget: EventTarget | null = null
|
|
94
|
+
let startX = 0
|
|
95
|
+
let startY = 0
|
|
96
|
+
let lastX = 0
|
|
97
|
+
let lastY = 0
|
|
98
|
+
let previousCursor: string | undefined
|
|
99
|
+
|
|
100
|
+
function state(
|
|
101
|
+
event: PointerEvent,
|
|
102
|
+
deltaX: number,
|
|
103
|
+
deltaY: number,
|
|
104
|
+
): DragState {
|
|
105
|
+
return {
|
|
106
|
+
x: event.screenX - startX,
|
|
107
|
+
y: event.screenY - startY,
|
|
108
|
+
deltaX,
|
|
109
|
+
deltaY,
|
|
110
|
+
clientX: event.clientX,
|
|
111
|
+
clientY: event.clientY,
|
|
112
|
+
event,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `user-select: none` only makes the element's own text unselectable; the
|
|
118
|
+
* browser still starts a selection on long press and grabs whatever text it
|
|
119
|
+
* finds nearby. Cancelling the selection outright is what actually stops it.
|
|
120
|
+
*/
|
|
121
|
+
function preventSelectStart(event: Event) {
|
|
122
|
+
event.preventDefault()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handlePointerDown(event: Event) {
|
|
126
|
+
const pointerEvent = event as PointerEvent
|
|
127
|
+
// Only one pointer drives the drag; ignore additional touches.
|
|
128
|
+
if (pointerId !== null) return
|
|
129
|
+
|
|
130
|
+
pointerId = pointerEvent.pointerId
|
|
131
|
+
startX = lastX = pointerEvent.screenX
|
|
132
|
+
startY = lastY = pointerEvent.screenY
|
|
133
|
+
|
|
134
|
+
if (cursor && style) {
|
|
135
|
+
previousCursor = style.cursor
|
|
136
|
+
style.cursor = cursor
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
capture.setPointerCapture?.(pointerId)
|
|
140
|
+
// With pointer capture the element receives the rest of the gesture.
|
|
141
|
+
// Without it (older engines, jsdom) fall back to the window.
|
|
142
|
+
moveTarget =
|
|
143
|
+
capture.hasPointerCapture?.(pointerId) === true
|
|
144
|
+
? element
|
|
145
|
+
: (globalThis.window ?? element)
|
|
146
|
+
|
|
147
|
+
moveTarget.addEventListener('pointermove', handlePointerMove)
|
|
148
|
+
moveTarget.addEventListener('pointerup', handlePointerUp)
|
|
149
|
+
moveTarget.addEventListener('pointercancel', handlePointerUp)
|
|
150
|
+
globalThis.document?.addEventListener('selectstart', preventSelectStart)
|
|
151
|
+
|
|
152
|
+
onDragStart?.(state(pointerEvent, 0, 0))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function handlePointerMove(event: Event) {
|
|
156
|
+
const pointerEvent = event as PointerEvent
|
|
157
|
+
if (pointerEvent.pointerId !== pointerId) return
|
|
158
|
+
|
|
159
|
+
const deltaX = pointerEvent.screenX - lastX
|
|
160
|
+
const deltaY = pointerEvent.screenY - lastY
|
|
161
|
+
lastX = pointerEvent.screenX
|
|
162
|
+
lastY = pointerEvent.screenY
|
|
163
|
+
|
|
164
|
+
// Movement below the threshold is dropped rather than accumulated.
|
|
165
|
+
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return
|
|
166
|
+
|
|
167
|
+
onDrag?.(state(pointerEvent, deltaX, deltaY))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handlePointerUp(event: Event) {
|
|
171
|
+
const pointerEvent = event as PointerEvent
|
|
172
|
+
if (pointerEvent.pointerId !== pointerId) return
|
|
173
|
+
|
|
174
|
+
const deltaX = pointerEvent.screenX - lastX
|
|
175
|
+
const deltaY = pointerEvent.screenY - lastY
|
|
176
|
+
const finalState = state(pointerEvent, deltaX, deltaY)
|
|
177
|
+
|
|
178
|
+
stopTracking()
|
|
179
|
+
onDragEnd?.(finalState)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stopTracking() {
|
|
183
|
+
if (pointerId === null) return
|
|
184
|
+
capture.releasePointerCapture?.(pointerId)
|
|
185
|
+
moveTarget?.removeEventListener('pointermove', handlePointerMove)
|
|
186
|
+
moveTarget?.removeEventListener('pointerup', handlePointerUp)
|
|
187
|
+
moveTarget?.removeEventListener('pointercancel', handlePointerUp)
|
|
188
|
+
globalThis.document?.removeEventListener('selectstart', preventSelectStart)
|
|
189
|
+
if (cursor && style) {
|
|
190
|
+
style.cursor = previousCursor ?? ''
|
|
191
|
+
previousCursor = undefined
|
|
192
|
+
}
|
|
193
|
+
moveTarget = null
|
|
194
|
+
pointerId = null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
element.addEventListener('pointerdown', handlePointerDown)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
destroy: () => {
|
|
201
|
+
stopTracking()
|
|
202
|
+
element.removeEventListener('pointerdown', handlePointerDown)
|
|
203
|
+
if (style) {
|
|
204
|
+
for (const [property] of MANAGED_STYLES) {
|
|
205
|
+
const previous = previousStyles.get(property)
|
|
206
|
+
if (previous) {
|
|
207
|
+
style.setProperty(property, previous)
|
|
208
|
+
} else {
|
|
209
|
+
style.removeProperty(property)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface WheelInstance {
|
|
2
|
+
destroy: () => void
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Listen to wheel events on an element.
|
|
7
|
+
*
|
|
8
|
+
* The listener is registered with `passive: false` so that the handler can call
|
|
9
|
+
* `preventDefault()` to stop the page from scrolling.
|
|
10
|
+
*/
|
|
11
|
+
export function createWheel(
|
|
12
|
+
element: Element,
|
|
13
|
+
onWheel: (event: WheelEvent) => void,
|
|
14
|
+
): WheelInstance {
|
|
15
|
+
const handler = (event: Event) => onWheel(event as WheelEvent)
|
|
16
|
+
|
|
17
|
+
element.addEventListener('wheel', handler, { passive: false })
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
destroy: () => {
|
|
21
|
+
// Only `capture` matters when removing, and it is false here.
|
|
22
|
+
element.removeEventListener('wheel', handler)
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|