@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
426 lines (405 loc) • 13.3 kB
JavaScript
/*!
* CoreUI otp-input.js v5.23.0 (https://coreui.io)
* Copyright 2025 The CoreUI Team (https://github.com/orgs/coreui/people)
* Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :
typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.OtpInput = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));
})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';
/**
* --------------------------------------------------------------------------
* CoreUI PRO password-input.js
* License (https://coreui.io/pro/license/)
* --------------------------------------------------------------------------
*/
/**
* Constants
*/
const NAME = 'otp-input';
const DATA_KEY = 'coreui.otp-input';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const ARROW_RIGHT_KEY = 'ArrowRight';
const ARROW_LEFT_KEY = 'ArrowLeft';
const BACKSPACE_KEY = 'Backspace';
const EVENT_CHANGE = `change${EVENT_KEY}`;
const EVENT_COMPLETE = `complete${EVENT_KEY}`;
const EVENT_FOCUS = `focus${EVENT_KEY}`;
const EVENT_INPUT = `input${EVENT_KEY}`;
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;
const EVENT_PASTE = `paste`;
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
const SELECTOR_FORM_OTP_CONTROL = '.form-otp-control';
const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="otp"]';
const Default = {
ariaLabel: (index, total) => `Digit ${index + 1} of ${total}`,
autoSubmit: false,
disabled: false,
id: null,
linear: true,
masked: false,
name: null,
placeholder: null,
readonly: false,
required: false,
type: 'number',
value: null
};
const DefaultType = {
ariaLabel: 'function',
autoSubmit: 'boolean',
disabled: 'boolean',
id: '(string|null)',
linear: 'boolean',
masked: 'boolean',
name: '(string|null)',
placeholder: '(number|string|null)',
readonly: 'boolean',
required: 'boolean',
type: 'string',
value: '(number|string|null)'
};
/**
* Class definition
*/
class OTPInput extends BaseComponent {
constructor(element, config) {
super(element, config);
this._config = this._getConfig(config);
this._inputElement = null;
this._createHiddenInput();
this._setRoleAttribute();
this._setInputsAttributes();
this._setInputsTabIndexes();
this._addEventListeners();
}
// Getters
static get Default() {
return Default;
}
static get DefaultType() {
return DefaultType;
}
static get NAME() {
return NAME;
}
// Public
clear() {
const inputs = this._getInputs();
for (const input of inputs) {
input.value = '';
}
this._setHiddenInputValue(null);
this._setInputsTabIndexes();
}
reset() {
const inputs = this._getInputs();
for (const [index, input] of inputs.entries()) {
const valueString = String(this._config.value || '');
input.value = valueString && valueString[index] ? valueString[index] : '';
}
this._setHiddenInputValue(null);
this._setInputsTabIndexes();
}
update(config) {
if (typeof config !== 'object') {
return;
}
this._config = {
...this._config,
...config
};
this._typeCheckConfig(this._config);
this._setInputsAttributes();
this._setInputsTabIndexes();
this._inputElement.remove();
this._createHiddenInput();
}
// Private
_addEventListeners() {
EventHandler.on(this._element, EVENT_FOCUS, SELECTOR_FORM_OTP_CONTROL, event => {
const {
target
} = event;
if (target.value) {
setTimeout(() => {
target.select();
}, 0);
return;
}
if (this._config.linear) {
const inputs = this._getInputs();
const firstEmptyInput = inputs.find(input => !input.value);
if (firstEmptyInput && firstEmptyInput !== target) {
firstEmptyInput.focus();
}
}
});
EventHandler.on(this._element, EVENT_INPUT, SELECTOR_FORM_OTP_CONTROL, event => {
const {
target
} = event;
if (target.value.length === 1 && !this._isValidInput(target.value)) {
target.value = '';
return;
}
if (target.value.length === 1) {
const inputs = this._getInputs();
if (!inputs.length) {
return;
}
const currentValue = inputs.map(input => input.value).join('');
this._setHiddenInputValue(currentValue);
const nextInput = index_js.getNextActiveElement(inputs, target, true);
if (nextInput) {
nextInput.focus();
}
this._setInputsTabIndexes();
this._checkAutoSubmit(inputs);
}
});
EventHandler.on(this._element, EVENT_KEYDOWN, SELECTOR_FORM_OTP_CONTROL, event => {
const {
key,
target
} = event;
if (key === BACKSPACE_KEY && target.value === '') {
const inputs = this._getInputs();
if (!inputs.length) {
return;
}
index_js.getNextActiveElement(inputs, target, false).focus();
const currentValue = inputs.map(input => input.value).join('');
this._setHiddenInputValue(currentValue);
this._setInputsTabIndexes();
return;
}
if (key === ARROW_RIGHT_KEY) {
if (this._config.linear && target.value === '') {
return;
}
const inputs = this._getInputs();
if (!inputs.length) {
return;
}
// In RTL mode, right arrow moves to previous input, in LTR mode it moves to next input
const shouldMoveNext = !index_js.isRTL();
index_js.getNextActiveElement(inputs, target, shouldMoveNext).focus();
return;
}
if (key === ARROW_LEFT_KEY) {
const inputs = this._getInputs();
if (!inputs.length) {
return;
}
// In RTL mode, left arrow moves to next input, in LTR mode it moves to previous input
const shouldMoveNext = index_js.isRTL();
index_js.getNextActiveElement(inputs, target, shouldMoveNext).focus();
}
});
EventHandler.on(this._element, EVENT_PASTE, SELECTOR_FORM_OTP_CONTROL, event => {
event.preventDefault();
const pastedData = event.clipboardData.getData('text');
const validChars = this._extractValidChars(pastedData);
if (!validChars) {
return;
}
const inputs = this._getInputs();
const currentIndex = inputs.indexOf(event.target);
for (let i = 0; i < validChars.length && currentIndex + i < inputs.length; i++) {
inputs[currentIndex + i].value = validChars[i];
}
// Focus the next empty input or the last filled input
const nextEmptyIndex = currentIndex + validChars.length;
if (nextEmptyIndex < inputs.length) {
inputs[nextEmptyIndex].focus();
} else {
inputs[inputs.length - 1].focus();
}
this._setHiddenInputValue(validChars);
this._setInputsTabIndexes();
this._checkAutoSubmit(inputs);
});
}
_checkAutoSubmit(inputs) {
if (!this._config.autoSubmit) {
return;
}
// Check if all inputs are filled
const allFilled = inputs.every(input => input.value.length === 1);
if (allFilled) {
// Find the closest form element
const form = this._element.closest('form');
if (form && typeof form.requestSubmit === 'function') {
form.requestSubmit();
}
}
}
_getInputs() {
return SelectorEngine.find(SELECTOR_FORM_OTP_CONTROL, this._element);
}
_createHiddenInput() {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
if (this._config.disabled) {
hiddenInput.disabled = true;
}
if (this._config.id) {
hiddenInput.id = this._config.id;
}
if (this._config.name) {
hiddenInput.name = this._config.name;
}
hiddenInput.value = this._config.value || '';
this._element.append(hiddenInput);
this._inputElement = hiddenInput;
}
_extractValidChars(text) {
switch (this._config.type) {
case 'number':
{
return text.replace(/\D/g, '');
}
default:
{
return text; // Allow all characters for unknown types
}
}
}
_isValidInput(value) {
if (value.length !== 1) {
return false;
}
switch (this._config.type) {
case 'number':
{
return /^\d$/.test(value);
}
default:
{
return /^[^]$/.test(value); // Allow any single character for unknown types
}
}
}
_setHiddenInputValue(value) {
if (this._inputElement) {
this._inputElement.value = value || '';
}
EventHandler.trigger(this._element, EVENT_CHANGE, {
value
});
if (value && value.length === this._getInputs().length) {
EventHandler.trigger(this._element, EVENT_COMPLETE, {
value
});
}
}
_setInputsAttributes() {
const inputs = SelectorEngine.find(SELECTOR_FORM_OTP_CONTROL, this._element);
for (const [index, input] of inputs.entries()) {
input.type = this._config.masked ? 'password' : 'text';
input.maxLength = 1;
input.autocomplete = 'off';
if (this._config.placeholder !== null) {
const placeholder = String(this._config.placeholder);
input.placeholder = placeholder.length > 1 ? placeholder[index] || '' : placeholder;
}
if (this._config.required !== null) {
input.setAttribute('required', true);
}
switch (this._config.type) {
case 'number':
{
input.inputMode = 'numeric';
input.pattern = '[0-9]*';
break;
}
default:
{
input.inputMode = 'text';
input.pattern = '.*';
}
}
if (this._config.disabled) {
input.disabled = true;
}
if (this._config.id) {
input.id = `${this._config.id}-${index}`;
}
if (this._config.name) {
input.name = `${this._config.name}-${index}`;
}
if (this._config.readonly) {
input.readOnly = true;
}
const valueString = String(this._config.value || '');
if (valueString && valueString[index]) {
input.value = valueString[index];
}
if (typeof this._config.ariaLabel === 'function') {
const ariaLabel = this._config.ariaLabel(index, inputs.length);
input.setAttribute('aria-label', ariaLabel);
}
}
}
_setInputsTabIndexes() {
if (!this._config.linear) {
return;
}
const inputs = this._getInputs();
let foundEmpty = false;
for (const input of inputs) {
const hasValue = input.value !== '';
if (hasValue) {
input.removeAttribute('tabindex');
} else if (foundEmpty) {
input.tabIndex = -1;
} else {
// First empty input - should be tabbable
input.removeAttribute('tabindex');
foundEmpty = true;
}
}
}
_setRoleAttribute() {
this._element.setAttribute('role', 'group');
}
// Static
static otpInputInterface(element, config) {
const data = OTPInput.getOrCreateInstance(element, config);
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`);
}
data[config]();
}
}
static jQueryInterface(config) {
return this.each(function () {
const data = OTPInput.getOrCreateInstance(this);
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`);
}
data[config]();
}
});
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const otp of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
OTPInput.otpInputInterface(otp);
}
});
/**
* jQuery
*/
index_js.defineJQueryPlugin(OTPInput);
return OTPInput;
}));
//# sourceMappingURL=otp-input.js.map