91cb011ded
* CodeInput. verify-email page * middleware * Loading toast * Countdown only for registration * Improve middleware * Fix middleware breaking auth flow * Remove old notifications * Remove old onboarding. New segmentation * Remove skip button * Block verify email when verified * useUserEmails composable. Cancel addition * Move user emails queries * Fix fragments etc * redirect updates * HeaderWithEmptyPage * Check env before enforcing * Join workspace * Updates * Fix console warnings on login * Fix register console warnings * Working cache updates * Verify secondary email * Force onboarding off * EMAIL WIP * useIsJustRegistered state * Improve isRequired * Uneeded change * Improved slots * Updates from CR * CR comments * Only show message if forced * Update onboarding middleware * Update loading bar * ref > computed to fix onboarding * Resend tooltip. Better errors * Add other to form. * Email changes * Updates to emails * Remove force email FF * Remove FF's * Hide header on embed * Update graphql.ts * Re-add FF * Update graphql.ts * GQL Fragments * Fix build
152 lines
3.8 KiB
Vue
152 lines
3.8 KiB
Vue
<template>
|
|
<div class="flex flex-col items-center">
|
|
<div class="flex gap-2">
|
|
<div v-for="(_, index) in digitCount" :key="index" class="w-10">
|
|
<FormTextInput
|
|
ref="inputRefs"
|
|
v-model="digits[index]"
|
|
class="text-center !text-[14px] py-6 !px-2 font-semibold"
|
|
color="foundation"
|
|
:name="`code-${index}`"
|
|
type="text"
|
|
inputmode="numeric"
|
|
:disabled="disabled"
|
|
:error="internalError"
|
|
:custom-error-message="internalError ? ' ' : undefined"
|
|
maxlength="1"
|
|
size="lg"
|
|
@input="onInput(index)"
|
|
@keydown="onKeyDown(index, $event)"
|
|
@paste="onPaste"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref, watch } from 'vue'
|
|
import { FormTextInput } from '~~/src/lib'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
modelValue: string
|
|
digitCount?: number
|
|
disabled?: boolean
|
|
error?: boolean
|
|
clearErrorOnEdit?: boolean
|
|
}>(),
|
|
{
|
|
digitCount: 6,
|
|
clearErrorOnEdit: true
|
|
}
|
|
)
|
|
|
|
const emit = defineEmits(['update:modelValue', 'complete'])
|
|
|
|
const inputRefs = ref<Array<HTMLInputElement | null>>([])
|
|
const digits = ref<string[]>(new Array(props.digitCount).fill('') as string[])
|
|
const internalError = ref(props.error)
|
|
|
|
const onInput = (index: number) => {
|
|
if (props.clearErrorOnEdit) {
|
|
internalError.value = false
|
|
}
|
|
|
|
digits.value[index] = digits.value[index].replace(/[^0-9]/g, '')
|
|
|
|
// Move to next input if available
|
|
if (digits.value[index] && index < props.digitCount - 1) {
|
|
inputRefs.value[index + 1]?.focus()
|
|
}
|
|
}
|
|
|
|
const onKeyDown = (index: number, event: KeyboardEvent) => {
|
|
if (event.key === 'Backspace' && !digits.value[index] && index > 0) {
|
|
if (props.clearErrorOnEdit) {
|
|
internalError.value = false
|
|
}
|
|
// Move to previous input on backspace if current is empty
|
|
digits.value[index - 1] = ''
|
|
inputRefs.value[index - 1]?.focus()
|
|
} else if (event.key === 'ArrowLeft' && index > 0) {
|
|
// Move to previous input on left arrow
|
|
inputRefs.value[index - 1]?.focus()
|
|
} else if (event.key === 'ArrowRight' && index < props.digitCount - 1) {
|
|
// Move to next input on right arrow
|
|
inputRefs.value[index + 1]?.focus()
|
|
}
|
|
}
|
|
|
|
const onPaste = (event: ClipboardEvent) => {
|
|
if (props.clearErrorOnEdit) {
|
|
internalError.value = false
|
|
}
|
|
event.preventDefault()
|
|
const pastedData = event.clipboardData?.getData('text')
|
|
if (!pastedData) return
|
|
|
|
const numbers = pastedData.replace(/[^0-9]/g, '').split('')
|
|
|
|
digits.value = [
|
|
...numbers.slice(0, props.digitCount),
|
|
...(Array(Math.max(0, props.digitCount - numbers.length)).fill('') as string[])
|
|
]
|
|
|
|
// Focus the next empty input or the last input
|
|
const nextEmptyIndex = digits.value.findIndex((d) => !d)
|
|
if (nextEmptyIndex !== -1) {
|
|
inputRefs.value[nextEmptyIndex]?.focus()
|
|
} else {
|
|
inputRefs.value[props.digitCount - 1]?.focus()
|
|
}
|
|
}
|
|
|
|
// Focus first input on mount
|
|
onMounted(() => {
|
|
if (inputRefs.value[0]) {
|
|
inputRefs.value[0].focus()
|
|
}
|
|
})
|
|
|
|
// Watch external error prop changes
|
|
watch(
|
|
() => props.error,
|
|
(newValue) => {
|
|
internalError.value = newValue
|
|
}
|
|
)
|
|
|
|
// Watch for external value changes
|
|
watch(
|
|
() => props.modelValue,
|
|
(newValue) => {
|
|
if (newValue) {
|
|
const newDigits = newValue.split('')
|
|
digits.value = [
|
|
...newDigits,
|
|
...(Array(props.digitCount - newDigits.length).fill('') as string[])
|
|
]
|
|
} else {
|
|
digits.value = Array(props.digitCount).fill('') as string[]
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Watch for completion
|
|
watch(
|
|
digits,
|
|
(newDigits) => {
|
|
const value = newDigits.join('')
|
|
emit('update:modelValue', value)
|
|
|
|
// Emit complete when all digits are filled
|
|
if (value.length === props.digitCount) {
|
|
emit('complete', value)
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
</script>
|