mirror of
https://github.com/immich-app/immich.git
synced 2026-05-28 02:22:34 -04:00
ac6938a629
* fix(web): allow pasting PIN code from clipboard or password manager The keydown handler was blocking Ctrl+V/Cmd+V since it called preventDefault() on all non-numeric keys. Also adds an onpaste handler to distribute pasted digits across the individual inputs. * refactor: handle paste in handleInput, remove maxlength * cleanup + fix digit focus --------- Co-authored-by: Preslav Penchev <preslav.penchev@acronis.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
130 lines
3.3 KiB
Svelte
130 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { Label } from '@immich/ui';
|
|
import { onMount } from 'svelte';
|
|
|
|
interface Props {
|
|
label: string;
|
|
value?: string;
|
|
pinLength?: number;
|
|
tabindexStart?: number;
|
|
autofocus?: boolean;
|
|
onFilled?: (value: string) => void;
|
|
type?: 'text' | 'password';
|
|
}
|
|
|
|
let {
|
|
label,
|
|
value = $bindable(''),
|
|
pinLength = 6,
|
|
tabindexStart = 0,
|
|
autofocus = false,
|
|
onFilled,
|
|
type = 'text',
|
|
}: Props = $props();
|
|
|
|
let pinValues = $state(Array.from({ length: pinLength }).fill(''));
|
|
let pinCodeInputElements: HTMLInputElement[] = $state([]);
|
|
|
|
$effect(() => {
|
|
if (value === '') {
|
|
pinValues = Array.from({ length: pinLength }).fill('');
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
if (autofocus) {
|
|
pinCodeInputElements[0]?.focus();
|
|
}
|
|
});
|
|
|
|
const focusNext = (index: number) => {
|
|
pinCodeInputElements[Math.min(index + 1, pinLength - 1)]?.focus();
|
|
};
|
|
|
|
const focusPrev = (index: number) => {
|
|
if (index > 0) {
|
|
pinCodeInputElements[index - 1]?.focus();
|
|
}
|
|
};
|
|
|
|
const handleInput = (event: Event, index: number) => {
|
|
const target = event.target as HTMLInputElement;
|
|
const digits = target.value.replaceAll(/\D/g, '').slice(0, pinLength - index);
|
|
|
|
if (digits.length === 0) {
|
|
pinValues[index] = '';
|
|
value = pinValues.join('').trim();
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < digits.length; i++) {
|
|
pinValues[index + i] = digits[i];
|
|
}
|
|
|
|
value = pinValues.join('').trim();
|
|
|
|
const lastFilledIndex = Math.min(index + digits.length, pinLength - 1);
|
|
pinCodeInputElements[lastFilledIndex]?.focus();
|
|
|
|
if (value.length === pinLength) {
|
|
onFilled?.(value);
|
|
}
|
|
};
|
|
|
|
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
|
|
const target = event.currentTarget as HTMLInputElement;
|
|
const index = pinCodeInputElements.indexOf(target);
|
|
|
|
switch (event.key) {
|
|
case 'Tab': {
|
|
return;
|
|
}
|
|
case 'Backspace': {
|
|
if (target.value === '' && index > 0) {
|
|
focusPrev(index);
|
|
pinValues[index - 1] = '';
|
|
} else if (target.value !== '') {
|
|
pinValues[index] = '';
|
|
}
|
|
value = pinValues.join('').trim();
|
|
return;
|
|
}
|
|
case 'ArrowLeft': {
|
|
if (index > 0) {
|
|
focusPrev(index);
|
|
}
|
|
return;
|
|
}
|
|
case 'ArrowRight': {
|
|
if (index < pinLength - 1) {
|
|
focusNext(index);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-1">
|
|
{#if label}
|
|
<Label for={pinCodeInputElements[0]?.id}>{label}</Label>
|
|
{/if}
|
|
<div class="flex gap-2">
|
|
{#each { length: pinLength } as _, index (index)}
|
|
<input
|
|
tabindex={tabindexStart + index}
|
|
{type}
|
|
inputmode="numeric"
|
|
pattern="[0-9]*"
|
|
bind:this={pinCodeInputElements[index]}
|
|
id="pin-code-{index}"
|
|
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
|
|
bind:value={pinValues[index]}
|
|
onkeydown={handleKeydown}
|
|
oninput={(event) => handleInput(event, index)}
|
|
aria-label={`PIN digit ${index + 1} of ${pinLength}${label ? ` for ${label}` : ''}`}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
</div>
|