import {IDialogWrapper} from './DialogWrapper' import cssTemplate from './HTMLDialogWrapper.css?inline' import {escapeHtml} from "./text"; let styleElement: HTMLStyleElement | null = null // Utility to auto-link URLs in text function linkify(text: string) { // Simple URL regex const urlRegex = /((https?:\/\/|www\.)[^\s<]+)/g; return text.replace(urlRegex, (url) => { let href = url; if (!href.startsWith('http')) href = 'https://' + href; // Use a span for the visible text to apply ellipsis return `${escapeHtml(url)}`; }); } // Render message with line breaks and links, safely function renderMessage(message: string) { // Escape HTML, then linkify, then replace line breaks const escaped = escapeHtml(message); const linked = linkify(escaped); return linked.replace(/\n/g, '
'); } function titleMessage(props: { title?: string; message: string; ok?: string }) { return ` ${props.title ? `
${escapeHtml(props.title)}
` : ''}
${renderMessage(props.message)}
`; } const alertTemplate = (props: { title?: string message: string ok?: string })=>`
${titleMessage(props)}
` const confirmTemplate = (props: { title?: string message: string ok?: string cancel?: string })=>`
${titleMessage(props)}
` const promptTemplate = (props: { title?: string message: string ok?: string cancel?: string defaultValue?: string })=>`
${titleMessage(props)}
` function setupStyle() { if (styleElement) return styleElement = document.createElement('style') styleElement.textContent = cssTemplate document.head.appendChild(styleElement) } function createDialogElement(template: (props: T) => string, {message, ...props}: Omit) { setupStyle() const dialog = document.createElement('div') const title = message?.split(':')[0] if (title && message) message = message.replace(title + ':', '').trim() // @ts-expect-error unk dialog.innerHTML = template({...props, message, title}) dialog.classList.add('dialog-container') document.body.appendChild(dialog) return dialog } /** * A custom dialog wrapper that uses HTML elements to create alert, prompt, and confirm dialogs inspired by radix/shadcn style. Provides API similar to the browser's built-in dialog methods, with better UI and more options. * @category Browser */ export const htmlDialogWrapper: IDialogWrapper = { alert: async(message?: string) => { const dialog = createDialogElement(alertTemplate, {message: message || 'Alert', ok: 'Okay'}) return new Promise((resolve) => { const okButton = dialog.querySelector('.dialog-ok') as HTMLButtonElement | null const keydown = (event: KeyboardEvent) => { if (event.key === 'Enter' || event.key === 'Escape') { event.preventDefault() okButton?.click() } } const remove = () => { window.removeEventListener('keydown', keydown) document.body.removeChild(dialog) } window.addEventListener('keydown', keydown) okButton?.addEventListener('click', () => { remove() resolve() }) okButton?.focus() }) }, prompt: async(message?: string, _default?: string, cancel = true) => { const dialog = createDialogElement(promptTemplate, { message: message || 'Enter some text', ok: 'OK', cancel: cancel ? 'Cancel' : undefined, defaultValue: _default, }) return new Promise((resolve) => { const okButton = dialog.querySelector('.dialog-ok') as HTMLButtonElement | null const cancelButton = dialog.querySelector('.dialog-cancel') as HTMLButtonElement | null const input = dialog.querySelector('.dialog-input')! as HTMLInputElement input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault() okButton?.click() } }) const keydown = (event: KeyboardEvent) => { if (event.key === 'Escape' && cancelButton) { event.preventDefault() cancelButton?.click() } } const remove = () => { window.removeEventListener('keydown', keydown) document.body.removeChild(dialog) } window.addEventListener('keydown', keydown) okButton?.addEventListener('click', () => { const value = input.value remove() resolve(value) }) cancelButton?.addEventListener('click', () => { remove() resolve(null) }) input?.focus() input?.select() }) }, confirm: async(message?: string) => { const dialog = createDialogElement(confirmTemplate, {message: message || 'Are you sure?', ok: 'Yes', cancel: 'No'}) return new Promise((resolve) => { const okButton = dialog.querySelector('.dialog-ok') as HTMLButtonElement | null const cancelButton = dialog.querySelector('.dialog-cancel') as HTMLButtonElement | null const keydown = (event: KeyboardEvent) => { if (event.key === 'Escape' && cancelButton) { event.preventDefault() cancelButton.click() } } const remove = () => { window.removeEventListener('keydown', keydown) document.body.removeChild(dialog) } window.addEventListener('keydown', keydown) okButton?.addEventListener('click', () => { remove() resolve(true) }) cancelButton?.addEventListener('click', () => { remove() resolve(false) }) cancelButton?.focus() }) }, confirmSync: (message?: string) => window.confirm(message), }