Newer
Older
FirstLaravel12 / resources / views / components / input-otp.blade.php
@props([
    'digits' => 6,
    'name' => 'code',
])

<div
    @focus-2fa-auth-code.window="$refs.input1?.focus()"
    @clear-2fa-auth-code.window="clearAll()"
    class="relative"
    x-data="{
        totalDigits: @js($digits),
        digitIndices: @js(range(1, $digits)),
        init() {
            $nextTick(() => {
                this.$refs.input1?.focus();
            });
        },
        getInput(index) {
            return this.$refs['input' + index];
        },
        setValue(index, value) {
            this.getInput(index).value = value;
        },
        getCode() {
            return this.digitIndices
                .map(i => this.getInput(i).value)
                .join('');
        },
        updateHiddenField() {
            this.$refs.code.value = this.getCode();
            this.$refs.code.dispatchEvent(new Event('input', { bubbles: true }));
            this.$refs.code.dispatchEvent(new Event('change', { bubbles: true }));
        },
        handleNumberKey(index, key) {
            this.setValue(index, key);

            if (index < this.totalDigits) {
                this.getInput(index + 1).focus();
            }

            $nextTick(() => {
                this.updateHiddenField();
            });
        },
        handleBackspace(index) {
            const currentInput = this.getInput(index);

            if (currentInput.value !== '') {
                currentInput.value = '';
                this.updateHiddenField();
                return;
            }

            if (index <= 1) {
                return;
            }

            const previousInput = this.getInput(index - 1);
    
            previousInput.value = '';
            previousInput.focus();

            this.updateHiddenField();
        },
        handleKeyDown(index, event) {
            const key = event.key;

            if (/^[0-9]$/.test(key)) {
                event.preventDefault();
                this.handleNumberKey(index, key);
                return;
            }

            if (key === 'Backspace') {
                event.preventDefault();
                this.handleBackspace(index);
                return;
            }
        },
        handlePaste(event) {
            event.preventDefault();

            const pastedText = (event.clipboardData || window.clipboardData).getData('text');
            const numericOnly = pastedText.replace(/[^0-9]/g, '');
            const digitsToFill = Math.min(numericOnly.length, this.totalDigits);

            this.digitIndices
                .slice(0, digitsToFill)
                .forEach(index => {
                    this.setValue(index, numericOnly[index - 1]);
                });

            if (numericOnly.length >= this.totalDigits) {
                this.updateHiddenField();
            }
        },
        clearAll() {
            this.digitIndices.forEach(index => {
                this.setValue(index, '');
            });

            this.$refs.code.value = '';
            this.$refs.input1?.focus();
        }
    }"
>
    <div class="flex items-center">
        @for ($x = 1; $x <= $digits; $x++)
            <input
                x-ref="input{{ $x }}"
                type="text"
                inputmode="numeric"
                pattern="[0-9]"
                maxlength="1"
                autocomplete="off"
                @paste="handlePaste"
                @keydown="handleKeyDown({{ $x }}, $event)"
                @focus="$el.select()"
                @input="$el.value = $el.value.replace(/[^0-9]/g, '').slice(0, 1)"
                @class([
                    'flex size-10 items-center justify-center border border-zinc-300 bg-accent-foreground text-center text-sm font-medium text-accent-content transition-colors focus:border-accent focus:border-2 focus:outline-none focus:relative focus:z-10 dark:border-zinc-700 dark:focus:border-accent',
                    'rounded-l-md' => $x === 1,
                    'rounded-r-md' => $x === $digits,
                    '-ml-px' => $x > 1,
                ])
            />
        @endfor
    </div>

    <input
        {{ $attributes->except(['digits']) }}
        type="hidden"
        x-ref="code"
        name="{{ $name }}"
        minlength="{{ $digits }}"
        maxlength="{{ $digits }}"
    />
</div>