@tremolo-ui/react 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +346 -1015
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -221
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +76 -221
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +333 -978
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +8 -13
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +19 -16
- package/src/components/NumberInput/InputField.tsx +12 -27
- package/src/components/NumberInput/Stepper.tsx +49 -97
- package/src/components/NumberInput/StepperButton.tsx +3 -4
- package/src/components/NumberInput/context.tsx +2 -1
- package/src/components/NumberInput/index.tsx +40 -71
- package/src/components/Piano/index.tsx +35 -142
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +38 -85
- package/src/components/PointsEditor/context.tsx +5 -56
- package/src/components/PointsEditor/index.tsx +29 -311
- package/src/components/Slider/Marks.tsx +3 -5
- package/src/components/Slider/MarksOption.tsx +7 -18
- package/src/components/Slider/Track.tsx +2 -1
- package/src/components/Slider/index.tsx +29 -28
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +3 -1
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +33 -46
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/index.ts +2 -27
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -46
- package/src/components/_util/css-length.ts +0 -13
- package/src/components/_util/decimal-digits.ts +0 -23
- package/src/components/_util/visually-hidden.ts +0 -25
- package/src/input-event.ts +0 -32
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useComposedRefs } from "./compose-refs.js";
|
|
2
2
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, KNOB_VIEWBOX_SIZE, POINTS_EDITOR_DEFAULT_KEYBOARD, POINTS_EDITOR_DEFAULT_WHEEL, POINT_AXIS, applyDelta, arrowKeyDirection, arrowKeyMove, blackKeyWidth, caretAtDecimalOffset, caretDecimalOffset, checkSteps, clampPoint, commitNumberInputText, createAnimationCanvas, createDrag, createDragValue, createDropZone, createLongPress, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createPointsEditor, createStepperDrag, createWheel, cssLength, elementMapping, fitWhiteKeyWidth, getNoteRangeArray, knobAngles, knobArcPath, knobArcRadius, notePosition, nudgeNumberInput, numberInputBounds, numberInputRanges, numberSpan, parseNumberText, partitionByAccept, pianoWidth, relativeMapping, selectModifier, sliderMarks, toXY, valuePercent, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { clamp, isWhiteKey, linearScale, noteKey
|
|
5
|
+
import { clamp, isWhiteKey, linearScale, noteKey } from "@tremolo-ui/functions";
|
|
6
6
|
//#region src/components/AnimationCanvas/index.tsx
|
|
7
7
|
/**
|
|
8
8
|
* A simple animatable canvas with requestAnimationFrame()
|
|
@@ -168,31 +168,6 @@ const DropZone = { Root: /* @__PURE__ */ forwardRef(({ accept, multiple = false,
|
|
|
168
168
|
});
|
|
169
169
|
}) };
|
|
170
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
171
|
//#region src/components/FileInput/context.tsx
|
|
197
172
|
const FileInputContext = /* @__PURE__ */ createContext(null);
|
|
198
173
|
const FileInputProvider = FileInputContext.Provider;
|
|
@@ -212,14 +187,10 @@ const FileInput = {
|
|
|
212
187
|
disabled
|
|
213
188
|
}), [inputId, disabled]);
|
|
214
189
|
function handleChange(event) {
|
|
215
|
-
const
|
|
190
|
+
const { accepted, rejected } = partitionByAccept(Array.from(event.target.files ?? []), accept);
|
|
216
191
|
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
192
|
if (accepted.length > 0) onChange?.(accepted);
|
|
193
|
+
if (rejected.length > 0) onReject?.(rejected);
|
|
223
194
|
}
|
|
224
195
|
return /* @__PURE__ */ jsx(FileInputProvider, {
|
|
225
196
|
value: context,
|
|
@@ -255,113 +226,23 @@ const FileInput = {
|
|
|
255
226
|
};
|
|
256
227
|
//#endregion
|
|
257
228
|
//#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
229
|
/**
|
|
332
230
|
* 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.
|
|
231
|
+
* a change the user can see. The probing is `checkSteps` in the core.
|
|
346
232
|
*
|
|
347
233
|
* @internal
|
|
348
234
|
*/
|
|
349
|
-
function useCheckSteps(
|
|
235
|
+
function useCheckSteps(options) {
|
|
236
|
+
const { component, axis, range, keyboard, wheel } = options;
|
|
350
237
|
const { min, max, step, scale } = range ?? {};
|
|
351
|
-
const where = axis ? `${component} (${axis})` : component;
|
|
352
238
|
useEffect(() => {
|
|
353
239
|
try {
|
|
354
240
|
if (process.env.NODE_ENV === "production") return;
|
|
355
|
-
|
|
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
|
-
}
|
|
241
|
+
for (const warning of checkSteps(options)) console.warn(warning);
|
|
362
242
|
} catch {}
|
|
363
243
|
}, [
|
|
364
|
-
|
|
244
|
+
component,
|
|
245
|
+
axis,
|
|
365
246
|
min,
|
|
366
247
|
max,
|
|
367
248
|
step,
|
|
@@ -469,49 +350,6 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
|
469
350
|
return setNode;
|
|
470
351
|
}
|
|
471
352
|
//#endregion
|
|
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
|
-
};
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/components/_util/css-length.ts
|
|
503
|
-
/**
|
|
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.
|
|
510
|
-
*/
|
|
511
|
-
function cssLength(value) {
|
|
512
|
-
return typeof value === "number" ? `${value}px` : value;
|
|
513
|
-
}
|
|
514
|
-
//#endregion
|
|
515
353
|
//#region src/components/_util/Placement.tsx
|
|
516
354
|
/**
|
|
517
355
|
* The subcomponent that established the current placement, if any.
|
|
@@ -553,49 +391,8 @@ function useCheckPlacement(child, parent) {
|
|
|
553
391
|
parent
|
|
554
392
|
]);
|
|
555
393
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
*
|
|
559
|
-
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
560
|
-
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
561
|
-
*/
|
|
562
|
-
function pointOnArc(angle, radius) {
|
|
563
|
-
return {
|
|
564
|
-
x: 50 + radius * Math.cos(radian(angle - 90)),
|
|
565
|
-
y: 50 + radius * Math.sin(radian(angle - 90))
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
569
|
-
function arcRadius(strokeWidth) {
|
|
570
|
-
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
571
|
-
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
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
|
-
}
|
|
586
|
-
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
587
|
-
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
588
|
-
const p = scale.normalize(value, min, max);
|
|
589
|
-
const s = scale.normalize(startValue, min, max);
|
|
590
|
-
const r1 = -angleRange / 2;
|
|
591
|
-
return {
|
|
592
|
-
p,
|
|
593
|
-
r1,
|
|
594
|
-
r2: r1 + Math.min(p, s) * angleRange,
|
|
595
|
-
r3: r1 + Math.max(p, s) * angleRange,
|
|
596
|
-
r4: angleRange / 2
|
|
597
|
-
};
|
|
598
|
-
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/components/Knob/context.tsx
|
|
599
396
|
const KnobContext = /* @__PURE__ */ createContext(null);
|
|
600
397
|
const KnobProvider = KnobContext.Provider;
|
|
601
398
|
function useKnobContext(selector) {
|
|
@@ -609,10 +406,10 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
609
406
|
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
610
407
|
const r2 = useKnobContext((s) => s.r2);
|
|
611
408
|
const r3 = useKnobContext((s) => s.r3);
|
|
612
|
-
const radius =
|
|
409
|
+
const radius = knobArcRadius(strokeWidth);
|
|
613
410
|
return /* @__PURE__ */ jsx("path", {
|
|
614
411
|
className,
|
|
615
|
-
d:
|
|
412
|
+
d: knobArcPath(r2, r3, radius),
|
|
616
413
|
fill: "none",
|
|
617
414
|
stroke,
|
|
618
415
|
strokeWidth,
|
|
@@ -630,17 +427,17 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
630
427
|
const r2 = useKnobContext((s) => s.r2);
|
|
631
428
|
const r3 = useKnobContext((s) => s.r3);
|
|
632
429
|
const r4 = useKnobContext((s) => s.r4);
|
|
633
|
-
const radius =
|
|
430
|
+
const radius = knobArcRadius(strokeWidth);
|
|
634
431
|
return /* @__PURE__ */ jsxs(Fragment, { children: [startValue > min && /* @__PURE__ */ jsx("path", {
|
|
635
432
|
className,
|
|
636
|
-
d:
|
|
433
|
+
d: knobArcPath(r1, r2, radius),
|
|
637
434
|
fill: "none",
|
|
638
435
|
stroke,
|
|
639
436
|
strokeWidth,
|
|
640
437
|
...props
|
|
641
438
|
}), startValue < max && /* @__PURE__ */ jsx("path", {
|
|
642
439
|
className,
|
|
643
|
-
d:
|
|
440
|
+
d: knobArcPath(r3, r4, radius),
|
|
644
441
|
fill: "none",
|
|
645
442
|
stroke,
|
|
646
443
|
strokeWidth,
|
|
@@ -651,7 +448,7 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
651
448
|
//#region src/components/Knob/SVGRoot.tsx
|
|
652
449
|
function SVGRoot({ children, style, ...props }) {
|
|
653
450
|
return /* @__PURE__ */ jsx("svg", {
|
|
654
|
-
viewBox: `0 0
|
|
451
|
+
viewBox: `0 0 ${KNOB_VIEWBOX_SIZE} ${KNOB_VIEWBOX_SIZE}`,
|
|
655
452
|
style: {
|
|
656
453
|
display: "block",
|
|
657
454
|
...style
|
|
@@ -681,7 +478,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
681
478
|
}), /* @__PURE__ */ jsx("line", {
|
|
682
479
|
className: classes?.thumbLine,
|
|
683
480
|
x1: "50%",
|
|
684
|
-
y1: `${(
|
|
481
|
+
y1: `${(KNOB_VIEWBOX_SIZE - clamp(thumbSize, 0, 100)) / 2}%`,
|
|
685
482
|
x2: "50%",
|
|
686
483
|
y2: `${thumbLineLength}%`,
|
|
687
484
|
stroke: thumbLine,
|
|
@@ -731,16 +528,10 @@ const Knob = {
|
|
|
731
528
|
});
|
|
732
529
|
const handleKeyDown = useCallback((event) => {
|
|
733
530
|
if (!keyboard || !onChange || inactive) return;
|
|
734
|
-
const
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
"ArrowUp",
|
|
739
|
-
"ArrowDown"
|
|
740
|
-
].includes(key)) {
|
|
741
|
-
event.preventDefault();
|
|
742
|
-
onChange(applyDelta(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
743
|
-
}
|
|
531
|
+
const direction = arrowKeyDirection(event.key);
|
|
532
|
+
if (direction === null) return;
|
|
533
|
+
event.preventDefault();
|
|
534
|
+
onChange(applyDelta(value, direction, keyboard, range, event));
|
|
744
535
|
}, [
|
|
745
536
|
keyboard,
|
|
746
537
|
onChange,
|
|
@@ -766,9 +557,11 @@ const Knob = {
|
|
|
766
557
|
});
|
|
767
558
|
const wheelRefCallback = useWheel((event) => {
|
|
768
559
|
if (!wheel || inactive) return;
|
|
560
|
+
const direction = wheelDirection(event);
|
|
561
|
+
if (direction === null) return;
|
|
769
562
|
event.preventDefault();
|
|
770
|
-
if (!onChange
|
|
771
|
-
onChange(applyDelta(value,
|
|
563
|
+
if (!onChange) return;
|
|
564
|
+
onChange(applyDelta(value, direction, wheel, range, event));
|
|
772
565
|
}, WHEEL_OPTIONS$2);
|
|
773
566
|
const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
|
|
774
567
|
const context = useMemo(() => {
|
|
@@ -783,7 +576,7 @@ const Knob = {
|
|
|
783
576
|
};
|
|
784
577
|
return {
|
|
785
578
|
...config,
|
|
786
|
-
...
|
|
579
|
+
...knobAngles(config)
|
|
787
580
|
};
|
|
788
581
|
}, [
|
|
789
582
|
value,
|
|
@@ -857,82 +650,36 @@ function useStepperContext() {
|
|
|
857
650
|
return useContext(StepperContext);
|
|
858
651
|
}
|
|
859
652
|
//#endregion
|
|
860
|
-
//#region src/hooks/
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
cleanupRef.current = cleanup;
|
|
872
|
-
return () => {
|
|
873
|
-
cleanup();
|
|
874
|
-
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
875
|
-
};
|
|
876
|
-
}, [
|
|
877
|
-
event,
|
|
878
|
-
target,
|
|
879
|
-
options,
|
|
880
|
-
listener
|
|
881
|
-
]);
|
|
882
|
-
return useCallback(() => cleanupRef.current(), []);
|
|
883
|
-
}
|
|
884
|
-
//#endregion
|
|
885
|
-
//#region src/hooks/useInterval.ts
|
|
886
|
-
function useInterval(callback, delay) {
|
|
887
|
-
const savedCallback = useRef(callback);
|
|
653
|
+
//#region src/hooks/useLongPress.ts
|
|
654
|
+
/**
|
|
655
|
+
* Repeat `callback` while a pointer is held down: once on the press, then
|
|
656
|
+
* every `interval` after `initialDelay`. The repeat itself is
|
|
657
|
+
* `createLongPress` in `@tremolo-ui/dom`.
|
|
658
|
+
*
|
|
659
|
+
* Returns the function that starts a press, for an `onPointerDown`.
|
|
660
|
+
*/
|
|
661
|
+
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
662
|
+
const onPress = useCallbackRef(callback);
|
|
663
|
+
const instanceRef = useRef(null);
|
|
888
664
|
useEffect(() => {
|
|
889
|
-
|
|
890
|
-
|
|
665
|
+
instanceRef.current?.update({
|
|
666
|
+
delay: initialDelay,
|
|
667
|
+
interval
|
|
668
|
+
});
|
|
669
|
+
});
|
|
891
670
|
useEffect(() => {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
671
|
+
const instance = createLongPress({
|
|
672
|
+
onPress: () => onPress(),
|
|
673
|
+
delay: initialDelay,
|
|
674
|
+
interval
|
|
675
|
+
});
|
|
676
|
+
instanceRef.current = instance;
|
|
896
677
|
return () => {
|
|
897
|
-
|
|
678
|
+
instanceRef.current = null;
|
|
679
|
+
instance.destroy();
|
|
898
680
|
};
|
|
899
|
-
}, [
|
|
900
|
-
|
|
901
|
-
//#endregion
|
|
902
|
-
//#region src/hooks/useLongPress.ts
|
|
903
|
-
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
904
|
-
const [pressed, setPressed] = useState(false);
|
|
905
|
-
const [delay, setDelay] = useState(initialDelay);
|
|
906
|
-
const activePointerId = useRef(null);
|
|
907
|
-
const isPressed = useRef(false);
|
|
908
|
-
useInterval(() => {
|
|
909
|
-
callback();
|
|
910
|
-
setDelay(interval);
|
|
911
|
-
}, pressed ? delay : null);
|
|
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;
|
|
917
|
-
setPressed(false);
|
|
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();
|
|
928
|
-
});
|
|
929
|
-
return useCallback((event) => {
|
|
930
|
-
if (isPressed.current || event && event.button !== 0) return;
|
|
931
|
-
isPressed.current = true;
|
|
932
|
-
activePointerId.current = event?.pointerId ?? null;
|
|
933
|
-
callback();
|
|
934
|
-
setPressed(true);
|
|
935
|
-
}, [callback]);
|
|
681
|
+
}, []);
|
|
682
|
+
return useCallback((event) => instanceRef.current?.start(event), []);
|
|
936
683
|
}
|
|
937
684
|
//#endregion
|
|
938
685
|
//#region src/components/NumberInput/StepperButton.tsx
|
|
@@ -944,7 +691,7 @@ function StepperButton({ direction, variant, icon, className, style, children, o
|
|
|
944
691
|
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
945
692
|
const stepper = useStepperContext();
|
|
946
693
|
const press = useLongPress(() => {
|
|
947
|
-
if (stepper?.
|
|
694
|
+
if (stepper?.moved()) return;
|
|
948
695
|
nudge(direction, ["raw", step]);
|
|
949
696
|
});
|
|
950
697
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -1003,21 +750,6 @@ function IncrementStepper(props) {
|
|
|
1003
750
|
}
|
|
1004
751
|
//#endregion
|
|
1005
752
|
//#region src/components/NumberInput/InputField.tsx
|
|
1006
|
-
/** The leading number of the displayed text, whatever the format put around it. */
|
|
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
|
-
};
|
|
1021
753
|
/**
|
|
1022
754
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
1023
755
|
*
|
|
@@ -1027,7 +759,7 @@ const decimalAnchor = (text) => {
|
|
|
1027
759
|
* How it behaves on focus, Enter and the arrow keys is set on `Root`
|
|
1028
760
|
* (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
|
|
1029
761
|
*/
|
|
1030
|
-
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
762
|
+
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
1031
763
|
const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
1032
764
|
const [focused, setFocused] = useState(false);
|
|
1033
765
|
const shown = unformatOnFocus && focused && !editing ? String(value) : text;
|
|
@@ -1036,7 +768,10 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
1036
768
|
const input = inputRef.current;
|
|
1037
769
|
if (!input) return;
|
|
1038
770
|
if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
|
|
1039
|
-
else
|
|
771
|
+
else {
|
|
772
|
+
const span = numberSpan(input.value);
|
|
773
|
+
if (span) input.setSelectionRange(span.start, span.end);
|
|
774
|
+
}
|
|
1040
775
|
}, [
|
|
1041
776
|
focused,
|
|
1042
777
|
selectOnFocus,
|
|
@@ -1053,8 +788,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
1053
788
|
caretAfterStep.current = null;
|
|
1054
789
|
const input = inputRef.current;
|
|
1055
790
|
if (!input || input.value === pending.from) return;
|
|
1056
|
-
const
|
|
1057
|
-
const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
|
|
791
|
+
const caret = caretAtDecimalOffset(input.value, pending.offset);
|
|
1058
792
|
input.setSelectionRange(caret, caret);
|
|
1059
793
|
});
|
|
1060
794
|
const composedRef = useComposedRefs(forwardedRef, inputRef);
|
|
@@ -1081,6 +815,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
1081
815
|
onChange: (event) => {
|
|
1082
816
|
caretAfterStep.current = null;
|
|
1083
817
|
setDraft(event.currentTarget.value);
|
|
818
|
+
onChange?.(event);
|
|
1084
819
|
},
|
|
1085
820
|
onFocus: (event) => {
|
|
1086
821
|
setFocused(true);
|
|
@@ -1100,7 +835,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
1100
835
|
event.preventDefault();
|
|
1101
836
|
const input = event.currentTarget;
|
|
1102
837
|
if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
|
|
1103
|
-
offset: input.
|
|
838
|
+
offset: caretDecimalOffset(input.value, input.selectionStart),
|
|
1104
839
|
from: input.value
|
|
1105
840
|
};
|
|
1106
841
|
nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
|
|
@@ -1111,117 +846,54 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
1111
846
|
});
|
|
1112
847
|
});
|
|
1113
848
|
//#endregion
|
|
1114
|
-
//#region src/hooks/useDrag.ts
|
|
1115
|
-
/**
|
|
1116
|
-
* Track a pointer drag on an element.
|
|
1117
|
-
*
|
|
1118
|
-
* @returns a ref callback to attach to the element being dragged
|
|
1119
|
-
*/
|
|
1120
|
-
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
1121
|
-
const dragHandler = useCallbackRef(onDrag);
|
|
1122
|
-
const dragStartHandler = useCallbackRef(onDragStart);
|
|
1123
|
-
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
1124
|
-
const [node, setNode] = useState(null);
|
|
1125
|
-
useEffect(() => {
|
|
1126
|
-
if (!node) return;
|
|
1127
|
-
const instance = createDrag(node, {
|
|
1128
|
-
threshold,
|
|
1129
|
-
cursor,
|
|
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)
|
|
1134
|
-
});
|
|
1135
|
-
return () => instance.destroy();
|
|
1136
|
-
}, [
|
|
1137
|
-
node,
|
|
1138
|
-
threshold,
|
|
1139
|
-
cursor,
|
|
1140
|
-
pointerLock,
|
|
1141
|
-
dragHandler,
|
|
1142
|
-
dragStartHandler,
|
|
1143
|
-
dragEndHandler
|
|
1144
|
-
]);
|
|
1145
|
-
return setNode;
|
|
1146
|
-
}
|
|
1147
|
-
//#endregion
|
|
1148
849
|
//#region src/components/NumberInput/Stepper.tsx
|
|
1149
850
|
/**
|
|
1150
851
|
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
1151
|
-
* up and down moves the value one `step` every `drag` pixels.
|
|
852
|
+
* up and down moves the value one `step` every `drag` pixels. The counting is
|
|
853
|
+
* `createStepperDrag` in the core.
|
|
1152
854
|
*
|
|
1153
855
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
1154
856
|
* off text selection on whatever element it is attached to.
|
|
1155
857
|
*/
|
|
1156
858
|
const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
1157
|
-
const { value,
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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);
|
|
1186
|
-
const dragRefCallback = useDrag({
|
|
1187
|
-
threshold: 1,
|
|
1188
|
-
cursor: inactive ? void 0 : "ns-resize",
|
|
1189
|
-
pointerLock: inactive ? false : pointerLock,
|
|
1190
|
-
onDragStart: (state) => {
|
|
1191
|
-
originRef.current = null;
|
|
1192
|
-
draggingRef.current = false;
|
|
1193
|
-
factorRef.current = selectModifier(dragSensitivity, state.event).value;
|
|
1194
|
-
},
|
|
1195
|
-
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1196
|
-
if (inactive || drag === null) return;
|
|
1197
|
-
if (!originRef.current) {
|
|
1198
|
-
originRef.current = {
|
|
1199
|
-
y,
|
|
1200
|
-
value
|
|
1201
|
-
};
|
|
1202
|
-
previousYRef.current = y;
|
|
1203
|
-
return;
|
|
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;
|
|
1214
|
-
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
1215
|
-
if (steps === 0) return;
|
|
1216
|
-
draggingRef.current = true;
|
|
1217
|
-
changeValue(applyDelta(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
1218
|
-
},
|
|
1219
|
-
onDragEnd: () => {
|
|
1220
|
-
draggingRef.current = false;
|
|
1221
|
-
}
|
|
859
|
+
const { value, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
|
|
860
|
+
const enabled = drag !== null && !disabled && !readonly;
|
|
861
|
+
const [node, setNode] = useState(null);
|
|
862
|
+
const instanceRef = useRef(null);
|
|
863
|
+
const latest = useRef({
|
|
864
|
+
value,
|
|
865
|
+
changeValue
|
|
866
|
+
});
|
|
867
|
+
const options = {
|
|
868
|
+
range: rawRange,
|
|
869
|
+
pixels: drag ?? 1,
|
|
870
|
+
sensitivity: dragSensitivity,
|
|
871
|
+
pointerLock
|
|
872
|
+
};
|
|
873
|
+
const optionsRef = useRef(options);
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
latest.current = {
|
|
876
|
+
value,
|
|
877
|
+
changeValue
|
|
878
|
+
};
|
|
879
|
+
optionsRef.current = options;
|
|
880
|
+
instanceRef.current?.update(options);
|
|
1222
881
|
});
|
|
1223
|
-
|
|
1224
|
-
|
|
882
|
+
useEffect(() => {
|
|
883
|
+
if (!node || !enabled) return;
|
|
884
|
+
const instance = createStepperDrag(node, {
|
|
885
|
+
...optionsRef.current,
|
|
886
|
+
getValue: () => latest.current.value,
|
|
887
|
+
onChange: (next) => latest.current.changeValue(next)
|
|
888
|
+
});
|
|
889
|
+
instanceRef.current = instance;
|
|
890
|
+
return () => {
|
|
891
|
+
instanceRef.current = null;
|
|
892
|
+
instance.destroy();
|
|
893
|
+
};
|
|
894
|
+
}, [node, enabled]);
|
|
895
|
+
const composedRef = useComposedRefs(forwardedRef, setNode);
|
|
896
|
+
const context = useMemo(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
|
|
1225
897
|
return /* @__PURE__ */ jsx(StepperProvider, {
|
|
1226
898
|
value: context,
|
|
1227
899
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -1235,17 +907,7 @@ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style,
|
|
|
1235
907
|
});
|
|
1236
908
|
//#endregion
|
|
1237
909
|
//#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
910
|
const defaultFormat = (value) => String(value);
|
|
1245
|
-
const defaultParse = (text) => {
|
|
1246
|
-
const match = text.match(LEADING_NUMBER);
|
|
1247
|
-
return match ? Number(match[1]) : NaN;
|
|
1248
|
-
};
|
|
1249
911
|
/**
|
|
1250
912
|
* Input with some useful functions for entering numerical values.
|
|
1251
913
|
*/
|
|
@@ -1259,24 +921,13 @@ const NumberInput = {
|
|
|
1259
921
|
const [draft, setDraft] = useState(null);
|
|
1260
922
|
const inactive = disabled || readonly;
|
|
1261
923
|
const format = formatProp ?? defaultFormat;
|
|
1262
|
-
const parse = parseProp ??
|
|
1263
|
-
const range = useMemo(() => ({
|
|
1264
|
-
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
1265
|
-
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
1266
|
-
step,
|
|
1267
|
-
scale
|
|
1268
|
-
}), [
|
|
1269
|
-
clampValue,
|
|
924
|
+
const parse = parseProp ?? parseNumberText;
|
|
925
|
+
const { normalized: range, raw: rawRange } = useMemo(() => numberInputRanges({
|
|
1270
926
|
min,
|
|
1271
927
|
max,
|
|
1272
928
|
step,
|
|
1273
|
-
scale
|
|
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
|
|
929
|
+
scale,
|
|
930
|
+
clampValue
|
|
1280
931
|
}), [
|
|
1281
932
|
clampValue,
|
|
1282
933
|
min,
|
|
@@ -1284,6 +935,10 @@ const NumberInput = {
|
|
|
1284
935
|
step,
|
|
1285
936
|
scale
|
|
1286
937
|
]);
|
|
938
|
+
const ranges = useMemo(() => ({
|
|
939
|
+
normalized: range,
|
|
940
|
+
raw: rawRange
|
|
941
|
+
}), [range, rawRange]);
|
|
1287
942
|
useCheckSteps({
|
|
1288
943
|
component: "NumberInput",
|
|
1289
944
|
range: useMemo(() => min !== void 0 && max !== void 0 && min < max ? {
|
|
@@ -1303,7 +958,13 @@ const NumberInput = {
|
|
|
1303
958
|
});
|
|
1304
959
|
const text = draft ?? format(value);
|
|
1305
960
|
const editing = draft !== null;
|
|
1306
|
-
const
|
|
961
|
+
const bounds = numberInputBounds(value, {
|
|
962
|
+
min,
|
|
963
|
+
max,
|
|
964
|
+
clampValue
|
|
965
|
+
});
|
|
966
|
+
const { atMin, atMax } = bounds;
|
|
967
|
+
const outOfRange = !editing && bounds.outOfRange;
|
|
1307
968
|
const handleDraft = useCallback((next) => {
|
|
1308
969
|
if (inactive) return;
|
|
1309
970
|
setDraft(next);
|
|
@@ -1325,15 +986,13 @@ const NumberInput = {
|
|
|
1325
986
|
]);
|
|
1326
987
|
const commitDraft = useCallback(() => {
|
|
1327
988
|
if (draft === null || inactive) return;
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1336
|
-
changeValue(committed);
|
|
989
|
+
const committed = commitNumberInputText(draft, parse, {
|
|
990
|
+
min,
|
|
991
|
+
max,
|
|
992
|
+
clampValue
|
|
993
|
+
});
|
|
994
|
+
if (committed === null) setDraft(null);
|
|
995
|
+
else changeValue(committed);
|
|
1337
996
|
}, [
|
|
1338
997
|
draft,
|
|
1339
998
|
inactive,
|
|
@@ -1344,18 +1003,17 @@ const NumberInput = {
|
|
|
1344
1003
|
changeValue
|
|
1345
1004
|
]);
|
|
1346
1005
|
const nudge = useCallback((direction, option, modifiers) => {
|
|
1347
|
-
|
|
1348
|
-
changeValue(applyDelta(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
|
|
1006
|
+
changeValue(nudgeNumberInput(value, direction, option, ranges, modifiers));
|
|
1349
1007
|
}, [
|
|
1350
1008
|
changeValue,
|
|
1351
1009
|
value,
|
|
1352
|
-
|
|
1353
|
-
range
|
|
1010
|
+
ranges
|
|
1354
1011
|
]);
|
|
1355
1012
|
const wheelRefCallback = useWheel((event) => {
|
|
1356
|
-
|
|
1013
|
+
const direction = wheelDirection(event);
|
|
1014
|
+
if (!wheel || inactive || direction === null) return;
|
|
1357
1015
|
event.preventDefault();
|
|
1358
|
-
nudge(
|
|
1016
|
+
nudge(direction, wheel, event);
|
|
1359
1017
|
}, { requireFocus: true });
|
|
1360
1018
|
const context = useMemo(() => ({
|
|
1361
1019
|
value,
|
|
@@ -1378,8 +1036,8 @@ const NumberInput = {
|
|
|
1378
1036
|
unformatOnFocus,
|
|
1379
1037
|
keepCaretOnStep,
|
|
1380
1038
|
blurOnEnter,
|
|
1381
|
-
atMin
|
|
1382
|
-
atMax
|
|
1039
|
+
atMin,
|
|
1040
|
+
atMax,
|
|
1383
1041
|
drag,
|
|
1384
1042
|
format,
|
|
1385
1043
|
parse,
|
|
@@ -1403,6 +1061,8 @@ const NumberInput = {
|
|
|
1403
1061
|
text,
|
|
1404
1062
|
editing,
|
|
1405
1063
|
outOfRange,
|
|
1064
|
+
atMin,
|
|
1065
|
+
atMax,
|
|
1406
1066
|
drag,
|
|
1407
1067
|
dragSensitivity,
|
|
1408
1068
|
pointerLock,
|
|
@@ -1445,78 +1105,15 @@ const NumberInput = {
|
|
|
1445
1105
|
IncrementStepper,
|
|
1446
1106
|
DecrementStepper
|
|
1447
1107
|
};
|
|
1448
|
-
//#endregion
|
|
1449
|
-
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
1450
|
-
/**
|
|
1451
|
-
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
1452
|
-
*/
|
|
1453
|
-
const SHORTCUTS = {
|
|
1454
|
-
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
1455
|
-
HOME_ROW: { keys: [
|
|
1456
|
-
"a",
|
|
1457
|
-
"w",
|
|
1458
|
-
"s",
|
|
1459
|
-
"e",
|
|
1460
|
-
"d",
|
|
1461
|
-
"f",
|
|
1462
|
-
"t",
|
|
1463
|
-
"g",
|
|
1464
|
-
"y",
|
|
1465
|
-
"h",
|
|
1466
|
-
"u",
|
|
1467
|
-
"j",
|
|
1468
|
-
"k",
|
|
1469
|
-
"o",
|
|
1470
|
-
"l",
|
|
1471
|
-
"p",
|
|
1472
|
-
";"
|
|
1473
|
-
] },
|
|
1474
|
-
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
1475
|
-
HOME_ROW_NATURAL: { keys: [
|
|
1476
|
-
"a",
|
|
1477
|
-
"",
|
|
1478
|
-
"s",
|
|
1479
|
-
"",
|
|
1480
|
-
"d",
|
|
1481
|
-
"f",
|
|
1482
|
-
"",
|
|
1483
|
-
"g",
|
|
1484
|
-
"",
|
|
1485
|
-
"h",
|
|
1486
|
-
"",
|
|
1487
|
-
"j",
|
|
1488
|
-
"k",
|
|
1489
|
-
"",
|
|
1490
|
-
"l",
|
|
1491
|
-
"",
|
|
1492
|
-
";"
|
|
1493
|
-
] }
|
|
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
|
-
}
|
|
1510
1108
|
/**
|
|
1511
1109
|
* Customizable piano component.
|
|
1512
1110
|
*/
|
|
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,
|
|
1111
|
+
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, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
1514
1112
|
const [node, setNode] = useState(null);
|
|
1515
1113
|
const [activeNotes, setActiveNotes] = useState([]);
|
|
1516
1114
|
/** Set while `resizable` is on, measured from the parent. */
|
|
1517
1115
|
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth);
|
|
1518
1116
|
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange]);
|
|
1519
|
-
const whiteKeyCount = useMemo(() => notes.filter(isWhiteKey).length, [notes]);
|
|
1520
1117
|
const layout = useMemo(() => ({
|
|
1521
1118
|
noteRange,
|
|
1522
1119
|
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
@@ -1536,21 +1133,20 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1536
1133
|
layout,
|
|
1537
1134
|
glissando,
|
|
1538
1135
|
midiMax,
|
|
1136
|
+
keyboardShortcuts,
|
|
1137
|
+
keyboardShortcutsScope,
|
|
1539
1138
|
onPlayNote,
|
|
1540
1139
|
onStopNote
|
|
1541
1140
|
});
|
|
1542
1141
|
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
|
-
}, []);
|
|
1548
1142
|
useEffect(() => {
|
|
1549
1143
|
if (!node) return;
|
|
1550
1144
|
const instance = createPianoInput(node, {
|
|
1551
1145
|
layout: latest.current.layout,
|
|
1552
1146
|
glissando: latest.current.glissando,
|
|
1553
1147
|
midiMax: latest.current.midiMax,
|
|
1148
|
+
keyboardShortcuts: latest.current.keyboardShortcuts,
|
|
1149
|
+
keyboardShortcutsScope: latest.current.keyboardShortcutsScope,
|
|
1554
1150
|
onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
|
|
1555
1151
|
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
1556
1152
|
onActiveNotesChange: setActiveNotes
|
|
@@ -1566,86 +1162,44 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1566
1162
|
layout,
|
|
1567
1163
|
glissando,
|
|
1568
1164
|
midiMax,
|
|
1165
|
+
keyboardShortcuts,
|
|
1166
|
+
keyboardShortcutsScope,
|
|
1569
1167
|
onPlayNote,
|
|
1570
1168
|
onStopNote
|
|
1571
1169
|
};
|
|
1572
1170
|
instanceRef.current?.update({
|
|
1573
1171
|
layout,
|
|
1574
1172
|
glissando,
|
|
1575
|
-
midiMax
|
|
1173
|
+
midiMax,
|
|
1174
|
+
keyboardShortcuts,
|
|
1175
|
+
keyboardShortcutsScope
|
|
1576
1176
|
});
|
|
1577
1177
|
});
|
|
1178
|
+
const { first, last } = noteRange;
|
|
1578
1179
|
useEffect(() => {
|
|
1579
1180
|
if (!resizable || !node) return;
|
|
1580
1181
|
const parent = node.parentElement;
|
|
1581
1182
|
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1582
1183
|
const resizeObserver = new ResizeObserver(() => {
|
|
1583
|
-
setResizedKeyWidth(node.clientWidth
|
|
1184
|
+
setResizedKeyWidth(fitWhiteKeyWidth(node.clientWidth, {
|
|
1185
|
+
noteRange: {
|
|
1186
|
+
first,
|
|
1187
|
+
last
|
|
1188
|
+
},
|
|
1189
|
+
keyGap,
|
|
1190
|
+
blackKeyWidthRatio
|
|
1191
|
+
}));
|
|
1584
1192
|
});
|
|
1585
1193
|
resizeObserver.observe(parent);
|
|
1586
1194
|
return () => resizeObserver.disconnect();
|
|
1587
1195
|
}, [
|
|
1588
1196
|
resizable,
|
|
1589
1197
|
node,
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
]);
|
|
1603
|
-
/** The note a shortcut key plays, or null when it has none. */
|
|
1604
|
-
function shortcutNote(key) {
|
|
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;
|
|
1609
|
-
}
|
|
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
|
|
1198
|
+
first,
|
|
1199
|
+
last,
|
|
1200
|
+
keyGap,
|
|
1201
|
+
blackKeyWidthRatio
|
|
1622
1202
|
]);
|
|
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;
|
|
1628
|
-
const note = shortcutNote(e.key);
|
|
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 });
|
|
1636
|
-
});
|
|
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 });
|
|
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);
|
|
1649
1203
|
useImperativeHandle(forwardedRef, () => ({
|
|
1650
1204
|
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1651
1205
|
source: "api",
|
|
@@ -1661,7 +1215,6 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1661
1215
|
tabIndex: 0,
|
|
1662
1216
|
style: {
|
|
1663
1217
|
width: resizable ? "100%" : pianoWidth(layout),
|
|
1664
|
-
"--height": cssLength(height),
|
|
1665
1218
|
position: "relative",
|
|
1666
1219
|
...style
|
|
1667
1220
|
},
|
|
@@ -1727,6 +1280,49 @@ function usePointsEditorContext(selector) {
|
|
|
1727
1280
|
return selector ? selector(context) : context;
|
|
1728
1281
|
}
|
|
1729
1282
|
//#endregion
|
|
1283
|
+
//#region src/components/PointsEditor/Container.tsx
|
|
1284
|
+
const Container = /* @__PURE__ */ forwardRef(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1285
|
+
const { containerRef, disabled, selectable, editor } = usePointsEditorContext();
|
|
1286
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1287
|
+
axis: POINT_AXIS,
|
|
1288
|
+
baseElementRef: containerRef,
|
|
1289
|
+
shouldStart: (event) => !disabled && !editor.isPointElement(event.target),
|
|
1290
|
+
onDragStart: ([x, y], state) => {
|
|
1291
|
+
editor.beginSelectionBox({
|
|
1292
|
+
x,
|
|
1293
|
+
y
|
|
1294
|
+
}, state.event);
|
|
1295
|
+
},
|
|
1296
|
+
onChange: ([x, y]) => editor.moveSelectionBox({
|
|
1297
|
+
x,
|
|
1298
|
+
y
|
|
1299
|
+
}),
|
|
1300
|
+
onDragEnd: () => editor.endSelectionBox()
|
|
1301
|
+
});
|
|
1302
|
+
useWheel((event) => {
|
|
1303
|
+
const move = wheelMove(event);
|
|
1304
|
+
if (!move) return;
|
|
1305
|
+
const axis = move.axis === 0 ? "x" : "y";
|
|
1306
|
+
if (editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
|
|
1307
|
+
}, { target: containerRef });
|
|
1308
|
+
const composedRef = useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1309
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1310
|
+
ref: composedRef,
|
|
1311
|
+
className,
|
|
1312
|
+
style: {
|
|
1313
|
+
position: "absolute",
|
|
1314
|
+
inset: 0,
|
|
1315
|
+
zIndex: 10,
|
|
1316
|
+
...style
|
|
1317
|
+
},
|
|
1318
|
+
...props,
|
|
1319
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
1320
|
+
name: "PointsEditor.Container",
|
|
1321
|
+
children
|
|
1322
|
+
})
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
//#endregion
|
|
1730
1326
|
//#region src/components/_util/VisuallyHiddenRangeInput.tsx
|
|
1731
1327
|
const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @__PURE__ */ jsx("input", {
|
|
1732
1328
|
...props,
|
|
@@ -1736,21 +1332,6 @@ const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @
|
|
|
1736
1332
|
}));
|
|
1737
1333
|
//#endregion
|
|
1738
1334
|
//#region src/components/PointsEditor/Point.tsx
|
|
1739
|
-
function clampPoint(point, min, max) {
|
|
1740
|
-
const { x, y } = point;
|
|
1741
|
-
return {
|
|
1742
|
-
x: clamp(x, min?.x ?? 0, max?.x ?? 1),
|
|
1743
|
-
y: clamp(y, min?.y ?? 0, max?.y ?? 1)
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
/**
|
|
1747
|
-
* A point is placed by its position within the container, so its value is a
|
|
1748
|
-
* position: 0..1 on each axis, with y growing downwards.
|
|
1749
|
-
*/
|
|
1750
|
-
const AXIS = {
|
|
1751
|
-
min: 0,
|
|
1752
|
-
max: 1
|
|
1753
|
-
};
|
|
1754
1335
|
/**
|
|
1755
1336
|
* One setting for both axes, or one per axis. The point holds a range input
|
|
1756
1337
|
* for each, so anything that names or describes it comes in a pair.
|
|
@@ -1758,8 +1339,8 @@ const AXIS = {
|
|
|
1758
1339
|
function perAxis(value, axis) {
|
|
1759
1340
|
return typeof value === "string" ? value : value?.[axis];
|
|
1760
1341
|
}
|
|
1761
|
-
function Point({ value, children, id: idProp, min, max,
|
|
1762
|
-
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection,
|
|
1342
|
+
function Point({ value, children, id: idProp, min, max, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
|
|
1343
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
|
|
1763
1344
|
const generatedId = useId();
|
|
1764
1345
|
const id = idProp ?? generatedId;
|
|
1765
1346
|
const selected = selection.includes(id);
|
|
@@ -1792,11 +1373,11 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1792
1373
|
wheel
|
|
1793
1374
|
};
|
|
1794
1375
|
});
|
|
1795
|
-
useEffect(() => registerPoint(id, registration), [id,
|
|
1376
|
+
useEffect(() => editor.registerPoint(id, () => registration.current), [id, editor]);
|
|
1796
1377
|
/** Where the pointer was when the drag started, to measure the move from. */
|
|
1797
1378
|
const pointerOrigin = useRef(null);
|
|
1798
1379
|
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1799
|
-
axis:
|
|
1380
|
+
axis: POINT_AXIS,
|
|
1800
1381
|
baseElementRef: containerRef,
|
|
1801
1382
|
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1802
1383
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
@@ -1804,13 +1385,13 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1804
1385
|
onChange: ([x, y]) => {
|
|
1805
1386
|
const origin = pointerOrigin.current;
|
|
1806
1387
|
if (!origin) return;
|
|
1807
|
-
movePointDrag({
|
|
1388
|
+
editor.movePointDrag({
|
|
1808
1389
|
x: x - origin.x,
|
|
1809
1390
|
y: y - origin.y
|
|
1810
1391
|
});
|
|
1811
1392
|
},
|
|
1812
1393
|
onDragStart: ([x, y], state) => {
|
|
1813
|
-
beginPointDrag(id, state.event);
|
|
1394
|
+
editor.beginPointDrag(id, state.event);
|
|
1814
1395
|
pointerOrigin.current = {
|
|
1815
1396
|
x,
|
|
1816
1397
|
y
|
|
@@ -1825,30 +1406,13 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1825
1406
|
onDragEnd?.(clampPoint(value, min, max));
|
|
1826
1407
|
}
|
|
1827
1408
|
});
|
|
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
|
-
});
|
|
1834
|
-
}, [
|
|
1835
|
-
value,
|
|
1836
|
-
id,
|
|
1837
|
-
nudgeSelection
|
|
1838
|
-
]);
|
|
1839
1409
|
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1840
1410
|
const handleKeyDown = (event) => {
|
|
1841
|
-
const
|
|
1842
|
-
if (!
|
|
1843
|
-
"ArrowRight",
|
|
1844
|
-
"ArrowLeft",
|
|
1845
|
-
"ArrowUp",
|
|
1846
|
-
"ArrowDown"
|
|
1847
|
-
].includes(key)) return;
|
|
1848
|
-
const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
|
|
1411
|
+
const move = arrowKeyMove(event.key);
|
|
1412
|
+
if (!move) return;
|
|
1849
1413
|
event.preventDefault();
|
|
1850
1414
|
if (!onChange || inactive || !keyboard) return;
|
|
1851
|
-
|
|
1415
|
+
editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
|
|
1852
1416
|
};
|
|
1853
1417
|
const current = clampPoint(value, min, max);
|
|
1854
1418
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -1861,8 +1425,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1861
1425
|
"data-selected": selected ? "" : void 0,
|
|
1862
1426
|
style: {
|
|
1863
1427
|
"--color": color,
|
|
1864
|
-
"--width": cssLength(size ?? width),
|
|
1865
|
-
"--height": cssLength(size ?? height),
|
|
1866
1428
|
position: "absolute",
|
|
1867
1429
|
translate: "var(--translate, -50% -50%)",
|
|
1868
1430
|
...style,
|
|
@@ -1897,7 +1459,7 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1897
1459
|
return;
|
|
1898
1460
|
}
|
|
1899
1461
|
const delta = event.currentTarget.valueAsNumber - value[axis];
|
|
1900
|
-
nudgeSelection(id, {
|
|
1462
|
+
editor.nudgeSelection(id, {
|
|
1901
1463
|
x: axis === "x" ? delta : 0,
|
|
1902
1464
|
y: axis === "y" ? delta : 0
|
|
1903
1465
|
});
|
|
@@ -1906,50 +1468,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1906
1468
|
});
|
|
1907
1469
|
}
|
|
1908
1470
|
//#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
1471
|
//#region src/components/PointsEditor/SelectionBox.tsx
|
|
1954
1472
|
/**
|
|
1955
1473
|
* The box a drag on empty space draws, and what it covers is selected.
|
|
@@ -1982,181 +1500,28 @@ function SelectionBox({ children, className, style, ...props }) {
|
|
|
1982
1500
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
1983
1501
|
const EMPTY = [];
|
|
1984
1502
|
/**
|
|
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
|
-
}
|
|
2007
|
-
/**
|
|
2008
|
-
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
2009
|
-
* in a hundred steps whatever its pixel size.
|
|
2010
|
-
*/
|
|
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
|
-
};
|
|
2021
|
-
/**
|
|
2022
1503
|
* Multiple Point Controller
|
|
2023
1504
|
*/
|
|
2024
1505
|
const PointsEditor = {
|
|
2025
|
-
Root: /* @__PURE__ */ forwardRef(({
|
|
1506
|
+
Root: /* @__PURE__ */ forwardRef(({ disabled = false, readonly = false, wheel = POINTS_EDITOR_DEFAULT_WHEEL, keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
2026
1507
|
const containerRef = useRef(null);
|
|
2027
1508
|
const { cursor = "grabbing" } = externalStyles ?? {};
|
|
2028
1509
|
const controlled = selectionProp !== void 0;
|
|
2029
1510
|
const [ownSelection, setOwnSelection] = useState(defaultSelection ?? []);
|
|
2030
1511
|
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
1512
|
const [selectionBox, setSelectionBox] = useState(null);
|
|
2128
|
-
const
|
|
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
|
-
});
|
|
1513
|
+
const [editor] = useState(() => createPointsEditor({ onSelectionBoxChange: setSelectionBox }));
|
|
2138
1514
|
useEffect(() => {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
if (!selectable) return;
|
|
2147
|
-
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
2148
|
-
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
2149
|
-
selection: selectionRef.current
|
|
1515
|
+
editor.update({
|
|
1516
|
+
selectable,
|
|
1517
|
+
selection,
|
|
1518
|
+
onSelectionChange: (next) => {
|
|
1519
|
+
if (!controlled) setOwnSelection(next);
|
|
1520
|
+
onSelectionChange?.(next);
|
|
1521
|
+
}
|
|
2150
1522
|
});
|
|
2151
|
-
}
|
|
2152
|
-
|
|
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
|
-
}, []);
|
|
1523
|
+
});
|
|
1524
|
+
useEffect(() => () => editor.destroy(), [editor]);
|
|
2160
1525
|
const context = useMemo(() => ({
|
|
2161
1526
|
disabled,
|
|
2162
1527
|
readonly,
|
|
@@ -2167,16 +1532,8 @@ const PointsEditor = {
|
|
|
2167
1532
|
containerRef,
|
|
2168
1533
|
selectable,
|
|
2169
1534
|
selection,
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
beginPointDrag,
|
|
2173
|
-
movePointDrag,
|
|
2174
|
-
nudgeSelection,
|
|
2175
|
-
nudgeFocusedPoint,
|
|
2176
|
-
selectionBox,
|
|
2177
|
-
beginSelectionBox,
|
|
2178
|
-
moveSelectionBox,
|
|
2179
|
-
endSelectionBox
|
|
1535
|
+
editor,
|
|
1536
|
+
selectionBox
|
|
2180
1537
|
}), [
|
|
2181
1538
|
disabled,
|
|
2182
1539
|
readonly,
|
|
@@ -2186,16 +1543,8 @@ const PointsEditor = {
|
|
|
2186
1543
|
cursor,
|
|
2187
1544
|
selectable,
|
|
2188
1545
|
selection,
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
beginPointDrag,
|
|
2192
|
-
movePointDrag,
|
|
2193
|
-
nudgeSelection,
|
|
2194
|
-
nudgeFocusedPoint,
|
|
2195
|
-
selectionBox,
|
|
2196
|
-
beginSelectionBox,
|
|
2197
|
-
moveSelectionBox,
|
|
2198
|
-
endSelectionBox
|
|
1546
|
+
editor,
|
|
1547
|
+
selectionBox
|
|
2199
1548
|
]);
|
|
2200
1549
|
return /* @__PURE__ */ jsx(PointsEditorProvider, {
|
|
2201
1550
|
value: context,
|
|
@@ -2205,8 +1554,6 @@ const PointsEditor = {
|
|
|
2205
1554
|
"data-disabled": disabled ? "" : void 0,
|
|
2206
1555
|
"data-readonly": readonly ? "" : void 0,
|
|
2207
1556
|
style: {
|
|
2208
|
-
"--width": cssLength(width),
|
|
2209
|
-
"--height": cssLength(height),
|
|
2210
1557
|
position: "relative",
|
|
2211
1558
|
...style
|
|
2212
1559
|
},
|
|
@@ -2231,36 +1578,30 @@ function useSliderContext(selector) {
|
|
|
2231
1578
|
}
|
|
2232
1579
|
//#endregion
|
|
2233
1580
|
//#region src/components/Slider/MarksOption.tsx
|
|
2234
|
-
function MarksOption({ value, mark = true, label, thickness, length, gap,
|
|
1581
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, classes, styles, className, style, ...props }) {
|
|
2235
1582
|
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
2236
1583
|
const min = useSliderContext((s) => s.min);
|
|
2237
1584
|
const max = useSliderContext((s) => s.max);
|
|
2238
1585
|
const scale = useSliderContext((s) => s.scale);
|
|
2239
1586
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2240
1587
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2241
|
-
const
|
|
2242
|
-
const percent = scale.normalize(value, min, max) * 100;
|
|
2243
|
-
return toFixed(vertical !== reverse ? 100 - percent : percent);
|
|
2244
|
-
}, [
|
|
2245
|
-
vertical,
|
|
2246
|
-
reverse,
|
|
2247
|
-
max,
|
|
1588
|
+
const percent = valuePercent(value, {
|
|
2248
1589
|
min,
|
|
1590
|
+
max,
|
|
2249
1591
|
scale
|
|
2250
|
-
|
|
1592
|
+
}, vertical !== reverse);
|
|
2251
1593
|
return /* @__PURE__ */ jsxs("div", {
|
|
2252
1594
|
className,
|
|
2253
1595
|
style: {
|
|
2254
1596
|
"--thickness": cssLength(thickness),
|
|
2255
1597
|
"--length": cssLength(length),
|
|
2256
1598
|
"--gap": cssLength(gap),
|
|
2257
|
-
"--label-width": cssLength(labelWidth),
|
|
2258
1599
|
position: "absolute",
|
|
2259
1600
|
translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
|
|
2260
1601
|
zIndex: 10,
|
|
2261
1602
|
...style,
|
|
2262
|
-
left: !vertical ? `${
|
|
2263
|
-
top: vertical ? `${
|
|
1603
|
+
left: !vertical ? `${percent}%` : void 0,
|
|
1604
|
+
top: vertical ? `${percent}%` : void 0
|
|
2264
1605
|
},
|
|
2265
1606
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2266
1607
|
...props,
|
|
@@ -2276,44 +1617,6 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
|
|
|
2276
1617
|
});
|
|
2277
1618
|
}
|
|
2278
1619
|
//#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
|
|
2300
|
-
//#region src/components/Slider/type.ts
|
|
2301
|
-
function generateOptionsList(options, min, max, step) {
|
|
2302
|
-
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
2303
|
-
const optionsList = [];
|
|
2304
|
-
const interval = per === "step" ? step : per;
|
|
2305
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
2306
|
-
for (let i = 0; i < count; i++) {
|
|
2307
|
-
const value = toFixed(interval * (Math.ceil(min / interval) + i), decimalDigits(interval));
|
|
2308
|
-
optionsList.push({
|
|
2309
|
-
value,
|
|
2310
|
-
mark,
|
|
2311
|
-
label
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
return optionsList;
|
|
2315
|
-
}
|
|
2316
|
-
//#endregion
|
|
2317
1620
|
//#region src/components/Slider/Marks.tsx
|
|
2318
1621
|
function Marks({ gap, options, children, className, style, ...props }) {
|
|
2319
1622
|
const min = useSliderContext((s) => s.min);
|
|
@@ -2321,7 +1624,7 @@ function Marks({ gap, options, children, className, style, ...props }) {
|
|
|
2321
1624
|
const step = useSliderContext((s) => s.step);
|
|
2322
1625
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2323
1626
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2324
|
-
const optionsList = options ?
|
|
1627
|
+
const optionsList = options ? sliderMarks(options, min, max, step) : [];
|
|
2325
1628
|
if (vertical !== reverse) optionsList.reverse();
|
|
2326
1629
|
return /* @__PURE__ */ jsx("div", {
|
|
2327
1630
|
className,
|
|
@@ -2444,10 +1747,12 @@ const Slider = {
|
|
|
2444
1747
|
..._externalStyles
|
|
2445
1748
|
};
|
|
2446
1749
|
const inactive = disabled || readonly;
|
|
2447
|
-
const p = toFixed(scale.normalize(value, min, max) * 100);
|
|
2448
|
-
const rev = toFixed(100 - p);
|
|
2449
1750
|
const displayReversed = vertical !== reverse;
|
|
2450
|
-
const percent =
|
|
1751
|
+
const percent = valuePercent(value, {
|
|
1752
|
+
min,
|
|
1753
|
+
max,
|
|
1754
|
+
scale
|
|
1755
|
+
}, displayReversed);
|
|
2451
1756
|
const axis = useMemo(() => ({
|
|
2452
1757
|
min,
|
|
2453
1758
|
max,
|
|
@@ -2468,18 +1773,11 @@ const Slider = {
|
|
|
2468
1773
|
wheel
|
|
2469
1774
|
});
|
|
2470
1775
|
const handleKeyDown = useCallback((event) => {
|
|
2471
|
-
const
|
|
2472
|
-
if (
|
|
2473
|
-
"ArrowRight",
|
|
2474
|
-
"ArrowLeft",
|
|
2475
|
-
"ArrowUp",
|
|
2476
|
-
"ArrowDown"
|
|
2477
|
-
].includes(key)) return;
|
|
1776
|
+
const direction = arrowKeyDirection(event.key);
|
|
1777
|
+
if (direction === null) return;
|
|
2478
1778
|
event.preventDefault();
|
|
2479
1779
|
if (!keyboard || !onChange || inactive) return;
|
|
2480
|
-
|
|
2481
|
-
if (reverse) direction *= -1;
|
|
2482
|
-
onChange(applyDelta(value, direction, keyboard, axis, event));
|
|
1780
|
+
onChange(applyDelta(value, reverse ? -direction : direction, keyboard, axis, event));
|
|
2483
1781
|
}, [
|
|
2484
1782
|
keyboard,
|
|
2485
1783
|
onChange,
|
|
@@ -2512,15 +1810,10 @@ const Slider = {
|
|
|
2512
1810
|
});
|
|
2513
1811
|
const wheelRefCallback = useWheel((event) => {
|
|
2514
1812
|
if (!wheel || !onChange || inactive) return;
|
|
1813
|
+
const direction = wheelDirection(event, { horizontal: !vertical });
|
|
1814
|
+
if (direction === null) return;
|
|
2515
1815
|
event.preventDefault();
|
|
2516
|
-
|
|
2517
|
-
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
2518
|
-
else {
|
|
2519
|
-
if (event.deltaY === 0) return;
|
|
2520
|
-
direction = event.deltaY > 0 ? -1 : 1;
|
|
2521
|
-
}
|
|
2522
|
-
if (reverse) direction *= -1;
|
|
2523
|
-
onChange(applyDelta(value, direction, wheel, axis, event));
|
|
1816
|
+
onChange(applyDelta(value, reverse ? -direction : direction, wheel, axis, event));
|
|
2524
1817
|
}, WHEEL_OPTIONS$1);
|
|
2525
1818
|
const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
2526
1819
|
const context = useMemo(() => ({
|
|
@@ -2605,16 +1898,13 @@ function useXYPadContext(selector) {
|
|
|
2605
1898
|
}
|
|
2606
1899
|
//#endregion
|
|
2607
1900
|
//#region src/components/XYPad/Area.tsx
|
|
2608
|
-
const Area = /* @__PURE__ */ forwardRef(function Area({
|
|
1901
|
+
const Area = /* @__PURE__ */ forwardRef(function Area({ children, className, style, ...props }, forwardedRef) {
|
|
2609
1902
|
const { areaRef } = useXYPadContext();
|
|
2610
1903
|
const composedRef = useComposedRefs(forwardedRef, areaRef);
|
|
2611
1904
|
return /* @__PURE__ */ jsx("div", {
|
|
2612
1905
|
ref: composedRef,
|
|
2613
1906
|
className,
|
|
2614
1907
|
style: {
|
|
2615
|
-
"--color": color,
|
|
2616
|
-
"--width": cssLength(width),
|
|
2617
|
-
"--height": cssLength(height),
|
|
2618
1908
|
position: "relative",
|
|
2619
1909
|
...style
|
|
2620
1910
|
},
|
|
@@ -2713,10 +2003,11 @@ const XYPad = {
|
|
|
2713
2003
|
const step = useMemo(() => toXY(_step), [_step]);
|
|
2714
2004
|
const scale = useMemo(() => toXY(_scale), [_scale]);
|
|
2715
2005
|
const reverse = useMemo(() => toXY(_reverse), [_reverse]);
|
|
2716
|
-
const percent = useMemo(() => {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2006
|
+
const percent = useMemo(() => [0, 1].map((i) => valuePercent(value[i], {
|
|
2007
|
+
min: min[i],
|
|
2008
|
+
max: max[i],
|
|
2009
|
+
scale: scale[i]
|
|
2010
|
+
}, reverse[i])), [
|
|
2720
2011
|
value,
|
|
2721
2012
|
min,
|
|
2722
2013
|
max,
|
|
@@ -2758,20 +2049,12 @@ const XYPad = {
|
|
|
2758
2049
|
withAxis
|
|
2759
2050
|
]);
|
|
2760
2051
|
const handleKeyDown = useCallback((event) => {
|
|
2761
|
-
const
|
|
2762
|
-
if (!
|
|
2763
|
-
"ArrowRight",
|
|
2764
|
-
"ArrowLeft",
|
|
2765
|
-
"ArrowUp",
|
|
2766
|
-
"ArrowDown"
|
|
2767
|
-
].includes(key)) return;
|
|
2768
|
-
const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
|
|
2052
|
+
const move = arrowKeyMove(event.key);
|
|
2053
|
+
if (!move) return;
|
|
2769
2054
|
event.preventDefault();
|
|
2770
2055
|
if (!onChange || inactive || !keyboard) return;
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
if (reverse[i]) direction *= -1;
|
|
2774
|
-
onChange(nudge(i, direction, keyboard, event));
|
|
2056
|
+
const { axis: i, direction } = move;
|
|
2057
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
|
|
2775
2058
|
}, [
|
|
2776
2059
|
onChange,
|
|
2777
2060
|
inactive,
|
|
@@ -2802,15 +2085,11 @@ const XYPad = {
|
|
|
2802
2085
|
});
|
|
2803
2086
|
const wheelRefCallback = useWheel((event) => {
|
|
2804
2087
|
if (!onChange || inactive || !wheel) return;
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
if (delta === 0) return;
|
|
2808
|
-
const i = horizontal || event.shiftKey ? 0 : 1;
|
|
2088
|
+
const move = wheelMove(event);
|
|
2089
|
+
if (!move) return;
|
|
2809
2090
|
event.preventDefault();
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
if (reverse[i]) direction *= -1;
|
|
2813
|
-
onChange(nudge(i, direction, wheel, event));
|
|
2091
|
+
const { axis: i, direction } = move;
|
|
2092
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event));
|
|
2814
2093
|
}, WHEEL_OPTIONS);
|
|
2815
2094
|
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
2816
2095
|
const context = useMemo(() => ({
|
|
@@ -2895,6 +2174,82 @@ function useAnimationFrame(callback = () => {}, deps = []) {
|
|
|
2895
2174
|
}, [runCallback, ...deps]);
|
|
2896
2175
|
}
|
|
2897
2176
|
//#endregion
|
|
2177
|
+
//#region src/hooks/useDrag.ts
|
|
2178
|
+
/**
|
|
2179
|
+
* Track a pointer drag on an element.
|
|
2180
|
+
*
|
|
2181
|
+
* @returns a ref callback to attach to the element being dragged
|
|
2182
|
+
*/
|
|
2183
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
2184
|
+
const dragHandler = useCallbackRef(onDrag);
|
|
2185
|
+
const dragStartHandler = useCallbackRef(onDragStart);
|
|
2186
|
+
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
2187
|
+
const [node, setNode] = useState(null);
|
|
2188
|
+
useEffect(() => {
|
|
2189
|
+
if (!node) return;
|
|
2190
|
+
const instance = createDrag(node, {
|
|
2191
|
+
threshold,
|
|
2192
|
+
cursor,
|
|
2193
|
+
pointerLock,
|
|
2194
|
+
onDragStart: (state) => dragStartHandler(state),
|
|
2195
|
+
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
2196
|
+
onDragEnd: (state) => dragEndHandler(state)
|
|
2197
|
+
});
|
|
2198
|
+
return () => instance.destroy();
|
|
2199
|
+
}, [
|
|
2200
|
+
node,
|
|
2201
|
+
threshold,
|
|
2202
|
+
cursor,
|
|
2203
|
+
pointerLock,
|
|
2204
|
+
dragHandler,
|
|
2205
|
+
dragStartHandler,
|
|
2206
|
+
dragEndHandler
|
|
2207
|
+
]);
|
|
2208
|
+
return setNode;
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region src/hooks/useEventListener.ts
|
|
2212
|
+
function useEventListener(target, event, handler, options) {
|
|
2213
|
+
const listener = useCallbackRef(handler);
|
|
2214
|
+
const cleanupRef = useRef(() => {});
|
|
2215
|
+
useEffect(() => {
|
|
2216
|
+
const node = typeof target === "function" ? target() : target ?? document;
|
|
2217
|
+
if (!node) return;
|
|
2218
|
+
node.addEventListener(event, listener, options);
|
|
2219
|
+
const cleanup = () => {
|
|
2220
|
+
node.removeEventListener(event, listener, options);
|
|
2221
|
+
};
|
|
2222
|
+
cleanupRef.current = cleanup;
|
|
2223
|
+
return () => {
|
|
2224
|
+
cleanup();
|
|
2225
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
2226
|
+
};
|
|
2227
|
+
}, [
|
|
2228
|
+
event,
|
|
2229
|
+
target,
|
|
2230
|
+
options,
|
|
2231
|
+
listener
|
|
2232
|
+
]);
|
|
2233
|
+
return useCallback(() => cleanupRef.current(), []);
|
|
2234
|
+
}
|
|
2235
|
+
//#endregion
|
|
2236
|
+
//#region src/hooks/useInterval.ts
|
|
2237
|
+
function useInterval(callback, delay) {
|
|
2238
|
+
const savedCallback = useRef(callback);
|
|
2239
|
+
useEffect(() => {
|
|
2240
|
+
savedCallback.current = callback;
|
|
2241
|
+
}, [callback]);
|
|
2242
|
+
useEffect(() => {
|
|
2243
|
+
if (delay === null) return;
|
|
2244
|
+
const id = setInterval(() => {
|
|
2245
|
+
savedCallback.current();
|
|
2246
|
+
}, delay);
|
|
2247
|
+
return () => {
|
|
2248
|
+
clearInterval(id);
|
|
2249
|
+
};
|
|
2250
|
+
}, [delay]);
|
|
2251
|
+
}
|
|
2252
|
+
//#endregion
|
|
2898
2253
|
//#region src/hooks/useMIDIAccess.ts
|
|
2899
2254
|
const INITIAL_STATE = {
|
|
2900
2255
|
midiAccess: null,
|
|
@@ -3031,6 +2386,6 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
|
3031
2386
|
});
|
|
3032
2387
|
}
|
|
3033
2388
|
//#endregion
|
|
3034
|
-
export { AnimationCanvas,
|
|
2389
|
+
export { AnimationCanvas, DropZone, FileInput, Knob, NumberInput, Piano, PointsEditor, Slider, XYPad, useAnimationFrame, useDrag, useDragValue, useDropZone, useEventListener, useFileInputContext, useInterval, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
|
|
3035
2390
|
|
|
3036
2391
|
//# sourceMappingURL=index.js.map
|