348 lines
8.3 KiB
Vue
348 lines
8.3 KiB
Vue
<template>
|
|
<div class="pin-codes-input" :style="`grid-template-columns: repeat(${length}, minmax(0, 1fr))`">
|
|
<div class="pin-code-input pin-code-input-outline"
|
|
:class="{ 'pin-code-input-focued': codes[index]!.focused }" :key="index"
|
|
v-for="(_, index) in codes">
|
|
<input ref="pin-code-input" min="0" maxlength="1" pattern="[0-9]*"
|
|
:value="codes[index]!.value"
|
|
:type="codes[index]!.inputType"
|
|
:disabled="disabled ? true : undefined"
|
|
:autofocus="autofocus && index === 0 ? true : undefined"
|
|
@focus="codes[index]!.focused = true"
|
|
@blur="codes[index]!.focused = false"
|
|
@keydown="onKeydown(index, $event)"
|
|
@paste="onPaste(index, $event)"
|
|
@change="onInput(index, $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch, useTemplateRef } from 'vue';
|
|
|
|
import { useI18n } from '@/locales/helpers.ts';
|
|
|
|
import { NumeralSystem } from '@/core/numeral.ts';
|
|
|
|
interface PinCode {
|
|
value: string;
|
|
inputType: string;
|
|
inputTimer: unknown | null;
|
|
focused: boolean;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
modelValue: string;
|
|
length: number;
|
|
disabled?: boolean;
|
|
autofocus?: boolean;
|
|
autoConfirm?: boolean;
|
|
secure?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: string): void;
|
|
(e: 'pincode:confirm', value: string): void;
|
|
}>();
|
|
|
|
const { getCurrentNumeralSystemType } = useI18n();
|
|
|
|
const pinCodeInputs = useTemplateRef<HTMLInputElement[]>('pin-code-input');
|
|
|
|
const codes = ref<PinCode[]>([]);
|
|
|
|
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
|
|
|
const finalPinCode = computed<string>(() => {
|
|
let ret = '';
|
|
|
|
for (const code of codes.value) {
|
|
if (code.value) {
|
|
ret += code.value;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
|
|
function init(length: number, value: string): void {
|
|
codes.value.length = 0;
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
const code: PinCode = {
|
|
value: '',
|
|
inputType: 'tel',
|
|
inputTimer: null,
|
|
focused: false
|
|
};
|
|
|
|
if (value && value[i]) {
|
|
code.value = value.charAt(i);
|
|
|
|
if (props.secure) {
|
|
code.inputType = 'password';
|
|
}
|
|
}
|
|
|
|
codes.value.push(code);
|
|
}
|
|
}
|
|
|
|
function autoFillText(index: number, text: string): void {
|
|
let lastIndex = index;
|
|
|
|
for (let i = index, j = 0; i < codes.value.length && j < text.length; i++, j++) {
|
|
const ch = text.charAt(j);
|
|
|
|
if (ch < '0' || ch > '9') {
|
|
codes.value[i]!.value = '';
|
|
break;
|
|
}
|
|
|
|
codes.value[i]!.value = ch;
|
|
setInputType(i);
|
|
lastIndex = i;
|
|
}
|
|
|
|
setFocus(lastIndex);
|
|
|
|
if (finalPinCode.value.length === length) {
|
|
emit('pincode:confirm', finalPinCode.value);
|
|
}
|
|
}
|
|
|
|
function setInputType(index: number): void {
|
|
if (!props.secure) {
|
|
return;
|
|
}
|
|
|
|
const code = codes.value[index];
|
|
|
|
if (!code) {
|
|
return;
|
|
}
|
|
|
|
if (!code.value) {
|
|
code.inputType = 'tel';
|
|
return;
|
|
}
|
|
|
|
if (code.inputTimer) {
|
|
return;
|
|
}
|
|
|
|
code.inputTimer = setTimeout(() => {
|
|
if (code.value) {
|
|
code.inputType = 'password';
|
|
} else {
|
|
code.inputType = 'tel';
|
|
}
|
|
|
|
code.inputTimer = null;
|
|
}, 300);
|
|
}
|
|
|
|
function setFocus(index: number): void {
|
|
if (pinCodeInputs.value && pinCodeInputs.value[index]) {
|
|
pinCodeInputs.value[index].focus();
|
|
pinCodeInputs.value[index].select();
|
|
}
|
|
}
|
|
|
|
function setPreviousFocus(index: number): void {
|
|
if (index > 0) {
|
|
setFocus(index - 1);
|
|
}
|
|
}
|
|
|
|
function setNextFocus(index: number): void {
|
|
if (index < props.length - 1) {
|
|
setFocus(index + 1);
|
|
}
|
|
}
|
|
|
|
function onKeydown(index: number, event: KeyboardEvent): void {
|
|
if (event.altKey || (event.key.indexOf('F') === 0 && (event.key.length === 2 || event.key.length === 3))) {
|
|
return;
|
|
}
|
|
|
|
if (index <= 0 && (event.shiftKey && event.key === 'Tab')) {
|
|
return;
|
|
}
|
|
|
|
if (index >= props.length - 1 && (!event.shiftKey && event.key === 'Tab')) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Enter' && finalPinCode.value.length === props.length) {
|
|
emit('pincode:confirm', finalPinCode.value);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) {
|
|
setPreviousFocus(index);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) {
|
|
setNextFocus(index);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Home') {
|
|
setFocus(0);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'End') {
|
|
setFocus(props.length - 1);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (((event.ctrlKey || event.metaKey) && event.key === 'v') || event.key === 'Paste') {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Del') {
|
|
for (let i = index; i < codes.value.length; i++) {
|
|
codes.value[i]!.value = '';
|
|
setInputType(i);
|
|
}
|
|
|
|
if (event.code === 'Backspace') {
|
|
setPreviousFocus(index);
|
|
}
|
|
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key.length === 1) {
|
|
let digit = '';
|
|
|
|
if (NumeralSystem.WesternArabicNumerals.isDigit(event.key)) {
|
|
digit = event.key;
|
|
} else if (numeralSystem.value.isDigit(event.key)) {
|
|
digit = numeralSystem.value.replaceLocalizedDigitsToWesternArabicDigits(event.key);
|
|
}
|
|
|
|
if (digit) {
|
|
codes.value[index]!.value = digit;
|
|
setInputType(index);
|
|
setNextFocus(index);
|
|
|
|
if (props.autoConfirm && finalPinCode.value.length === props.length) {
|
|
emit('pincode:confirm', finalPinCode.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
function onPaste(index: number, event: ClipboardEvent): void {
|
|
if (!event.clipboardData) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
const text = event.clipboardData.getData('Text');
|
|
|
|
if (!text) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
autoFillText(index, text);
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
function onInput(index: number, event: Event | { target: { value: string }, preventDefault: () => void }): void {
|
|
if (!event.target || !(event.target as { value: string }).value) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
autoFillText(index, (event.target as { value: string }).value);
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
watch(() => props.length, newValue => {
|
|
init(newValue, props.modelValue);
|
|
});
|
|
|
|
watch(() => props.modelValue, newValue => {
|
|
if (newValue === finalPinCode.value) {
|
|
return;
|
|
}
|
|
|
|
init(props.length, newValue);
|
|
});
|
|
|
|
watch(codes, () => {
|
|
emit('update:modelValue', finalPinCode.value);
|
|
}, {
|
|
deep: true
|
|
});
|
|
|
|
init(props.length, props.modelValue);
|
|
</script>
|
|
|
|
<style>
|
|
.pin-codes-input {
|
|
--ebk-pin-code-border-color: #bbb;
|
|
--ebk-pin-code-focued-color: #c67e48;
|
|
--ebk-pin-code-border-radius: 8px;
|
|
--ebk-pin-code-input-height: 46px;
|
|
--ebk-pin-code-input-gap: 8px;
|
|
--ebk-pin-code-transition-duration: 200ms;
|
|
display: grid;
|
|
gap: var(--ebk-pin-code-input-gap);
|
|
}
|
|
|
|
.pin-code-input {
|
|
position: relative;
|
|
}
|
|
|
|
.pin-code-input input {
|
|
text-align: center;
|
|
padding-inline-start: 10px;
|
|
padding-inline-end: 10px;
|
|
width: 100%;
|
|
height: var(--ebk-pin-code-input-height) !important;
|
|
}
|
|
|
|
.pin-code-input input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.pin-code-input-outline::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
border: 1px solid var(--ebk-pin-code-border-color);
|
|
border-radius: var(--ebk-pin-code-border-radius);
|
|
pointer-events: none;
|
|
box-sizing: border-box;
|
|
transition-duration: var(--ebk-pin-code-transition-duration);
|
|
}
|
|
|
|
.pin-code-input-outline.pin-code-input-focued::after {
|
|
border-width: 2px;
|
|
border-color: var(--ebk-pin-code-focued-color);
|
|
}
|
|
</style>
|