UNPKG

vuetify

Version:

Vue Material Component Framework

392 lines (389 loc) 14.4 kB
import { createVNode as _createVNode, createElementVNode as _createElementVNode, Fragment as _Fragment, mergeProps as _mergeProps } from "vue"; // Styles import "./VNumberInput.css"; // Components import { VBtn } from "../VBtn/index.js"; import { VDefaultsProvider } from "../VDefaultsProvider/index.js"; import { VDivider } from "../VDivider/index.js"; import { makeVTextFieldProps, VTextField } from "../VTextField/VTextField.js"; // Composables import { useHold } from "./hold.js"; import { useFocus } from "../../composables/focus.js"; import { useForm } from "../../composables/form.js"; import { forwardRefs } from "../../composables/forwardRefs.js"; import { useProxiedModel } from "../../composables/proxiedModel.js"; // Utilities import { computed, nextTick, onMounted, ref, shallowRef, toRef, watch, watchEffect } from 'vue'; import { clamp, extractNumber, genericComponent, omit, propsFactory, useRender } from "../../util/index.js"; // Types const makeVNumberInputProps = propsFactory({ controlVariant: { type: String, default: 'default' }, inset: Boolean, hideInput: Boolean, modelValue: { type: Number, default: null }, min: { type: Number, default: Number.MIN_SAFE_INTEGER }, max: { type: Number, default: Number.MAX_SAFE_INTEGER }, step: { type: Number, default: 1 }, precision: { type: Number, default: 0 }, ...omit(makeVTextFieldProps(), ['modelValue', 'validationValue']) }, 'VNumberInput'); export const VNumberInput = genericComponent()({ name: 'VNumberInput', props: { ...makeVNumberInputProps() }, emits: { 'update:modelValue': val => true }, setup(props, _ref) { let { slots } = _ref; const vTextFieldRef = ref(); const { holdStart, holdStop } = useHold({ toggleUpDown }); const form = useForm(props); const controlsDisabled = computed(() => form.isDisabled.value || form.isReadonly.value); const { isFocused, focus, blur } = useFocus(props); function correctPrecision(val) { let precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : props.precision; const fixed = precision == null ? String(val) : val.toFixed(precision); return isFocused.value ? Number(fixed).toString() // trim zeros : fixed; } const model = useProxiedModel(props, 'modelValue', null, val => val ?? null, val => val == null ? val ?? null : clamp(Number(val), props.min, props.max)); const _inputText = shallowRef(null); watchEffect(() => { if (isFocused.value && !controlsDisabled.value) { // ignore external changes } else if (model.value == null) { _inputText.value = null; } else if (!isNaN(model.value)) { _inputText.value = correctPrecision(model.value); } }); const inputText = computed({ get: () => _inputText.value, set(val) { if (val === null || val === '') { model.value = null; _inputText.value = null; } else if (!isNaN(Number(val)) && Number(val) <= props.max && Number(val) >= props.min) { model.value = Number(val); _inputText.value = val; } } }); const canIncrease = computed(() => { if (controlsDisabled.value) return false; return (model.value ?? 0) + props.step <= props.max; }); const canDecrease = computed(() => { if (controlsDisabled.value) return false; return (model.value ?? 0) - props.step >= props.min; }); const controlVariant = computed(() => { return props.hideInput ? 'stacked' : props.controlVariant; }); const incrementIcon = toRef(() => controlVariant.value === 'split' ? '$plus' : '$collapse'); const decrementIcon = toRef(() => controlVariant.value === 'split' ? '$minus' : '$expand'); const controlNodeSize = toRef(() => controlVariant.value === 'split' ? 'default' : 'small'); const controlNodeDefaultHeight = toRef(() => controlVariant.value === 'stacked' ? 'auto' : '100%'); const incrementSlotProps = { props: { style: { touchAction: 'none' }, onClick: onControlClick, onPointerup: onControlMouseup, onPointerdown: onUpControlMousedown } }; const decrementSlotProps = { props: { style: { touchAction: 'none' }, onClick: onControlClick, onPointerup: onControlMouseup, onPointerdown: onDownControlMousedown } }; watch(() => props.precision, () => formatInputValue()); onMounted(() => { clampModel(); }); function inferPrecision(value) { if (value == null) return 0; const str = value.toString(); const idx = str.indexOf('.'); return ~idx ? str.length - idx : 0; } function toggleUpDown() { let increment = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (controlsDisabled.value) return; if (model.value == null) { inputText.value = correctPrecision(clamp(0, props.min, props.max)); return; } let inferredPrecision = Math.max(inferPrecision(model.value), inferPrecision(props.step)); if (props.precision != null) inferredPrecision = Math.max(inferredPrecision, props.precision); if (increment) { if (canIncrease.value) inputText.value = correctPrecision(model.value + props.step, inferredPrecision); } else { if (canDecrease.value) inputText.value = correctPrecision(model.value - props.step, inferredPrecision); } } function onBeforeinput(e) { if (!e.data) return; const inputElement = e.target; const { value: existingTxt, selectionStart, selectionEnd } = inputElement ?? {}; const potentialNewInputVal = existingTxt ? existingTxt.slice(0, selectionStart) + e.data + existingTxt.slice(selectionEnd) : e.data; const potentialNewNumber = extractNumber(potentialNewInputVal, props.precision); // Only numbers, "-", "." are allowed // AND "-", "." are allowed only once // AND "-" is only allowed at the start if (!/^-?(\d+(\.\d*)?|(\.\d+)|\d*|\.)$/.test(potentialNewInputVal)) { e.preventDefault(); inputElement.value = potentialNewNumber; } if (props.precision == null) return; // Ignore decimal digits above precision limit if (potentialNewInputVal.split('.')[1]?.length > props.precision) { e.preventDefault(); inputElement.value = potentialNewNumber; } // Ignore decimal separator when precision = 0 if (props.precision === 0 && potentialNewInputVal.includes('.')) { e.preventDefault(); inputElement.value = potentialNewNumber; } } async function onKeydown(e) { if (['Enter', 'ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab'].includes(e.key) || e.ctrlKey) return; if (['ArrowDown', 'ArrowUp'].includes(e.key)) { e.preventDefault(); clampModel(); // _model is controlled, so need to wait until props['modelValue'] is updated await nextTick(); if (e.key === 'ArrowDown') { toggleUpDown(false); } else { toggleUpDown(); } } } function onControlClick(e) { e.stopPropagation(); } function onControlMouseup(e) { const el = e.currentTarget; el?.releasePointerCapture(e.pointerId); e.preventDefault(); e.stopPropagation(); holdStop(); } function onUpControlMousedown(e) { const el = e.currentTarget; el?.setPointerCapture(e.pointerId); e.preventDefault(); e.stopPropagation(); holdStart('up'); } function onDownControlMousedown(e) { const el = e.currentTarget; el?.setPointerCapture(e.pointerId); e.preventDefault(); e.stopPropagation(); holdStart('down'); } function clampModel() { if (controlsDisabled.value) return; if (!vTextFieldRef.value) return; const actualText = vTextFieldRef.value.value; if (actualText && !isNaN(Number(actualText))) { inputText.value = correctPrecision(clamp(Number(actualText), props.min, props.max)); } else { inputText.value = null; } } function formatInputValue() { if (controlsDisabled.value) return; if (model.value === null || isNaN(model.value)) { inputText.value = null; return; } inputText.value = props.precision == null ? String(model.value) : model.value.toFixed(props.precision); } function trimDecimalZeros() { if (controlsDisabled.value) return; if (model.value === null || isNaN(model.value)) { inputText.value = null; return; } inputText.value = model.value.toString(); } function onFocus() { focus(); trimDecimalZeros(); } function onBlur() { blur(); clampModel(); } useRender(() => { const { modelValue: _, ...textFieldProps } = VTextField.filterProps(props); function incrementControlNode() { return !slots.increment ? _createVNode(VBtn, { "aria-hidden": "true", "data-testid": "increment", "disabled": !canIncrease.value, "flat": true, "height": controlNodeDefaultHeight.value, "icon": incrementIcon.value, "key": "increment-btn", "onClick": onControlClick, "onPointerdown": onUpControlMousedown, "onPointerup": onControlMouseup, "size": controlNodeSize.value, "style": "touch-action: none", "tabindex": "-1" }, null) : _createVNode(VDefaultsProvider, { "key": "increment-defaults", "defaults": { VBtn: { disabled: !canIncrease.value, flat: true, height: controlNodeDefaultHeight.value, size: controlNodeSize.value, icon: incrementIcon.value } } }, { default: () => [slots.increment(incrementSlotProps)] }); } function decrementControlNode() { return !slots.decrement ? _createVNode(VBtn, { "aria-hidden": "true", "data-testid": "decrement", "disabled": !canDecrease.value, "flat": true, "height": controlNodeDefaultHeight.value, "icon": decrementIcon.value, "key": "decrement-btn", "onClick": onControlClick, "onPointerdown": onDownControlMousedown, "onPointerup": onControlMouseup, "size": controlNodeSize.value, "style": "touch-action: none", "tabindex": "-1" }, null) : _createVNode(VDefaultsProvider, { "key": "decrement-defaults", "defaults": { VBtn: { disabled: !canDecrease.value, flat: true, height: controlNodeDefaultHeight.value, size: controlNodeSize.value, icon: decrementIcon.value } } }, { default: () => [slots.decrement(decrementSlotProps)] }); } function controlNode() { return _createElementVNode("div", { "class": "v-number-input__control" }, [decrementControlNode(), _createVNode(VDivider, { "vertical": controlVariant.value !== 'stacked' }, null), incrementControlNode()]); } function dividerNode() { return !props.hideInput && !props.inset ? _createVNode(VDivider, { "vertical": true }, null) : undefined; } const appendInnerControl = controlVariant.value === 'split' ? _createElementVNode("div", { "class": "v-number-input__control" }, [_createVNode(VDivider, { "vertical": true }, null), incrementControlNode()]) : props.reverse || controlVariant.value === 'hidden' ? undefined : _createElementVNode(_Fragment, null, [dividerNode(), controlNode()]); const hasAppendInner = slots['append-inner'] || appendInnerControl; const prependInnerControl = controlVariant.value === 'split' ? _createElementVNode("div", { "class": "v-number-input__control" }, [decrementControlNode(), _createVNode(VDivider, { "vertical": true }, null)]) : props.reverse && controlVariant.value !== 'hidden' ? _createElementVNode(_Fragment, null, [controlNode(), dividerNode()]) : undefined; const hasPrependInner = slots['prepend-inner'] || prependInnerControl; return _createVNode(VTextField, _mergeProps({ "ref": vTextFieldRef, "modelValue": inputText.value, "onUpdate:modelValue": $event => inputText.value = $event, "validationValue": model.value, "onBeforeinput": onBeforeinput, "onFocus": onFocus, "onBlur": onBlur, "onKeydown": onKeydown, "class": ['v-number-input', { 'v-number-input--default': controlVariant.value === 'default', 'v-number-input--hide-input': props.hideInput, 'v-number-input--inset': props.inset, 'v-number-input--reverse': props.reverse, 'v-number-input--split': controlVariant.value === 'split', 'v-number-input--stacked': controlVariant.value === 'stacked' }, props.class] }, textFieldProps, { "style": props.style, "inputmode": "decimal" }), { ...slots, 'append-inner': hasAppendInner ? function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _createElementVNode(_Fragment, null, [slots['append-inner']?.(...args), appendInnerControl]); } : undefined, 'prepend-inner': hasPrependInner ? function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _createElementVNode(_Fragment, null, [prependInnerControl, slots['prepend-inner']?.(...args)]); } : undefined }); }); return forwardRefs({}, vTextFieldRef); } }); //# sourceMappingURL=VNumberInput.js.map