initial
This commit is contained in:
209
layers/ui/components/code-input/index.vue
Normal file
209
layers/ui/components/code-input/index.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div :class="[cn.b(), cn.is('invalid', invalid)]">
|
||||
<div :class="[cn.e('label')]">
|
||||
<slot name="title">
|
||||
<p v-if="!hideTitle">
|
||||
{{ title }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<div :class="[cn.e('wrapper')]">
|
||||
<input
|
||||
v-for="i in 6"
|
||||
:key="i"
|
||||
ref="digitInputs"
|
||||
type="text"
|
||||
:class="[cn.e('digit')]"
|
||||
:value="value.charAt(i - 1)"
|
||||
inputmode="numeric"
|
||||
@focus="handleFocus(i)"
|
||||
@blur="onBlur"
|
||||
@keydown="handleKeydown(i, $event)"
|
||||
@paste.prevent="handleClipboardPaste"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
v-if="invalid"
|
||||
:class="[cn.e('validation-message')]"
|
||||
>
|
||||
{{ realMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, toRef } from 'vue'
|
||||
import { useField } from 'vee-validate'
|
||||
import useClassname from '../../composables/use-classname'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '2FA',
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const cn = useClassname('ui-code-input')
|
||||
const { value, errorMessage, meta, handleChange, handleBlur } = useField(
|
||||
toRef(props, 'id'),
|
||||
'required|length:6',
|
||||
{
|
||||
validateOnValueUpdate: false,
|
||||
initialValue: props.modelValue,
|
||||
syncVModel: true,
|
||||
},
|
||||
)
|
||||
|
||||
const digitInputs = ref(null)
|
||||
let ignoreBlurEvent = false
|
||||
|
||||
const invalid = computed(
|
||||
() => props.invalid || (!!errorMessage.value && meta.touched),
|
||||
)
|
||||
|
||||
const realMessage = computed(() =>
|
||||
props.invalid ? t('validation.invalid_code') : errorMessage.value,
|
||||
)
|
||||
|
||||
function handleFocus(index) {
|
||||
if (value.value.length < 6 && index > value.value.length)
|
||||
digitInputs.value[value.value.length].focus()
|
||||
}
|
||||
|
||||
async function handleKeydown(index, event) {
|
||||
const currentValue = value.value
|
||||
|
||||
let nextInput
|
||||
if (/^\d+$/.test(event.key)) {
|
||||
const value
|
||||
= currentValue.substring(0, index - 1)
|
||||
+ event.key
|
||||
+ currentValue.substring(index)
|
||||
|
||||
handleChange(value, false)
|
||||
|
||||
nextInput = digitInputs.value[index]
|
||||
}
|
||||
else if (event.key === 'Backspace' && currentValue) {
|
||||
const currentChar = currentValue.substring(index - 1, index)
|
||||
|
||||
handleChange(value.value.substring(0, index - 1), false)
|
||||
|
||||
if (!currentChar)
|
||||
nextInput = digitInputs.value[Math.max(index - 2, 0)]
|
||||
}
|
||||
|
||||
if (
|
||||
!event.ctrlKey
|
||||
&& !event.metaKey
|
||||
&& !['Tab', 'Enter'].includes(event.key)
|
||||
)
|
||||
event.preventDefault()
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (nextInput) {
|
||||
ignoreBlurEvent = true
|
||||
|
||||
event.target.blur()
|
||||
nextInput.focus()
|
||||
|
||||
ignoreBlurEvent = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClipboardPaste(event) {
|
||||
const value = event.clipboardData?.getData('text')
|
||||
|
||||
if (/^\d{6}/.test(value)) {
|
||||
handleChange(value.substring(0, 6))
|
||||
event.target.blur()
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur(event) {
|
||||
if (ignoreBlurEvent)
|
||||
return
|
||||
|
||||
handleBlur(event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus)
|
||||
digitInputs.value[0].focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-code-input {
|
||||
text-align: left;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include txt-m-b('code-input-label');
|
||||
|
||||
margin-bottom: var(--code-input-label-margin, 8px);
|
||||
}
|
||||
|
||||
&__digit {
|
||||
--border-color: #{$clr-grey-300};
|
||||
--border-width: 1px;
|
||||
|
||||
width: var(--code-input-size, 62px);
|
||||
height: var(--code-input-size, 62px);
|
||||
border-radius: var(--code-input-border-radius, 16px);
|
||||
outline: var(--border-width) solid var(--border-color);
|
||||
outline-offset: calc(var(--border-width) * -1);
|
||||
border: none;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
transition: outline-color 0.2s ease-out;
|
||||
caret-color: $clr-cyan-500;
|
||||
|
||||
&:focus {
|
||||
--border-width: 2px;
|
||||
--border-color: #{$clr-cyan-500};
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: var(--code-input-gap, 25px);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
--code-input-border-color: #{$clr-red-500};
|
||||
}
|
||||
|
||||
&__validation-message {
|
||||
margin-top: 8px;
|
||||
color: $clr-red-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user