element-plus
Version:
A Component Library for Vue 3
1 lines • 7.74 kB
Source Map (JSON)
{"version":3,"file":"input-otp2.mjs","names":[],"sources":["../../../../../../packages/components/input-otp/src/input-otp.vue"],"sourcesContent":["<template>\n <div\n :id=\"inputId\"\n :class=\"[ns.b(), ns.m(size), ns.m(type), ns.is('disabled', disabled)]\"\n role=\"group\"\n :aria-label=\"\n !isLabeledByFormItem\n ? ariaLabel || t('el.inputOTP.groupLabel')\n : undefined\n \"\n :aria-labelledby=\"isLabeledByFormItem ? formItem?.labelId : undefined\"\n >\n <template v-for=\"(_, index) in length\" :key=\"index\">\n <label :class=\"ns.e('input-field')\">\n <input\n ref=\"inputRefs\"\n :value=\"innerValue[index]\"\n :class=\"ns.e('input')\"\n :type=\"mask ? 'password' : 'text'\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n :inputmode=\"inputmode\"\n autocomplete=\"one-time-code\"\n :aria-label=\"t('el.inputOTP.defaultLabel', { index: index + 1 })\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n @click=\"focus(index)\"\n @keydown=\"handleKeydown($event, index)\"\n @input=\"handleInput($event, index)\"\n />\n </label>\n <slot\n v-if=\"($slots.separator || separators[index]) && index < length - 1\"\n name=\"separator\"\n :index=\"index\"\n >\n <component :is=\"() => separators[index]\" />\n </slot>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, ref, watch } from 'vue'\nimport { clamp } from '@vueuse/core'\nimport { useLocale, useNamespace } from '@element-plus/hooks'\nimport { NOOP, getEventCode, isFunction, rAF } from '@element-plus/utils'\nimport {\n useFormDisabled,\n useFormItem,\n useFormItemInputId,\n} from '@element-plus/components/form'\nimport {\n CHANGE_EVENT,\n EVENT_CODE,\n UPDATE_MODEL_EVENT,\n} from '@element-plus/constants'\nimport { inputOtpEmits } from './input-otp'\n\nimport type { InputOtpProps } from './input-otp'\n\ndefineOptions({\n name: 'ElInputOtp',\n})\n\nconst props = withDefaults(defineProps<InputOtpProps>(), {\n length: 6,\n validator: () => true,\n type: 'outlined',\n size: 'default',\n disabled: undefined,\n validateEvent: true,\n})\nconst emit = defineEmits(inputOtpEmits)\n\nconst initialValue = computed(() => {\n const value = String(props.modelValue ?? '')\n return Array.from({ length: props.length }, (_, i) => value.charAt(i))\n})\n\nconst separators = computed(() => {\n const { separator } = props\n const _separator = isFunction(separator) ? separator : () => separator\n return Array.from({ length: props.length - 1 }, (_, i) => _separator(i))\n})\n\nconst innerValue = ref<string[]>(initialValue.value)\nconst isFocused = ref(false)\nconst inputRefs = ref<(HTMLInputElement | undefined)[]>([])\n\nconst ns = useNamespace('input-otp')\nconst { t } = useLocale()\nconst { formItem } = useFormItem()\nconst { inputId, isLabeledByFormItem } = useFormItemInputId(props, {\n formItemContext: formItem,\n})\nconst disabled = useFormDisabled()\nlet modelValueOnFocus = props.modelValue\n\nconst getFirstIndex = (maxIndex: number) => {\n const index = innerValue.value.findIndex((char, i) => !char && i <= maxIndex)\n return index === -1 ? maxIndex : index\n}\n\nconst handleFocus = (event: FocusEvent) => {\n if (inputRefs.value?.includes(event.relatedTarget as any)) {\n return\n }\n\n isFocused.value = true\n emit('focus', event)\n}\n\nconst handleBlur = (event: FocusEvent) => {\n if (inputRefs.value?.includes(event.relatedTarget as any)) {\n return\n }\n\n isFocused.value = false\n emit('blur', event)\n if (props.validateEvent) {\n formItem?.validate?.('blur').catch(NOOP)\n }\n}\n\nconst updateModelValue = (emitFinish = true) => {\n const value = innerValue.value.join('').slice(0, props.length)\n if (value !== props.modelValue) {\n emit(UPDATE_MODEL_EVENT, value)\n if (emitFinish && value.length === props.length) {\n emit('finish', value)\n }\n }\n}\n\nconst handleKeydown = (event: KeyboardEvent, index: number) => {\n const code = getEventCode(event)\n let preventDefault = true\n\n switch (code) {\n case EVENT_CODE.backspace:\n if (props.readonly) break\n innerValue.value[index] = ''\n focus(index - 1)\n updateModelValue()\n break\n case EVENT_CODE.delete:\n if (props.readonly) break\n innerValue.value[index] = ''\n focus(index)\n updateModelValue()\n break\n case EVENT_CODE.up:\n case EVENT_CODE.left:\n focus(index - 1)\n break\n case EVENT_CODE.down:\n case EVENT_CODE.right:\n focus(index + 1)\n break\n default:\n preventDefault = false\n }\n\n if (preventDefault) {\n event.preventDefault()\n }\n}\n\nconst handleInput = (event: Event, index: number) => {\n const target = event.target as HTMLInputElement\n const targetIndex = getFirstIndex(index)\n let focusIndex = targetIndex + 1\n let value = target.value\n\n // Safari and Firefox do not trigger the paste event during autofill,\n // so the paste logic needs to be handled in the input event\n if (value.length > 1) {\n const chars = castValues(value, targetIndex)\n\n // Avoid innerValue inconsistency with the input box value after pasting when char has no change\n target.value = innerValue.value[index] ?? ''\n\n chars.forEach((char, i) => (innerValue.value[targetIndex + i] = char))\n focus(targetIndex + chars.length)\n updateModelValue()\n return\n }\n\n if (!props.validator(value, targetIndex)) {\n target.value = innerValue.value[index] ?? ''\n value = target.value\n focusIndex = targetIndex\n }\n\n innerValue.value[targetIndex] = value\n if (targetIndex !== index) {\n target.value = innerValue.value[index] ?? ''\n }\n focus(focusIndex)\n updateModelValue()\n}\n\nconst castValues = (value: InputOtpProps['modelValue'], startIndex = 0) => {\n const chars = `${value ?? ''}`.split('')\n const result: string[] = []\n for (const char of chars) {\n if (result.length + startIndex >= props.length) {\n break\n }\n if (props.validator(char, result.length + startIndex)) {\n result.push(char)\n }\n }\n return result\n}\n\nconst focus = (index: number = 0) => {\n const focusIndex = clamp(index, 0, props.length - 1)\n const target = inputRefs.value?.[focusIndex]\n\n if (document.activeElement !== target) {\n target?.focus()\n }\n rAF(() => {\n // When it is called, the focus may have already been captured by another element.\n // e.g. typing quickly and deleting.\n if (!props.readonly && document.activeElement === target) {\n ;(target as HTMLInputElement | null)?.select()\n }\n })\n}\n\nconst blur = () => {\n const target = inputRefs.value?.find(\n (input) => document.activeElement === input\n )\n target?.blur()\n}\n\nwatch(\n () => props.modelValue,\n () => {\n innerValue.value = initialValue.value\n\n if (props.validateEvent) {\n formItem?.validate?.('change').catch(NOOP)\n }\n }\n)\n\nwatch(\n () => props.length,\n () => {\n innerValue.value = initialValue.value\n updateModelValue(false)\n }\n)\n\nwatch(isFocused, (value) => {\n if (value) {\n modelValueOnFocus = props.modelValue\n return\n }\n if (modelValueOnFocus !== props.modelValue) {\n emit(CHANGE_EVENT, props.modelValue as string)\n }\n})\n\ndefineExpose({\n /**\n * @description HTML input elements array\n */\n inputRefs,\n /**\n * @description Focus an OTP input field\n */\n focus,\n /**\n * @description Blur the focused OTP input field\n */\n blur,\n})\n</script>\n"],"mappings":""}