@tremolo-ui/react 0.5.0 → 0.7.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/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +1669 -541
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1571 -486
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1571 -486
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1640 -538
- package/dist/index.js.map +1 -1
- package/package.json +29 -27
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +156 -60
- package/src/components/NumberInput/DecrementStepper.tsx +3 -10
- package/src/components/NumberInput/IncrementStepper.tsx +3 -10
- package/src/components/NumberInput/InputField.tsx +185 -108
- package/src/components/NumberInput/Stepper.tsx +120 -84
- package/src/components/NumberInput/StepperButton.tsx +100 -0
- package/src/components/NumberInput/context.tsx +34 -8
- package/src/components/NumberInput/index.tsx +270 -82
- package/src/components/Piano/index.tsx +363 -200
- package/src/components/PointsEditor/Background.tsx +13 -10
- package/src/components/PointsEditor/Container.tsx +83 -29
- package/src/components/PointsEditor/Point.tsx +241 -79
- package/src/components/PointsEditor/SelectionBox.tsx +48 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -41
- package/src/components/Slider/Marks.tsx +48 -19
- package/src/components/Slider/MarksOption.tsx +64 -45
- package/src/components/Slider/Thumb.tsx +107 -55
- package/src/components/Slider/Track.tsx +66 -62
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +136 -77
- package/src/components/Slider/type.ts +29 -9
- package/src/components/XYPad/Area.tsx +46 -41
- package/src/components/XYPad/Thumb.tsx +131 -64
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +183 -53
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +169 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +51 -12
- package/src/hooks/useDragValue.ts +71 -11
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useEventListener.ts +10 -6
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +45 -25
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/NumberInput/stepperButton.tsx +0 -90
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import { NOT_SUPPORTED, PERMISSION_DENIED, createAnimationCanvas, createDrag, createDragValue, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createWheel, elementMapping, relativeMapping, toXY } from "@tremolo-ui/dom";
|
|
1
|
+
import { useComposedRefs } from "./compose-refs.js";
|
|
2
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { NOT_SUPPORTED, PERMISSION_DENIED, PITCH_BEND_CENTER, UNAVAILABLE, applyDelta, blackKeyWidth, createAnimationCanvas, createDrag, createDragValue, createDropZone, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createSelectionBox, createWheel, elementMapping, getNoteRangeArray, mapModifier, matchesAccept, notePosition, pianoWidth, relativeMapping, selectModifier, toXY } from "@tremolo-ui/dom";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import {
|
|
5
|
+
import { clamp, isWhiteKey, linearScale, noteKey, radian, toFixed, toPrecision } from "@tremolo-ui/functions";
|
|
6
6
|
//#region src/components/AnimationCanvas/index.tsx
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* A simple animatable canvas with requestAnimationFrame()
|
|
9
|
+
*/
|
|
10
|
+
const AnimationCanvas = /* @__PURE__ */ forwardRef(function AnimationCanvas({ draw, init, animate = true, options, reduceFlickering = true, width = 100, height = 100, resizable = false, className, onContextMenu = (event) => event.preventDefault(), ...props }, forwardedRef) {
|
|
8
11
|
const [node, setNode] = useState(null);
|
|
12
|
+
const composedRef = useComposedRefs(forwardedRef, setNode);
|
|
9
13
|
const latest = useRef({
|
|
10
14
|
draw,
|
|
11
15
|
init,
|
|
@@ -16,30 +20,6 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
16
20
|
options
|
|
17
21
|
});
|
|
18
22
|
const instanceRef = useRef(null);
|
|
19
|
-
/** Set while the instance below is newer than the effect that pushes into it. */
|
|
20
|
-
const justCreated = useRef(false);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!node) return;
|
|
23
|
-
const current = latest.current;
|
|
24
|
-
const instance = createAnimationCanvas(node, {
|
|
25
|
-
draw: (context, frame) => latest.current.draw(context, frame),
|
|
26
|
-
init: (context, size) => latest.current.init?.(context, size),
|
|
27
|
-
animate: current.animate,
|
|
28
|
-
size: {
|
|
29
|
-
width: current.width,
|
|
30
|
-
height: current.height
|
|
31
|
-
},
|
|
32
|
-
reduceFlickering: current.reduceFlickering,
|
|
33
|
-
relativeSize,
|
|
34
|
-
contextAttributes: current.options
|
|
35
|
-
});
|
|
36
|
-
instanceRef.current = instance;
|
|
37
|
-
justCreated.current = true;
|
|
38
|
-
return () => {
|
|
39
|
-
instanceRef.current = null;
|
|
40
|
-
instance.destroy();
|
|
41
|
-
};
|
|
42
|
-
}, [node, relativeSize]);
|
|
43
23
|
useEffect(() => {
|
|
44
24
|
latest.current = {
|
|
45
25
|
draw,
|
|
@@ -50,10 +30,6 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
50
30
|
reduceFlickering,
|
|
51
31
|
options
|
|
52
32
|
};
|
|
53
|
-
if (justCreated.current) {
|
|
54
|
-
justCreated.current = false;
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
33
|
instanceRef.current?.update({
|
|
58
34
|
animate,
|
|
59
35
|
size: {
|
|
@@ -63,13 +39,34 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
63
39
|
reduceFlickering
|
|
64
40
|
});
|
|
65
41
|
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!node) return;
|
|
44
|
+
const current = latest.current;
|
|
45
|
+
const instance = createAnimationCanvas(node, {
|
|
46
|
+
draw: (context, frame) => latest.current.draw(context, frame),
|
|
47
|
+
init: (context, size) => latest.current.init?.(context, size),
|
|
48
|
+
animate: current.animate,
|
|
49
|
+
size: {
|
|
50
|
+
width: current.width,
|
|
51
|
+
height: current.height
|
|
52
|
+
},
|
|
53
|
+
reduceFlickering: current.reduceFlickering,
|
|
54
|
+
resizable,
|
|
55
|
+
contextAttributes: current.options
|
|
56
|
+
});
|
|
57
|
+
instanceRef.current = instance;
|
|
58
|
+
return () => {
|
|
59
|
+
instanceRef.current = null;
|
|
60
|
+
instance.destroy();
|
|
61
|
+
};
|
|
62
|
+
}, [node, resizable]);
|
|
66
63
|
return /* @__PURE__ */ jsx("canvas", {
|
|
67
|
-
className
|
|
68
|
-
ref:
|
|
64
|
+
className,
|
|
65
|
+
ref: composedRef,
|
|
69
66
|
onContextMenu,
|
|
70
67
|
...props
|
|
71
68
|
});
|
|
72
|
-
}
|
|
69
|
+
});
|
|
73
70
|
//#endregion
|
|
74
71
|
//#region src/hooks/_internal/useCallbackRef.ts
|
|
75
72
|
/**
|
|
@@ -86,6 +83,294 @@ function useCallbackRef(callback, deps = []) {
|
|
|
86
83
|
return useCallback((...args) => callbackRef.current?.(...args), deps);
|
|
87
84
|
}
|
|
88
85
|
//#endregion
|
|
86
|
+
//#region src/hooks/useDropZone.ts
|
|
87
|
+
/**
|
|
88
|
+
* Take files dropped onto an element.
|
|
89
|
+
*
|
|
90
|
+
* Any element can become a drop target this way — a waveform display, a track
|
|
91
|
+
* lane, a canvas — without wrapping it in anything.
|
|
92
|
+
*
|
|
93
|
+
* @returns a ref callback for the element, and whether a drag is over it
|
|
94
|
+
*/
|
|
95
|
+
function useDropZone({ accept, multiple, disabled, onDrop, onReject } = {}) {
|
|
96
|
+
const dropHandler = useCallbackRef(onDrop);
|
|
97
|
+
const rejectHandler = useCallbackRef(onReject);
|
|
98
|
+
const [state, setState] = useState({
|
|
99
|
+
over: false,
|
|
100
|
+
invalid: false
|
|
101
|
+
});
|
|
102
|
+
const [node, setNode] = useState(null);
|
|
103
|
+
const instanceRef = useRef(null);
|
|
104
|
+
const latest = useRef({
|
|
105
|
+
accept,
|
|
106
|
+
multiple,
|
|
107
|
+
disabled
|
|
108
|
+
});
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!node) return;
|
|
111
|
+
const instance = createDropZone(node, {
|
|
112
|
+
...latest.current,
|
|
113
|
+
onDrop: (files, event) => dropHandler(files, event),
|
|
114
|
+
onReject: (files, event) => rejectHandler(files, event),
|
|
115
|
+
onStateChange: (next) => setState(next)
|
|
116
|
+
});
|
|
117
|
+
instanceRef.current = instance;
|
|
118
|
+
return () => {
|
|
119
|
+
instanceRef.current = null;
|
|
120
|
+
instance.destroy();
|
|
121
|
+
setState({
|
|
122
|
+
over: false,
|
|
123
|
+
invalid: false
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
node,
|
|
128
|
+
dropHandler,
|
|
129
|
+
rejectHandler
|
|
130
|
+
]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
latest.current = {
|
|
133
|
+
accept,
|
|
134
|
+
multiple,
|
|
135
|
+
disabled
|
|
136
|
+
};
|
|
137
|
+
instanceRef.current?.update({
|
|
138
|
+
accept,
|
|
139
|
+
multiple,
|
|
140
|
+
disabled
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
refCallback: setNode,
|
|
145
|
+
over: state.over,
|
|
146
|
+
invalid: state.invalid
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* An area that takes files dropped onto it.
|
|
151
|
+
*/
|
|
152
|
+
const DropZone = { Root: /* @__PURE__ */ forwardRef(({ accept, multiple = false, disabled = false, onDrop, onReject, children, ...props }, ref) => {
|
|
153
|
+
const { refCallback, over, invalid } = useDropZone({
|
|
154
|
+
accept,
|
|
155
|
+
multiple,
|
|
156
|
+
disabled,
|
|
157
|
+
onDrop,
|
|
158
|
+
onReject
|
|
159
|
+
});
|
|
160
|
+
const composedRef = useComposedRefs(ref, refCallback);
|
|
161
|
+
return /* @__PURE__ */ jsx("div", {
|
|
162
|
+
ref: composedRef,
|
|
163
|
+
"data-dragover": over ? "" : void 0,
|
|
164
|
+
"data-invalid": invalid ? "" : void 0,
|
|
165
|
+
"data-disabled": disabled ? "" : void 0,
|
|
166
|
+
...props,
|
|
167
|
+
children
|
|
168
|
+
});
|
|
169
|
+
}) };
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/components/_util/visually-hidden.ts
|
|
172
|
+
/**
|
|
173
|
+
* Take an element out of sight while leaving it in the accessibility tree and
|
|
174
|
+
* in the tab order.
|
|
175
|
+
*
|
|
176
|
+
* `display: none` and `visibility: hidden` would remove it from both, and the
|
|
177
|
+
* native control underneath a headless component is what carries the ARIA and
|
|
178
|
+
* the keyboard behaviour. `pointer-events: none` is safe because nothing is
|
|
179
|
+
* ever clicked here directly: a `<label>` forwards its click, and a drag is
|
|
180
|
+
* handled by the part that is visible.
|
|
181
|
+
*/
|
|
182
|
+
const visuallyHiddenStyle = {
|
|
183
|
+
position: "absolute",
|
|
184
|
+
width: 1,
|
|
185
|
+
height: 1,
|
|
186
|
+
padding: 0,
|
|
187
|
+
margin: -1,
|
|
188
|
+
overflow: "hidden",
|
|
189
|
+
clip: "rect(0, 0, 0, 0)",
|
|
190
|
+
clipPath: "inset(50%)",
|
|
191
|
+
whiteSpace: "nowrap",
|
|
192
|
+
border: 0,
|
|
193
|
+
pointerEvents: "none"
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/components/FileInput/context.tsx
|
|
197
|
+
const FileInputContext = /* @__PURE__ */ createContext(null);
|
|
198
|
+
const FileInputProvider = FileInputContext.Provider;
|
|
199
|
+
function useFileInputContext(selector) {
|
|
200
|
+
const context = useContext(FileInputContext);
|
|
201
|
+
if (!context) throw new Error("Missing FileInputContext.Provider in the tree");
|
|
202
|
+
return selector ? selector(context) : context;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Picks files, and hands them over as `File`s.
|
|
206
|
+
*/
|
|
207
|
+
const FileInput = {
|
|
208
|
+
Root: /* @__PURE__ */ forwardRef(({ accept, multiple = false, disabled = false, onChange, onReject, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, ...props }, ref) => {
|
|
209
|
+
const inputId = useId();
|
|
210
|
+
const context = useMemo(() => ({
|
|
211
|
+
inputId,
|
|
212
|
+
disabled
|
|
213
|
+
}), [inputId, disabled]);
|
|
214
|
+
function handleChange(event) {
|
|
215
|
+
const picked = Array.from(event.target.files ?? []);
|
|
216
|
+
event.target.value = "";
|
|
217
|
+
const accepted = [];
|
|
218
|
+
const rejected = [];
|
|
219
|
+
for (const file of picked) if (matchesAccept(file, accept)) accepted.push(file);
|
|
220
|
+
else rejected.push(file);
|
|
221
|
+
if (rejected.length > 0) onReject?.(rejected);
|
|
222
|
+
if (accepted.length > 0) onChange?.(accepted);
|
|
223
|
+
}
|
|
224
|
+
return /* @__PURE__ */ jsx(FileInputProvider, {
|
|
225
|
+
value: context,
|
|
226
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
227
|
+
ref,
|
|
228
|
+
"data-disabled": disabled ? "" : void 0,
|
|
229
|
+
...props,
|
|
230
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
231
|
+
id: inputId,
|
|
232
|
+
type: "file",
|
|
233
|
+
accept,
|
|
234
|
+
multiple,
|
|
235
|
+
disabled,
|
|
236
|
+
style: visuallyHiddenStyle,
|
|
237
|
+
"aria-label": ariaLabel,
|
|
238
|
+
"aria-labelledby": ariaLabelledby,
|
|
239
|
+
"aria-describedby": ariaDescribedby,
|
|
240
|
+
onChange: handleChange
|
|
241
|
+
}), children]
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
}),
|
|
245
|
+
Trigger: /* @__PURE__ */ forwardRef(({ children, ...props }, ref) => {
|
|
246
|
+
const { inputId, disabled } = useFileInputContext();
|
|
247
|
+
return /* @__PURE__ */ jsx("label", {
|
|
248
|
+
ref,
|
|
249
|
+
htmlFor: inputId,
|
|
250
|
+
"data-disabled": disabled ? "" : void 0,
|
|
251
|
+
...props,
|
|
252
|
+
children
|
|
253
|
+
});
|
|
254
|
+
})
|
|
255
|
+
};
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/hooks/_internal/useCheckSteps.ts
|
|
258
|
+
/** Positions probed across the travel. The ends are left out so that the
|
|
259
|
+
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
|
260
|
+
const PROBES = [
|
|
261
|
+
.1,
|
|
262
|
+
.2,
|
|
263
|
+
.3,
|
|
264
|
+
.4,
|
|
265
|
+
.5,
|
|
266
|
+
.6,
|
|
267
|
+
.7,
|
|
268
|
+
.8,
|
|
269
|
+
.9
|
|
270
|
+
];
|
|
271
|
+
const NONE = {
|
|
272
|
+
shiftKey: false,
|
|
273
|
+
altKey: false,
|
|
274
|
+
ctrlKey: false,
|
|
275
|
+
metaKey: false
|
|
276
|
+
};
|
|
277
|
+
const MODIFIER_STATE = {
|
|
278
|
+
shift: {
|
|
279
|
+
...NONE,
|
|
280
|
+
shiftKey: true
|
|
281
|
+
},
|
|
282
|
+
alt: {
|
|
283
|
+
...NONE,
|
|
284
|
+
altKey: true
|
|
285
|
+
},
|
|
286
|
+
ctrl: {
|
|
287
|
+
...NONE,
|
|
288
|
+
ctrlKey: true
|
|
289
|
+
},
|
|
290
|
+
meta: {
|
|
291
|
+
...NONE,
|
|
292
|
+
metaKey: true
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
296
|
+
function entries(options) {
|
|
297
|
+
if (Array.isArray(options)) return [{
|
|
298
|
+
name: "",
|
|
299
|
+
modifiers: NONE
|
|
300
|
+
}];
|
|
301
|
+
return Object.keys(options).map((key) => ({
|
|
302
|
+
name: key === "default" ? "" : key,
|
|
303
|
+
modifiers: MODIFIER_STATE[key] ?? NONE
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Press every entry of `options` at nine points along the travel and report
|
|
308
|
+
* whether anything came of it.
|
|
309
|
+
*
|
|
310
|
+
* Run against `applyDelta` itself rather than against a reading of `step`:
|
|
311
|
+
* the whole point is that the amount, the step and the scale interact, and
|
|
312
|
+
* the pipeline is the only thing that knows how.
|
|
313
|
+
*/
|
|
314
|
+
function probe(options, range, format) {
|
|
315
|
+
const { min, max, scale = linearScale } = range;
|
|
316
|
+
const outcome = {
|
|
317
|
+
moved: false,
|
|
318
|
+
visible: false
|
|
319
|
+
};
|
|
320
|
+
for (const { modifiers } of entries(options)) for (const position of PROBES) {
|
|
321
|
+
const value = scale.denormalize(position, min, max);
|
|
322
|
+
const up = applyDelta(value, 1, options, range, modifiers);
|
|
323
|
+
const down = applyDelta(value, -1, options, range, modifiers);
|
|
324
|
+
if (up !== value || down !== value) outcome.moved = true;
|
|
325
|
+
if (!format) continue;
|
|
326
|
+
const shown = format(value);
|
|
327
|
+
if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
|
|
328
|
+
}
|
|
329
|
+
return outcome;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
333
|
+
* a change the user can see.
|
|
334
|
+
*
|
|
335
|
+
* Two settings that are each fine on their own can cancel out, and nothing
|
|
336
|
+
* fails when they do — the control simply sits there:
|
|
337
|
+
*
|
|
338
|
+
* - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
|
|
339
|
+
* `step={1}` rounds every press straight back to where it started
|
|
340
|
+
* - **the display coarser than the amount.** A `format` showing two decimals
|
|
341
|
+
* of a kHz value cannot show a press worth 1 Hz
|
|
342
|
+
*
|
|
343
|
+
* The second is only reported when the press is invisible at *every* point
|
|
344
|
+
* along the travel. A display that rounds is a deliberate choice and is
|
|
345
|
+
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
346
|
+
*
|
|
347
|
+
* @internal
|
|
348
|
+
*/
|
|
349
|
+
function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
|
|
350
|
+
const { min, max, step, scale } = range ?? {};
|
|
351
|
+
const where = axis ? `${component} (${axis})` : component;
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
try {
|
|
354
|
+
if (process.env.NODE_ENV === "production") return;
|
|
355
|
+
if (!range || min === void 0 || max === void 0 || !(min < max)) return;
|
|
356
|
+
for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
|
|
357
|
+
if (!options) continue;
|
|
358
|
+
const { moved, visible } = probe(options, range, format);
|
|
359
|
+
if (!moved) console.warn(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (step !== void 0 ? ` Each press is smaller than \`step\` (${step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
|
|
360
|
+
else if (format && !visible) console.warn(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
|
|
361
|
+
}
|
|
362
|
+
} catch {}
|
|
363
|
+
}, [
|
|
364
|
+
where,
|
|
365
|
+
min,
|
|
366
|
+
max,
|
|
367
|
+
step,
|
|
368
|
+
scale,
|
|
369
|
+
keyboard,
|
|
370
|
+
wheel
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
89
374
|
//#region src/hooks/useDragValue.ts
|
|
90
375
|
/**
|
|
91
376
|
* Drive a value with a pointer drag.
|
|
@@ -93,7 +378,7 @@ function useCallbackRef(callback, deps = []) {
|
|
|
93
378
|
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
94
379
|
*/
|
|
95
380
|
function useDragValue(options) {
|
|
96
|
-
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor } = options;
|
|
381
|
+
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor, pointerLock } = options;
|
|
97
382
|
if (!baseElementRef && !options.getValue) throw new Error("useDragValue: give either baseElementRef or getValue, so that the drag has something to move");
|
|
98
383
|
const [dragging, setDragging] = useState(false);
|
|
99
384
|
const valueGetter = useCallbackRef(options.getValue);
|
|
@@ -108,19 +393,24 @@ function useDragValue(options) {
|
|
|
108
393
|
if (!node) return;
|
|
109
394
|
const instance = createDragValue(node, {
|
|
110
395
|
axis: latest.current.axis,
|
|
111
|
-
mapping: baseElementRef ? elementMapping(() => baseElementRef.current
|
|
396
|
+
mapping: baseElementRef ? elementMapping(() => baseElementRef.current, { sensitivity: (state) => latest.current.sensitivity?.(state) ?? 1 }) : relativeMapping({
|
|
397
|
+
pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100],
|
|
398
|
+
sensitivity: (state) => latest.current.sensitivity?.(state) ?? 1
|
|
399
|
+
}),
|
|
112
400
|
getValue: () => valueGetter() ?? [0, 0],
|
|
113
401
|
updateOnPointerDown: latest.current.updateOnPointerDown,
|
|
114
402
|
threshold: latest.current.threshold,
|
|
115
403
|
cursor: latest.current.cursor,
|
|
116
|
-
|
|
117
|
-
|
|
404
|
+
pointerLock: latest.current.pointerLock,
|
|
405
|
+
shouldStart: (event) => latest.current.shouldStart?.(event) ?? true,
|
|
406
|
+
onChange: (value, state) => changeHandler(value, state),
|
|
407
|
+
onDragStart: (value, state) => {
|
|
118
408
|
setDragging(true);
|
|
119
|
-
dragStartHandler(value);
|
|
409
|
+
dragStartHandler(value, state);
|
|
120
410
|
},
|
|
121
|
-
onDragEnd: (value) => {
|
|
411
|
+
onDragEnd: (value, state) => {
|
|
122
412
|
setDragging(false);
|
|
123
|
-
dragEndHandler(value);
|
|
413
|
+
dragEndHandler(value, state);
|
|
124
414
|
}
|
|
125
415
|
});
|
|
126
416
|
instanceRef.current = instance;
|
|
@@ -144,7 +434,8 @@ function useDragValue(options) {
|
|
|
144
434
|
axis,
|
|
145
435
|
updateOnPointerDown,
|
|
146
436
|
threshold,
|
|
147
|
-
cursor
|
|
437
|
+
cursor,
|
|
438
|
+
pointerLock
|
|
148
439
|
});
|
|
149
440
|
});
|
|
150
441
|
return {
|
|
@@ -178,50 +469,89 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
|
178
469
|
return setNode;
|
|
179
470
|
}
|
|
180
471
|
//#endregion
|
|
181
|
-
//#region src/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
472
|
+
//#region src/input-event.ts
|
|
473
|
+
/**
|
|
474
|
+
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
475
|
+
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
476
|
+
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
477
|
+
* `useCheckSteps` warns about.
|
|
478
|
+
*
|
|
479
|
+
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
480
|
+
* amount move at all — see `applyDelta` in `@tremolo-ui/dom`.
|
|
481
|
+
*/
|
|
482
|
+
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
483
|
+
default: ["raw", 1],
|
|
484
|
+
shift: ["raw", .1]
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
488
|
+
*
|
|
489
|
+
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
490
|
+
* and fills `deltaX`, so no modifier is bound here.
|
|
491
|
+
*/
|
|
492
|
+
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
493
|
+
/**
|
|
494
|
+
* The drag sensitivity used by value controls by default. Shift makes the
|
|
495
|
+
* same movement cover a tenth of the range, matching the arrow keys.
|
|
496
|
+
*/
|
|
497
|
+
const DEFAULT_DRAG_SENSITIVITY = {
|
|
498
|
+
default: 1,
|
|
499
|
+
shift: .1
|
|
500
|
+
};
|
|
188
501
|
//#endregion
|
|
189
|
-
//#region src/components/_util/
|
|
502
|
+
//#region src/components/_util/css-length.ts
|
|
190
503
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
504
|
+
* A number as a CSS length, for a custom property.
|
|
505
|
+
*
|
|
506
|
+
* React appends `px` to a bare number for the properties it knows take a
|
|
507
|
+
* length, and a custom property is never one of them: `--size: 50` would come
|
|
508
|
+
* out as the invalid `50`. Anything already a string is passed through, so
|
|
509
|
+
* `'3rem'` and `'100%'` still work.
|
|
193
510
|
*/
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
else if (ref !== null && ref !== void 0) ref.current = value;
|
|
511
|
+
function cssLength(value) {
|
|
512
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
197
513
|
}
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/components/_util/Placement.tsx
|
|
198
516
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
517
|
+
* The subcomponent that established the current placement, if any.
|
|
518
|
+
*
|
|
519
|
+
* @internal
|
|
201
520
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (typeof cleanup == "function") cleanup();
|
|
214
|
-
else setRef(refs[i], null);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
};
|
|
521
|
+
const PlacementContext = /* @__PURE__ */ createContext(null);
|
|
522
|
+
/**
|
|
523
|
+
* Marks its children as being inside `name`, for {@link useCheckPlacement}.
|
|
524
|
+
*
|
|
525
|
+
* @internal
|
|
526
|
+
*/
|
|
527
|
+
function Placement({ name, children }) {
|
|
528
|
+
return /* @__PURE__ */ jsx(PlacementContext.Provider, {
|
|
529
|
+
value: name,
|
|
530
|
+
children
|
|
531
|
+
});
|
|
218
532
|
}
|
|
219
533
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
534
|
+
* Warn, in development only, when `child` is not rendered inside `parent`.
|
|
535
|
+
*
|
|
536
|
+
* A compound component renders its children exactly as they are composed, so a
|
|
537
|
+
* subcomponent put at the wrong level still renders — it only loses whatever
|
|
538
|
+
* its layout depended on, and does so silently. `Slider.Thumb` outside
|
|
539
|
+
* `Slider.Track` keeps its `position: absolute` but resolves it against the
|
|
540
|
+
* page rather than the track, and neither the build nor the tests notice.
|
|
541
|
+
*
|
|
542
|
+
* @internal
|
|
222
543
|
*/
|
|
223
|
-
function
|
|
224
|
-
|
|
544
|
+
function useCheckPlacement(child, parent) {
|
|
545
|
+
const found = useContext(PlacementContext);
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
try {
|
|
548
|
+
if (process.env.NODE_ENV !== "production" && found !== parent) console.warn(`[tremolo-ui] ${child} has to be rendered inside ${parent}` + (found === null ? "." : `, but it is inside ${found}.`) + " It renders either way, so nothing fails — its position just comes out wrong.");
|
|
549
|
+
} catch {}
|
|
550
|
+
}, [
|
|
551
|
+
found,
|
|
552
|
+
child,
|
|
553
|
+
parent
|
|
554
|
+
]);
|
|
225
555
|
}
|
|
226
556
|
/**
|
|
227
557
|
* 角度から円弧上の点を求める。
|
|
@@ -240,6 +570,19 @@ function arcRadius(strokeWidth) {
|
|
|
240
570
|
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
241
571
|
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
242
572
|
}
|
|
573
|
+
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
574
|
+
function arcPath(startAngle, endAngle, radius) {
|
|
575
|
+
const start = pointOnArc(startAngle, radius);
|
|
576
|
+
const sweep = endAngle - startAngle;
|
|
577
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
|
|
578
|
+
const segmentSweep = sweep / segmentCount;
|
|
579
|
+
let path = `M ${start.x} ${start.y}`;
|
|
580
|
+
for (let i = 1; i <= segmentCount; i += 1) {
|
|
581
|
+
const end = pointOnArc(startAngle + segmentSweep * i, radius);
|
|
582
|
+
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
|
|
583
|
+
}
|
|
584
|
+
return path;
|
|
585
|
+
}
|
|
243
586
|
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
244
587
|
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
245
588
|
const p = scale.normalize(value, min, max);
|
|
@@ -253,7 +596,7 @@ function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
|
253
596
|
r4: angleRange / 2
|
|
254
597
|
};
|
|
255
598
|
}
|
|
256
|
-
const KnobContext = createContext(null);
|
|
599
|
+
const KnobContext = /* @__PURE__ */ createContext(null);
|
|
257
600
|
const KnobProvider = KnobContext.Provider;
|
|
258
601
|
function useKnobContext(selector) {
|
|
259
602
|
const context = useContext(KnobContext);
|
|
@@ -263,14 +606,13 @@ function useKnobContext(selector) {
|
|
|
263
606
|
//#endregion
|
|
264
607
|
//#region src/components/Knob/ActiveLine.tsx
|
|
265
608
|
function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
609
|
+
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
266
610
|
const r2 = useKnobContext((s) => s.r2);
|
|
267
611
|
const r3 = useKnobContext((s) => s.r3);
|
|
268
612
|
const radius = arcRadius(strokeWidth);
|
|
269
|
-
const start = pointOnArc(r2, radius);
|
|
270
|
-
const end = pointOnArc(r3, radius);
|
|
271
613
|
return /* @__PURE__ */ jsx("path", {
|
|
272
|
-
className
|
|
273
|
-
d:
|
|
614
|
+
className,
|
|
615
|
+
d: arcPath(r2, r3, radius),
|
|
274
616
|
fill: "none",
|
|
275
617
|
stroke,
|
|
276
618
|
strokeWidth,
|
|
@@ -280,6 +622,7 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
280
622
|
//#endregion
|
|
281
623
|
//#region src/components/Knob/InactiveLine.tsx
|
|
282
624
|
function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
625
|
+
useCheckPlacement("Knob.InactiveLine", "Knob.SVGRoot");
|
|
283
626
|
const min = useKnobContext((s) => s.min);
|
|
284
627
|
const max = useKnobContext((s) => s.max);
|
|
285
628
|
const startValue = useKnobContext((s) => s.startValue);
|
|
@@ -288,20 +631,16 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
288
631
|
const r3 = useKnobContext((s) => s.r3);
|
|
289
632
|
const r4 = useKnobContext((s) => s.r4);
|
|
290
633
|
const radius = arcRadius(strokeWidth);
|
|
291
|
-
const p1 = pointOnArc(r1, radius);
|
|
292
|
-
const p2 = pointOnArc(r2, radius);
|
|
293
|
-
const p3 = pointOnArc(r3, radius);
|
|
294
|
-
const p4 = pointOnArc(r4, radius);
|
|
295
634
|
return /* @__PURE__ */ jsxs(Fragment, { children: [startValue > min && /* @__PURE__ */ jsx("path", {
|
|
296
|
-
className
|
|
297
|
-
d:
|
|
635
|
+
className,
|
|
636
|
+
d: arcPath(r1, r2, radius),
|
|
298
637
|
fill: "none",
|
|
299
638
|
stroke,
|
|
300
639
|
strokeWidth,
|
|
301
640
|
...props
|
|
302
641
|
}), startValue < max && /* @__PURE__ */ jsx("path", {
|
|
303
|
-
className
|
|
304
|
-
d:
|
|
642
|
+
className,
|
|
643
|
+
d: arcPath(r3, r4, radius),
|
|
305
644
|
fill: "none",
|
|
306
645
|
stroke,
|
|
307
646
|
strokeWidth,
|
|
@@ -318,17 +657,21 @@ function SVGRoot({ children, style, ...props }) {
|
|
|
318
657
|
...style
|
|
319
658
|
},
|
|
320
659
|
...props,
|
|
321
|
-
children
|
|
660
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
661
|
+
name: "Knob.SVGRoot",
|
|
662
|
+
children
|
|
663
|
+
})
|
|
322
664
|
});
|
|
323
665
|
}
|
|
324
666
|
//#endregion
|
|
325
667
|
//#region src/components/Knob/Thumb.tsx
|
|
326
668
|
function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
|
|
669
|
+
useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
|
|
327
670
|
const angleRange = useKnobContext((s) => s.angleRange);
|
|
328
671
|
const p = useKnobContext((s) => s.p);
|
|
329
672
|
const r1 = useKnobContext((s) => s.r1);
|
|
330
673
|
return /* @__PURE__ */ jsxs("svg", {
|
|
331
|
-
className
|
|
674
|
+
className,
|
|
332
675
|
...props,
|
|
333
676
|
children: [/* @__PURE__ */ jsx("circle", {
|
|
334
677
|
cx: "50%",
|
|
@@ -336,7 +679,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
336
679
|
r: `${thumbSize / 2}%`,
|
|
337
680
|
fill: thumb
|
|
338
681
|
}), /* @__PURE__ */ jsx("line", {
|
|
339
|
-
className:
|
|
682
|
+
className: classes?.thumbLine,
|
|
340
683
|
x1: "50%",
|
|
341
684
|
y1: `${(100 - clamp(thumbSize, 0, 100)) / 2}%`,
|
|
342
685
|
x2: "50%",
|
|
@@ -352,10 +695,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
352
695
|
}
|
|
353
696
|
//#endregion
|
|
354
697
|
//#region src/components/Knob/index.tsx
|
|
355
|
-
const defaultExternalStyles$2 = {
|
|
356
|
-
userSelectNone: true,
|
|
357
|
-
cursor: "grabbing"
|
|
358
|
-
};
|
|
698
|
+
const defaultExternalStyles$2 = { cursor: "grabbing" };
|
|
359
699
|
/**
|
|
360
700
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
361
701
|
* the control does not change its value.
|
|
@@ -365,12 +705,13 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
|
|
|
365
705
|
* Interactive rotary knob component implemented in SVG.
|
|
366
706
|
*/
|
|
367
707
|
const Knob = {
|
|
368
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel =
|
|
708
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
|
|
369
709
|
const elmRef = useRef(null);
|
|
370
710
|
const externalStyles = {
|
|
371
711
|
...defaultExternalStyles$2,
|
|
372
712
|
..._externalStyles
|
|
373
713
|
};
|
|
714
|
+
const inactive = disabled || readonly;
|
|
374
715
|
const range = useMemo(() => ({
|
|
375
716
|
min,
|
|
376
717
|
max,
|
|
@@ -382,8 +723,14 @@ const Knob = {
|
|
|
382
723
|
step,
|
|
383
724
|
scale
|
|
384
725
|
]);
|
|
726
|
+
useCheckSteps({
|
|
727
|
+
component: "Knob",
|
|
728
|
+
range,
|
|
729
|
+
keyboard,
|
|
730
|
+
wheel
|
|
731
|
+
});
|
|
385
732
|
const handleKeyDown = useCallback((event) => {
|
|
386
|
-
if (!keyboard || !onChange ||
|
|
733
|
+
if (!keyboard || !onChange || inactive) return;
|
|
387
734
|
const key = event.key;
|
|
388
735
|
if ([
|
|
389
736
|
"ArrowRight",
|
|
@@ -392,12 +739,12 @@ const Knob = {
|
|
|
392
739
|
"ArrowDown"
|
|
393
740
|
].includes(key)) {
|
|
394
741
|
event.preventDefault();
|
|
395
|
-
onChange(applyDelta(value, key
|
|
742
|
+
onChange(applyDelta(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
396
743
|
}
|
|
397
744
|
}, [
|
|
398
745
|
keyboard,
|
|
399
746
|
onChange,
|
|
400
|
-
|
|
747
|
+
inactive,
|
|
401
748
|
value,
|
|
402
749
|
range
|
|
403
750
|
]);
|
|
@@ -407,25 +754,23 @@ const Knob = {
|
|
|
407
754
|
reverse: true
|
|
408
755
|
}], [range]),
|
|
409
756
|
getValue: () => [value, value],
|
|
757
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
410
758
|
threshold: 1,
|
|
411
|
-
cursor:
|
|
759
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
760
|
+
pointerLock: inactive ? false : pointerLock,
|
|
761
|
+
shouldStart: () => !inactive,
|
|
412
762
|
onChange: (v) => {
|
|
413
|
-
if (
|
|
763
|
+
if (inactive) return;
|
|
414
764
|
onChange?.(v[1]);
|
|
415
|
-
},
|
|
416
|
-
onDragStart: () => {
|
|
417
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
418
|
-
},
|
|
419
|
-
onDragEnd: () => {
|
|
420
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
421
765
|
}
|
|
422
766
|
});
|
|
423
|
-
const
|
|
424
|
-
if (!wheel ||
|
|
767
|
+
const wheelRefCallback = useWheel((event) => {
|
|
768
|
+
if (!wheel || inactive) return;
|
|
425
769
|
event.preventDefault();
|
|
426
|
-
if (!onChange || event.deltaY
|
|
427
|
-
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range));
|
|
428
|
-
}, WHEEL_OPTIONS$2)
|
|
770
|
+
if (!onChange || event.deltaY === 0) return;
|
|
771
|
+
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event));
|
|
772
|
+
}, WHEEL_OPTIONS$2);
|
|
773
|
+
const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
|
|
429
774
|
const context = useMemo(() => {
|
|
430
775
|
const config = {
|
|
431
776
|
value,
|
|
@@ -452,34 +797,35 @@ const Knob = {
|
|
|
452
797
|
useImperativeHandle(forwardedRef, () => {
|
|
453
798
|
return {
|
|
454
799
|
focus() {
|
|
455
|
-
elmRef.current?.focus();
|
|
800
|
+
if (!disabled) elmRef.current?.focus();
|
|
456
801
|
},
|
|
457
802
|
blur() {
|
|
458
803
|
elmRef.current?.blur();
|
|
459
804
|
}
|
|
460
805
|
};
|
|
461
|
-
}, []);
|
|
806
|
+
}, [disabled]);
|
|
462
807
|
return /* @__PURE__ */ jsx(KnobProvider, {
|
|
463
808
|
value: context,
|
|
464
809
|
children: /* @__PURE__ */ jsx("div", {
|
|
465
810
|
ref: rootRefCallback,
|
|
466
|
-
className
|
|
467
|
-
tabIndex: 0,
|
|
811
|
+
className,
|
|
812
|
+
tabIndex: disabled ? -1 : 0,
|
|
468
813
|
role: "slider",
|
|
469
814
|
"aria-valuenow": value,
|
|
470
815
|
"aria-valuemin": min,
|
|
471
816
|
"aria-valuemax": max,
|
|
472
817
|
"aria-disabled": disabled,
|
|
473
818
|
"aria-readonly": readonly,
|
|
474
|
-
"data-
|
|
819
|
+
"data-disabled": disabled ? "" : void 0,
|
|
820
|
+
"data-readonly": readonly ? "" : void 0,
|
|
821
|
+
"data-dragging": dragging ? "" : void 0,
|
|
475
822
|
style: {
|
|
476
|
-
|
|
477
|
-
height: size,
|
|
823
|
+
"--knob-size": cssLength(size),
|
|
478
824
|
...style
|
|
479
825
|
},
|
|
480
826
|
onPointerDown,
|
|
481
827
|
onDoubleClick: (event) => {
|
|
482
|
-
if (enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
828
|
+
if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
483
829
|
onDoubleClick?.(event);
|
|
484
830
|
},
|
|
485
831
|
onKeyDown: (event) => {
|
|
@@ -498,14 +844,14 @@ const Knob = {
|
|
|
498
844
|
};
|
|
499
845
|
//#endregion
|
|
500
846
|
//#region src/components/NumberInput/context.tsx
|
|
501
|
-
const NumberInputContext = createContext(null);
|
|
847
|
+
const NumberInputContext = /* @__PURE__ */ createContext(null);
|
|
502
848
|
const NumberInputProvider = NumberInputContext.Provider;
|
|
503
849
|
function useNumberInputContext(selector) {
|
|
504
850
|
const context = useContext(NumberInputContext);
|
|
505
851
|
if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
|
|
506
852
|
return selector ? selector(context) : context;
|
|
507
853
|
}
|
|
508
|
-
const StepperContext = createContext(null);
|
|
854
|
+
const StepperContext = /* @__PURE__ */ createContext(null);
|
|
509
855
|
const StepperProvider = StepperContext.Provider;
|
|
510
856
|
function useStepperContext() {
|
|
511
857
|
return useContext(StepperContext);
|
|
@@ -514,22 +860,26 @@ function useStepperContext() {
|
|
|
514
860
|
//#region src/hooks/useEventListener.ts
|
|
515
861
|
function useEventListener(target, event, handler, options) {
|
|
516
862
|
const listener = useCallbackRef(handler);
|
|
863
|
+
const cleanupRef = useRef(() => {});
|
|
517
864
|
useEffect(() => {
|
|
518
865
|
const node = typeof target === "function" ? target() : target ?? document;
|
|
519
866
|
if (!node) return;
|
|
520
867
|
node.addEventListener(event, listener, options);
|
|
521
|
-
|
|
868
|
+
const cleanup = () => {
|
|
522
869
|
node.removeEventListener(event, listener, options);
|
|
523
870
|
};
|
|
871
|
+
cleanupRef.current = cleanup;
|
|
872
|
+
return () => {
|
|
873
|
+
cleanup();
|
|
874
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
875
|
+
};
|
|
524
876
|
}, [
|
|
525
877
|
event,
|
|
526
878
|
target,
|
|
527
879
|
options,
|
|
528
880
|
listener
|
|
529
881
|
]);
|
|
530
|
-
return () =>
|
|
531
|
-
(typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
|
|
532
|
-
};
|
|
882
|
+
return useCallback(() => cleanupRef.current(), []);
|
|
533
883
|
}
|
|
534
884
|
//#endregion
|
|
535
885
|
//#region src/hooks/useInterval.ts
|
|
@@ -553,47 +903,61 @@ function useInterval(callback, delay) {
|
|
|
553
903
|
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
554
904
|
const [pressed, setPressed] = useState(false);
|
|
555
905
|
const [delay, setDelay] = useState(initialDelay);
|
|
906
|
+
const activePointerId = useRef(null);
|
|
907
|
+
const isPressed = useRef(false);
|
|
556
908
|
useInterval(() => {
|
|
557
909
|
callback();
|
|
558
910
|
setDelay(interval);
|
|
559
911
|
}, pressed ? delay : null);
|
|
560
|
-
|
|
912
|
+
const stop = useCallback((pointerId) => {
|
|
913
|
+
if (!isPressed.current) return;
|
|
914
|
+
if (pointerId !== void 0 && activePointerId.current !== null && activePointerId.current !== pointerId) return;
|
|
915
|
+
isPressed.current = false;
|
|
916
|
+
activePointerId.current = null;
|
|
561
917
|
setPressed(false);
|
|
562
918
|
setDelay(initialDelay);
|
|
919
|
+
}, [initialDelay]);
|
|
920
|
+
useEventListener(globalThis.window, "pointerup", (event) => {
|
|
921
|
+
stop(event.pointerId);
|
|
922
|
+
});
|
|
923
|
+
useEventListener(globalThis.window, "pointercancel", (event) => {
|
|
924
|
+
stop(event.pointerId);
|
|
925
|
+
});
|
|
926
|
+
useEventListener(globalThis.window, "blur", () => {
|
|
927
|
+
stop();
|
|
563
928
|
});
|
|
564
|
-
return useCallback(() => {
|
|
929
|
+
return useCallback((event) => {
|
|
930
|
+
if (isPressed.current || event && event.button !== 0) return;
|
|
931
|
+
isPressed.current = true;
|
|
932
|
+
activePointerId.current = event?.pointerId ?? null;
|
|
565
933
|
callback();
|
|
566
934
|
setPressed(true);
|
|
567
935
|
}, [callback]);
|
|
568
936
|
}
|
|
569
937
|
//#endregion
|
|
570
|
-
//#region src/components/NumberInput/
|
|
938
|
+
//#region src/components/NumberInput/StepperButton.tsx
|
|
571
939
|
/**
|
|
572
940
|
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
573
941
|
* in which way they move the value and which arrow they draw.
|
|
574
942
|
*/
|
|
575
943
|
function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
|
|
576
|
-
const { step, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
944
|
+
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
577
945
|
const stepper = useStepperContext();
|
|
578
|
-
const press = useLongPress(
|
|
946
|
+
const press = useLongPress(() => {
|
|
579
947
|
if (stepper?.draggingRef.current) return;
|
|
580
948
|
nudge(direction, ["raw", step]);
|
|
581
|
-
}
|
|
582
|
-
stepper,
|
|
583
|
-
nudge,
|
|
584
|
-
direction,
|
|
585
|
-
step
|
|
586
|
-
]));
|
|
949
|
+
});
|
|
587
950
|
return /* @__PURE__ */ jsx("div", {
|
|
588
|
-
className
|
|
951
|
+
className,
|
|
589
952
|
role: "button",
|
|
590
953
|
tabIndex: -1,
|
|
591
954
|
"aria-label": direction > 0 ? "Increment" : "Decrement",
|
|
592
|
-
"aria-disabled": direction > 0 ? atMax : atMin,
|
|
593
|
-
"
|
|
955
|
+
"aria-disabled": disabled || (direction > 0 ? atMax : atMin),
|
|
956
|
+
"data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
|
|
957
|
+
"data-readonly": readonly ? "" : void 0,
|
|
594
958
|
style,
|
|
595
959
|
onPointerDown: (event) => {
|
|
596
|
-
if (!readonly) press();
|
|
960
|
+
if (!disabled && !readonly) press(event);
|
|
597
961
|
onPointerDown?.(event);
|
|
598
962
|
},
|
|
599
963
|
...props,
|
|
@@ -602,7 +966,10 @@ function StepperButton({ direction, variant, icon, className, style, children, o
|
|
|
602
966
|
}
|
|
603
967
|
function StepperArrow({ up }) {
|
|
604
968
|
return /* @__PURE__ */ jsx("svg", {
|
|
605
|
-
|
|
969
|
+
style: {
|
|
970
|
+
width: "var(--stepper-icon-size)",
|
|
971
|
+
height: "var(--stepper-icon-size)"
|
|
972
|
+
},
|
|
606
973
|
viewBox: "0 0 24 24",
|
|
607
974
|
fill: "none",
|
|
608
975
|
stroke: "currentColor",
|
|
@@ -638,56 +1005,111 @@ function IncrementStepper(props) {
|
|
|
638
1005
|
//#region src/components/NumberInput/InputField.tsx
|
|
639
1006
|
/** The leading number of the displayed text, whatever the format put around it. */
|
|
640
1007
|
const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
|
|
1008
|
+
const numberEnd = (text) => text.match(NUMBER_PREFIX)?.[0].length ?? text.length;
|
|
1009
|
+
/**
|
|
1010
|
+
* The index the caret is measured against: the decimal point, or where one
|
|
1011
|
+
* would go if the number has none.
|
|
1012
|
+
*
|
|
1013
|
+
* Measuring from an end instead would slide the caret across a digit whenever
|
|
1014
|
+
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
1015
|
+
* to `9` loses one — which is exactly what stepping does.
|
|
1016
|
+
*/
|
|
1017
|
+
const decimalAnchor = (text) => {
|
|
1018
|
+
const dot = text.indexOf(".");
|
|
1019
|
+
return dot === -1 ? numberEnd(text) : dot;
|
|
1020
|
+
};
|
|
641
1021
|
/**
|
|
642
1022
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
643
1023
|
*
|
|
644
1024
|
* While the user types, their own text stands rather than `format(value)`, so
|
|
645
1025
|
* that a half-finished entry is not rewritten under the caret.
|
|
1026
|
+
*
|
|
1027
|
+
* How it behaves on focus, Enter and the arrow keys is set on `Root`
|
|
1028
|
+
* (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
|
|
646
1029
|
*/
|
|
647
|
-
|
|
648
|
-
const { value, min, max, step, disabled, readonly, keyboard, text, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
1030
|
+
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
1031
|
+
const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
1032
|
+
const [focused, setFocused] = useState(false);
|
|
1033
|
+
const shown = unformatOnFocus && focused && !editing ? String(value) : text;
|
|
1034
|
+
useEffect(() => {
|
|
1035
|
+
if (!focused || selectOnFocus === "none") return;
|
|
1036
|
+
const input = inputRef.current;
|
|
1037
|
+
if (!input) return;
|
|
1038
|
+
if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
|
|
1039
|
+
else input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
|
|
1040
|
+
}, [
|
|
1041
|
+
focused,
|
|
1042
|
+
selectOnFocus,
|
|
1043
|
+
inputRef
|
|
1044
|
+
]);
|
|
1045
|
+
/**
|
|
1046
|
+
* Where to put the caret once the stepped value has been rendered, and the
|
|
1047
|
+
* text it was measured against.
|
|
1048
|
+
*/
|
|
1049
|
+
const caretAfterStep = useRef(null);
|
|
1050
|
+
useLayoutEffect(() => {
|
|
1051
|
+
const pending = caretAfterStep.current;
|
|
1052
|
+
if (!pending) return;
|
|
1053
|
+
caretAfterStep.current = null;
|
|
1054
|
+
const input = inputRef.current;
|
|
1055
|
+
if (!input || input.value === pending.from) return;
|
|
1056
|
+
const place = decimalAnchor(input.value) + pending.offset;
|
|
1057
|
+
const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
|
|
1058
|
+
input.setSelectionRange(caret, caret);
|
|
1059
|
+
});
|
|
1060
|
+
const composedRef = useComposedRefs(forwardedRef, inputRef);
|
|
649
1061
|
return /* @__PURE__ */ jsx("input", {
|
|
650
|
-
ref:
|
|
651
|
-
className
|
|
1062
|
+
ref: composedRef,
|
|
1063
|
+
className,
|
|
652
1064
|
type: "text",
|
|
653
1065
|
inputMode: "decimal",
|
|
654
1066
|
role: "spinbutton",
|
|
655
|
-
value:
|
|
1067
|
+
value: shown,
|
|
1068
|
+
disabled,
|
|
656
1069
|
readOnly: readonly,
|
|
657
1070
|
"aria-disabled": disabled,
|
|
658
1071
|
"aria-readonly": readonly,
|
|
1072
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1073
|
+
"data-readonly": readonly ? "" : void 0,
|
|
659
1074
|
"aria-valuenow": value,
|
|
660
1075
|
"aria-valuemin": min,
|
|
661
1076
|
"aria-valuemax": max,
|
|
662
1077
|
"aria-valuetext": text,
|
|
663
1078
|
step,
|
|
664
|
-
"data-out-of-range": outOfRange,
|
|
1079
|
+
"data-out-of-range": outOfRange ? "" : void 0,
|
|
665
1080
|
style,
|
|
666
|
-
onChange: (event) =>
|
|
1081
|
+
onChange: (event) => {
|
|
1082
|
+
caretAfterStep.current = null;
|
|
1083
|
+
setDraft(event.currentTarget.value);
|
|
1084
|
+
},
|
|
667
1085
|
onFocus: (event) => {
|
|
668
|
-
|
|
669
|
-
if (selectOnFocus == "all") input.setSelectionRange(0, input.value.length);
|
|
670
|
-
else if (selectOnFocus == "number") input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
|
|
1086
|
+
setFocused(true);
|
|
671
1087
|
onFocus?.(event);
|
|
672
1088
|
},
|
|
673
1089
|
onBlur: (event) => {
|
|
1090
|
+
setFocused(false);
|
|
674
1091
|
commitDraft();
|
|
675
1092
|
onBlur?.(event);
|
|
676
1093
|
},
|
|
677
1094
|
onKeyDown: (event) => {
|
|
678
1095
|
const key = event.key;
|
|
679
|
-
if (key
|
|
1096
|
+
if (key === "Enter") {
|
|
680
1097
|
commitDraft();
|
|
681
1098
|
if (blurOnEnter) event.currentTarget.blur();
|
|
682
|
-
} else if (keyboard && !readonly && (key
|
|
1099
|
+
} else if (keyboard && !disabled && !readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
|
|
683
1100
|
event.preventDefault();
|
|
684
|
-
|
|
1101
|
+
const input = event.currentTarget;
|
|
1102
|
+
if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
|
|
1103
|
+
offset: input.selectionStart - decimalAnchor(input.value),
|
|
1104
|
+
from: input.value
|
|
1105
|
+
};
|
|
1106
|
+
nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
|
|
685
1107
|
}
|
|
686
1108
|
onKeyDown?.(event);
|
|
687
1109
|
},
|
|
688
1110
|
...props
|
|
689
1111
|
});
|
|
690
|
-
}
|
|
1112
|
+
});
|
|
691
1113
|
//#endregion
|
|
692
1114
|
//#region src/hooks/useDrag.ts
|
|
693
1115
|
/**
|
|
@@ -695,7 +1117,7 @@ function InputField({ selectOnFocus = "none", blurOnEnter = true, className, sty
|
|
|
695
1117
|
*
|
|
696
1118
|
* @returns a ref callback to attach to the element being dragged
|
|
697
1119
|
*/
|
|
698
|
-
function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
1120
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
699
1121
|
const dragHandler = useCallbackRef(onDrag);
|
|
700
1122
|
const dragStartHandler = useCallbackRef(onDragStart);
|
|
701
1123
|
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
@@ -705,15 +1127,17 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
|
705
1127
|
const instance = createDrag(node, {
|
|
706
1128
|
threshold,
|
|
707
1129
|
cursor,
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1130
|
+
pointerLock,
|
|
1131
|
+
onDragStart: (state) => dragStartHandler(state),
|
|
1132
|
+
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
1133
|
+
onDragEnd: (state) => dragEndHandler(state)
|
|
711
1134
|
});
|
|
712
1135
|
return () => instance.destroy();
|
|
713
1136
|
}, [
|
|
714
1137
|
node,
|
|
715
1138
|
threshold,
|
|
716
1139
|
cursor,
|
|
1140
|
+
pointerLock,
|
|
717
1141
|
dragHandler,
|
|
718
1142
|
dragStartHandler,
|
|
719
1143
|
dragEndHandler
|
|
@@ -729,8 +1153,17 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
|
729
1153
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
730
1154
|
* off text selection on whatever element it is attached to.
|
|
731
1155
|
*/
|
|
732
|
-
function Stepper({ className, style, children,
|
|
733
|
-
const { value, step, readonly, drag,
|
|
1156
|
+
const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
1157
|
+
const { value, step, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
|
|
1158
|
+
const inactive = disabled || readonly;
|
|
1159
|
+
/**
|
|
1160
|
+
* The sensitivity as an amount per `drag` pixels, so that the drag goes
|
|
1161
|
+
* through the same `applyDelta` the wheel and the arrow keys do. Carrying
|
|
1162
|
+
* the modifier map over rather than resolving it here is what keeps `step`
|
|
1163
|
+
* out of the pipeline for a modifier entry — naming one is a request to move
|
|
1164
|
+
* off the grid.
|
|
1165
|
+
*/
|
|
1166
|
+
const dragOptions = useMemo(() => mapModifier(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
|
|
734
1167
|
/**
|
|
735
1168
|
* Set once the drag has actually moved the value, so that the press-and-hold
|
|
736
1169
|
* repeat of a stepper stands down and leaves the value to the drag. A ref,
|
|
@@ -743,60 +1176,90 @@ function Stepper({ className, style, children, ref, ...props }) {
|
|
|
743
1176
|
* have been nudged once, and the drag should carry on from there.
|
|
744
1177
|
*/
|
|
745
1178
|
const originRef = useRef(null);
|
|
1179
|
+
/**
|
|
1180
|
+
* Which modifier the drag is currently counting at, and where the previous
|
|
1181
|
+
* event was — a key produces no pointer event of its own, so a change is
|
|
1182
|
+
* only seen on the next move and has to be dated back to the one before it.
|
|
1183
|
+
*/
|
|
1184
|
+
const factorRef = useRef(1);
|
|
1185
|
+
const previousYRef = useRef(0);
|
|
746
1186
|
const dragRefCallback = useDrag({
|
|
747
1187
|
threshold: 1,
|
|
748
|
-
cursor:
|
|
749
|
-
|
|
1188
|
+
cursor: inactive ? void 0 : "ns-resize",
|
|
1189
|
+
pointerLock: inactive ? false : pointerLock,
|
|
1190
|
+
onDragStart: (state) => {
|
|
750
1191
|
originRef.current = null;
|
|
751
1192
|
draggingRef.current = false;
|
|
1193
|
+
factorRef.current = selectModifier(dragSensitivity, state.event).value;
|
|
752
1194
|
},
|
|
753
|
-
onDrag: (_x, y) => {
|
|
754
|
-
if (
|
|
1195
|
+
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1196
|
+
if (inactive || drag === null) return;
|
|
755
1197
|
if (!originRef.current) {
|
|
756
1198
|
originRef.current = {
|
|
757
1199
|
y,
|
|
758
1200
|
value
|
|
759
1201
|
};
|
|
1202
|
+
previousYRef.current = y;
|
|
760
1203
|
return;
|
|
761
1204
|
}
|
|
1205
|
+
const factor = selectModifier(dragSensitivity, state.event).value;
|
|
1206
|
+
if (factor !== factorRef.current) {
|
|
1207
|
+
originRef.current = {
|
|
1208
|
+
y: previousYRef.current,
|
|
1209
|
+
value
|
|
1210
|
+
};
|
|
1211
|
+
factorRef.current = factor;
|
|
1212
|
+
}
|
|
1213
|
+
previousYRef.current = y;
|
|
762
1214
|
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
763
|
-
if (steps
|
|
1215
|
+
if (steps === 0) return;
|
|
764
1216
|
draggingRef.current = true;
|
|
765
|
-
changeValue(applyDelta(originRef.current.value, steps,
|
|
1217
|
+
changeValue(applyDelta(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
766
1218
|
},
|
|
767
1219
|
onDragEnd: () => {
|
|
768
1220
|
draggingRef.current = false;
|
|
769
1221
|
}
|
|
770
1222
|
});
|
|
771
|
-
const composedRef = useComposedRefs(
|
|
1223
|
+
const composedRef = useComposedRefs(forwardedRef, drag !== null && !inactive ? dragRefCallback : void 0);
|
|
1224
|
+
const context = useMemo(() => ({ draggingRef }), []);
|
|
772
1225
|
return /* @__PURE__ */ jsx(StepperProvider, {
|
|
773
|
-
value:
|
|
1226
|
+
value: context,
|
|
774
1227
|
children: /* @__PURE__ */ jsx("div", {
|
|
775
1228
|
ref: composedRef,
|
|
776
|
-
className
|
|
1229
|
+
className,
|
|
777
1230
|
style,
|
|
778
1231
|
...props,
|
|
779
1232
|
children
|
|
780
1233
|
})
|
|
781
1234
|
});
|
|
782
|
-
}
|
|
1235
|
+
});
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/components/NumberInput/index.tsx
|
|
1238
|
+
/**
|
|
1239
|
+
* A number at the start of the text, and nothing read after it. A half-typed
|
|
1240
|
+
* entry still yields the number in front of it, but text with no number is
|
|
1241
|
+
* `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
|
|
1242
|
+
*/
|
|
1243
|
+
const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;
|
|
1244
|
+
const defaultFormat = (value) => String(value);
|
|
1245
|
+
const defaultParse = (text) => {
|
|
1246
|
+
const match = text.match(LEADING_NUMBER);
|
|
1247
|
+
return match ? Number(match[1]) : NaN;
|
|
1248
|
+
};
|
|
783
1249
|
/**
|
|
784
1250
|
* Input with some useful functions for entering numerical values.
|
|
785
1251
|
*/
|
|
786
1252
|
const NumberInput = {
|
|
787
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale,
|
|
1253
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
|
|
788
1254
|
const inputRef = useRef(null);
|
|
789
1255
|
/**
|
|
790
1256
|
* The text being typed. The only state here: it is not a copy of `value`,
|
|
791
1257
|
* but the half-finished entry that has no value to be derived from yet.
|
|
792
1258
|
*/
|
|
793
1259
|
const [draft, setDraft] = useState(null);
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
digit
|
|
798
|
-
]);
|
|
799
|
-
const parse = useCallback((t) => parseProp ? parseProp(t) : parseValue(t, units), [parseProp, units]);
|
|
1260
|
+
const inactive = disabled || readonly;
|
|
1261
|
+
const format = formatProp ?? defaultFormat;
|
|
1262
|
+
const parse = parseProp ?? defaultParse;
|
|
800
1263
|
const range = useMemo(() => ({
|
|
801
1264
|
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
802
1265
|
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
@@ -809,49 +1272,90 @@ const NumberInput = {
|
|
|
809
1272
|
step,
|
|
810
1273
|
scale
|
|
811
1274
|
]);
|
|
1275
|
+
const rawRange = useMemo(() => ({
|
|
1276
|
+
min: (clampValue ? min : void 0) ?? -Number.MAX_VALUE,
|
|
1277
|
+
max: (clampValue ? max : void 0) ?? Number.MAX_VALUE,
|
|
1278
|
+
step,
|
|
1279
|
+
scale
|
|
1280
|
+
}), [
|
|
1281
|
+
clampValue,
|
|
1282
|
+
min,
|
|
1283
|
+
max,
|
|
1284
|
+
step,
|
|
1285
|
+
scale
|
|
1286
|
+
]);
|
|
1287
|
+
useCheckSteps({
|
|
1288
|
+
component: "NumberInput",
|
|
1289
|
+
range: useMemo(() => min !== void 0 && max !== void 0 && min < max ? {
|
|
1290
|
+
min,
|
|
1291
|
+
max,
|
|
1292
|
+
step,
|
|
1293
|
+
scale
|
|
1294
|
+
} : null, [
|
|
1295
|
+
min,
|
|
1296
|
+
max,
|
|
1297
|
+
step,
|
|
1298
|
+
scale
|
|
1299
|
+
]),
|
|
1300
|
+
keyboard,
|
|
1301
|
+
wheel,
|
|
1302
|
+
format
|
|
1303
|
+
});
|
|
812
1304
|
const text = draft ?? format(value);
|
|
813
|
-
const editing = draft
|
|
814
|
-
const outOfRange = !editing && (min
|
|
1305
|
+
const editing = draft !== null;
|
|
1306
|
+
const outOfRange = !editing && (min !== void 0 && value < min || max !== void 0 && value > max);
|
|
815
1307
|
const handleDraft = useCallback((next) => {
|
|
816
|
-
if (
|
|
1308
|
+
if (inactive) return;
|
|
817
1309
|
setDraft(next);
|
|
818
1310
|
const parsed = parse(next);
|
|
819
1311
|
if (Number.isFinite(parsed)) onChange?.(parsed);
|
|
820
1312
|
}, [
|
|
821
|
-
|
|
1313
|
+
inactive,
|
|
822
1314
|
parse,
|
|
823
1315
|
onChange
|
|
824
1316
|
]);
|
|
825
1317
|
const changeValue = useCallback((next) => {
|
|
826
|
-
if (
|
|
1318
|
+
if (inactive) return;
|
|
827
1319
|
setDraft(null);
|
|
828
|
-
if (next
|
|
1320
|
+
if (next !== value) onChange?.(next);
|
|
829
1321
|
}, [
|
|
830
|
-
|
|
1322
|
+
inactive,
|
|
831
1323
|
value,
|
|
832
1324
|
onChange
|
|
833
1325
|
]);
|
|
834
1326
|
const commitDraft = useCallback(() => {
|
|
835
|
-
if (draft
|
|
836
|
-
|
|
1327
|
+
if (draft === null || inactive) return;
|
|
1328
|
+
const parsed = parse(draft);
|
|
1329
|
+
if (!Number.isFinite(parsed)) {
|
|
1330
|
+
setDraft(null);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
let committed = parsed;
|
|
1334
|
+
if (clampValue && min !== void 0) committed = Math.max(committed, min);
|
|
1335
|
+
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1336
|
+
changeValue(committed);
|
|
837
1337
|
}, [
|
|
838
1338
|
draft,
|
|
839
|
-
|
|
1339
|
+
inactive,
|
|
840
1340
|
parse,
|
|
841
|
-
|
|
1341
|
+
clampValue,
|
|
1342
|
+
min,
|
|
1343
|
+
max,
|
|
842
1344
|
changeValue
|
|
843
1345
|
]);
|
|
844
|
-
const nudge = useCallback((direction, option) => {
|
|
845
|
-
|
|
1346
|
+
const nudge = useCallback((direction, option, modifiers) => {
|
|
1347
|
+
const [mode] = selectModifier(option, modifiers).value;
|
|
1348
|
+
changeValue(applyDelta(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
|
|
846
1349
|
}, [
|
|
847
1350
|
changeValue,
|
|
848
1351
|
value,
|
|
1352
|
+
rawRange,
|
|
849
1353
|
range
|
|
850
1354
|
]);
|
|
851
1355
|
const wheelRefCallback = useWheel((event) => {
|
|
852
|
-
if (!wheel ||
|
|
1356
|
+
if (!wheel || inactive || event.deltaY === 0) return;
|
|
853
1357
|
event.preventDefault();
|
|
854
|
-
nudge(-Math.sign(event.deltaY), wheel);
|
|
1358
|
+
nudge(-Math.sign(event.deltaY), wheel, event);
|
|
855
1359
|
}, { requireFocus: true });
|
|
856
1360
|
const context = useMemo(() => ({
|
|
857
1361
|
value,
|
|
@@ -863,12 +1367,19 @@ const NumberInput = {
|
|
|
863
1367
|
readonly,
|
|
864
1368
|
clampValue,
|
|
865
1369
|
range,
|
|
1370
|
+
rawRange,
|
|
866
1371
|
keyboard,
|
|
867
1372
|
text,
|
|
868
1373
|
editing,
|
|
869
1374
|
outOfRange,
|
|
870
|
-
|
|
871
|
-
|
|
1375
|
+
dragSensitivity,
|
|
1376
|
+
pointerLock,
|
|
1377
|
+
selectOnFocus,
|
|
1378
|
+
unformatOnFocus,
|
|
1379
|
+
keepCaretOnStep,
|
|
1380
|
+
blurOnEnter,
|
|
1381
|
+
atMin: clampValue && min !== void 0 && value <= min,
|
|
1382
|
+
atMax: clampValue && max !== void 0 && value >= max,
|
|
872
1383
|
drag,
|
|
873
1384
|
format,
|
|
874
1385
|
parse,
|
|
@@ -887,11 +1398,18 @@ const NumberInput = {
|
|
|
887
1398
|
readonly,
|
|
888
1399
|
clampValue,
|
|
889
1400
|
range,
|
|
1401
|
+
rawRange,
|
|
890
1402
|
keyboard,
|
|
891
1403
|
text,
|
|
892
1404
|
editing,
|
|
893
1405
|
outOfRange,
|
|
894
1406
|
drag,
|
|
1407
|
+
dragSensitivity,
|
|
1408
|
+
pointerLock,
|
|
1409
|
+
selectOnFocus,
|
|
1410
|
+
unformatOnFocus,
|
|
1411
|
+
keepCaretOnStep,
|
|
1412
|
+
blurOnEnter,
|
|
895
1413
|
format,
|
|
896
1414
|
parse,
|
|
897
1415
|
handleDraft,
|
|
@@ -902,20 +1420,20 @@ const NumberInput = {
|
|
|
902
1420
|
useImperativeHandle(forwardedRef, () => {
|
|
903
1421
|
return {
|
|
904
1422
|
focus() {
|
|
905
|
-
inputRef.current?.focus();
|
|
1423
|
+
if (!disabled) inputRef.current?.focus();
|
|
906
1424
|
},
|
|
907
1425
|
blur() {
|
|
908
1426
|
inputRef.current?.blur();
|
|
909
1427
|
}
|
|
910
1428
|
};
|
|
911
|
-
}, []);
|
|
1429
|
+
}, [disabled]);
|
|
912
1430
|
return /* @__PURE__ */ jsx(NumberInputProvider, {
|
|
913
1431
|
value: context,
|
|
914
1432
|
children: /* @__PURE__ */ jsx("div", {
|
|
915
1433
|
ref: wheelRefCallback,
|
|
916
|
-
className
|
|
917
|
-
"
|
|
918
|
-
"
|
|
1434
|
+
className,
|
|
1435
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1436
|
+
"data-readonly": readonly ? "" : void 0,
|
|
919
1437
|
style,
|
|
920
1438
|
...props,
|
|
921
1439
|
children
|
|
@@ -928,11 +1446,12 @@ const NumberInput = {
|
|
|
928
1446
|
DecrementStepper
|
|
929
1447
|
};
|
|
930
1448
|
//#endregion
|
|
931
|
-
//#region src/components/Piano/
|
|
1449
|
+
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
932
1450
|
/**
|
|
933
1451
|
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
934
1452
|
*/
|
|
935
1453
|
const SHORTCUTS = {
|
|
1454
|
+
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
936
1455
|
HOME_ROW: { keys: [
|
|
937
1456
|
"a",
|
|
938
1457
|
"w",
|
|
@@ -952,6 +1471,7 @@ const SHORTCUTS = {
|
|
|
952
1471
|
"p",
|
|
953
1472
|
";"
|
|
954
1473
|
] },
|
|
1474
|
+
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
955
1475
|
HOME_ROW_NATURAL: { keys: [
|
|
956
1476
|
"a",
|
|
957
1477
|
"",
|
|
@@ -972,26 +1492,41 @@ const SHORTCUTS = {
|
|
|
972
1492
|
";"
|
|
973
1493
|
] }
|
|
974
1494
|
};
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/components/Piano/index.tsx
|
|
1497
|
+
function isEditableTarget(target) {
|
|
1498
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
1499
|
+
if (target.matches("input, textarea, select")) return true;
|
|
1500
|
+
for (let element = target; element;) {
|
|
1501
|
+
const contentEditable = element.getAttribute("contenteditable");
|
|
1502
|
+
if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
|
|
1503
|
+
element = element.parentElement;
|
|
1504
|
+
}
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
function shortcutKey(event) {
|
|
1508
|
+
return event.code || event.key;
|
|
1509
|
+
}
|
|
975
1510
|
/**
|
|
976
1511
|
* Customizable piano component.
|
|
977
1512
|
*/
|
|
978
|
-
const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
|
|
1513
|
+
const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, height, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
979
1514
|
const [node, setNode] = useState(null);
|
|
980
1515
|
const [activeNotes, setActiveNotes] = useState([]);
|
|
981
|
-
/** Set while `
|
|
982
|
-
const [
|
|
1516
|
+
/** Set while `resizable` is on, measured from the parent. */
|
|
1517
|
+
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth);
|
|
983
1518
|
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange]);
|
|
984
1519
|
const whiteKeyCount = useMemo(() => notes.filter(isWhiteKey).length, [notes]);
|
|
985
1520
|
const layout = useMemo(() => ({
|
|
986
1521
|
noteRange,
|
|
987
|
-
whiteKeyWidth:
|
|
1522
|
+
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
988
1523
|
keyGap,
|
|
989
1524
|
blackKeyWidthRatio,
|
|
990
1525
|
blackKeyHeightRatio
|
|
991
1526
|
}), [
|
|
992
1527
|
noteRange,
|
|
993
|
-
|
|
994
|
-
|
|
1528
|
+
resizable,
|
|
1529
|
+
resizedKeyWidth,
|
|
995
1530
|
whiteKeyWidth,
|
|
996
1531
|
keyGap,
|
|
997
1532
|
blackKeyWidthRatio,
|
|
@@ -1005,6 +1540,11 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1005
1540
|
onStopNote
|
|
1006
1541
|
});
|
|
1007
1542
|
const instanceRef = useRef(null);
|
|
1543
|
+
const shortcutNotes = useRef(/* @__PURE__ */ new Map());
|
|
1544
|
+
const releaseShortcutNotes = useCallback(() => {
|
|
1545
|
+
for (const { note, source } of shortcutNotes.current.values()) instanceRef.current?.noteOff(note, { source });
|
|
1546
|
+
shortcutNotes.current.clear();
|
|
1547
|
+
}, []);
|
|
1008
1548
|
useEffect(() => {
|
|
1009
1549
|
if (!node) return;
|
|
1010
1550
|
const instance = createPianoInput(node, {
|
|
@@ -1036,35 +1576,76 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1036
1576
|
});
|
|
1037
1577
|
});
|
|
1038
1578
|
useEffect(() => {
|
|
1039
|
-
if (!
|
|
1579
|
+
if (!resizable || !node) return;
|
|
1040
1580
|
const parent = node.parentElement;
|
|
1041
1581
|
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1042
1582
|
const resizeObserver = new ResizeObserver(() => {
|
|
1043
|
-
|
|
1583
|
+
setResizedKeyWidth(node.clientWidth / Math.max(whiteKeyCount, 1) - keyGap);
|
|
1044
1584
|
});
|
|
1045
1585
|
resizeObserver.observe(parent);
|
|
1046
1586
|
return () => resizeObserver.disconnect();
|
|
1047
1587
|
}, [
|
|
1048
|
-
|
|
1588
|
+
resizable,
|
|
1049
1589
|
node,
|
|
1050
1590
|
whiteKeyCount,
|
|
1051
1591
|
keyGap
|
|
1052
1592
|
]);
|
|
1593
|
+
const shortcutKeys = keyboardShortcuts?.keys;
|
|
1594
|
+
const hasShortcuts = shortcutKeys !== void 0;
|
|
1595
|
+
const shortcutMapping = shortcutKeys?.join("\0");
|
|
1596
|
+
useEffect(() => releaseShortcutNotes, [
|
|
1597
|
+
releaseShortcutNotes,
|
|
1598
|
+
shortcutMapping,
|
|
1599
|
+
keyboardShortcutsScope,
|
|
1600
|
+
noteRange.first,
|
|
1601
|
+
noteRange.last
|
|
1602
|
+
]);
|
|
1053
1603
|
/** The note a shortcut key plays, or null when it has none. */
|
|
1054
1604
|
function shortcutNote(key) {
|
|
1055
|
-
if (!
|
|
1056
|
-
const index =
|
|
1057
|
-
|
|
1605
|
+
if (!shortcutKeys || key === "") return null;
|
|
1606
|
+
const index = shortcutKeys.indexOf(key);
|
|
1607
|
+
const note = noteRange.first + index;
|
|
1608
|
+
return index === -1 || note > noteRange.last ? null : note;
|
|
1058
1609
|
}
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
1610
|
+
const shortcutTarget = useMemo(() => () => {
|
|
1611
|
+
if (!hasShortcuts) return null;
|
|
1612
|
+
return keyboardShortcutsScope === "window" ? globalThis.window : node;
|
|
1613
|
+
}, [
|
|
1614
|
+
hasShortcuts,
|
|
1615
|
+
keyboardShortcutsScope,
|
|
1616
|
+
node
|
|
1617
|
+
]);
|
|
1618
|
+
const focusOutTarget = useMemo(() => () => hasShortcuts && keyboardShortcutsScope === "root" ? node : null, [
|
|
1619
|
+
hasShortcuts,
|
|
1620
|
+
keyboardShortcutsScope,
|
|
1621
|
+
node
|
|
1622
|
+
]);
|
|
1623
|
+
const windowBlurTarget = useMemo(() => () => hasShortcuts ? globalThis.window : null, [hasShortcuts]);
|
|
1624
|
+
useEventListener(shortcutTarget, "keydown", (e) => {
|
|
1625
|
+
const key = shortcutKey(e);
|
|
1626
|
+
if (e.repeat || shortcutNotes.current.has(key)) return;
|
|
1627
|
+
if (isEditableTarget(e.target)) return;
|
|
1061
1628
|
const note = shortcutNote(e.key);
|
|
1062
|
-
if (note
|
|
1629
|
+
if (note === null) return;
|
|
1630
|
+
const source = `keyboard:${key}`;
|
|
1631
|
+
shortcutNotes.current.set(key, {
|
|
1632
|
+
note,
|
|
1633
|
+
source
|
|
1634
|
+
});
|
|
1635
|
+
instanceRef.current?.noteOn(note, { source });
|
|
1063
1636
|
});
|
|
1064
|
-
useEventListener(
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1637
|
+
useEventListener(shortcutTarget, "keyup", (e) => {
|
|
1638
|
+
const key = shortcutKey(e);
|
|
1639
|
+
const shortcut = shortcutNotes.current.get(key);
|
|
1640
|
+
if (!shortcut) return;
|
|
1641
|
+
shortcutNotes.current.delete(key);
|
|
1642
|
+
instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source });
|
|
1067
1643
|
});
|
|
1644
|
+
useEventListener(focusOutTarget, "focusout", (e) => {
|
|
1645
|
+
if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget)) return;
|
|
1646
|
+
releaseShortcutNotes();
|
|
1647
|
+
});
|
|
1648
|
+
useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
|
|
1068
1649
|
useImperativeHandle(forwardedRef, () => ({
|
|
1069
1650
|
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1070
1651
|
source: "api",
|
|
@@ -1074,10 +1655,14 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1074
1655
|
}), []);
|
|
1075
1656
|
return /* @__PURE__ */ jsx("div", {
|
|
1076
1657
|
ref: setNode,
|
|
1077
|
-
className
|
|
1658
|
+
className,
|
|
1659
|
+
"data-resizable": resizable ? "" : void 0,
|
|
1660
|
+
role: "group",
|
|
1661
|
+
tabIndex: 0,
|
|
1078
1662
|
style: {
|
|
1079
|
-
width:
|
|
1080
|
-
height,
|
|
1663
|
+
width: resizable ? "100%" : pianoWidth(layout),
|
|
1664
|
+
"--height": cssLength(height),
|
|
1665
|
+
position: "relative",
|
|
1081
1666
|
...style
|
|
1082
1667
|
},
|
|
1083
1668
|
...props,
|
|
@@ -1092,22 +1677,24 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1092
1677
|
const { className: keyClassName, style: keyStyle, ...rest } = keyProps?.(note, state) ?? {};
|
|
1093
1678
|
const content = label?.(note, state);
|
|
1094
1679
|
return /* @__PURE__ */ jsx("div", {
|
|
1095
|
-
className:
|
|
1680
|
+
className: keyClassName,
|
|
1096
1681
|
"data-note": note,
|
|
1097
1682
|
"data-note-key": noteKey(note),
|
|
1098
|
-
"data-active": state.active,
|
|
1099
|
-
"
|
|
1683
|
+
"data-active": state.active ? "" : void 0,
|
|
1684
|
+
"data-disabled": state.disabled ? "" : void 0,
|
|
1100
1685
|
...rest,
|
|
1101
1686
|
style: {
|
|
1687
|
+
position: "absolute",
|
|
1688
|
+
zIndex: keyType === "white" ? 1 : 2,
|
|
1102
1689
|
...keyStyle,
|
|
1103
1690
|
left: notePosition(note, layout),
|
|
1104
1691
|
width: keyType === "white" ? layout.whiteKeyWidth : blackKeyWidth(layout),
|
|
1105
1692
|
height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
|
|
1106
1693
|
},
|
|
1107
1694
|
children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
1108
|
-
className:
|
|
1695
|
+
className: classes?.keyLabelWrapper,
|
|
1109
1696
|
children: /* @__PURE__ */ jsx("div", {
|
|
1110
|
-
className:
|
|
1697
|
+
className: classes?.keyLabel,
|
|
1111
1698
|
children: content
|
|
1112
1699
|
})
|
|
1113
1700
|
})
|
|
@@ -1117,16 +1704,22 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1117
1704
|
}) };
|
|
1118
1705
|
//#endregion
|
|
1119
1706
|
//#region src/components/PointsEditor/Background.tsx
|
|
1120
|
-
function Background({ className, children, ...props }) {
|
|
1707
|
+
function Background({ className, children, style, ...props }) {
|
|
1121
1708
|
return /* @__PURE__ */ jsx("div", {
|
|
1122
|
-
className
|
|
1709
|
+
className,
|
|
1710
|
+
style: {
|
|
1711
|
+
position: "absolute",
|
|
1712
|
+
inset: 0,
|
|
1713
|
+
zIndex: 0,
|
|
1714
|
+
...style
|
|
1715
|
+
},
|
|
1123
1716
|
...props,
|
|
1124
1717
|
children
|
|
1125
1718
|
});
|
|
1126
1719
|
}
|
|
1127
1720
|
//#endregion
|
|
1128
1721
|
//#region src/components/PointsEditor/context.tsx
|
|
1129
|
-
const PointsEditorContext = createContext(null);
|
|
1722
|
+
const PointsEditorContext = /* @__PURE__ */ createContext(null);
|
|
1130
1723
|
const PointsEditorProvider = PointsEditorContext.Provider;
|
|
1131
1724
|
function usePointsEditorContext(selector) {
|
|
1132
1725
|
const context = useContext(PointsEditorContext);
|
|
@@ -1134,15 +1727,13 @@ function usePointsEditorContext(selector) {
|
|
|
1134
1727
|
return selector ? selector(context) : context;
|
|
1135
1728
|
}
|
|
1136
1729
|
//#endregion
|
|
1137
|
-
//#region src/components/
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1730
|
+
//#region src/components/_util/VisuallyHiddenRangeInput.tsx
|
|
1731
|
+
const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @__PURE__ */ jsx("input", {
|
|
1732
|
+
...props,
|
|
1733
|
+
ref,
|
|
1734
|
+
type: "range",
|
|
1735
|
+
style: visuallyHiddenStyle
|
|
1736
|
+
}));
|
|
1146
1737
|
//#endregion
|
|
1147
1738
|
//#region src/components/PointsEditor/Point.tsx
|
|
1148
1739
|
function clampPoint(point, min, max) {
|
|
@@ -1160,138 +1751,463 @@ const AXIS = {
|
|
|
1160
1751
|
min: 0,
|
|
1161
1752
|
max: 1
|
|
1162
1753
|
};
|
|
1163
|
-
|
|
1164
|
-
|
|
1754
|
+
/**
|
|
1755
|
+
* One setting for both axes, or one per axis. The point holds a range input
|
|
1756
|
+
* for each, so anything that names or describes it comes in a pair.
|
|
1757
|
+
*/
|
|
1758
|
+
function perAxis(value, axis) {
|
|
1759
|
+
return typeof value === "string" ? value : value?.[axis];
|
|
1760
|
+
}
|
|
1761
|
+
function Point({ value, children, id: idProp, min, max, size, width, height, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
|
|
1762
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, registerPoint, beginPointDrag, movePointDrag, nudgeSelection } = usePointsEditorContext();
|
|
1763
|
+
const generatedId = useId();
|
|
1764
|
+
const id = idProp ?? generatedId;
|
|
1765
|
+
const selected = selection.includes(id);
|
|
1165
1766
|
const disabled = _disabled ?? rootDisabled;
|
|
1166
1767
|
const readonly = _readonly ?? rootReadonly;
|
|
1768
|
+
const inactive = disabled || readonly;
|
|
1167
1769
|
const wheel = _wheel === void 0 ? rootWheel : _wheel;
|
|
1168
1770
|
const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
|
|
1771
|
+
useCheckPlacement("PointsEditor.Point", "PointsEditor.Container");
|
|
1169
1772
|
const [element, setElement] = useState(null);
|
|
1773
|
+
const xInputRef = useRef(null);
|
|
1774
|
+
const yInputRef = useRef(null);
|
|
1775
|
+
const registration = useRef({
|
|
1776
|
+
value,
|
|
1777
|
+
min,
|
|
1778
|
+
max,
|
|
1779
|
+
readonly: inactive,
|
|
1780
|
+
onChange,
|
|
1781
|
+
element,
|
|
1782
|
+
wheel
|
|
1783
|
+
});
|
|
1784
|
+
useEffect(() => {
|
|
1785
|
+
registration.current = {
|
|
1786
|
+
value,
|
|
1787
|
+
min,
|
|
1788
|
+
max,
|
|
1789
|
+
readonly: inactive,
|
|
1790
|
+
onChange,
|
|
1791
|
+
element,
|
|
1792
|
+
wheel
|
|
1793
|
+
};
|
|
1794
|
+
});
|
|
1795
|
+
useEffect(() => registerPoint(id, registration), [id, registerPoint]);
|
|
1796
|
+
/** Where the pointer was when the drag started, to measure the move from. */
|
|
1797
|
+
const pointerOrigin = useRef(null);
|
|
1170
1798
|
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1171
1799
|
axis: AXIS,
|
|
1172
1800
|
baseElementRef: containerRef,
|
|
1173
|
-
|
|
1801
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1802
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
1803
|
+
shouldStart: () => !disabled,
|
|
1174
1804
|
onChange: ([x, y]) => {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1805
|
+
const origin = pointerOrigin.current;
|
|
1806
|
+
if (!origin) return;
|
|
1807
|
+
movePointDrag({
|
|
1808
|
+
x: x - origin.x,
|
|
1809
|
+
y: y - origin.y
|
|
1810
|
+
});
|
|
1180
1811
|
},
|
|
1181
|
-
onDragStart: ([x, y]) => {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
onDragStart?.(clampPoint({
|
|
1812
|
+
onDragStart: ([x, y], state) => {
|
|
1813
|
+
beginPointDrag(id, state.event);
|
|
1814
|
+
pointerOrigin.current = {
|
|
1185
1815
|
x,
|
|
1186
1816
|
y
|
|
1187
|
-
}
|
|
1817
|
+
};
|
|
1818
|
+
xInputRef.current?.focus();
|
|
1819
|
+
if (inactive) return;
|
|
1820
|
+
onDragStart?.(clampPoint(value, min, max));
|
|
1188
1821
|
},
|
|
1189
|
-
onDragEnd: (
|
|
1190
|
-
|
|
1191
|
-
if (
|
|
1192
|
-
onDragEnd?.(clampPoint(
|
|
1193
|
-
x,
|
|
1194
|
-
y
|
|
1195
|
-
}, min, max));
|
|
1822
|
+
onDragEnd: () => {
|
|
1823
|
+
pointerOrigin.current = null;
|
|
1824
|
+
if (inactive) return;
|
|
1825
|
+
onDragEnd?.(clampPoint(value, min, max));
|
|
1196
1826
|
}
|
|
1197
1827
|
});
|
|
1198
|
-
const nudge = useCallback((axis, direction, option) => {
|
|
1199
|
-
const next = applyDelta(value[axis], direction, option, AXIS);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1828
|
+
const nudge = useCallback((axis, direction, option, modifiers) => {
|
|
1829
|
+
const next = applyDelta(value[axis], direction, option, AXIS, modifiers);
|
|
1830
|
+
nudgeSelection(id, {
|
|
1831
|
+
x: axis === "x" ? next - value.x : 0,
|
|
1832
|
+
y: axis === "y" ? next - value.y : 0
|
|
1833
|
+
});
|
|
1204
1834
|
}, [
|
|
1205
1835
|
value,
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
onChange
|
|
1836
|
+
id,
|
|
1837
|
+
nudgeSelection
|
|
1209
1838
|
]);
|
|
1210
|
-
useWheel((event) => {
|
|
1211
|
-
if (!onChange || readonly || !wheel) return;
|
|
1212
|
-
if (!element || element.ownerDocument.activeElement !== element) return;
|
|
1213
|
-
event.preventDefault();
|
|
1214
|
-
nudge(event.shiftKey ? "x" : "y", event.deltaY < 0 ? -1 : 1, wheel);
|
|
1215
|
-
}, { target: containerRef });
|
|
1216
1839
|
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1217
1840
|
const handleKeyDown = (event) => {
|
|
1218
|
-
if (!onChange || readonly || !keyboard) return;
|
|
1219
1841
|
const key = event.key;
|
|
1220
|
-
if ([
|
|
1842
|
+
if (![
|
|
1221
1843
|
"ArrowRight",
|
|
1222
1844
|
"ArrowLeft",
|
|
1223
1845
|
"ArrowUp",
|
|
1224
1846
|
"ArrowDown"
|
|
1225
|
-
].includes(key))
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1847
|
+
].includes(key)) return;
|
|
1848
|
+
const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
|
|
1849
|
+
event.preventDefault();
|
|
1850
|
+
if (!onChange || inactive || !keyboard) return;
|
|
1851
|
+
nudge(axis, key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1, keyboard, event);
|
|
1229
1852
|
};
|
|
1230
|
-
const
|
|
1231
|
-
return /* @__PURE__ */
|
|
1853
|
+
const current = clampPoint(value, min, max);
|
|
1854
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1232
1855
|
ref: refCallback,
|
|
1233
|
-
className
|
|
1234
|
-
|
|
1235
|
-
"
|
|
1236
|
-
|
|
1237
|
-
"data-dragging": dragging,
|
|
1856
|
+
className,
|
|
1857
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1858
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1859
|
+
tabIndex: -1,
|
|
1860
|
+
"data-dragging": dragging ? "" : void 0,
|
|
1861
|
+
"data-selected": selected ? "" : void 0,
|
|
1238
1862
|
style: {
|
|
1239
|
-
|
|
1240
|
-
width: size ?? width,
|
|
1241
|
-
height: size ?? height,
|
|
1863
|
+
"--color": color,
|
|
1864
|
+
"--width": cssLength(size ?? width),
|
|
1865
|
+
"--height": cssLength(size ?? height),
|
|
1866
|
+
position: "absolute",
|
|
1867
|
+
translate: "var(--translate, -50% -50%)",
|
|
1868
|
+
...style,
|
|
1242
1869
|
left: `${value.x * 100}%`,
|
|
1243
|
-
top: `${value.y * 100}
|
|
1244
|
-
...style
|
|
1870
|
+
top: `${value.y * 100}%`
|
|
1245
1871
|
},
|
|
1246
1872
|
onPointerDown,
|
|
1873
|
+
onFocus: (event) => {
|
|
1874
|
+
if (!disabled && event.target === event.currentTarget) xInputRef.current?.focus();
|
|
1875
|
+
onFocus?.(event);
|
|
1876
|
+
},
|
|
1247
1877
|
onKeyDown: (event) => {
|
|
1248
1878
|
handleKeyDown(event);
|
|
1249
1879
|
onKeyDown?.(event);
|
|
1250
1880
|
},
|
|
1251
|
-
...props
|
|
1881
|
+
...props,
|
|
1882
|
+
children: [["x", "y"].map((axis) => /* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
1883
|
+
ref: axis === "x" ? xInputRef : yInputRef,
|
|
1884
|
+
"data-axis": axis,
|
|
1885
|
+
value: current[axis],
|
|
1886
|
+
min: min?.[axis] ?? 0,
|
|
1887
|
+
max: max?.[axis] ?? 1,
|
|
1888
|
+
step: "any",
|
|
1889
|
+
disabled,
|
|
1890
|
+
"aria-readonly": readonly,
|
|
1891
|
+
"aria-orientation": axis === "x" ? "horizontal" : "vertical",
|
|
1892
|
+
"aria-label": perAxis(ariaLabel, axis) ?? axis,
|
|
1893
|
+
"aria-valuetext": perAxis(ariaValuetext, axis),
|
|
1894
|
+
onChange: (event) => {
|
|
1895
|
+
if (readonly) {
|
|
1896
|
+
event.currentTarget.value = String(current[axis]);
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const delta = event.currentTarget.valueAsNumber - value[axis];
|
|
1900
|
+
nudgeSelection(id, {
|
|
1901
|
+
x: axis === "x" ? delta : 0,
|
|
1902
|
+
y: axis === "y" ? delta : 0
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
}, axis)), children]
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
//#endregion
|
|
1909
|
+
//#region src/components/PointsEditor/Container.tsx
|
|
1910
|
+
const Container = /* @__PURE__ */ forwardRef(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1911
|
+
const { containerRef, disabled, selectable, isPointElement, nudgeFocusedPoint, beginSelectionBox, moveSelectionBox, endSelectionBox } = usePointsEditorContext();
|
|
1912
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1913
|
+
axis: AXIS,
|
|
1914
|
+
baseElementRef: containerRef,
|
|
1915
|
+
shouldStart: (event) => !disabled && !isPointElement(event.target),
|
|
1916
|
+
onDragStart: ([x, y], state) => {
|
|
1917
|
+
beginSelectionBox({
|
|
1918
|
+
x,
|
|
1919
|
+
y
|
|
1920
|
+
}, state.event);
|
|
1921
|
+
},
|
|
1922
|
+
onChange: ([x, y]) => moveSelectionBox({
|
|
1923
|
+
x,
|
|
1924
|
+
y
|
|
1925
|
+
}),
|
|
1926
|
+
onDragEnd: endSelectionBox
|
|
1927
|
+
});
|
|
1928
|
+
useWheel((event) => {
|
|
1929
|
+
const horizontal = event.deltaX !== 0;
|
|
1930
|
+
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
1931
|
+
if (delta === 0) return;
|
|
1932
|
+
const axis = horizontal || event.shiftKey ? "x" : "y";
|
|
1933
|
+
if (nudgeFocusedPoint(axis, delta < 0 ? -1 : 1, event)) event.preventDefault();
|
|
1934
|
+
}, { target: containerRef });
|
|
1935
|
+
const composedRef = useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1936
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1937
|
+
ref: composedRef,
|
|
1938
|
+
className,
|
|
1939
|
+
style: {
|
|
1940
|
+
position: "absolute",
|
|
1941
|
+
inset: 0,
|
|
1942
|
+
zIndex: 10,
|
|
1943
|
+
...style
|
|
1944
|
+
},
|
|
1945
|
+
...props,
|
|
1946
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
1947
|
+
name: "PointsEditor.Container",
|
|
1948
|
+
children
|
|
1949
|
+
})
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
//#endregion
|
|
1953
|
+
//#region src/components/PointsEditor/SelectionBox.tsx
|
|
1954
|
+
/**
|
|
1955
|
+
* The box a drag on empty space draws, and what it covers is selected.
|
|
1956
|
+
*
|
|
1957
|
+
* Nothing is rendered while no drag is running, and leaving it out leaves the
|
|
1958
|
+
* editor without one — the selection still works, it just cannot be seen.
|
|
1959
|
+
*/
|
|
1960
|
+
function SelectionBox({ children, className, style, ...props }) {
|
|
1961
|
+
useCheckPlacement("PointsEditor.SelectionBox", "PointsEditor.Container");
|
|
1962
|
+
const selectionBox = usePointsEditorContext((s) => s.selectionBox);
|
|
1963
|
+
if (!selectionBox) return null;
|
|
1964
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1965
|
+
className,
|
|
1966
|
+
...props,
|
|
1967
|
+
style: {
|
|
1968
|
+
position: "absolute",
|
|
1969
|
+
zIndex: 20,
|
|
1970
|
+
pointerEvents: "none",
|
|
1971
|
+
...style,
|
|
1972
|
+
left: `${selectionBox.x * 100}%`,
|
|
1973
|
+
top: `${selectionBox.y * 100}%`,
|
|
1974
|
+
width: `${selectionBox.width * 100}%`,
|
|
1975
|
+
height: `${selectionBox.height * 100}%`
|
|
1976
|
+
},
|
|
1977
|
+
children
|
|
1252
1978
|
});
|
|
1253
1979
|
}
|
|
1254
1980
|
//#endregion
|
|
1255
1981
|
//#region src/components/PointsEditor/index.tsx
|
|
1982
|
+
/** One array for every editor with selection turned off, so memos hold still. */
|
|
1983
|
+
const EMPTY = [];
|
|
1984
|
+
/**
|
|
1985
|
+
* How far a point may move before something in the selection leaves its range.
|
|
1986
|
+
*
|
|
1987
|
+
* Clamping each point on its own would break the shape of the selection: the
|
|
1988
|
+
* one that reached the edge would stop while the rest carried on. One amount
|
|
1989
|
+
* for all of them means the whole selection stops together.
|
|
1990
|
+
*/
|
|
1991
|
+
function allowedDelta(delta, entries) {
|
|
1992
|
+
let loX = -Infinity;
|
|
1993
|
+
let hiX = Infinity;
|
|
1994
|
+
let loY = -Infinity;
|
|
1995
|
+
let hiY = Infinity;
|
|
1996
|
+
for (const { start, registration } of entries) {
|
|
1997
|
+
loX = Math.max(loX, (registration.min?.x ?? 0) - start.x);
|
|
1998
|
+
hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x);
|
|
1999
|
+
loY = Math.max(loY, (registration.min?.y ?? 0) - start.y);
|
|
2000
|
+
hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y);
|
|
2001
|
+
}
|
|
2002
|
+
return {
|
|
2003
|
+
x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
|
|
2004
|
+
y: hiY < loY ? 0 : clamp(delta.y, loY, hiY)
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
1256
2007
|
/**
|
|
1257
2008
|
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1258
2009
|
* in a hundred steps whatever its pixel size.
|
|
1259
2010
|
*/
|
|
1260
|
-
const
|
|
2011
|
+
const DEFAULT_WHEEL = ["normalized", .01];
|
|
2012
|
+
/**
|
|
2013
|
+
* Shift is the fine-adjustment key everywhere else, so it is bound here too
|
|
2014
|
+
* — but only on the keyboard. On the wheel it already means the x axis, and
|
|
2015
|
+
* browsers hand shift+wheel over as horizontal scrolling anyway.
|
|
2016
|
+
*/
|
|
2017
|
+
const DEFAULT_KEYBOARD = {
|
|
2018
|
+
default: ["normalized", .01],
|
|
2019
|
+
shift: ["normalized", .001]
|
|
2020
|
+
};
|
|
1261
2021
|
/**
|
|
1262
2022
|
* Multiple Point Controller
|
|
1263
2023
|
*/
|
|
1264
2024
|
const PointsEditor = {
|
|
1265
|
-
Root: forwardRef(({ width
|
|
2025
|
+
Root: /* @__PURE__ */ forwardRef(({ width, height, disabled = false, readonly = false, wheel = DEFAULT_WHEEL, keyboard = DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
1266
2026
|
const containerRef = useRef(null);
|
|
1267
|
-
const {
|
|
2027
|
+
const { cursor = "grabbing" } = externalStyles ?? {};
|
|
2028
|
+
const controlled = selectionProp !== void 0;
|
|
2029
|
+
const [ownSelection, setOwnSelection] = useState(defaultSelection ?? []);
|
|
2030
|
+
const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
|
|
2031
|
+
const selectionRef = useRef(selection);
|
|
2032
|
+
const changeHandlerRef = useRef(onSelectionChange);
|
|
2033
|
+
useEffect(() => {
|
|
2034
|
+
selectionRef.current = selection;
|
|
2035
|
+
changeHandlerRef.current = onSelectionChange;
|
|
2036
|
+
});
|
|
2037
|
+
const changeSelection = useCallback((next) => {
|
|
2038
|
+
selectionRef.current = next;
|
|
2039
|
+
if (!controlled) setOwnSelection(next);
|
|
2040
|
+
changeHandlerRef.current?.(next);
|
|
2041
|
+
}, [controlled]);
|
|
2042
|
+
/**
|
|
2043
|
+
* Every mounted point, by id. A registration is a ref rather than a value:
|
|
2044
|
+
* the value inside changes on every frame of a drag, and a registry keyed
|
|
2045
|
+
* on it would be rebuilt just as often.
|
|
2046
|
+
*/
|
|
2047
|
+
const points = useRef(/* @__PURE__ */ new Map());
|
|
2048
|
+
const registerPoint = useCallback((id, entry) => {
|
|
2049
|
+
points.current.set(id, entry);
|
|
2050
|
+
return () => {
|
|
2051
|
+
points.current.delete(id);
|
|
2052
|
+
};
|
|
2053
|
+
}, []);
|
|
2054
|
+
/** What the current drag picked up, and where those points started. */
|
|
2055
|
+
const dragRef = useRef([]);
|
|
2056
|
+
const snapshot = useCallback((ids) => {
|
|
2057
|
+
return ids.flatMap((id) => {
|
|
2058
|
+
const registration = points.current.get(id)?.current;
|
|
2059
|
+
return registration ? [{
|
|
2060
|
+
id,
|
|
2061
|
+
start: { ...registration.value }
|
|
2062
|
+
}] : [];
|
|
2063
|
+
});
|
|
2064
|
+
}, []);
|
|
2065
|
+
const applyDeltaTo = useCallback((entries, delta) => {
|
|
2066
|
+
const withRegistration = entries.flatMap((entry) => {
|
|
2067
|
+
const registration = points.current.get(entry.id)?.current;
|
|
2068
|
+
return registration ? [{
|
|
2069
|
+
...entry,
|
|
2070
|
+
registration
|
|
2071
|
+
}] : [];
|
|
2072
|
+
});
|
|
2073
|
+
const allowed = allowedDelta(delta, withRegistration);
|
|
2074
|
+
for (const { start, registration } of withRegistration) {
|
|
2075
|
+
if (registration.readonly) continue;
|
|
2076
|
+
registration.onChange?.({
|
|
2077
|
+
x: toPrecision(start.x + allowed.x),
|
|
2078
|
+
y: toPrecision(start.y + allowed.y)
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}, []);
|
|
2082
|
+
const beginPointDrag = useCallback((id, modifiers) => {
|
|
2083
|
+
if (!selectable) {
|
|
2084
|
+
dragRef.current = snapshot([id]);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const current = selectionRef.current;
|
|
2088
|
+
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
2089
|
+
let next;
|
|
2090
|
+
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
2091
|
+
else if (current.includes(id)) next = current;
|
|
2092
|
+
else next = [id];
|
|
2093
|
+
changeSelection([...next]);
|
|
2094
|
+
dragRef.current = next.includes(id) ? snapshot(next) : [];
|
|
2095
|
+
}, [
|
|
2096
|
+
selectable,
|
|
2097
|
+
changeSelection,
|
|
2098
|
+
snapshot
|
|
2099
|
+
]);
|
|
2100
|
+
const movePointDrag = useCallback((delta) => applyDeltaTo(dragRef.current, delta), [applyDeltaTo]);
|
|
2101
|
+
const nudgeSelection = useCallback((id, delta) => {
|
|
2102
|
+
const ids = selectionRef.current.includes(id) ? selectionRef.current : [id];
|
|
2103
|
+
applyDeltaTo(snapshot(ids), delta);
|
|
2104
|
+
}, [applyDeltaTo, snapshot]);
|
|
2105
|
+
const isPointElement = useCallback((element) => {
|
|
2106
|
+
if (!element) return false;
|
|
2107
|
+
for (const [, entry] of points.current) if (entry.current.element?.contains(element)) return true;
|
|
2108
|
+
return false;
|
|
2109
|
+
}, []);
|
|
2110
|
+
const nudgeFocusedPoint = useCallback((axis, direction, modifiers) => {
|
|
2111
|
+
const container = containerRef.current;
|
|
2112
|
+
const active = container?.ownerDocument.activeElement;
|
|
2113
|
+
if (!container || !active || !container.contains(active)) return false;
|
|
2114
|
+
for (const [id, entry] of points.current) {
|
|
2115
|
+
const { element, wheel, readonly, onChange, value } = entry.current;
|
|
2116
|
+
if (!element?.contains(active)) continue;
|
|
2117
|
+
if (!wheel || readonly || !onChange) return false;
|
|
2118
|
+
const next = applyDelta(value[axis], direction, wheel, AXIS, modifiers);
|
|
2119
|
+
nudgeSelection(id, {
|
|
2120
|
+
x: axis === "x" ? next - value.x : 0,
|
|
2121
|
+
y: axis === "y" ? next - value.y : 0
|
|
2122
|
+
});
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
return false;
|
|
2126
|
+
}, [nudgeSelection]);
|
|
2127
|
+
const [selectionBox, setSelectionBox] = useState(null);
|
|
2128
|
+
const selectionBoxRef = useRef(null);
|
|
2129
|
+
selectionBoxRef.current ??= createSelectionBox({
|
|
2130
|
+
*items() {
|
|
2131
|
+
for (const [id, entry] of points.current) {
|
|
2132
|
+
const { x, y } = entry.current.value;
|
|
2133
|
+
yield [id, [x, y]];
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
onBoxChange: setSelectionBox
|
|
2137
|
+
});
|
|
2138
|
+
useEffect(() => {
|
|
2139
|
+
selectionBoxRef.current?.update({ onSelectionChange: changeSelection });
|
|
2140
|
+
}, [changeSelection]);
|
|
2141
|
+
useEffect(() => {
|
|
2142
|
+
const box = selectionBoxRef.current;
|
|
2143
|
+
return () => box?.destroy();
|
|
2144
|
+
}, []);
|
|
2145
|
+
const beginSelectionBox = useCallback((at, modifiers) => {
|
|
2146
|
+
if (!selectable) return;
|
|
2147
|
+
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
2148
|
+
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
2149
|
+
selection: selectionRef.current
|
|
2150
|
+
});
|
|
2151
|
+
}, [selectable]);
|
|
2152
|
+
const moveSelectionBox = useCallback((to) => {
|
|
2153
|
+
selectionBoxRef.current?.move([to.x, to.y]);
|
|
2154
|
+
}, []);
|
|
2155
|
+
const endSelectionBox = useCallback(() => {
|
|
2156
|
+
if (!selectionBoxRef.current?.end()) return;
|
|
2157
|
+
const [first] = selectionRef.current;
|
|
2158
|
+
if (first) points.current.get(first)?.current.element?.focus();
|
|
2159
|
+
}, []);
|
|
2160
|
+
const context = useMemo(() => ({
|
|
2161
|
+
disabled,
|
|
2162
|
+
readonly,
|
|
2163
|
+
wheel,
|
|
2164
|
+
keyboard,
|
|
2165
|
+
dragSensitivity,
|
|
2166
|
+
externalStyles: { cursor },
|
|
2167
|
+
containerRef,
|
|
2168
|
+
selectable,
|
|
2169
|
+
selection,
|
|
2170
|
+
registerPoint,
|
|
2171
|
+
isPointElement,
|
|
2172
|
+
beginPointDrag,
|
|
2173
|
+
movePointDrag,
|
|
2174
|
+
nudgeSelection,
|
|
2175
|
+
nudgeFocusedPoint,
|
|
2176
|
+
selectionBox,
|
|
2177
|
+
beginSelectionBox,
|
|
2178
|
+
moveSelectionBox,
|
|
2179
|
+
endSelectionBox
|
|
2180
|
+
}), [
|
|
2181
|
+
disabled,
|
|
2182
|
+
readonly,
|
|
2183
|
+
wheel,
|
|
2184
|
+
keyboard,
|
|
2185
|
+
dragSensitivity,
|
|
2186
|
+
cursor,
|
|
2187
|
+
selectable,
|
|
2188
|
+
selection,
|
|
2189
|
+
registerPoint,
|
|
2190
|
+
isPointElement,
|
|
2191
|
+
beginPointDrag,
|
|
2192
|
+
movePointDrag,
|
|
2193
|
+
nudgeSelection,
|
|
2194
|
+
nudgeFocusedPoint,
|
|
2195
|
+
selectionBox,
|
|
2196
|
+
beginSelectionBox,
|
|
2197
|
+
moveSelectionBox,
|
|
2198
|
+
endSelectionBox
|
|
2199
|
+
]);
|
|
1268
2200
|
return /* @__PURE__ */ jsx(PointsEditorProvider, {
|
|
1269
|
-
value:
|
|
1270
|
-
disabled,
|
|
1271
|
-
readonly,
|
|
1272
|
-
wheel,
|
|
1273
|
-
keyboard,
|
|
1274
|
-
externalStyles: {
|
|
1275
|
-
userSelectNone,
|
|
1276
|
-
cursor
|
|
1277
|
-
},
|
|
1278
|
-
containerRef
|
|
1279
|
-
}), [
|
|
1280
|
-
disabled,
|
|
1281
|
-
readonly,
|
|
1282
|
-
wheel,
|
|
1283
|
-
keyboard,
|
|
1284
|
-
userSelectNone,
|
|
1285
|
-
cursor
|
|
1286
|
-
]),
|
|
2201
|
+
value: context,
|
|
1287
2202
|
children: /* @__PURE__ */ jsx("div", {
|
|
1288
2203
|
ref: forwardedRef,
|
|
1289
|
-
className
|
|
1290
|
-
"
|
|
1291
|
-
"
|
|
2204
|
+
className,
|
|
2205
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2206
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1292
2207
|
style: {
|
|
1293
|
-
width,
|
|
1294
|
-
height,
|
|
2208
|
+
"--width": cssLength(width),
|
|
2209
|
+
"--height": cssLength(height),
|
|
2210
|
+
position: "relative",
|
|
1295
2211
|
...style
|
|
1296
2212
|
},
|
|
1297
2213
|
...props,
|
|
@@ -1301,11 +2217,12 @@ const PointsEditor = {
|
|
|
1301
2217
|
}),
|
|
1302
2218
|
Background,
|
|
1303
2219
|
Container,
|
|
1304
|
-
Point
|
|
2220
|
+
Point,
|
|
2221
|
+
SelectionBox
|
|
1305
2222
|
};
|
|
1306
2223
|
//#endregion
|
|
1307
2224
|
//#region src/components/Slider/context.tsx
|
|
1308
|
-
const SliderContext = createContext(null);
|
|
2225
|
+
const SliderContext = /* @__PURE__ */ createContext(null);
|
|
1309
2226
|
const SliderProvider = SliderContext.Provider;
|
|
1310
2227
|
function useSliderContext(selector) {
|
|
1311
2228
|
const context = useContext(SliderContext);
|
|
@@ -1314,7 +2231,8 @@ function useSliderContext(selector) {
|
|
|
1314
2231
|
}
|
|
1315
2232
|
//#endregion
|
|
1316
2233
|
//#region src/components/Slider/MarksOption.tsx
|
|
1317
|
-
function MarksOption({ value,
|
|
2234
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, labelWidth, classes, styles, className, style, ...props }) {
|
|
2235
|
+
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
1318
2236
|
const min = useSliderContext((s) => s.min);
|
|
1319
2237
|
const max = useSliderContext((s) => s.max);
|
|
1320
2238
|
const scale = useSliderContext((s) => s.scale);
|
|
@@ -1322,7 +2240,7 @@ function MarksOption({ value, type = "mark-number", label, thickness = 1, length
|
|
|
1322
2240
|
const reverse = useSliderContext((s) => s.reverse);
|
|
1323
2241
|
const calcPercent = useCallback((value) => {
|
|
1324
2242
|
const percent = scale.normalize(value, min, max) * 100;
|
|
1325
|
-
return toFixed(
|
|
2243
|
+
return toFixed(vertical !== reverse ? 100 - percent : percent);
|
|
1326
2244
|
}, [
|
|
1327
2245
|
vertical,
|
|
1328
2246
|
reverse,
|
|
@@ -1331,140 +2249,184 @@ function MarksOption({ value, type = "mark-number", label, thickness = 1, length
|
|
|
1331
2249
|
scale
|
|
1332
2250
|
]);
|
|
1333
2251
|
return /* @__PURE__ */ jsxs("div", {
|
|
1334
|
-
className
|
|
2252
|
+
className,
|
|
1335
2253
|
style: {
|
|
2254
|
+
"--thickness": cssLength(thickness),
|
|
2255
|
+
"--length": cssLength(length),
|
|
2256
|
+
"--gap": cssLength(gap),
|
|
2257
|
+
"--label-width": cssLength(labelWidth),
|
|
2258
|
+
position: "absolute",
|
|
2259
|
+
translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
|
|
2260
|
+
zIndex: 10,
|
|
2261
|
+
...style,
|
|
1336
2262
|
left: !vertical ? `${calcPercent(value)}%` : void 0,
|
|
1337
|
-
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1338
|
-
...style
|
|
2263
|
+
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1339
2264
|
},
|
|
1340
|
-
"data-vertical":
|
|
2265
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1341
2266
|
...props,
|
|
1342
|
-
children: [
|
|
1343
|
-
className:
|
|
1344
|
-
style:
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
},
|
|
1351
|
-
"data-vertical": vertical
|
|
1352
|
-
}), type != "mark" && /* @__PURE__ */ jsx("div", {
|
|
1353
|
-
className: clsx("tremolo-slider-marks-option-label", classes?.label),
|
|
1354
|
-
style: {
|
|
1355
|
-
width: labelWidth,
|
|
1356
|
-
...styles?.label
|
|
1357
|
-
},
|
|
1358
|
-
children: label || value
|
|
2267
|
+
children: [mark && /* @__PURE__ */ jsx("div", {
|
|
2268
|
+
className: classes?.mark,
|
|
2269
|
+
style: styles?.mark,
|
|
2270
|
+
"data-orientation": vertical ? "vertical" : "horizontal"
|
|
2271
|
+
}), label !== null && /* @__PURE__ */ jsx("div", {
|
|
2272
|
+
className: classes?.label,
|
|
2273
|
+
style: styles?.label,
|
|
2274
|
+
children: label ?? value
|
|
1359
2275
|
})]
|
|
1360
2276
|
});
|
|
1361
2277
|
}
|
|
1362
2278
|
//#endregion
|
|
2279
|
+
//#region src/components/_util/decimal-digits.ts
|
|
2280
|
+
/**
|
|
2281
|
+
* How many digits a number carries after the decimal point.
|
|
2282
|
+
*
|
|
2283
|
+
* `Slider.Marks` builds its labels by multiplying an interval, which leaves
|
|
2284
|
+
* binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
|
|
2285
|
+
* count is what tells `toFixed` how far to round that back.
|
|
2286
|
+
*
|
|
2287
|
+
* The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
|
|
2288
|
+
* which has no decimal point at all, so reading the text after the point
|
|
2289
|
+
* reports no digits and the interval rounds away to whole numbers.
|
|
2290
|
+
*/
|
|
2291
|
+
function decimalDigits(x) {
|
|
2292
|
+
if (!Number.isFinite(x)) return 0;
|
|
2293
|
+
const text = String(x);
|
|
2294
|
+
const e = text.indexOf("e");
|
|
2295
|
+
if (e === -1) return text.split(".")[1]?.length ?? 0;
|
|
2296
|
+
const fraction = text.slice(0, e).split(".")[1]?.length ?? 0;
|
|
2297
|
+
return Math.max(0, fraction - Number(text.slice(e + 1)));
|
|
2298
|
+
}
|
|
2299
|
+
//#endregion
|
|
1363
2300
|
//#region src/components/Slider/type.ts
|
|
1364
2301
|
function generateOptionsList(options, min, max, step) {
|
|
2302
|
+
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
1365
2303
|
const optionsList = [];
|
|
1366
|
-
const
|
|
1367
|
-
const count = Math.floor(max /
|
|
2304
|
+
const interval = per === "step" ? step : per;
|
|
2305
|
+
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
1368
2306
|
for (let i = 0; i < count; i++) {
|
|
1369
|
-
const value = toFixed(
|
|
2307
|
+
const value = toFixed(interval * (Math.ceil(min / interval) + i), decimalDigits(interval));
|
|
1370
2308
|
optionsList.push({
|
|
1371
2309
|
value,
|
|
1372
|
-
|
|
2310
|
+
mark,
|
|
2311
|
+
label
|
|
1373
2312
|
});
|
|
1374
2313
|
}
|
|
1375
2314
|
return optionsList;
|
|
1376
2315
|
}
|
|
1377
2316
|
//#endregion
|
|
1378
2317
|
//#region src/components/Slider/Marks.tsx
|
|
1379
|
-
function Marks({ gap
|
|
2318
|
+
function Marks({ gap, options, children, className, style, ...props }) {
|
|
1380
2319
|
const min = useSliderContext((s) => s.min);
|
|
1381
2320
|
const max = useSliderContext((s) => s.max);
|
|
1382
2321
|
const step = useSliderContext((s) => s.step);
|
|
1383
2322
|
const vertical = useSliderContext((s) => s.vertical);
|
|
1384
2323
|
const reverse = useSliderContext((s) => s.reverse);
|
|
1385
2324
|
const optionsList = options ? generateOptionsList(options, min, max, step) : [];
|
|
1386
|
-
if (
|
|
2325
|
+
if (vertical !== reverse) optionsList.reverse();
|
|
1387
2326
|
return /* @__PURE__ */ jsx("div", {
|
|
1388
|
-
className
|
|
2327
|
+
className,
|
|
2328
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1389
2329
|
style: {
|
|
1390
|
-
|
|
1391
|
-
|
|
2330
|
+
"--gap": cssLength(gap),
|
|
2331
|
+
position: "relative",
|
|
1392
2332
|
...style
|
|
1393
2333
|
},
|
|
1394
2334
|
...props,
|
|
1395
|
-
children:
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2335
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2336
|
+
name: "Slider.Marks",
|
|
2337
|
+
children: options ? optionsList.map(({ value, mark, label }, index) => {
|
|
2338
|
+
return /* @__PURE__ */ jsx(MarksOption, {
|
|
2339
|
+
value,
|
|
2340
|
+
mark,
|
|
2341
|
+
label: label ? void 0 : null
|
|
2342
|
+
}, index);
|
|
2343
|
+
}) : children
|
|
2344
|
+
})
|
|
1401
2345
|
});
|
|
1402
2346
|
}
|
|
1403
2347
|
//#endregion
|
|
1404
2348
|
//#region src/components/Slider/Thumb.tsx
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
const { vertical, disabled, readonly, percent, thumbRef } = useSliderContext();
|
|
2349
|
+
const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
|
|
2350
|
+
const inputRef = useRef(null);
|
|
2351
|
+
const { value, min, max, step, vertical, disabled, readonly, onChange, percent, thumbRef } = useSliderContext();
|
|
2352
|
+
useCheckPlacement("Slider.Thumb", "Slider.Track");
|
|
1408
2353
|
const methods = () => ({
|
|
1409
2354
|
focus() {
|
|
1410
|
-
|
|
2355
|
+
if (!disabled) inputRef.current?.focus();
|
|
1411
2356
|
},
|
|
1412
2357
|
blur() {
|
|
1413
|
-
|
|
2358
|
+
inputRef.current?.blur();
|
|
1414
2359
|
}
|
|
1415
2360
|
});
|
|
1416
|
-
useImperativeHandle(
|
|
1417
|
-
useImperativeHandle(thumbRef, methods, []);
|
|
1418
|
-
return /* @__PURE__ */
|
|
1419
|
-
className
|
|
2361
|
+
useImperativeHandle(forwardedRef, methods, [disabled]);
|
|
2362
|
+
useImperativeHandle(thumbRef, methods, [disabled]);
|
|
2363
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2364
|
+
className,
|
|
2365
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2366
|
+
"data-readonly": readonly ? "" : void 0,
|
|
2367
|
+
...props,
|
|
1420
2368
|
style: {
|
|
2369
|
+
"--color": color,
|
|
2370
|
+
position: "absolute",
|
|
2371
|
+
translate: "var(--translate, -50% -50%)",
|
|
2372
|
+
zIndex: 100,
|
|
2373
|
+
...style,
|
|
1421
2374
|
top: vertical ? `${percent}%` : "50%",
|
|
1422
2375
|
left: !vertical ? `${percent}%` : "50%"
|
|
1423
2376
|
},
|
|
1424
|
-
children:
|
|
1425
|
-
ref:
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2377
|
+
children: [/* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
2378
|
+
ref: inputRef,
|
|
2379
|
+
value,
|
|
2380
|
+
min,
|
|
2381
|
+
max,
|
|
2382
|
+
step,
|
|
2383
|
+
disabled,
|
|
1429
2384
|
"aria-readonly": readonly,
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2385
|
+
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
2386
|
+
"aria-label": ariaLabel,
|
|
2387
|
+
"aria-labelledby": ariaLabelledby,
|
|
2388
|
+
"aria-describedby": ariaDescribedby,
|
|
2389
|
+
"aria-valuetext": ariaValuetext,
|
|
2390
|
+
onChange: (event) => {
|
|
2391
|
+
if (readonly) {
|
|
2392
|
+
event.currentTarget.value = String(value);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
onChange?.(event.currentTarget.valueAsNumber);
|
|
1433
2396
|
}
|
|
1434
|
-
})
|
|
2397
|
+
}), children]
|
|
1435
2398
|
});
|
|
1436
|
-
}
|
|
1437
|
-
|
|
2399
|
+
});
|
|
2400
|
+
//#endregion
|
|
2401
|
+
//#region src/components/Slider/Track.tsx
|
|
2402
|
+
const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, active, inactive, children, className, style, ...props }, forwardedRef) {
|
|
1438
2403
|
const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
|
|
1439
|
-
const composedRef = useComposedRefs(
|
|
1440
|
-
const direction = vertical ? "bottom" : "right";
|
|
1441
|
-
const colors = {
|
|
1442
|
-
"--active": active,
|
|
1443
|
-
"--inactive": inactive
|
|
1444
|
-
};
|
|
2404
|
+
const composedRef = useComposedRefs(forwardedRef, trackRef);
|
|
1445
2405
|
return /* @__PURE__ */ jsx("div", {
|
|
1446
2406
|
ref: composedRef,
|
|
1447
|
-
className
|
|
1448
|
-
"
|
|
1449
|
-
"data-vertical":
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2407
|
+
className,
|
|
2408
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2409
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2410
|
+
"data-flipped": vertical !== reverse ? "" : void 0,
|
|
2411
|
+
style: {
|
|
2412
|
+
"--active": active,
|
|
2413
|
+
"--inactive": inactive,
|
|
2414
|
+
"--length": cssLength(length),
|
|
2415
|
+
"--thickness": cssLength(thickness),
|
|
2416
|
+
"--percent": `${percent}%`,
|
|
2417
|
+
position: "relative",
|
|
1456
2418
|
...style
|
|
1457
2419
|
},
|
|
1458
2420
|
...props,
|
|
1459
|
-
children
|
|
2421
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2422
|
+
name: "Slider.Track",
|
|
2423
|
+
children
|
|
2424
|
+
})
|
|
1460
2425
|
});
|
|
1461
|
-
}
|
|
2426
|
+
});
|
|
1462
2427
|
//#endregion
|
|
1463
2428
|
//#region src/components/Slider/index.tsx
|
|
1464
|
-
const defaultExternalStyles$1 = {
|
|
1465
|
-
userSelectNone: true,
|
|
1466
|
-
cursor: "pointer"
|
|
1467
|
-
};
|
|
2429
|
+
const defaultExternalStyles$1 = { cursor: "pointer" };
|
|
1468
2430
|
/**
|
|
1469
2431
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1470
2432
|
* the control does not change its value.
|
|
@@ -1474,16 +2436,17 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
|
|
|
1474
2436
|
* Customizable slider
|
|
1475
2437
|
*/
|
|
1476
2438
|
const Slider = {
|
|
1477
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel =
|
|
2439
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
|
|
1478
2440
|
const trackRef = useRef(null);
|
|
1479
2441
|
const thumbRef = useRef(null);
|
|
1480
2442
|
const externalStyles = {
|
|
1481
2443
|
...defaultExternalStyles$1,
|
|
1482
2444
|
..._externalStyles
|
|
1483
2445
|
};
|
|
2446
|
+
const inactive = disabled || readonly;
|
|
1484
2447
|
const p = toFixed(scale.normalize(value, min, max) * 100);
|
|
1485
2448
|
const rev = toFixed(100 - p);
|
|
1486
|
-
const displayReversed =
|
|
2449
|
+
const displayReversed = vertical !== reverse;
|
|
1487
2450
|
const percent = displayReversed ? rev : p;
|
|
1488
2451
|
const axis = useMemo(() => ({
|
|
1489
2452
|
min,
|
|
@@ -1498,24 +2461,29 @@ const Slider = {
|
|
|
1498
2461
|
scale,
|
|
1499
2462
|
displayReversed
|
|
1500
2463
|
]);
|
|
2464
|
+
useCheckSteps({
|
|
2465
|
+
component: "Slider",
|
|
2466
|
+
range: axis,
|
|
2467
|
+
keyboard,
|
|
2468
|
+
wheel
|
|
2469
|
+
});
|
|
1501
2470
|
const handleKeyDown = useCallback((event) => {
|
|
1502
|
-
if (!keyboard || !onChange || readonly) return;
|
|
1503
2471
|
const key = event.key;
|
|
1504
|
-
if ([
|
|
2472
|
+
if (![
|
|
1505
2473
|
"ArrowRight",
|
|
1506
2474
|
"ArrowLeft",
|
|
1507
2475
|
"ArrowUp",
|
|
1508
2476
|
"ArrowDown"
|
|
1509
|
-
].includes(key))
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2477
|
+
].includes(key)) return;
|
|
2478
|
+
event.preventDefault();
|
|
2479
|
+
if (!keyboard || !onChange || inactive) return;
|
|
2480
|
+
let direction = key === "ArrowRight" || key === "ArrowUp" ? 1 : -1;
|
|
2481
|
+
if (reverse) direction *= -1;
|
|
2482
|
+
onChange(applyDelta(value, direction, keyboard, axis, event));
|
|
1515
2483
|
}, [
|
|
1516
2484
|
keyboard,
|
|
1517
2485
|
onChange,
|
|
1518
|
-
|
|
2486
|
+
inactive,
|
|
1519
2487
|
reverse,
|
|
1520
2488
|
value,
|
|
1521
2489
|
axis
|
|
@@ -1524,36 +2492,37 @@ const Slider = {
|
|
|
1524
2492
|
const { refCallback: dragRefCallback } = useDragValue({
|
|
1525
2493
|
axis,
|
|
1526
2494
|
baseElementRef: trackRef,
|
|
1527
|
-
cursor:
|
|
2495
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2496
|
+
shouldStart: () => !inactive,
|
|
2497
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1528
2498
|
updateOnPointerDown: true,
|
|
1529
2499
|
onChange: (v) => {
|
|
1530
|
-
if (
|
|
2500
|
+
if (inactive) return;
|
|
1531
2501
|
onChange?.(valueOf(v));
|
|
1532
2502
|
},
|
|
1533
2503
|
onDragStart: (v) => {
|
|
1534
|
-
if (
|
|
1535
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
2504
|
+
if (inactive) return;
|
|
1536
2505
|
thumbRef.current?.focus();
|
|
1537
2506
|
onDragStart?.(valueOf(v));
|
|
1538
2507
|
},
|
|
1539
2508
|
onDragEnd: (v) => {
|
|
1540
|
-
if (
|
|
1541
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
2509
|
+
if (inactive) return;
|
|
1542
2510
|
onDragEnd?.(valueOf(v));
|
|
1543
2511
|
}
|
|
1544
2512
|
});
|
|
1545
|
-
const
|
|
1546
|
-
if (!wheel || !onChange ||
|
|
2513
|
+
const wheelRefCallback = useWheel((event) => {
|
|
2514
|
+
if (!wheel || !onChange || inactive) return;
|
|
1547
2515
|
event.preventDefault();
|
|
1548
2516
|
let direction;
|
|
1549
|
-
if (!vertical && event.deltaX
|
|
2517
|
+
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
1550
2518
|
else {
|
|
1551
|
-
if (event.deltaY
|
|
2519
|
+
if (event.deltaY === 0) return;
|
|
1552
2520
|
direction = event.deltaY > 0 ? -1 : 1;
|
|
1553
2521
|
}
|
|
1554
|
-
if (
|
|
1555
|
-
onChange(applyDelta(value, direction, wheel, axis));
|
|
1556
|
-
}, WHEEL_OPTIONS$1)
|
|
2522
|
+
if (reverse) direction *= -1;
|
|
2523
|
+
onChange(applyDelta(value, direction, wheel, axis, event));
|
|
2524
|
+
}, WHEEL_OPTIONS$1);
|
|
2525
|
+
const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
1557
2526
|
const context = useMemo(() => ({
|
|
1558
2527
|
value,
|
|
1559
2528
|
min,
|
|
@@ -1564,6 +2533,7 @@ const Slider = {
|
|
|
1564
2533
|
reverse,
|
|
1565
2534
|
disabled,
|
|
1566
2535
|
readonly,
|
|
2536
|
+
onChange,
|
|
1567
2537
|
percent,
|
|
1568
2538
|
trackRef,
|
|
1569
2539
|
thumbRef
|
|
@@ -1577,32 +2547,29 @@ const Slider = {
|
|
|
1577
2547
|
reverse,
|
|
1578
2548
|
disabled,
|
|
1579
2549
|
readonly,
|
|
2550
|
+
onChange,
|
|
1580
2551
|
percent
|
|
1581
2552
|
]);
|
|
1582
2553
|
useImperativeHandle(forwardedRef, () => {
|
|
1583
2554
|
return {
|
|
1584
2555
|
focus() {
|
|
1585
|
-
thumbRef.current?.focus();
|
|
2556
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1586
2557
|
},
|
|
1587
2558
|
blur() {
|
|
1588
2559
|
thumbRef.current?.blur();
|
|
1589
2560
|
}
|
|
1590
2561
|
};
|
|
1591
|
-
}, []);
|
|
2562
|
+
}, [disabled]);
|
|
1592
2563
|
return /* @__PURE__ */ jsx(SliderProvider, {
|
|
1593
2564
|
value: context,
|
|
1594
2565
|
children: /* @__PURE__ */ jsx("div", {
|
|
1595
|
-
className
|
|
2566
|
+
className,
|
|
1596
2567
|
ref: rootRefCallback,
|
|
2568
|
+
role: "group",
|
|
1597
2569
|
tabIndex: -1,
|
|
1598
|
-
|
|
1599
|
-
"
|
|
1600
|
-
"
|
|
1601
|
-
"aria-valuemax": max,
|
|
1602
|
-
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
1603
|
-
"aria-disabled": disabled,
|
|
1604
|
-
"aria-readonly": readonly,
|
|
1605
|
-
"data-vertical": vertical,
|
|
2570
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2571
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2572
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1606
2573
|
style,
|
|
1607
2574
|
onPointerDown,
|
|
1608
2575
|
onKeyDown: (event) => {
|
|
@@ -1610,7 +2577,7 @@ const Slider = {
|
|
|
1610
2577
|
onKeyDown?.(event);
|
|
1611
2578
|
},
|
|
1612
2579
|
onFocus: (event) => {
|
|
1613
|
-
thumbRef.current?.focus();
|
|
2580
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1614
2581
|
onFocus?.(event);
|
|
1615
2582
|
},
|
|
1616
2583
|
onBlur: (event) => {
|
|
@@ -1629,7 +2596,7 @@ const Slider = {
|
|
|
1629
2596
|
};
|
|
1630
2597
|
//#endregion
|
|
1631
2598
|
//#region src/components/XYPad/context.tsx
|
|
1632
|
-
const XYPadContext = createContext(null);
|
|
2599
|
+
const XYPadContext = /* @__PURE__ */ createContext(null);
|
|
1633
2600
|
const XYPadProvider = XYPadContext.Provider;
|
|
1634
2601
|
function useXYPadContext(selector) {
|
|
1635
2602
|
const context = useContext(XYPadContext);
|
|
@@ -1638,63 +2605,91 @@ function useXYPadContext(selector) {
|
|
|
1638
2605
|
}
|
|
1639
2606
|
//#endregion
|
|
1640
2607
|
//#region src/components/XYPad/Area.tsx
|
|
1641
|
-
function Area({ width
|
|
2608
|
+
const Area = /* @__PURE__ */ forwardRef(function Area({ width, height, color, children, className, style, ...props }, forwardedRef) {
|
|
1642
2609
|
const { areaRef } = useXYPadContext();
|
|
2610
|
+
const composedRef = useComposedRefs(forwardedRef, areaRef);
|
|
1643
2611
|
return /* @__PURE__ */ jsx("div", {
|
|
1644
|
-
ref:
|
|
1645
|
-
className
|
|
2612
|
+
ref: composedRef,
|
|
2613
|
+
className,
|
|
1646
2614
|
style: {
|
|
1647
2615
|
"--color": color,
|
|
1648
|
-
width,
|
|
1649
|
-
height,
|
|
2616
|
+
"--width": cssLength(width),
|
|
2617
|
+
"--height": cssLength(height),
|
|
2618
|
+
position: "relative",
|
|
1650
2619
|
...style
|
|
1651
2620
|
},
|
|
1652
2621
|
...props,
|
|
1653
|
-
children
|
|
2622
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2623
|
+
name: "XYPad.Area",
|
|
2624
|
+
children
|
|
2625
|
+
})
|
|
1654
2626
|
});
|
|
1655
|
-
}
|
|
2627
|
+
});
|
|
1656
2628
|
//#endregion
|
|
1657
2629
|
//#region src/components/XYPad/Thumb.tsx
|
|
1658
|
-
function Thumb({ color, children,
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
2630
|
+
const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
|
|
2631
|
+
const xInputRef = useRef(null);
|
|
2632
|
+
const yInputRef = useRef(null);
|
|
2633
|
+
const { value, min, max, step, disabled, readonly, onChange, percent, thumbRef } = useXYPadContext();
|
|
2634
|
+
useCheckPlacement("XYPad.Thumb", "XYPad.Area");
|
|
2635
|
+
const labels = toXY(ariaLabel);
|
|
2636
|
+
const labelledby = toXY(ariaLabelledby);
|
|
2637
|
+
const describedby = toXY(ariaDescribedby);
|
|
2638
|
+
const valueText = toXY(ariaValuetext);
|
|
1661
2639
|
const methods = () => ({
|
|
1662
2640
|
focus() {
|
|
1663
|
-
|
|
2641
|
+
if (!disabled) xInputRef.current?.focus();
|
|
1664
2642
|
},
|
|
1665
2643
|
blur() {
|
|
1666
|
-
|
|
2644
|
+
xInputRef.current?.blur();
|
|
2645
|
+
yInputRef.current?.blur();
|
|
1667
2646
|
}
|
|
1668
2647
|
});
|
|
1669
|
-
useImperativeHandle(
|
|
1670
|
-
useImperativeHandle(thumbRef, methods, []);
|
|
1671
|
-
return /* @__PURE__ */
|
|
1672
|
-
className
|
|
2648
|
+
useImperativeHandle(forwardedRef, methods, [disabled]);
|
|
2649
|
+
useImperativeHandle(thumbRef, methods, [disabled]);
|
|
2650
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2651
|
+
className,
|
|
2652
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2653
|
+
"data-readonly": readonly ? "" : void 0,
|
|
2654
|
+
...props,
|
|
1673
2655
|
style: {
|
|
2656
|
+
"--color": color,
|
|
2657
|
+
position: "absolute",
|
|
2658
|
+
translate: "var(--translate, -50% -50%)",
|
|
2659
|
+
zIndex: 100,
|
|
2660
|
+
...style,
|
|
1674
2661
|
left: `${percent[0]}%`,
|
|
1675
|
-
top: `${percent[1]}
|
|
1676
|
-
...wrapperStyle
|
|
2662
|
+
top: `${percent[1]}%`
|
|
1677
2663
|
},
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2664
|
+
children: [[0, 1].map((axis) => /* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
2665
|
+
ref: axis === 0 ? xInputRef : yInputRef,
|
|
2666
|
+
"data-axis": axis === 0 ? "x" : "y",
|
|
2667
|
+
value: value[axis],
|
|
2668
|
+
min: min[axis],
|
|
2669
|
+
max: max[axis],
|
|
2670
|
+
step: step[axis],
|
|
2671
|
+
disabled,
|
|
1684
2672
|
"aria-readonly": readonly,
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2673
|
+
"aria-orientation": axis === 0 ? "horizontal" : "vertical",
|
|
2674
|
+
"aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
|
|
2675
|
+
"aria-labelledby": labelledby[axis],
|
|
2676
|
+
"aria-describedby": describedby[axis],
|
|
2677
|
+
"aria-valuetext": valueText[axis],
|
|
2678
|
+
onChange: (event) => {
|
|
2679
|
+
if (readonly) {
|
|
2680
|
+
event.currentTarget.value = String(value[axis]);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
const next = [...value];
|
|
2684
|
+
next[axis] = event.currentTarget.valueAsNumber;
|
|
2685
|
+
onChange?.(next);
|
|
1688
2686
|
}
|
|
1689
|
-
})
|
|
2687
|
+
}, axis)), children]
|
|
1690
2688
|
});
|
|
1691
|
-
}
|
|
2689
|
+
});
|
|
1692
2690
|
//#endregion
|
|
1693
2691
|
//#region src/components/XYPad/index.tsx
|
|
1694
|
-
const defaultExternalStyles = {
|
|
1695
|
-
userSelectNone: true,
|
|
1696
|
-
cursor: "pointer"
|
|
1697
|
-
};
|
|
2692
|
+
const defaultExternalStyles = { cursor: "pointer" };
|
|
1698
2693
|
/**
|
|
1699
2694
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1700
2695
|
* the control does not change its value.
|
|
@@ -1704,7 +2699,7 @@ const WHEEL_OPTIONS = { requireFocus: true };
|
|
|
1704
2699
|
* Simple XYPad
|
|
1705
2700
|
*/
|
|
1706
2701
|
const XYPad = {
|
|
1707
|
-
Root: forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = linearScale, reverse: _reverse = false, wheel =
|
|
2702
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = linearScale, reverse: _reverse = false, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
|
|
1708
2703
|
const rootRef = useRef(null);
|
|
1709
2704
|
const areaRef = useRef(null);
|
|
1710
2705
|
const thumbRef = useRef(null);
|
|
@@ -1712,6 +2707,7 @@ const XYPad = {
|
|
|
1712
2707
|
...defaultExternalStyles,
|
|
1713
2708
|
..._externalStyles
|
|
1714
2709
|
};
|
|
2710
|
+
const inactive = disabled || readonly;
|
|
1715
2711
|
const min = useMemo(() => toXY(_min), [_min]);
|
|
1716
2712
|
const max = useMemo(() => toXY(_max), [_max]);
|
|
1717
2713
|
const step = useMemo(() => toXY(_step), [_step]);
|
|
@@ -1740,32 +2736,45 @@ const XYPad = {
|
|
|
1740
2736
|
scale,
|
|
1741
2737
|
reverse
|
|
1742
2738
|
]);
|
|
1743
|
-
|
|
2739
|
+
useCheckSteps({
|
|
2740
|
+
component: "XYPad",
|
|
2741
|
+
axis: "x",
|
|
2742
|
+
range: axis[0],
|
|
2743
|
+
keyboard,
|
|
2744
|
+
wheel
|
|
2745
|
+
});
|
|
2746
|
+
useCheckSteps({
|
|
2747
|
+
component: "XYPad",
|
|
2748
|
+
axis: "y",
|
|
2749
|
+
range: axis[1],
|
|
2750
|
+
keyboard,
|
|
2751
|
+
wheel
|
|
2752
|
+
});
|
|
2753
|
+
const withAxis = useCallback((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
|
|
1744
2754
|
/** @param i 0 = x, 1 = y */
|
|
1745
|
-
const nudge = useCallback((i, direction, option) => withAxis(i, applyDelta(value[i], direction, option, axis[i])), [
|
|
2755
|
+
const nudge = useCallback((i, direction, option, modifiers) => withAxis(i, applyDelta(value[i], direction, option, axis[i], modifiers)), [
|
|
1746
2756
|
value,
|
|
1747
2757
|
axis,
|
|
1748
2758
|
withAxis
|
|
1749
2759
|
]);
|
|
1750
2760
|
const handleKeyDown = useCallback((event) => {
|
|
1751
|
-
if (!onChange || readonly || !keyboard) return;
|
|
1752
2761
|
const key = event.key;
|
|
1753
|
-
if ([
|
|
2762
|
+
if (![
|
|
1754
2763
|
"ArrowRight",
|
|
1755
2764
|
"ArrowLeft",
|
|
1756
2765
|
"ArrowUp",
|
|
1757
2766
|
"ArrowDown"
|
|
1758
|
-
].includes(key))
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2767
|
+
].includes(key)) return;
|
|
2768
|
+
const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
|
|
2769
|
+
event.preventDefault();
|
|
2770
|
+
if (!onChange || inactive || !keyboard) return;
|
|
2771
|
+
let direction = 1;
|
|
2772
|
+
if (key === "ArrowLeft" || key === "ArrowUp") direction *= -1;
|
|
2773
|
+
if (reverse[i]) direction *= -1;
|
|
2774
|
+
onChange(nudge(i, direction, keyboard, event));
|
|
1766
2775
|
}, [
|
|
1767
2776
|
onChange,
|
|
1768
|
-
|
|
2777
|
+
inactive,
|
|
1769
2778
|
keyboard,
|
|
1770
2779
|
reverse,
|
|
1771
2780
|
nudge
|
|
@@ -1773,33 +2782,37 @@ const XYPad = {
|
|
|
1773
2782
|
const { refCallback: dragRefCallback } = useDragValue({
|
|
1774
2783
|
axis,
|
|
1775
2784
|
baseElementRef: areaRef,
|
|
2785
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1776
2786
|
updateOnPointerDown: true,
|
|
1777
|
-
cursor:
|
|
2787
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2788
|
+
shouldStart: () => !inactive,
|
|
1778
2789
|
onChange: (v) => {
|
|
1779
|
-
if (
|
|
2790
|
+
if (inactive) return;
|
|
1780
2791
|
onChange?.(v);
|
|
1781
2792
|
},
|
|
1782
2793
|
onDragStart: (v) => {
|
|
1783
|
-
if (
|
|
1784
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
2794
|
+
if (inactive) return;
|
|
1785
2795
|
thumbRef.current?.focus();
|
|
1786
2796
|
onDragStart?.(v);
|
|
1787
2797
|
},
|
|
1788
2798
|
onDragEnd: (v) => {
|
|
1789
|
-
if (
|
|
1790
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
2799
|
+
if (inactive) return;
|
|
1791
2800
|
onDragEnd?.(v);
|
|
1792
2801
|
}
|
|
1793
2802
|
});
|
|
1794
|
-
const
|
|
1795
|
-
if (!onChange ||
|
|
1796
|
-
const
|
|
2803
|
+
const wheelRefCallback = useWheel((event) => {
|
|
2804
|
+
if (!onChange || inactive || !wheel) return;
|
|
2805
|
+
const horizontal = event.deltaX !== 0;
|
|
2806
|
+
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
2807
|
+
if (delta === 0) return;
|
|
2808
|
+
const i = horizontal || event.shiftKey ? 0 : 1;
|
|
1797
2809
|
event.preventDefault();
|
|
1798
2810
|
let direction = 1;
|
|
1799
|
-
if (
|
|
2811
|
+
if (delta < 0) direction *= -1;
|
|
1800
2812
|
if (reverse[i]) direction *= -1;
|
|
1801
|
-
onChange(nudge(i, direction, wheel));
|
|
1802
|
-
}, WHEEL_OPTIONS)
|
|
2813
|
+
onChange(nudge(i, direction, wheel, event));
|
|
2814
|
+
}, WHEEL_OPTIONS);
|
|
2815
|
+
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
1803
2816
|
const context = useMemo(() => ({
|
|
1804
2817
|
value,
|
|
1805
2818
|
min,
|
|
@@ -1809,6 +2822,7 @@ const XYPad = {
|
|
|
1809
2822
|
reverse,
|
|
1810
2823
|
disabled,
|
|
1811
2824
|
readonly,
|
|
2825
|
+
onChange,
|
|
1812
2826
|
percent,
|
|
1813
2827
|
areaRef,
|
|
1814
2828
|
thumbRef
|
|
@@ -1821,27 +2835,29 @@ const XYPad = {
|
|
|
1821
2835
|
reverse,
|
|
1822
2836
|
disabled,
|
|
1823
2837
|
readonly,
|
|
2838
|
+
onChange,
|
|
1824
2839
|
percent
|
|
1825
2840
|
]);
|
|
1826
2841
|
useImperativeHandle(forwardedRef, () => {
|
|
1827
2842
|
return {
|
|
1828
2843
|
focus() {
|
|
1829
|
-
thumbRef.current?.focus();
|
|
2844
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1830
2845
|
},
|
|
1831
2846
|
blur() {
|
|
1832
2847
|
thumbRef.current?.blur();
|
|
1833
2848
|
},
|
|
1834
2849
|
original: rootRef
|
|
1835
2850
|
};
|
|
1836
|
-
}, []);
|
|
2851
|
+
}, [disabled]);
|
|
1837
2852
|
return /* @__PURE__ */ jsx(XYPadProvider, {
|
|
1838
2853
|
value: context,
|
|
1839
2854
|
children: /* @__PURE__ */ jsx("div", {
|
|
1840
|
-
className
|
|
2855
|
+
className,
|
|
1841
2856
|
ref: rootRefCallback,
|
|
2857
|
+
role: "group",
|
|
1842
2858
|
tabIndex: -1,
|
|
1843
|
-
"
|
|
1844
|
-
"
|
|
2859
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2860
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1845
2861
|
style,
|
|
1846
2862
|
onPointerDown,
|
|
1847
2863
|
onKeyDown: (event) => {
|
|
@@ -1849,7 +2865,7 @@ const XYPad = {
|
|
|
1849
2865
|
onKeyDown?.(event);
|
|
1850
2866
|
},
|
|
1851
2867
|
onFocus: (event) => {
|
|
1852
|
-
thumbRef.current?.focus();
|
|
2868
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1853
2869
|
onFocus?.(event);
|
|
1854
2870
|
},
|
|
1855
2871
|
onBlur: (event) => {
|
|
@@ -1868,67 +2884,153 @@ const XYPad = {
|
|
|
1868
2884
|
//#region src/hooks/useAnimationFrame.ts
|
|
1869
2885
|
function useAnimationFrame(callback = () => {}, deps = []) {
|
|
1870
2886
|
const reqIdRef = useRef(-1);
|
|
1871
|
-
const
|
|
1872
|
-
reqIdRef.current = requestAnimationFrame(loop);
|
|
1873
|
-
callback();
|
|
1874
|
-
}, [callback, ...deps]);
|
|
2887
|
+
const runCallback = useCallbackRef(callback);
|
|
1875
2888
|
useEffect(() => {
|
|
2889
|
+
const loop = () => {
|
|
2890
|
+
reqIdRef.current = requestAnimationFrame(loop);
|
|
2891
|
+
runCallback();
|
|
2892
|
+
};
|
|
1876
2893
|
reqIdRef.current = requestAnimationFrame(loop);
|
|
1877
2894
|
return () => cancelAnimationFrame(reqIdRef.current);
|
|
1878
|
-
}, [
|
|
2895
|
+
}, [runCallback, ...deps]);
|
|
1879
2896
|
}
|
|
1880
2897
|
//#endregion
|
|
1881
2898
|
//#region src/hooks/useMIDIAccess.ts
|
|
2899
|
+
const INITIAL_STATE = {
|
|
2900
|
+
midiAccess: null,
|
|
2901
|
+
error: null,
|
|
2902
|
+
inputs: []
|
|
2903
|
+
};
|
|
2904
|
+
function createMIDIAccessStore() {
|
|
2905
|
+
let instance = null;
|
|
2906
|
+
let unsubscribe = null;
|
|
2907
|
+
let state = INITIAL_STATE;
|
|
2908
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2909
|
+
function emit() {
|
|
2910
|
+
for (const listener of listeners) listener();
|
|
2911
|
+
}
|
|
2912
|
+
function connect() {
|
|
2913
|
+
const nextInstance = createMIDIAccess();
|
|
2914
|
+
instance = nextInstance;
|
|
2915
|
+
state = nextInstance.getState();
|
|
2916
|
+
unsubscribe = nextInstance.subscribe(() => {
|
|
2917
|
+
state = nextInstance.getState();
|
|
2918
|
+
emit();
|
|
2919
|
+
});
|
|
2920
|
+
emit();
|
|
2921
|
+
return nextInstance;
|
|
2922
|
+
}
|
|
2923
|
+
function disconnect(currentInstance) {
|
|
2924
|
+
if (instance !== currentInstance) return;
|
|
2925
|
+
unsubscribe?.();
|
|
2926
|
+
unsubscribe = null;
|
|
2927
|
+
instance = null;
|
|
2928
|
+
state = INITIAL_STATE;
|
|
2929
|
+
currentInstance.destroy();
|
|
2930
|
+
emit();
|
|
2931
|
+
}
|
|
2932
|
+
function request(options) {
|
|
2933
|
+
instance?.request(options);
|
|
2934
|
+
}
|
|
2935
|
+
return {
|
|
2936
|
+
connect,
|
|
2937
|
+
disconnect,
|
|
2938
|
+
request,
|
|
2939
|
+
getState: () => state,
|
|
2940
|
+
getServerState: () => INITIAL_STATE,
|
|
2941
|
+
subscribe: (listener) => {
|
|
2942
|
+
listeners.add(listener);
|
|
2943
|
+
return () => listeners.delete(listener);
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
1882
2947
|
/**
|
|
1883
2948
|
* Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
|
|
2949
|
+
*
|
|
2950
|
+
* `inputs` follows the devices: it changes as one is plugged in or unplugged,
|
|
2951
|
+
* so a list of devices needs no `statechange` listener of its own.
|
|
2952
|
+
*
|
|
2953
|
+
* `request` takes `{ sysex: true }` when the app needs system exclusive
|
|
2954
|
+
* messages. Browsers treat that as a separate, more sensitive permission, so
|
|
2955
|
+
* ask for it only when it is actually used.
|
|
1884
2956
|
*/
|
|
1885
2957
|
function useMIDIAccess(requestOnMount = true) {
|
|
1886
|
-
const
|
|
1887
|
-
const { midiAccess, error } = useSyncExternalStore(
|
|
2958
|
+
const store = useMemo(() => createMIDIAccessStore(), []);
|
|
2959
|
+
const { midiAccess, error, inputs } = useSyncExternalStore(store.subscribe, store.getState, store.getServerState);
|
|
1888
2960
|
useEffect(() => {
|
|
1889
|
-
|
|
1890
|
-
|
|
2961
|
+
const instance = store.connect();
|
|
2962
|
+
return () => store.disconnect(instance);
|
|
2963
|
+
}, [store]);
|
|
1891
2964
|
useEffect(() => {
|
|
1892
|
-
|
|
1893
|
-
}, [
|
|
2965
|
+
if (requestOnMount) store.request();
|
|
2966
|
+
}, [store, requestOnMount]);
|
|
1894
2967
|
return {
|
|
1895
|
-
request:
|
|
2968
|
+
request: store.request,
|
|
1896
2969
|
midiAccess,
|
|
1897
|
-
error
|
|
2970
|
+
error,
|
|
2971
|
+
inputs
|
|
1898
2972
|
};
|
|
1899
2973
|
}
|
|
1900
2974
|
//#endregion
|
|
1901
2975
|
//#region src/hooks/useMIDIInput.ts
|
|
1902
2976
|
/**
|
|
1903
|
-
*
|
|
2977
|
+
* Handle MIDI input events. To be used with {@link useMIDIAccess}.
|
|
2978
|
+
*
|
|
2979
|
+
* The handlers are read fresh on every event, so writing them inline is fine:
|
|
2980
|
+
* the listeners are attached once per `midiAccess` and stay attached. Devices
|
|
2981
|
+
* plugged in later are picked up without anything having to be rebuilt.
|
|
2982
|
+
*
|
|
2983
|
+
* @example
|
|
2984
|
+
* const { midiAccess } = useMIDIAccess()
|
|
2985
|
+
* useMIDIInput(midiAccess, {
|
|
2986
|
+
* onNoteOnEvent: (note, velocity) => play(note, velocity / 127),
|
|
2987
|
+
* onNoteOffEvent: (note) => stop(note),
|
|
2988
|
+
* onControlChangeEvent: (controller, value) => {
|
|
2989
|
+
* if (controller === 1) setModulation(value / 127)
|
|
2990
|
+
* },
|
|
2991
|
+
* })
|
|
1904
2992
|
*/
|
|
1905
|
-
function useMIDIInput(midiAccess,
|
|
2993
|
+
function useMIDIInput(midiAccess, handlers) {
|
|
2994
|
+
const latest = useRef(handlers);
|
|
2995
|
+
const instanceRef = useRef(null);
|
|
1906
2996
|
useEffect(() => {
|
|
1907
|
-
const instance = createMIDIInput(midiAccess,
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
}, [
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
]);
|
|
2997
|
+
const instance = createMIDIInput(midiAccess, latest.current);
|
|
2998
|
+
instanceRef.current = instance;
|
|
2999
|
+
return () => {
|
|
3000
|
+
instanceRef.current = null;
|
|
3001
|
+
instance.destroy();
|
|
3002
|
+
};
|
|
3003
|
+
}, [midiAccess]);
|
|
3004
|
+
useEffect(() => {
|
|
3005
|
+
latest.current = handlers;
|
|
3006
|
+
instanceRef.current?.update(handlers);
|
|
3007
|
+
});
|
|
1919
3008
|
}
|
|
1920
3009
|
//#endregion
|
|
1921
3010
|
//#region src/hooks/useMIDIMessage.ts
|
|
1922
3011
|
/**
|
|
1923
3012
|
* Hooks for when you want to process MIDI events in more detail than useMIDIInput.
|
|
3013
|
+
*
|
|
3014
|
+
* The handler is read fresh on every event, so writing it inline is fine: the
|
|
3015
|
+
* listeners are attached once per `midiAccess` and stay attached, and devices
|
|
3016
|
+
* plugged in later are picked up.
|
|
1924
3017
|
*/
|
|
1925
3018
|
function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
3019
|
+
const latest = useRef(onMIDIMessage);
|
|
3020
|
+
const instanceRef = useRef(null);
|
|
1926
3021
|
useEffect(() => {
|
|
1927
|
-
const instance = createMIDIMessage(midiAccess,
|
|
1928
|
-
|
|
1929
|
-
|
|
3022
|
+
const instance = createMIDIMessage(midiAccess, (event) => latest.current(event));
|
|
3023
|
+
instanceRef.current = instance;
|
|
3024
|
+
return () => {
|
|
3025
|
+
instanceRef.current = null;
|
|
3026
|
+
instance.destroy();
|
|
3027
|
+
};
|
|
3028
|
+
}, [midiAccess]);
|
|
3029
|
+
useEffect(() => {
|
|
3030
|
+
latest.current = onMIDIMessage;
|
|
3031
|
+
});
|
|
1930
3032
|
}
|
|
1931
3033
|
//#endregion
|
|
1932
|
-
export { AnimationCanvas, Knob, NOT_SUPPORTED, NumberInput, PERMISSION_DENIED, Piano, PointsEditor, SHORTCUTS, Slider, XYPad, clampPoint, useAnimationFrame, useDrag, useDragValue, useEventListener, useInterval, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel };
|
|
3034
|
+
export { AnimationCanvas, DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, DropZone, FileInput, Knob, NOT_SUPPORTED, NumberInput, PERMISSION_DENIED, PITCH_BEND_CENTER, Piano, PointsEditor, SHORTCUTS, Slider, UNAVAILABLE, XYPad, clampPoint, useAnimationFrame, useDrag, useDragValue, useDropZone, useEventListener, useFileInputContext, useInterval, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
|
|
1933
3035
|
|
|
1934
3036
|
//# sourceMappingURL=index.js.map
|