@coreui/vue-pro
Version:
UI Components Library for Vue.js
394 lines (390 loc) • 15.8 kB
JavaScript
;
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