@zag-js/number-input
Version:
Core logic for the number-input widget implemented as a state machine
914 lines (907 loc) • 31.5 kB
JavaScript
var anatomy$1 = require('@zag-js/anatomy');
var domQuery = require('@zag-js/dom-query');
var utils = require('@zag-js/utils');
var core = require('@zag-js/core');
var number = require('@internationalized/number');
var types = require('@zag-js/types');
// src/number-input.anatomy.ts
var anatomy = anatomy$1.createAnatomy("numberInput").parts(
"root",
"label",
"input",
"control",
"valueText",
"incrementTrigger",
"decrementTrigger",
"scrubber"
);
var parts = anatomy.build();
var getRootId = (ctx) => ctx.ids?.root ?? `number-input:${ctx.id}`;
var getInputId = (ctx) => ctx.ids?.input ?? `number-input:${ctx.id}:input`;
var getIncrementTriggerId = (ctx) => ctx.ids?.incrementTrigger ?? `number-input:${ctx.id}:inc`;
var getDecrementTriggerId = (ctx) => ctx.ids?.decrementTrigger ?? `number-input:${ctx.id}:dec`;
var getScrubberId = (ctx) => ctx.ids?.scrubber ?? `number-input:${ctx.id}:scrubber`;
var getCursorId = (ctx) => `number-input:${ctx.id}:cursor`;
var getLabelId = (ctx) => ctx.ids?.label ?? `number-input:${ctx.id}:label`;
var getInputEl = (ctx) => ctx.getById(getInputId(ctx));
var getIncrementTriggerEl = (ctx) => ctx.getById(getIncrementTriggerId(ctx));
var getDecrementTriggerEl = (ctx) => ctx.getById(getDecrementTriggerId(ctx));
var getCursorEl = (ctx) => ctx.getDoc().getElementById(getCursorId(ctx));
var getPressedTriggerEl = (ctx, hint) => {
let btnEl = null;
if (hint === "increment") {
btnEl = getIncrementTriggerEl(ctx);
}
if (hint === "decrement") {
btnEl = getDecrementTriggerEl(ctx);
}
return btnEl;
};
var setupVirtualCursor = (ctx, point) => {
if (domQuery.isSafari()) return;
createVirtualCursor(ctx, point);
return () => {
getCursorEl(ctx)?.remove();
};
};
var preventTextSelection = (ctx) => {
const doc = ctx.getDoc();
const html = doc.documentElement;
const body = doc.body;
body.style.pointerEvents = "none";
html.style.userSelect = "none";
html.style.cursor = "ew-resize";
return () => {
body.style.pointerEvents = "";
html.style.userSelect = "";
html.style.cursor = "";
if (!html.style.length) {
html.removeAttribute("style");
}
if (!body.style.length) {
body.removeAttribute("style");
}
};
};
var getMousemoveValue = (ctx, opts) => {
const { point, isRtl, event } = opts;
const win = ctx.getWin();
const x = utils.roundToDpr(event.movementX, win.devicePixelRatio);
const y = utils.roundToDpr(event.movementY, win.devicePixelRatio);
let hint = x > 0 ? "increment" : x < 0 ? "decrement" : null;
if (isRtl && hint === "increment") hint = "decrement";
if (isRtl && hint === "decrement") hint = "increment";
const newPoint = { x: point.x + x, y: point.y + y };
const width = win.innerWidth;
const half = utils.roundToDpr(7.5, win.devicePixelRatio);
newPoint.x = utils.wrap(newPoint.x + half, width) - half;
return { hint, point: newPoint };
};
var createVirtualCursor = (ctx, point) => {
const doc = ctx.getDoc();
const el = doc.createElement("div");
el.className = "scrubber--cursor";
el.id = getCursorId(ctx);
Object.assign(el.style, {
width: "15px",
height: "15px",
position: "fixed",
pointerEvents: "none",
left: "0px",
top: "0px",
zIndex: domQuery.MAX_Z_INDEX,
transform: point ? `translate3d(${point.x}px, ${point.y}px, 0px)` : void 0,
willChange: "transform"
});
el.innerHTML = `
<svg width="46" height="15" style="left: -15.5px; position: absolute; top: 0; filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 1px 1.1px);">
<g transform="translate(2 3)">
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" style="stroke-width: 2px; stroke: white;"></path>
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z"></path>
</g>
</svg>`;
doc.body.appendChild(el);
};
// src/cursor.ts
function recordCursor(inputEl, scope) {
if (!inputEl || !scope.isActiveElement(inputEl)) return;
try {
const { selectionStart: start, selectionEnd: end, value } = inputEl;
if (start == null || end == null) return void 0;
return { start, end, value };
} catch {
return void 0;
}
}
function restoreCursor(inputEl, selection, scope) {
if (!inputEl || !scope.isActiveElement(inputEl)) return;
if (!selection) {
const len = inputEl.value.length;
inputEl.setSelectionRange(len, len);
return;
}
try {
const newValue = inputEl.value;
const { start, end, value: oldValue } = selection;
if (newValue === oldValue) {
inputEl.setSelectionRange(start, end);
return;
}
const newStart = getNewCursorPosition(oldValue, newValue, start);
const newEnd = start === end ? newStart : getNewCursorPosition(oldValue, newValue, end);
const clampedStart = Math.max(0, Math.min(newStart, newValue.length));
const clampedEnd = Math.max(clampedStart, Math.min(newEnd, newValue.length));
inputEl.setSelectionRange(clampedStart, clampedEnd);
} catch {
const len = inputEl.value.length;
inputEl.setSelectionRange(len, len);
}
}
function getNewCursorPosition(oldValue, newValue, oldPosition) {
const beforeCursor = oldValue.slice(0, oldPosition);
const afterCursor = oldValue.slice(oldPosition);
let prefixLength = 0;
const maxPrefixLength = Math.min(beforeCursor.length, newValue.length);
for (let i = 0; i < maxPrefixLength; i++) {
if (beforeCursor[i] === newValue[i]) {
prefixLength = i + 1;
} else {
break;
}
}
let suffixLength = 0;
const maxSuffixLength = Math.min(afterCursor.length, newValue.length - prefixLength);
for (let i = 0; i < maxSuffixLength; i++) {
const oldIndex = afterCursor.length - 1 - i;
const newIndex = newValue.length - 1 - i;
if (afterCursor[oldIndex] === newValue[newIndex]) {
suffixLength = i + 1;
} else {
break;
}
}
if (prefixLength >= beforeCursor.length) {
return prefixLength;
}
if (suffixLength >= afterCursor.length) {
return newValue.length - suffixLength;
}
if (prefixLength > 0) {
return prefixLength;
}
if (suffixLength > 0) {
return newValue.length - suffixLength;
}
if (oldValue.length > 0) {
const ratio = oldPosition / oldValue.length;
return Math.round(ratio * newValue.length);
}
return newValue.length;
}
// src/number-input.connect.ts
function connect(service, normalize) {
const { state, send, prop, scope, computed } = service;
const focused = state.hasTag("focus");
const disabled = computed("isDisabled");
const readOnly = !!prop("readOnly");
const required = !!prop("required");
const scrubbing = state.matches("scrubbing");
const empty = computed("isValueEmpty");
const invalid = computed("isOutOfRange") || !!prop("invalid");
const isIncrementDisabled = disabled || !computed("canIncrement") || readOnly;
const isDecrementDisabled = disabled || !computed("canDecrement") || readOnly;
const translations = prop("translations");
return {
focused,
invalid,
empty,
value: computed("formattedValue"),
valueAsNumber: computed("valueAsNumber"),
setValue(value) {
send({ type: "VALUE.SET", value });
},
clearValue() {
send({ type: "VALUE.CLEAR" });
},
increment() {
send({ type: "VALUE.INCREMENT" });
},
decrement() {
send({ type: "VALUE.DECREMENT" });
},
setToMax() {
send({ type: "VALUE.SET", value: prop("max") });
},
setToMin() {
send({ type: "VALUE.SET", value: prop("min") });
},
focus() {
getInputEl(scope)?.focus();
},
getRootProps() {
return normalize.element({
id: getRootId(scope),
...parts.root.attrs,
dir: prop("dir"),
"data-disabled": domQuery.dataAttr(disabled),
"data-focus": domQuery.dataAttr(focused),
"data-invalid": domQuery.dataAttr(invalid),
"data-scrubbing": domQuery.dataAttr(scrubbing)
});
},
getLabelProps() {
return normalize.label({
...parts.label.attrs,
dir: prop("dir"),
"data-disabled": domQuery.dataAttr(disabled),
"data-focus": domQuery.dataAttr(focused),
"data-invalid": domQuery.dataAttr(invalid),
"data-required": domQuery.dataAttr(required),
"data-scrubbing": domQuery.dataAttr(scrubbing),
id: getLabelId(scope),
htmlFor: getInputId(scope)
});
},
getControlProps() {
return normalize.element({
...parts.control.attrs,
dir: prop("dir"),
role: "group",
"aria-disabled": disabled,
"data-focus": domQuery.dataAttr(focused),
"data-disabled": domQuery.dataAttr(disabled),
"data-invalid": domQuery.dataAttr(invalid),
"data-scrubbing": domQuery.dataAttr(scrubbing),
"aria-invalid": domQuery.ariaAttr(invalid)
});
},
getValueTextProps() {
return normalize.element({
...parts.valueText.attrs,
dir: prop("dir"),
"data-disabled": domQuery.dataAttr(disabled),
"data-invalid": domQuery.dataAttr(invalid),
"data-focus": domQuery.dataAttr(focused),
"data-scrubbing": domQuery.dataAttr(scrubbing)
});
},
getInputProps() {
return normalize.input({
...parts.input.attrs,
dir: prop("dir"),
name: prop("name"),
form: prop("form"),
id: getInputId(scope),
role: "spinbutton",
defaultValue: computed("formattedValue"),
pattern: prop("formatOptions") ? void 0 : prop("pattern"),
inputMode: prop("inputMode"),
"aria-invalid": domQuery.ariaAttr(invalid),
"data-invalid": domQuery.dataAttr(invalid),
disabled,
"data-disabled": domQuery.dataAttr(disabled),
readOnly,
required: prop("required"),
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
type: "text",
"aria-roledescription": "numberfield",
"aria-valuemin": prop("min"),
"aria-valuemax": prop("max"),
"aria-valuenow": Number.isNaN(computed("valueAsNumber")) ? void 0 : computed("valueAsNumber"),
"aria-valuetext": computed("valueText"),
"data-scrubbing": domQuery.dataAttr(scrubbing),
onFocus() {
send({ type: "INPUT.FOCUS" });
},
onBlur() {
send({ type: "INPUT.BLUR" });
},
onInput(event) {
const selection = recordCursor(event.currentTarget, scope);
send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set", selection });
},
onBeforeInput(event) {
try {
const { selectionStart, selectionEnd, value } = event.currentTarget;
const nextValue = value.slice(0, selectionStart) + (event.data ?? "") + value.slice(selectionEnd);
const isValid = computed("parser").isValidPartialNumber(nextValue);
if (!isValid) {
event.preventDefault();
}
} catch {
}
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (readOnly) return;
if (domQuery.isComposingEvent(event)) return;
const step = domQuery.getEventStep(event) * prop("step");
const keyMap = {
ArrowUp() {
send({ type: "INPUT.ARROW_UP", step });
event.preventDefault();
},
ArrowDown() {
send({ type: "INPUT.ARROW_DOWN", step });
event.preventDefault();
},
Home() {
if (domQuery.isModifierKey(event)) return;
send({ type: "INPUT.HOME" });
event.preventDefault();
},
End() {
if (domQuery.isModifierKey(event)) return;
send({ type: "INPUT.END" });
event.preventDefault();
},
Enter() {
send({ type: "INPUT.ENTER" });
}
};
const exec = keyMap[event.key];
exec?.(event);
}
});
},
getDecrementTriggerProps() {
return normalize.button({
...parts.decrementTrigger.attrs,
dir: prop("dir"),
id: getDecrementTriggerId(scope),
disabled: isDecrementDisabled,
"data-disabled": domQuery.dataAttr(isDecrementDisabled),
"aria-label": translations.decrementLabel,
type: "button",
tabIndex: -1,
"aria-controls": getInputId(scope),
"data-scrubbing": domQuery.dataAttr(scrubbing),
onPointerDown(event) {
if (isDecrementDisabled) return;
if (!domQuery.isLeftClick(event)) return;
send({ type: "TRIGGER.PRESS_DOWN", hint: "decrement", pointerType: event.pointerType });
if (event.pointerType === "mouse") {
event.preventDefault();
}
if (event.pointerType === "touch") {
event.currentTarget?.focus({ preventScroll: true });
}
},
onPointerUp(event) {
send({ type: "TRIGGER.PRESS_UP", hint: "decrement", pointerType: event.pointerType });
},
onPointerLeave() {
if (isDecrementDisabled) return;
send({ type: "TRIGGER.PRESS_UP", hint: "decrement" });
}
});
},
getIncrementTriggerProps() {
return normalize.button({
...parts.incrementTrigger.attrs,
dir: prop("dir"),
id: getIncrementTriggerId(scope),
disabled: isIncrementDisabled,
"data-disabled": domQuery.dataAttr(isIncrementDisabled),
"aria-label": translations.incrementLabel,
type: "button",
tabIndex: -1,
"aria-controls": getInputId(scope),
"data-scrubbing": domQuery.dataAttr(scrubbing),
onPointerDown(event) {
if (isIncrementDisabled || !domQuery.isLeftClick(event)) return;
send({ type: "TRIGGER.PRESS_DOWN", hint: "increment", pointerType: event.pointerType });
if (event.pointerType === "mouse") {
event.preventDefault();
}
if (event.pointerType === "touch") {
event.currentTarget?.focus({ preventScroll: true });
}
},
onPointerUp(event) {
send({ type: "TRIGGER.PRESS_UP", hint: "increment", pointerType: event.pointerType });
},
onPointerLeave(event) {
send({ type: "TRIGGER.PRESS_UP", hint: "increment", pointerType: event.pointerType });
}
});
},
getScrubberProps() {
return normalize.element({
...parts.scrubber.attrs,
dir: prop("dir"),
"data-disabled": domQuery.dataAttr(disabled),
id: getScrubberId(scope),
role: "presentation",
"data-scrubbing": domQuery.dataAttr(scrubbing),
onMouseDown(event) {
if (disabled) return;
if (!domQuery.isLeftClick(event)) return;
const point = domQuery.getEventPoint(event);
const win = domQuery.getWindow(event.currentTarget);
const dpr = win.devicePixelRatio;
point.x = point.x - utils.roundToDpr(7.5, dpr);
point.y = point.y - utils.roundToDpr(7.5, dpr);
send({ type: "SCRUBBER.PRESS_DOWN", point });
event.preventDefault();
},
style: {
cursor: disabled ? void 0 : "ew-resize"
}
});
}
};
}
var createFormatter = (locale, options = {}) => {
return new Intl.NumberFormat(locale, options);
};
var createParser = (locale, options = {}) => {
return new number.NumberParser(locale, options);
};
var parseValue = (value, params) => {
const { prop, computed } = params;
if (!prop("formatOptions")) return parseFloat(value);
if (value === "") return Number.NaN;
return computed("parser").parse(value);
};
var formatValue = (value, params) => {
const { prop, computed } = params;
if (Number.isNaN(value)) return "";
if (!prop("formatOptions")) return value.toString();
return computed("formatter").format(value);
};
var getDefaultStep = (step, formatOptions) => {
let defaultStep = step !== void 0 && !Number.isNaN(step) ? step : 1;
if (formatOptions?.style === "percent" && (step === void 0 || Number.isNaN(step))) {
defaultStep = 0.01;
}
return defaultStep;
};
// src/number-input.machine.ts
var { choose, guards, createMachine } = core.setup();
var { not, and } = guards;
var machine = createMachine({
props({ props: props2 }) {
const step = getDefaultStep(props2.step, props2.formatOptions);
return {
dir: "ltr",
locale: "en-US",
focusInputOnChange: true,
clampValueOnBlur: !props2.allowOverflow,
allowOverflow: false,
inputMode: "decimal",
pattern: "-?[0-9]*(.[0-9]+)?",
defaultValue: "",
step,
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
spinOnPress: true,
...props2,
translations: {
incrementLabel: "increment value",
decrementLabel: "decrease value",
...props2.translations
}
};
},
initialState() {
return "idle";
},
context({ prop, bindable, getComputed }) {
return {
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
onChange(value) {
const computed = getComputed();
const valueAsNumber = parseValue(value, { computed, prop });
prop("onValueChange")?.({ value, valueAsNumber });
}
})),
hint: bindable(() => ({ defaultValue: null })),
scrubberCursorPoint: bindable(() => ({
defaultValue: null,
hash(value) {
return value ? `x:${value.x}, y:${value.y}` : "";
}
})),
fieldsetDisabled: bindable(() => ({ defaultValue: false }))
};
},
computed: {
isRtl: ({ prop }) => prop("dir") === "rtl",
valueAsNumber: ({ context, computed, prop }) => parseValue(context.get("value"), { computed, prop }),
formattedValue: ({ computed, prop }) => formatValue(computed("valueAsNumber"), { computed, prop }),
isAtMin: ({ computed, prop }) => utils.isValueAtMin(computed("valueAsNumber"), prop("min")),
isAtMax: ({ computed, prop }) => utils.isValueAtMax(computed("valueAsNumber"), prop("max")),
isOutOfRange: ({ computed, prop }) => !utils.isValueWithinRange(computed("valueAsNumber"), prop("min"), prop("max")),
isValueEmpty: ({ context }) => context.get("value") === "",
isDisabled: ({ prop, context }) => !!prop("disabled") || context.get("fieldsetDisabled"),
canIncrement: ({ prop, computed }) => prop("allowOverflow") || !computed("isAtMax"),
canDecrement: ({ prop, computed }) => prop("allowOverflow") || !computed("isAtMin"),
valueText: ({ prop, context }) => prop("translations").valueText?.(context.get("value")),
formatter: core.memo(
({ prop }) => [prop("locale"), prop("formatOptions")],
([locale, formatOptions]) => createFormatter(locale, formatOptions)
),
parser: core.memo(
({ prop }) => [prop("locale"), prop("formatOptions")],
([locale, formatOptions]) => createParser(locale, formatOptions)
)
},
watch({ track, action, context, computed, prop }) {
track([() => context.get("value"), () => prop("locale")], () => {
action(["syncInputElement"]);
});
track([() => computed("isOutOfRange")], () => {
action(["invokeOnInvalid"]);
});
track([() => context.hash("scrubberCursorPoint")], () => {
action(["setVirtualCursorPosition"]);
});
},
effects: ["trackFormControl"],
on: {
"VALUE.SET": {
actions: ["setRawValue"]
},
"VALUE.CLEAR": {
actions: ["clearValue"]
},
"VALUE.INCREMENT": {
actions: ["increment"]
},
"VALUE.DECREMENT": {
actions: ["decrement"]
}
},
states: {
idle: {
on: {
"TRIGGER.PRESS_DOWN": [
{ guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] },
{
target: "before:spin",
actions: ["focusInput", "invokeOnFocus", "setHint"]
}
],
"SCRUBBER.PRESS_DOWN": {
target: "scrubbing",
actions: ["focusInput", "invokeOnFocus", "setHint", "setCursorPoint"]
},
"INPUT.FOCUS": {
target: "focused",
actions: ["focusInput", "invokeOnFocus"]
}
}
},
focused: {
tags: ["focus"],
effects: ["attachWheelListener"],
on: {
"TRIGGER.PRESS_DOWN": [
{ guard: "isTouchPointer", target: "before:spin", actions: ["setHint"] },
{ target: "before:spin", actions: ["focusInput", "setHint"] }
],
"SCRUBBER.PRESS_DOWN": {
target: "scrubbing",
actions: ["focusInput", "setHint", "setCursorPoint"]
},
"INPUT.ARROW_UP": {
actions: ["increment"]
},
"INPUT.ARROW_DOWN": {
actions: ["decrement"]
},
"INPUT.HOME": {
actions: ["decrementToMin"]
},
"INPUT.END": {
actions: ["incrementToMax"]
},
"INPUT.CHANGE": {
actions: ["setValue", "setHint"]
},
"INPUT.BLUR": [
{
guard: and("clampValueOnBlur", not("isInRange")),
target: "idle",
actions: ["setClampedValue", "clearHint", "invokeOnBlur"]
},
{
guard: not("isInRange"),
target: "idle",
actions: ["setFormattedValue", "clearHint", "invokeOnBlur", "invokeOnInvalid"]
},
{
target: "idle",
actions: ["setFormattedValue", "clearHint", "invokeOnBlur"]
}
],
"INPUT.ENTER": {
actions: ["setFormattedValue", "clearHint", "invokeOnBlur"]
}
}
},
"before:spin": {
tags: ["focus"],
effects: ["trackButtonDisabled", "waitForChangeDelay"],
entry: choose([
{ guard: "isIncrementHint", actions: ["increment"] },
{ guard: "isDecrementHint", actions: ["decrement"] }
]),
on: {
CHANGE_DELAY: {
target: "spinning",
guard: and("isInRange", "spinOnPress")
},
"TRIGGER.PRESS_UP": [
{ guard: "isTouchPointer", target: "focused", actions: ["clearHint"] },
{ target: "focused", actions: ["focusInput", "clearHint"] }
]
}
},
spinning: {
tags: ["focus"],
effects: ["trackButtonDisabled", "spinValue"],
on: {
SPIN: [
{
guard: "isIncrementHint",
actions: ["increment"]
},
{
guard: "isDecrementHint",
actions: ["decrement"]
}
],
"TRIGGER.PRESS_UP": {
target: "focused",
actions: ["focusInput", "clearHint"]
}
}
},
scrubbing: {
tags: ["focus"],
effects: ["activatePointerLock", "trackMousemove", "setupVirtualCursor", "preventTextSelection"],
on: {
"SCRUBBER.POINTER_UP": {
target: "focused",
actions: ["focusInput", "clearCursorPoint"]
},
"SCRUBBER.POINTER_MOVE": [
{
guard: "isIncrementHint",
actions: ["increment", "setCursorPoint"]
},
{
guard: "isDecrementHint",
actions: ["decrement", "setCursorPoint"]
}
]
}
}
},
implementations: {
guards: {
clampValueOnBlur: ({ prop }) => prop("clampValueOnBlur"),
spinOnPress: ({ prop }) => !!prop("spinOnPress"),
isInRange: ({ computed }) => !computed("isOutOfRange"),
isDecrementHint: ({ context, event }) => (event.hint ?? context.get("hint")) === "decrement",
isIncrementHint: ({ context, event }) => (event.hint ?? context.get("hint")) === "increment",
isTouchPointer: ({ event }) => event.pointerType === "touch"
},
effects: {
waitForChangeDelay({ send }) {
const id = setTimeout(() => {
send({ type: "CHANGE_DELAY" });
}, 300);
return () => clearTimeout(id);
},
spinValue({ send }) {
const id = setInterval(() => {
send({ type: "SPIN" });
}, 50);
return () => clearInterval(id);
},
trackFormControl({ context, scope }) {
const inputEl = getInputEl(scope);
return domQuery.trackFormControl(inputEl, {
onFieldsetDisabledChange(disabled) {
context.set("fieldsetDisabled", disabled);
},
onFormReset() {
context.set("value", context.initial("value"));
}
});
},
setupVirtualCursor({ context, scope }) {
const point = context.get("scrubberCursorPoint");
return setupVirtualCursor(scope, point);
},
preventTextSelection({ scope }) {
return preventTextSelection(scope);
},
trackButtonDisabled({ context, scope, send }) {
const hint = context.get("hint");
const btn = getPressedTriggerEl(scope, hint);
return domQuery.observeAttributes(btn, {
attributes: ["disabled"],
callback() {
send({ type: "TRIGGER.PRESS_UP", src: "attr" });
}
});
},
attachWheelListener({ scope, send, prop }) {
const inputEl = getInputEl(scope);
if (!inputEl || !scope.isActiveElement(inputEl) || !prop("allowMouseWheel")) return;
function onWheel(event) {
event.preventDefault();
const dir = Math.sign(event.deltaY) * -1;
if (dir === 1) {
send({ type: "VALUE.INCREMENT" });
} else if (dir === -1) {
send({ type: "VALUE.DECREMENT" });
}
}
return domQuery.addDomEvent(inputEl, "wheel", onWheel, { passive: false });
},
activatePointerLock({ scope }) {
if (domQuery.isSafari()) return;
return domQuery.requestPointerLock(scope.getDoc());
},
trackMousemove({ scope, send, context, computed }) {
const doc = scope.getDoc();
function onMousemove(event) {
const point = context.get("scrubberCursorPoint");
const isRtl = computed("isRtl");
const value = getMousemoveValue(scope, { point, isRtl, event });
if (!value.hint) return;
send({
type: "SCRUBBER.POINTER_MOVE",
hint: value.hint,
point: value.point
});
}
function onMouseup() {
send({ type: "SCRUBBER.POINTER_UP" });
}
return utils.callAll(domQuery.addDomEvent(doc, "mousemove", onMousemove, false), domQuery.addDomEvent(doc, "mouseup", onMouseup, false));
}
},
actions: {
focusInput({ scope, prop }) {
if (!prop("focusInputOnChange")) return;
const inputEl = getInputEl(scope);
if (scope.isActiveElement(inputEl)) return;
domQuery.raf(() => inputEl?.focus({ preventScroll: true }));
},
increment({ context, event, prop, computed }) {
let nextValue = utils.incrementValue(computed("valueAsNumber"), event.step ?? prop("step"));
if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max"));
context.set("value", formatValue(nextValue, { computed, prop }));
},
decrement({ context, event, prop, computed }) {
let nextValue = utils.decrementValue(computed("valueAsNumber"), event.step ?? prop("step"));
if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max"));
context.set("value", formatValue(nextValue, { computed, prop }));
},
setClampedValue({ context, prop, computed }) {
const nextValue = utils.clampValue(computed("valueAsNumber"), prop("min"), prop("max"));
context.set("value", formatValue(nextValue, { computed, prop }));
},
setRawValue({ context, event, prop, computed }) {
let nextValue = parseValue(event.value, { computed, prop });
if (!prop("allowOverflow")) nextValue = utils.clampValue(nextValue, prop("min"), prop("max"));
context.set("value", formatValue(nextValue, { computed, prop }));
},
setValue({ context, event }) {
const value = event.target?.value ?? event.value;
context.set("value", value);
},
clearValue({ context }) {
context.set("value", "");
},
incrementToMax({ context, prop, computed }) {
const value = formatValue(prop("max"), { computed, prop });
context.set("value", value);
},
decrementToMin({ context, prop, computed }) {
const value = formatValue(prop("min"), { computed, prop });
context.set("value", value);
},
setHint({ context, event }) {
context.set("hint", event.hint);
},
clearHint({ context }) {
context.set("hint", null);
},
invokeOnFocus({ computed, prop }) {
prop("onFocusChange")?.({
focused: true,
value: computed("formattedValue"),
valueAsNumber: computed("valueAsNumber")
});
},
invokeOnBlur({ computed, prop }) {
prop("onFocusChange")?.({
focused: false,
value: computed("formattedValue"),
valueAsNumber: computed("valueAsNumber")
});
},
invokeOnInvalid({ computed, prop, event }) {
if (event.type === "INPUT.CHANGE") return;
const reason = computed("valueAsNumber") > prop("max") ? "rangeOverflow" : "rangeUnderflow";
prop("onValueInvalid")?.({
reason,
value: computed("formattedValue"),
valueAsNumber: computed("valueAsNumber")
});
},
syncInputElement({ context, event, computed, scope }) {
const value = event.type.endsWith("CHANGE") ? context.get("value") : computed("formattedValue");
const inputEl = getInputEl(scope);
const sel = event.selection;
domQuery.raf(() => {
domQuery.setElementValue(inputEl, value);
restoreCursor(inputEl, sel, scope);
});
},
setFormattedValue({ context, computed }) {
context.set("value", computed("formattedValue"));
},
setCursorPoint({ context, event }) {
context.set("scrubberCursorPoint", event.point);
},
clearCursorPoint({ context }) {
context.set("scrubberCursorPoint", null);
},
setVirtualCursorPosition({ context, scope }) {
const cursorEl = getCursorEl(scope);
const point = context.get("scrubberCursorPoint");
if (!cursorEl || !point) return;
cursorEl.style.transform = `translate3d(${point.x}px, ${point.y}px, 0px)`;
}
}
}
});
var props = types.createProps()([
"allowMouseWheel",
"allowOverflow",
"clampValueOnBlur",
"dir",
"disabled",
"focusInputOnChange",
"form",
"formatOptions",
"getRootNode",
"id",
"ids",
"inputMode",
"invalid",
"locale",
"max",
"min",
"name",
"onFocusChange",
"onValueChange",
"onValueInvalid",
"pattern",
"required",
"readOnly",
"spinOnPress",
"step",
"translations",
"value",
"defaultValue"
]);
var splitProps = utils.createSplitProps(props);
exports.anatomy = anatomy;
exports.connect = connect;
exports.machine = machine;
exports.props = props;
exports.splitProps = splitProps;
;