budgit/assets/js/inputotp.js
2026-04-12 16:07:06 +00:00

207 lines
6.3 KiB
JavaScript

(function () {
'use strict';
function getEventTargetElement(event) {
return event.target instanceof Element ? event.target : null;
}
// Utility functions
function getSlots(container) {
return Array.from(container.querySelectorAll('[data-tui-inputotp-slot]')).sort(
(a, b) => parseInt(a.getAttribute('data-tui-inputotp-index')) - parseInt(b.getAttribute('data-tui-inputotp-index'))
);
}
function focusSlot(slot) {
if (!slot) return;
slot.focus();
setTimeout(() => slot.select(), 0);
}
function updateHiddenValue(container) {
const hiddenInput = container.querySelector('[data-tui-inputotp-value-target]');
const slots = getSlots(container);
if (hiddenInput && slots.length) {
hiddenInput.value = slots.map(s => s.value).join('');
}
}
function findFirstEmptySlot(container) {
const slots = getSlots(container);
for (const slot of slots) {
if (!slot.value) return slot;
}
return null;
}
function getNextSlot(container, currentSlot) {
const slots = getSlots(container);
const index = slots.indexOf(currentSlot);
return index >= 0 && index < slots.length - 1 ? slots[index + 1] : null;
}
function getPrevSlot(container, currentSlot) {
const slots = getSlots(container);
const index = slots.indexOf(currentSlot);
return index > 0 ? slots[index - 1] : null;
}
// Event handlers
document.addEventListener('input', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
// Handle space as empty
if (slot.value === ' ') {
slot.value = '';
return;
}
// Keep only last character
if (slot.value.length > 1) {
slot.value = slot.value.slice(-1);
}
// Move to next slot if filled
if (slot.value) {
const nextSlot = getNextSlot(container, slot);
if (nextSlot) focusSlot(nextSlot);
}
updateHiddenValue(container);
});
document.addEventListener('keydown', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
if (e.key === 'Backspace') {
e.preventDefault();
if (slot.value) {
slot.value = '';
updateHiddenValue(container);
} else {
const prevSlot = getPrevSlot(container, slot);
if (prevSlot) {
prevSlot.value = '';
updateHiddenValue(container);
focusSlot(prevSlot);
}
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prevSlot = getPrevSlot(container, slot);
if (prevSlot) focusSlot(prevSlot);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
const nextSlot = getNextSlot(container, slot);
if (nextSlot) focusSlot(nextSlot);
}
});
document.addEventListener('focus', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
// Redirect to first empty slot
const firstEmpty = findFirstEmptySlot(container);
if (firstEmpty && firstEmpty !== slot) {
focusSlot(firstEmpty);
return;
}
setTimeout(() => slot.select(), 0);
}, true);
document.addEventListener('paste', (e) => {
const target = getEventTargetElement(e);
const slot = target?.closest('[data-tui-inputotp-slot]');
if (!slot) return;
e.preventDefault();
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
const pastedData = (e.clipboardData || window.clipboardData).getData('text');
const chars = pastedData.replace(/\s/g, '').split('');
const slots = getSlots(container);
let startIndex = slots.indexOf(slot);
for (let i = 0; i < chars.length && startIndex + i < slots.length; i++) {
slots[startIndex + i].value = chars[i];
}
updateHiddenValue(container);
// Focus next empty or last filled slot
const nextEmpty = findFirstEmptySlot(container);
focusSlot(nextEmpty || slots[Math.min(startIndex + chars.length, slots.length - 1)]);
});
// Label click handling
document.addEventListener('click', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('label[for]')) return;
const targetId = target.getAttribute('for');
const hiddenInput = document.getElementById(targetId);
if (!hiddenInput?.matches('[data-tui-inputotp-value-target]')) return;
e.preventDefault();
const container = hiddenInput.closest('[data-tui-inputotp]');
const slots = getSlots(container);
if (slots.length > 0) focusSlot(slots[0]);
});
// Form reset
document.addEventListener('reset', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('form')) return;
target.querySelectorAll('[data-tui-inputotp]').forEach(container => {
getSlots(container).forEach(slot => {
slot.value = '';
});
updateHiddenValue(container);
});
});
// MutationObserver for initial setup and autofocus
new MutationObserver(() => {
document.querySelectorAll('[data-tui-inputotp]').forEach(container => {
const slots = getSlots(container);
if (slots.length === 0) return;
// Set initial value if provided
const initialValue = container.getAttribute('data-tui-inputotp-value');
if (initialValue && !slots[0].value) {
for (let i = 0; i < slots.length && i < initialValue.length; i++) {
if (!slots[i].value) slots[i].value = initialValue[i];
}
updateHiddenValue(container);
}
// Handle autofocus
if (container.hasAttribute('autofocus') && !slots.some(s => s === document.activeElement)) {
requestAnimationFrame(() => {
if (slots[0] && !slots.some(s => s === document.activeElement)) {
focusSlot(slots[0]);
}
});
}
});
}).observe(document.body, { childList: true, subtree: true });
})();