158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
/**
|
|
* User confirmation utility
|
|
* Non-blocking confirmation overlay to avoid `window.confirm()` (`no-alert`).
|
|
* Used for critical confirmations (e.g. destructive actions) without requiring
|
|
* a React modal or additional global state.
|
|
*/
|
|
export function userConfirm(message: string): Promise<boolean> {
|
|
return confirmOverlay(message)
|
|
}
|
|
|
|
type ConfirmOverlayElements = {
|
|
overlay: HTMLDivElement
|
|
cancel: HTMLButtonElement
|
|
confirm: HTMLButtonElement
|
|
}
|
|
|
|
type ConfirmOverlayHandlerParams = ConfirmOverlayElements & {
|
|
resolve: (value: boolean) => void
|
|
}
|
|
|
|
function confirmOverlay(message: string): Promise<boolean> {
|
|
const doc = globalThis.document
|
|
if (!doc) {
|
|
return Promise.resolve(false)
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const { overlay, cancel, confirm } = buildConfirmOverlay(doc, message)
|
|
doc.body.append(overlay)
|
|
overlay.focus()
|
|
attachConfirmOverlayHandlers({ overlay, cancel, confirm, resolve })
|
|
})
|
|
}
|
|
|
|
function buildConfirmOverlay(doc: Document, message: string): ConfirmOverlayElements {
|
|
const overlay = createOverlay(doc)
|
|
const panel = createPanel(doc)
|
|
const text = createText(doc, message)
|
|
const buttons = createButtonsContainer(doc)
|
|
const cancel = createCancelButton(doc)
|
|
const confirm = createConfirmButton(doc)
|
|
buttons.append(cancel, confirm)
|
|
panel.append(text, buttons)
|
|
overlay.append(panel)
|
|
|
|
return { overlay, cancel, confirm }
|
|
}
|
|
|
|
function createOverlay(doc: Document): HTMLDivElement {
|
|
const overlay = doc.createElement('div')
|
|
overlay.setAttribute('role', 'dialog')
|
|
overlay.setAttribute('aria-modal', 'true')
|
|
overlay.tabIndex = -1
|
|
overlay.style.position = 'fixed'
|
|
overlay.style.inset = '0'
|
|
overlay.style.background = 'rgba(0,0,0,0.6)'
|
|
overlay.style.display = 'flex'
|
|
overlay.style.alignItems = 'center'
|
|
overlay.style.justifyContent = 'center'
|
|
overlay.style.zIndex = '9999'
|
|
return overlay
|
|
}
|
|
|
|
function createPanel(doc: Document): HTMLDivElement {
|
|
const panel = doc.createElement('div')
|
|
panel.style.background = '#fff'
|
|
panel.style.borderRadius = '12px'
|
|
panel.style.padding = '16px'
|
|
panel.style.maxWidth = '520px'
|
|
panel.style.width = 'calc(100% - 32px)'
|
|
panel.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)'
|
|
return panel
|
|
}
|
|
|
|
function createText(doc: Document, message: string): HTMLParagraphElement {
|
|
const text = doc.createElement('p')
|
|
text.textContent = message
|
|
text.style.margin = '0 0 16px 0'
|
|
text.style.color = '#111827'
|
|
return text
|
|
}
|
|
|
|
function createButtonsContainer(doc: Document): HTMLDivElement {
|
|
const buttons = doc.createElement('div')
|
|
buttons.style.display = 'flex'
|
|
buttons.style.gap = '12px'
|
|
buttons.style.justifyContent = 'flex-end'
|
|
return buttons
|
|
}
|
|
|
|
function createCancelButton(doc: Document): HTMLButtonElement {
|
|
const cancel = doc.createElement('button')
|
|
cancel.type = 'button'
|
|
cancel.textContent = 'Cancel'
|
|
cancel.style.padding = '8px 12px'
|
|
cancel.style.borderRadius = '10px'
|
|
cancel.style.border = '1px solid #e5e7eb'
|
|
cancel.style.background = '#f3f4f6'
|
|
return cancel
|
|
}
|
|
|
|
function createConfirmButton(doc: Document): HTMLButtonElement {
|
|
const confirm = doc.createElement('button')
|
|
confirm.type = 'button'
|
|
confirm.textContent = 'Confirm'
|
|
confirm.style.padding = '8px 12px'
|
|
confirm.style.borderRadius = '10px'
|
|
confirm.style.border = '1px solid #ef4444'
|
|
confirm.style.background = '#fee2e2'
|
|
confirm.style.color = '#991b1b'
|
|
return confirm
|
|
}
|
|
|
|
function attachConfirmOverlayHandlers(params: ConfirmOverlayHandlerParams): void {
|
|
const { overlay, cancel, confirm, resolve } = params
|
|
let resolved = false
|
|
|
|
function resolveOnce(next: boolean): void {
|
|
if (resolved) {
|
|
return
|
|
}
|
|
resolved = true
|
|
cleanup()
|
|
resolve(next)
|
|
}
|
|
|
|
function onCancel(): void {
|
|
resolveOnce(false)
|
|
}
|
|
|
|
function onConfirm(): void {
|
|
resolveOnce(true)
|
|
}
|
|
|
|
function onKeyDown(e: KeyboardEvent): void {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault()
|
|
resolveOnce(false)
|
|
return
|
|
}
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
resolveOnce(true)
|
|
}
|
|
}
|
|
|
|
function cleanup(): void {
|
|
overlay.removeEventListener('keydown', onKeyDown)
|
|
cancel.removeEventListener('click', onCancel)
|
|
confirm.removeEventListener('click', onConfirm)
|
|
overlay.remove()
|
|
}
|
|
|
|
cancel.addEventListener('click', onCancel)
|
|
confirm.addEventListener('click', onConfirm)
|
|
overlay.addEventListener('keydown', onKeyDown)
|
|
}
|