UNPKG

@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
'use strict';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;