import { mergeProps as _mergeProps, createVNode as _createVNode, Fragment as _Fragment } from "vue"; // Styles import "./VOtpInput.css"; // Components import { makeVFieldProps, VField } from "../VField/VField.mjs"; import { VOverlay } from "../VOverlay/VOverlay.mjs"; import { VProgressCircular } from "../VProgressCircular/VProgressCircular.mjs"; // Composables import { provideDefaults } from "../../composables/defaults.mjs"; import { makeDimensionProps, useDimension } from "../../composables/dimensions.mjs"; import { makeFocusProps, useFocus } from "../../composables/focus.mjs"; import { useLocale } from "../../composables/locale.mjs"; import { useProxiedModel } from "../../composables/proxiedModel.mjs"; // Utilities import { computed, nextTick, ref, watch } from 'vue'; import { filterInputAttrs, focusChild, genericComponent, only, propsFactory, useRender } from "../../util/index.mjs"; // Types // Types export const makeVOtpInputProps = propsFactory({ autofocus: Boolean, divider: String, focusAll: Boolean, label: { type: String, default: '$vuetify.input.otp' }, length: { type: [Number, String], default: 6 }, modelValue: { type: [Number, String], default: undefined }, placeholder: String, type: { type: String, default: 'number' }, ...makeDimensionProps(), ...makeFocusProps(), ...only(makeVFieldProps({ variant: 'outlined' }), ['baseColor', 'bgColor', 'class', 'color', 'disabled', 'error', 'loading', 'rounded', 'style', 'theme', 'variant']) }, 'VOtpInput'); export const VOtpInput = genericComponent()({ name: 'VOtpInput', props: makeVOtpInputProps(), emits: { finish: val => true, 'update:focused': val => true, 'update:modelValue': val => true }, setup(props, _ref) { let { attrs, emit, slots } = _ref; const { dimensionStyles } = useDimension(props); const { isFocused, focus, blur } = useFocus(props); const model = useProxiedModel(props, 'modelValue', '', val => String(val).split(''), val => val.join('')); const { t } = useLocale(); const length = computed(() => Number(props.length)); const fields = computed(() => Array(length.value).fill(0)); const focusIndex = ref(-1); const contentRef = ref(); const inputRef = ref([]); const current = computed(() => inputRef.value[focusIndex.value]); function onInput() { // The maxlength attribute doesn't work for the number type input, so the text type is used. // The following logic simulates the behavior of a number input. if (props.type === 'number' && /[^0-9]/g.test(current.value.value)) { current.value.value = ''; return; } const array = model.value.slice(); const value = current.value.value; array[focusIndex.value] = value; let target = null; if (focusIndex.value > model.value.length) { target = model.value.length + 1; } else if (focusIndex.value + 1 !== length.value) { target = 'next'; } model.value = array; if (target) focusChild(contentRef.value, target); } function onKeydown(e) { const array = model.value.slice(); const index = focusIndex.value; let target = null; if (!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete'].includes(e.key)) return; e.preventDefault(); if (e.key === 'ArrowLeft') { target = 'prev'; } else if (e.key === 'ArrowRight') { target = 'next'; } else if (['Backspace', 'Delete'].includes(e.key)) { array[focusIndex.value] = ''; model.value = array; if (focusIndex.value > 0 && e.key === 'Backspace') { target = 'prev'; } else { requestAnimationFrame(() => { inputRef.value[index]?.select(); }); } } requestAnimationFrame(() => { if (target != null) { focusChild(contentRef.value, target); } }); } function onPaste(index, e) { e.preventDefault(); e.stopPropagation(); model.value = (e?.clipboardData?.getData('Text') ?? '').split(''); inputRef.value?.[index].blur(); } function reset() { model.value = []; } function onFocus(e, index) { focus(); focusIndex.value = index; } function onBlur() { blur(); focusIndex.value = -1; } provideDefaults({ VField: { color: computed(() => props.color), bgColor: computed(() => props.color), baseColor: computed(() => props.baseColor), disabled: computed(() => props.disabled), error: computed(() => props.error), variant: computed(() => props.variant) } }, { scoped: true }); watch(model, val => { if (val.length === length.value) emit('finish', val.join('')); }, { deep: true }); watch(focusIndex, val => { if (val < 0) return; nextTick(() => { inputRef.value[val]?.select(); }); }); useRender(() => { const [rootAttrs, inputAttrs] = filterInputAttrs(attrs); return _createVNode("div", _mergeProps({ "class": ['v-otp-input', { 'v-otp-input--divided': !!props.divider }, props.class], "style": [props.style] }, rootAttrs), [_createVNode("div", { "ref": contentRef, "class": "v-otp-input__content", "style": [dimensionStyles.value] }, [fields.value.map((_, i) => _createVNode(_Fragment, null, [props.divider && i !== 0 && _createVNode("span", { "class": "v-otp-input__divider" }, [props.divider]), _createVNode(VField, { "focused": isFocused.value && props.focusAll || focusIndex.value === i, "key": i }, { ...slots, loader: undefined, default: () => { return _createVNode("input", { "ref": val => inputRef.value[i] = val, "aria-label": t(props.label, i + 1), "autofocus": i === 0 && props.autofocus, "autocomplete": "one-time-code", "class": ['v-otp-input__field'], "disabled": props.disabled, "inputmode": props.type === 'number' ? 'numeric' : 'text', "min": props.type === 'number' ? 0 : undefined, "maxlength": "1", "placeholder": props.placeholder, "type": props.type === 'number' ? 'text' : props.type, "value": model.value[i], "onInput": onInput, "onFocus": e => onFocus(e, i), "onBlur": onBlur, "onKeydown": onKeydown, "onPaste": event => onPaste(i, event) }, null); } })])), _createVNode("input", _mergeProps({ "class": "v-otp-input-input", "type": "hidden" }, inputAttrs, { "value": model.value.join('') }), null), _createVNode(VOverlay, { "contained": true, "content-class": "v-otp-input__loader", "model-value": !!props.loading, "persistent": true }, { default: () => [slots.loader?.() ?? _createVNode(VProgressCircular, { "color": typeof props.loading === 'boolean' ? undefined : props.loading, "indeterminate": true, "size": "24", "width": "2" }, null)] }), slots.default?.()])]); }); return { blur: () => { inputRef.value?.some(input => input.blur()); }, focus: () => { inputRef.value?.[0].focus(); }, reset, isFocused }; } }); //# sourceMappingURL=VOtpInput.mjs.map