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),
}