umap/umap/static/umap/js/modules/ui/dialog.js
2024-07-15 13:33:07 -04:00

194 lines
5.3 KiB
JavaScript

import { translate } from '../i18n.js'
import { WithTemplate } from '../utils.js'
const TEMPLATE = `
<dialog data-ref="dialog">
<form method="dialog" data-ref="form">
<ul class="buttons">
<li><i class="icon icon-16 icon-close" data-close></i></li>
</ul>
<h3 data-ref="message" id="${Math.round(Date.now()).toString(36)}"></h3>
<fieldset data-ref="fieldset" role="document">
<div data-ref="template"></div>
</fieldset>
<menu>
<button type="button" data-ref="cancel" data-close value="cancel"></button>
<button type="submit" class="button" data-ref="accept" value="accept"></button>
</menu>
</form>
</dialog>
`
// From https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/
export default class Dialog extends WithTemplate {
constructor(settings = {}) {
super()
this.settings = Object.assign(
{
accept: translate('OK'),
cancel: translate('Cancel'),
className: '',
message: '',
template: '',
},
settings
)
this.init()
}
collectFormData(formData) {
const object = {}
formData.forEach((value, key) => {
if (!Reflect.has(object, key)) {
object[key] = value
return
}
if (!Array.isArray(object[key])) {
object[key] = [object[key]]
}
object[key].push(value)
})
return object
}
getFocusable() {
return [
...this.dialog.querySelectorAll(
'button,[href],select,textarea,input:not([type="hidden"]),[tabindex]:not([tabindex="-1"])'
),
]
}
init() {
this.dialogSupported = typeof HTMLDialogElement === 'function'
this.dialog = this.loadTemplate(TEMPLATE)
this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
document.body.appendChild(this.dialog)
this.focusable = []
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
this.dialog.addEventListener('click', (event) => {
if (event.target.closest('[data-close]')) {
this.close()
}
})
if (!this.dialogSupported) {
this.elements.form.addEventListener('submit', (event) => {
event.preventDefault()
this.dialog.returnValue = 'accept'
this.close()
this.dialog.returnValue = undefined
})
}
this.dialog.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (!this.dialogSupported) {
e.preventDefault()
this.elements.form.requestSubmit()
}
}
if (e.key === 'Escape') {
e.stopPropagation()
this.close()
}
})
}
currentZIndex() {
return Math.max(
...Array.from(document.querySelectorAll('dialog')).map((el) => {
const zIndex = window.getComputedStyle(el).getPropertyValue('z-index')
if (zIndex === 'auto') return 0
return zIndex || 0
})
)
}
open(settings = {}) {
const dialog = Object.assign({}, this.settings, settings)
this.dialog.className = 'umap-dialog window'
if (dialog.className) {
this.dialog.classList.add(...dialog.className.split(' '))
}
this.elements.accept.textContent = dialog.accept
this.elements.accept.hidden = !dialog.accept
this.elements.cancel.textContent = dialog.cancel
this.elements.cancel.hidden = !dialog.cancel
this.elements.message.textContent = dialog.message
this.elements.message.hidden = !dialog.message
this.elements.target = dialog.target || ''
this.elements.template.innerHTML = ''
if (dialog.template?.nodeType === 1) {
this.elements.template.appendChild(dialog.template)
} else {
this.elements.template.innerHTML = dialog.template || ''
}
this.focusable = this.getFocusable()
this.hasFormData = this.elements.fieldset.elements.length > 0
const currentZIndex = this.currentZIndex()
if (currentZIndex) {
this.dialog.style.zIndex = currentZIndex + 1
}
this.toggle(true)
if (this.hasFormData) this.focusable[0].focus()
else this.elements.accept.focus()
return this.waitForUser()
}
close() {
this.toggle(false)
}
toggle(open = false) {
if (this.dialogSupported) {
if (open) this.dialog.show()
else this.dialog.close()
} else {
this.dialog.hidden = !open
if (this.elements.target && !open) {
this.elements.target.focus()
}
if (!open) {
this.dialog.dispatchEvent(new CustomEvent('close'))
}
}
}
waitForUser() {
return new Promise((resolve) => {
this.dialog.addEventListener(
'close',
(event) => {
if (this.dialog.returnValue === 'accept') {
const value = this.hasFormData
? this.collectFormData(new FormData(this.elements.form))
: true
resolve(value)
}
},
{ once: true }
)
})
}
alert(config) {
return this.open(
Object.assign({}, config, { cancel: false, message, template: false })
)
}
confirm(message, config = {}) {
return this.open(Object.assign({}, config, { message, template: false }))
}
prompt(message, fallback = '', config = {}) {
const template = `<input type="text" name="prompt" value="${fallback}">`
return this.open(Object.assign({}, config, { message, template }))
}
}