@hauzhengyi/vue3-otp-input
Version:
A fully customizable, OTP (one-time password) input component built with Vue 3.x and Vue Composition API.
514 lines (502 loc) • 17.2 kB
JavaScript
var vue=require('vue');function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = !0,
o = !1;
try {
if (i = (t = t.call(r)).next, 0 === l) {
if (Object(t) !== t) return;
f = !1;
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = !0, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}var script$1 = vue.defineComponent({
name: "SingleOtpInput",
props: {
inputType: {
type: String,
validator: function validator(value) {
return ["number", "tel", "letter-numeric", "password"].includes(value);
},
default: "tel"
},
inputmode: {
type: String,
default: "numeric"
},
value: {
type: [String, Number]
},
separator: {
type: String
},
focus: {
type: Boolean
},
inputClasses: {
type: [String, Array]
},
conditionalClass: {
type: String
},
shouldAutoFocus: {
type: Boolean
},
isLastChild: {
type: Boolean
},
placeholder: {
type: String
},
isDisabled: {
type: Boolean
}
},
emits: ["on-change", "on-keydown", "on-paste", "on-focus", "on-blur"],
setup: function setup(props, _ref) {
var emit = _ref.emit;
var model = vue.ref(props.value || "");
var input = vue.ref(null);
var handleOnChange = function handleOnChange() {
model.value = model.value.toString();
if (model.value.length > 1) {
// model.value = model.value.slice(0, 1);
// for iOS Chrome paste event
return emit("on-paste", model.value);
}
return emit("on-change", model.value);
};
var isCodeLetter = function isCodeLetter(charCode) {
return charCode >= 65 && charCode <= 90;
};
var isCodeNumeric = function isCodeNumeric(charCode) {
return charCode >= 48 && charCode <= 57 || charCode >= 96 && charCode <= 105;
};
// numeric keys and numpad keys
var handleOnKeyDown = function handleOnKeyDown(event) {
if (props.isDisabled) {
event.preventDefault();
}
// Only allow characters 0-9, DEL, Backspace, Enter, Right and Left Arrows, and Pasting
var keyEvent = event || window.event;
var charCode = keyEvent.which ? keyEvent.which : keyEvent.keyCode;
if (isCodeNumeric(charCode) || props.inputType === "letter-numeric" && isCodeLetter(charCode) || [8, 9, 13, 37, 39, 46, 86].includes(charCode)) {
emit("on-keydown", event);
} else {
keyEvent.preventDefault();
}
};
var handleOnPaste = function handleOnPaste(event) {
var _event$clipboardData;
event.preventDefault();
var pastedData = (_event$clipboardData = event.clipboardData) === null || _event$clipboardData === void 0 ? void 0 : _event$clipboardData.getData("text/plain");
emit("on-paste", pastedData);
};
var handleOnFocus = function handleOnFocus() {
input.value.select();
return emit("on-focus");
};
var handleOnBlur = function handleOnBlur() {
return emit("on-blur");
};
vue.watch(function () {
return props.value;
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
model.value = newValue;
}
});
vue.watch(function () {
return props.focus;
}, function (newFocusValue, oldFocusValue) {
// Check if focusedInput changed
// Prevent calling function if input already in focus
if (oldFocusValue !== newFocusValue && input.value && props.focus) {
input.value.focus();
input.value.select();
}
});
vue.onMounted(function () {
if (input.value && props.focus && props.shouldAutoFocus) {
input.value.focus();
input.value.select();
}
});
return {
handleOnChange: handleOnChange,
handleOnKeyDown: handleOnKeyDown,
handleOnPaste: handleOnPaste,
handleOnFocus: handleOnFocus,
handleOnBlur: handleOnBlur,
input: input,
model: model,
inputTypeValue: ["letter-numeric", "number"].includes(props.inputType) ? "text" : props.inputType
};
}
});var _hoisted_1$1 = {
style: {
"display": "flex",
"align-items": "center"
}
};
var _hoisted_2$1 = ["type", "inputmode", "placeholder", "disabled", "maxlength"];
var _hoisted_3 = {
key: 0
};
var _hoisted_4 = ["innerHTML"];
function render$1(_ctx, _cache, $props, $setup, $data, $options) {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [vue.withDirectives(vue.createElementVNode("input", {
"data-test": "single-input",
type: _ctx.inputTypeValue,
inputmode: _ctx.inputmode,
placeholder: _ctx.placeholder,
disabled: _ctx.isDisabled,
ref: "input",
min: "0",
max: "9",
maxlength: _ctx.isLastChild ? '1' : '9',
pattern: "[0-9]",
"onUpdate:modelValue": _cache[0] || (_cache[0] = function ($event) {
return _ctx.model = $event;
}),
class: vue.normalizeClass([_ctx.inputClasses, _ctx.conditionalClass, {
'is-complete': _ctx.model
}]),
onInput: _cache[1] || (_cache[1] = function () {
return _ctx.handleOnChange && _ctx.handleOnChange.apply(_ctx, arguments);
}),
onKeydown: _cache[2] || (_cache[2] = function () {
return _ctx.handleOnKeyDown && _ctx.handleOnKeyDown.apply(_ctx, arguments);
}),
onPaste: _cache[3] || (_cache[3] = function () {
return _ctx.handleOnPaste && _ctx.handleOnPaste.apply(_ctx, arguments);
}),
onFocus: _cache[4] || (_cache[4] = function () {
return _ctx.handleOnFocus && _ctx.handleOnFocus.apply(_ctx, arguments);
}),
onBlur: _cache[5] || (_cache[5] = function () {
return _ctx.handleOnBlur && _ctx.handleOnBlur.apply(_ctx, arguments);
})
}, null, 42, _hoisted_2$1), [[vue.vModelDynamic, _ctx.model]]), !_ctx.isLastChild && _ctx.separator ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3, [vue.createElementVNode("span", {
innerHTML: _ctx.separator
}, null, 8, _hoisted_4)])) : vue.createCommentVNode("", true)]);
}script$1.render = render$1;// keyCode constants
var BACKSPACE = 8;
var LEFT_ARROW = 37;
var RIGHT_ARROW = 39;
var DELETE = 46;
var script = /* #__PURE__ */vue.defineComponent({
name: "Vue3OtpInput",
// vue component name
components: {
SingleOtpInput: script$1
},
props: {
value: {
type: String,
default: "",
required: true
},
numInputs: {
default: 4
},
separator: {
type: String,
default: ""
},
inputClasses: {
type: [String, Array]
},
conditionalClass: {
type: Array,
default: []
},
inputType: {
type: String,
validator: function validator(value) {
return ["number", "tel", "letter-numeric", "password"].includes(value);
}
},
inputmode: {
type: String,
validator: function validator(value) {
return ["numeric", "text", "tel", "none"].includes(value);
},
default: "text"
},
shouldAutoFocus: {
type: Boolean,
default: false
},
placeholder: {
type: Array,
default: []
},
isDisabled: {
type: Boolean,
default: false
},
shouldFocusOrder: {
type: Boolean,
default: false
}
},
setup: function setup(props, _ref) {
var emit = _ref.emit;
var activeInput = vue.ref(0);
var otp = vue.ref([]);
var oldOtp = vue.ref([]);
vue.watch(function () {
return props.value;
}, function (val) {
// fix issue: https://github.com/ejirocodes/vue3-otp-input/issues/34
if (val.length === props.numInputs || otp.value.length === 0) {
var fill = val.split("");
otp.value = fill;
}
}, {
immediate: true
});
var handleOnFocus = function handleOnFocus(index) {
activeInput.value = index;
};
var handleOnBlur = function handleOnBlur() {
activeInput.value = -1;
};
// Helper to return OTP from input
var checkFilledAllInputs = function checkFilledAllInputs() {
if (otp.value.join("").length === props.numInputs) {
emit("update:value", otp.value.join(""));
return emit("on-complete", otp.value.join(""));
}
return "Wait until the user enters the required number of characters";
};
// Focus on input by index
var focusInput = function focusInput(input) {
activeInput.value = Math.max(Math.min(props.numInputs - 1, input), 0);
};
// Focus on next input
var focusNextInput = function focusNextInput() {
focusInput(activeInput.value + 1);
};
// Focus on previous input
var focusPrevInput = function focusPrevInput() {
focusInput(activeInput.value - 1);
};
// Change OTP value at focused input
var changeCodeAtFocus = function changeCodeAtFocus(value) {
oldOtp.value = Object.assign([], otp.value);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
otp.value[activeInput.value] = value;
if (oldOtp.value.join("") !== otp.value.join("")) {
emit("update:value", otp.value.join(""));
emit("on-change", otp.value.join(""));
checkFilledAllInputs();
}
};
// Handle pasted OTP
var handleOnPaste = function handleOnPaste(data) {
var pastedData = data.slice(0, props.numInputs - activeInput.value).split("");
if (props.inputType === "number" && !pastedData.join("").match(/^\d+$/)) {
return "Invalid pasted data";
}
if (props.inputType === "letter-numeric" && !pastedData.join("").match(/^\w+$/)) {
return "Invalid pasted data";
}
// Paste data from focused input onwards
var currentCharsInOtp = otp.value.slice(0, activeInput.value);
var combinedWithPastedData = currentCharsInOtp.concat(pastedData);
combinedWithPastedData.slice(0, props.numInputs).forEach(function (value, i) {
otp.value[i] = value;
});
focusInput(combinedWithPastedData.slice(0, props.numInputs).length);
return checkFilledAllInputs();
};
var handleOnChange = function handleOnChange(value) {
changeCodeAtFocus(value);
focusNextInput();
};
var clearInput = function clearInput() {
if (otp.value.length > 0) {
emit("update:value", "");
emit("on-change", "");
}
otp.value = [];
activeInput.value = 0;
};
var fillInput = function fillInput(value) {
var fill = value.split("");
if (fill.length === props.numInputs) {
otp.value = fill;
emit("update:value", otp.value.join(""));
emit("on-complete", otp.value.join(""));
}
};
// Handle cases of backspace, delete, left arrow, right arrow
var handleOnKeyDown = function handleOnKeyDown(event, index) {
switch (event.keyCode) {
case BACKSPACE:
event.preventDefault();
changeCodeAtFocus("");
focusPrevInput();
break;
case DELETE:
event.preventDefault();
changeCodeAtFocus("");
break;
case LEFT_ARROW:
event.preventDefault();
focusPrevInput();
break;
case RIGHT_ARROW:
event.preventDefault();
focusNextInput();
break;
default:
focusOrder(index);
break;
}
};
/**
*
* @param currentIndex - index of the input
* @description - This function is used to focus the input in the order of the input index
*
* @example
* 1. If the user is entering the OTP in the order of the input index, then the input will be focused in the order of the input index
* 2. If the user is entering the OTP in the reverse order of the input index, then the input will be focused in the reverse order of the input index
*/
var focusOrder = function focusOrder(currentIndex) {
if (props.shouldFocusOrder) {
setTimeout(function () {
var len = otp.value.join("").length;
if (currentIndex - len >= 0) {
activeInput.value = len;
otp.value[currentIndex] = "";
}
}, 100);
}
};
return {
activeInput: activeInput,
otp: otp,
oldOtp: oldOtp,
clearInput: clearInput,
handleOnPaste: handleOnPaste,
handleOnKeyDown: handleOnKeyDown,
handleOnBlur: handleOnBlur,
changeCodeAtFocus: changeCodeAtFocus,
focusInput: focusInput,
focusNextInput: focusNextInput,
focusPrevInput: focusPrevInput,
handleOnFocus: handleOnFocus,
checkFilledAllInputs: checkFilledAllInputs,
handleOnChange: handleOnChange,
fillInput: fillInput
};
}
});var _hoisted_1 = {
style: {
"display": "flex"
},
class: "otp-input-container"
};
var _hoisted_2 = {
key: 0,
autocomplete: "off",
name: "hidden",
type: "text",
style: {
"display": "none"
}
};
function render(_ctx, _cache, $props, $setup, $data, $options) {
var _component_SingleOtpInput = vue.resolveComponent("SingleOtpInput");
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [_ctx.inputType === 'password' ? (vue.openBlock(), vue.createElementBlock("input", _hoisted_2)) : vue.createCommentVNode("", true), (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(_ctx.numInputs, function (_, i) {
return vue.openBlock(), vue.createBlock(_component_SingleOtpInput, {
key: i,
focus: _ctx.activeInput === i,
value: _ctx.otp[i],
separator: _ctx.separator,
"input-type": _ctx.inputType,
inputmode: _ctx.inputmode,
"input-classes": _ctx.inputClasses,
conditionalClass: _ctx.conditionalClass[i],
"is-last-child": i === _ctx.numInputs - 1,
"should-auto-focus": _ctx.shouldAutoFocus,
placeholder: _ctx.placeholder[i],
"is-disabled": _ctx.isDisabled,
onOnChange: _ctx.handleOnChange,
onOnKeydown: function onOnKeydown($event) {
return _ctx.handleOnKeyDown($event, i);
},
onOnPaste: _ctx.handleOnPaste,
onOnFocus: function onOnFocus($event) {
return _ctx.handleOnFocus(i);
},
onOnBlur: _ctx.handleOnBlur
}, null, 8, ["focus", "value", "separator", "input-type", "inputmode", "input-classes", "conditionalClass", "is-last-child", "should-auto-focus", "placeholder", "is-disabled", "onOnChange", "onOnKeydown", "onOnPaste", "onOnFocus", "onOnBlur"]);
}), 128))]);
}script.render = render;// Import vue component
// Define typescript interfaces for installable component
// Default export is installable instance of component.
// IIFE injects install function into component, allowing component
// to be registered via Vue.use() as well as Vue.component(),
var component = /*#__PURE__*/(function () {
// Assign InstallableComponent type
var installable = script;
// Attach install function executed by Vue.use()
installable.install = function (app) {
app.component("Vue3OtpInput", installable);
};
return installable;
})();
// It's possible to expose named exports when writing components that can
// also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo';
// export const RollupDemoDirective = directive;
var namedExports=/*#__PURE__*/Object.freeze({__proto__:null,'default':component});// Attach named exports directly to component. IIFE/CJS will
// only expose one global var, with named exports exposed as properties of
// that global var (eg. plugin.namedExport)
Object.entries(namedExports).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
exportName = _ref2[0],
exported = _ref2[1];
if (exportName !== 'default') component[exportName] = exported;
});module.exports=component;
;