UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

394 lines (390 loc) 15.8 kB
'use strict'; var vue = require('vue'); var CFormControlWrapper = require('../form/CFormControlWrapper.js'); var COneTimePasswordInput = require('./COneTimePasswordInput.js'); var utils = require('./utils.js'); var getNextActiveElement = require('../../utils/getNextActiveElement.js'); var isRTL = require('../../utils/isRTL.js'); const COneTimePassword = vue.defineComponent({ name: 'COneTimePassword', inheritAttrs: false, props: { /** * Function to generate aria-label for each input field. Receives current index (0-based) and total number of inputs. */ ariaLabel: { type: Function, default: (index, total) => `Digit ${index + 1} of ${total}`, }, /** * Automatically submit the form when all one time password fields are filled. */ autoSubmit: { type: Boolean, default: false, }, /** * Disable all one time password (OTP) input fields. */ disabled: { type: Boolean, default: false, }, /** * Initial value for uncontrolled Vue.js one time password input. */ defaultValue: [String, Number], /** * Provide valuable, actionable feedback. */ feedback: String, /** * Provide valuable, actionable feedback. */ feedbackInvalid: String, /** * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. */ feedbackValid: String, /** * A string of all className you want applied to the floating label wrapper. */ floatingClassName: String, /** * Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. */ floatingLabel: String, /** * ID attribute for the hidden input field. */ id: String, /** * Set component validation state to invalid. */ invalid: Boolean, /** * Add a caption for a component. */ label: String, /** * Enforce sequential input (users must fill fields in order). */ linear: { type: Boolean, default: true, }, /** * Show input as password (masked characters). */ masked: { type: Boolean, default: false, }, /** * The default name for a value passed using v-model. */ modelValue: [String, Number], /** * Name attribute for the hidden input field. */ name: String, /** * Placeholder text for input fields. Single character applies to all fields, longer strings apply character-by-character. */ placeholder: String, /** * Make Vue.js OTP input component read-only. */ readonly: { type: Boolean, default: false, }, /** * Makes the input field required for form validation. */ required: { type: Boolean, default: false, }, /** * Sets the visual size of the Vue.js one time password (OTP) input. Use 'sm' for small or 'lg' for large input fields. */ size: { type: String, validator: (value) => ['sm', 'lg'].includes(value), }, /** * Add helper text to the component. */ text: String, /** * Display validation feedback in a styled tooltip. */ tooltipFeedback: Boolean, /** * Input validation type: 'number' for digits only, or 'text' for free text. */ type: { type: String, default: 'number', validator: (value) => ['number', 'text'].includes(value), }, /** * Set component validation state to valid. */ valid: Boolean, /** * The current value of the one time password input. */ value: [String, Number], }, emits: [ /** * Callback triggered when the Vue.js one time password (OTP) value changes. */ 'update:modelValue', /** * Callback triggered when the Vue.js one time password (OTP) value changes. */ 'change', /** * Callback triggered when all Vue.js one time password (OTP) fields are filled. */ 'complete', ], setup(props, { attrs, slots, emit }) { const inputRefs = vue.ref([]); const hiddenInputRef = vue.ref(null); const inputValues = vue.ref([]); // Count valid OTP input children const inputCount = vue.computed(() => { return inputRefs.value.filter((ref) => ref !== null).length; }); // Initialize input values const initializeValues = () => { const initialValue = String(props.modelValue ?? props.value ?? props.defaultValue ?? ''); inputValues.value = Array.from({ length: inputCount.value }, (_, i) => initialValue[i] || ''); }; // Watch for changes in modelValue or value (controlled mode) vue.watch(() => props.modelValue ?? props.value, (newValue) => { if (newValue !== undefined) { const valueString = String(newValue); inputValues.value = Array.from({ length: inputCount.value }, (_, i) => valueString[i] || ''); } }, { immediate: true }); // Watch for changes in inputCount vue.watch(inputCount, initializeValues, { immediate: true }); // Update hidden input and trigger events const updateValue = (newValues) => { const newValue = newValues.join(''); if (hiddenInputRef.value) { hiddenInputRef.value.value = newValue; } emit('update:modelValue', newValue); emit('change', newValue); if (newValue.length === inputCount.value) { emit('complete', newValue); if (props.autoSubmit) { vue.nextTick(() => { const form = hiddenInputRef.value?.closest('form'); if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); } }); } } }; const handleInputChange = (index, event) => { const target = event.target; const inputValue = target.value; if (inputValue.length === 1 && !utils.isValidInput(inputValue, props.type)) { return; } const newValues = [...inputValues.value]; newValues[index] = inputValue.length === 1 ? inputValue : ''; inputValues.value = newValues; updateValue(newValues); if (inputValue.length === 1) { const nextInput = getNextActiveElement.default(inputRefs.value.filter(Boolean), target, true, false); nextInput?.focus(); } }; const handleInputFocus = (event) => { const target = event.target; if (target.value) { setTimeout(() => { target.select(); }, 0); return; } if (props.linear) { const firstEmptyInput = inputRefs.value.find((input) => !input?.value); if (firstEmptyInput && firstEmptyInput !== target) { firstEmptyInput.focus(); } } }; const handleKeyDown = (event) => { const { key, target } = event; if (key === 'Backspace' && target.value === '') { const newValues = [...inputValues.value]; const prevInput = getNextActiveElement.default(inputRefs.value.filter(Boolean), target, false, false); if (prevInput) { const prevIndex = inputRefs.value.indexOf(prevInput); if (prevIndex !== -1) { newValues[prevIndex] = ''; inputValues.value = newValues; updateValue(newValues); prevInput.focus(); } } return; } if (key === 'ArrowRight') { if (props.linear && target.value === '') { return; } const shouldMoveNext = !isRTL.default(target); const nextInput = getNextActiveElement.default(inputRefs.value.filter(Boolean), target, shouldMoveNext, false); nextInput?.focus(); return; } if (key === 'ArrowLeft') { const shouldMoveNext = isRTL.default(target); const prevInput = getNextActiveElement.default(inputRefs.value.filter(Boolean), target, shouldMoveNext, false); prevInput?.focus(); } }; const handlePaste = (index, event) => { event.preventDefault(); const pastedData = event.clipboardData?.getData('text') || ''; const validChars = utils.extractValidChars(pastedData, props.type); if (!validChars) { return; } const newValues = [...inputValues.value]; const startIndex = index; for (let i = 0; i < validChars.length && startIndex + i < inputCount.value; i++) { newValues[startIndex + i] = validChars[i]; } inputValues.value = newValues; updateValue(newValues); // Focus the next empty input or the last filled input const nextEmptyIndex = startIndex + validChars.length; if (nextEmptyIndex < inputCount.value) { inputRefs.value[nextEmptyIndex]?.focus(); } else { inputRefs.value[inputRefs.value.length - 1]?.focus(); } }; return () => { if (!slots.default) { return null; } const children = slots.default(); let inputIndex = 0; const processedChildren = children?.map((child) => { if (child.type && child.type.name === 'COneTimePasswordInput') { const currentInputIndex = inputIndex++; return vue.h(COneTimePasswordInput.COneTimePasswordInput, { ...child.props, key: `otp-input-${currentInputIndex}`, type: props.masked ? 'password' : 'text', class: [ { 'is-invalid': props.invalid, 'is-valid': props.valid, }, child.props?.class, ], id: child.props?.id || (props.id ? `${props.id}-${currentInputIndex}` : undefined), name: child.props?.name || (props.name ? `${props.name}-${currentInputIndex}` : undefined), placeholder: child.props?.placeholder || (props.placeholder && props.placeholder.length > 1 ? props.placeholder[currentInputIndex] : props.placeholder), value: inputValues.value[currentInputIndex] || '', tabindex: currentInputIndex === 0 ? 0 : inputValues.value[currentInputIndex - 1] ? 0 : -1, disabled: props.disabled || child.props?.disabled, readonly: props.readonly || child.props?.readonly, required: props.required || child.props?.required, 'aria-label': child.props?.['aria-label'] || props.ariaLabel(currentInputIndex, inputCount.value), inputmode: props.type === 'number' ? 'numeric' : 'text', pattern: props.type === 'number' ? '[0-9]*' : '.*', onInput: (event) => { handleInputChange(currentInputIndex, event); }, onFocus: handleInputFocus, onKeydown: (event) => { handleKeyDown(event); }, onPaste: (event) => { handlePaste(currentInputIndex, event); }, ref: (el) => { // Get the actual DOM element - handle both direct elements and component instances if (el) { // If it's a component instance, get the DOM element const domElement = el.$el || el; // Ensure it's actually an HTMLInputElement if (domElement && domElement.tagName === 'INPUT') { inputRefs.value[currentInputIndex] = domElement; } else { inputRefs.value[currentInputIndex] = el; } } else { inputRefs.value[currentInputIndex] = null; } }, }); } return child; }); return vue.h(CFormControlWrapper.CFormControlWrapper, { ...(typeof attrs['aria-describedby'] === 'string' && { describedby: attrs['aria-describedby'], }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, floatingClassName: props.floatingClassName, floatingLabel: props.floatingLabel, id: props.id, invalid: props.invalid, label: props.label, text: props.text, tooltipFeedback: props.tooltipFeedback, valid: props.valid, }, { default: () => [ vue.h('div', { class: [ 'form-otp', { [`form-otp-${props.size}`]: props.size, }, attrs.class, ], role: 'group', ...attrs, }, [ ...processedChildren, vue.h('input', { type: 'hidden', id: props.id, name: props.name, value: inputValues.value.join(''), disabled: props.disabled, ref: hiddenInputRef, }), ]), ], }); }; }, }); exports.COneTimePassword = COneTimePassword; //# sourceMappingURL=COneTimePassword.js.map