| 1 | // Inline button two-click confirmation for destructive actions. |
| 2 | // First click arms, second click confirms, timeout resets. |
| 3 | |
| 4 | import { ICON_SELECTOR, getIconName, setIconName } from "./icons.js"; |
| 5 | |
| 6 | const CONFIRM_TIMEOUT = 2000; |
| 7 | const CONFIRM_CLASS = 'confirming'; |
| 8 | const CONFIRM_ICON = 'check'; |
| 9 | const CONFIRM_TEXT = 'Confirm'; |
| 10 | |
| 11 | const buttonStates = new WeakMap(); |
| 12 | |
| 13 | // Handles inline two-click confirmation for a button. |
| 14 | export function confirmClick(event, action) { |
| 15 | const button = event.currentTarget; |
| 16 | if (!button) return; |
| 17 | |
| 18 | const state = buttonStates.get(button); |
| 19 | |
| 20 | if (state?.confirming) { |
| 21 | clearTimeout(state.timeoutId); |
| 22 | resetButton(button, state); |
| 23 | buttonStates.delete(button); |
| 24 | action(); |
| 25 | } else { |
| 26 | const iconEl = button.querySelector(ICON_SELECTOR); |
| 27 | const isIconButton = iconEl && button.textContent.trim() === iconEl.textContent.trim(); |
| 28 | |
| 29 | const newState = { |
| 30 | confirming: true, |
| 31 | isIconButton, |
| 32 | originalIcon: getIconName(iconEl), |
| 33 | originalHTML: isIconButton ? null : button.innerHTML, |
| 34 | timeoutId: setTimeout(() => { |
| 35 | resetButton(button, newState); |
| 36 | buttonStates.delete(button); |
| 37 | }, CONFIRM_TIMEOUT) |
| 38 | }; |
| 39 | |
| 40 | buttonStates.set(button, newState); |
| 41 | button.classList.add(CONFIRM_CLASS); |
| 42 | |
| 43 | if (isIconButton && iconEl) { |
| 44 | // Icon-only button: just swap icon |
| 45 | setIconName(iconEl, CONFIRM_ICON); |
| 46 | } else { |
| 47 | // Text button: show icon + optional "Confirm" text |
| 48 | const originalText = button.textContent.trim(); |
| 49 | const confirmContent = originalText.length >= 4 |
| 50 | ? `<x-icon name="${CONFIRM_ICON}"></x-icon>${CONFIRM_TEXT}` |
| 51 | : `<x-icon name="${CONFIRM_ICON}"></x-icon>`; |
| 52 | button.innerHTML = confirmContent; |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // Reset button to original state |
| 58 | function resetButton(button, state) { |
| 59 | button.classList.remove(CONFIRM_CLASS); |
| 60 | if (state.isIconButton) { |
| 61 | const iconEl = button.querySelector(ICON_SELECTOR); |
| 62 | if (iconEl && state.originalIcon) { |
| 63 | setIconName(iconEl, state.originalIcon); |
| 64 | } |
| 65 | } else if (state.originalHTML) { |
| 66 | button.innerHTML = state.originalHTML; |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // Register Alpine magic helper |
| 71 | export function registerAlpineMagic() { |
| 72 | if (globalThis.Alpine) { |
| 73 | globalThis.Alpine.magic('confirmClick', () => confirmClick); |
| 74 | } |
| 75 | } |