Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save enlineaweb/6bdd9a5d6486793da0e4fc0519a01613 to your computer and use it in GitHub Desktop.
Save enlineaweb/6bdd9a5d6486793da0e4fc0519a01613 to your computer and use it in GitHub Desktop.
alert, confirm and prompt using Dialog
<button type="button" id="btnAlert">Alert</button>
<button type="button" id="btnConfirm">Confirm</button>
<button type="button" id="btnPrompt">Prompt</button>
<button type="button" id="btnCustom">Custom</button>
/**
* Dialog module.
* @module dialog.js
* @version 1.0.0
* @summary 02-01-2022
* @author Mads Stoumann
* @description Custom versions of `alert`, `confirm` and `prompt`, using `<dialog>`
*/
class Dialog {
constructor(settings = {}) {
this.settings = Object.assign(
{
accept: 'OK',
bodyClass: 'dialog-open',
cancel: 'Cancel',
dialogClass: '',
message: '',
soundAccept: '',
soundOpen: '',
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 = document.createElement('dialog')
this.dialog.role = 'dialog'
this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog';
this.dialog.innerHTML = `
<form method="dialog" data-ref="form">
<fieldset data-ref="fieldset" role="document">
<legend data-ref="message" id="${(Math.round(Date.now())).toString(36)}"></legend>
<div data-ref="template"></div>
</fieldset>
<menu>
<button${this.dialogSupported ? '' : ` type="button"`} data-ref="cancel" value="cancel"></button>
<button${this.dialogSupported ? '' : ` type="button"`} data-ref="accept" value="default"></button>
</menu>
<audio data-ref="soundAccept"></audio>
<audio data-ref="soundOpen"></audio>
</form>`
document.body.appendChild(this.dialog)
this.elements = {}
this.focusable = []
this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
this.elements.cancel.addEventListener('click', () => { this.dialog.dispatchEvent(new Event('cancel')) })
this.dialog.addEventListener('keydown', e => {
if (e.key === 'Enter') {
if (!this.dialogSupported) e.preventDefault()
this.elements.accept.dispatchEvent(new Event('click'))
}
if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel'))
if (e.key === 'Tab') {
e.preventDefault()
const len = this.focusable.length - 1;
let index = this.focusable.indexOf(e.target);
index = e.shiftKey ? index - 1 : index + 1;
if (index < 0) index = len;
if (index > len) index = 0;
this.focusable[index].focus();
}
})
this.toggle()
}
open(settings = {}) {
const dialog = Object.assign({}, this.settings, settings)
this.dialog.className = dialog.dialogClass || ''
this.elements.accept.innerText = dialog.accept
this.elements.cancel.innerText = dialog.cancel
this.elements.cancel.hidden = dialog.cancel === ''
this.elements.message.innerText = dialog.message
this.elements.soundAccept.src = dialog.soundAccept || ''
this.elements.soundOpen.src = dialog.soundOpen || ''
this.elements.target = dialog.target || ''
this.elements.template.innerHTML = dialog.template || ''
this.focusable = this.getFocusable()
this.hasFormData = this.elements.fieldset.elements.length > 0
if (dialog.soundOpen) {
this.elements.soundOpen.play()
}
this.toggle(true)
if (this.hasFormData) {
this.focusable[0].focus()
this.focusable[0].select()
}
else {
this.elements.accept.focus()
}
}
toggle(open = false) {
if (this.dialogSupported && open) this.dialog.showModal()
if (!this.dialogSupported) {
document.body.classList.toggle(this.settings.bodyClass, open)
this.dialog.hidden = !open
if (this.elements.target && !open) {
this.elements.target.focus()
}
}
}
waitForUser() {
return new Promise(resolve => {
this.dialog.addEventListener('cancel', () => {
this.toggle()
resolve(false)
}, { once: true })
this.elements.accept.addEventListener('click', () => {
let value = this.hasFormData ? this.collectFormData(new FormData(this.elements.form)) : true;
if (this.elements.soundAccept.getAttribute('src').length > 0) this.elements.soundAccept.play()
this.toggle()
resolve(value)
}, { once: true })
})
}
alert(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { cancel: '', message, template: '' })
this.open(settings)
return this.waitForUser()
}
confirm(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { message, template: '' })
this.open(settings)
return this.waitForUser()
}
prompt(message, value, config = { target: event.target }) {
const template = `<label aria-label="${message}"><input type="text" name="prompt" value="${value}"></label>`
const settings = Object.assign({}, config, { message, template })
this.open(settings)
return this.waitForUser()
}
}
/* FOR DEMO */
const dialog = new Dialog();
/* alert */
document.getElementById('btnAlert').addEventListener('click', (e) => {
dialog.alert('Please refresh your browser!').then((res) => { console.log(res) })
});
/* confirm */
document.getElementById('btnConfirm').addEventListener('click', () => {
dialog.confirm('Do you want to continue?').then((res) => { console.log(res) })
});
/* prompt */
document.getElementById('btnPrompt').addEventListener('click', (e) => {
dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })
});
/* custom */
document.getElementById('btnCustom').addEventListener('click', (e) => {
dialog.open({
accept: 'Sign in',
dialogClass: 'custom',
message: 'Please enter your credentials',
soundAccept: 'https://assets.stoumann.dk/audio/accept.mp3',
soundOpen: 'https://assets.stoumann.dk/audio/open.mp3',
target: e.target,
template: `
<label>Username<input type="text" name="username" value="admin"></label>
<label>Password<input type="password" name="password" value="password"></label>`
})
dialog.waitForUser().then((res) => { console.log(res) })
});
[data-component*="dialog"] * {
box-sizing: border-box;
outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%))
}
:where([data-component*="dialog"]) {
--dlg-gap: 1em;
background: var(--dlg-bg, #fff);
border: var(--dlg-b, 0);
border-radius: var(--dlg-bdrs, 0.25em);
box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25));
font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif);
min-inline-size: var(--dlg-mis, auto);
padding: var(--dlg-p, var(--dlg-gap));
width: var(--dlg-w, fit-content);
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
inset-block-start: var(--dlg-gap);
inset-inline-start: 50%;
position: fixed;
transform: translateX(-50%);
}
:where([data-component*="dialog"] menu) {
display: flex;
gap: calc(var(--dlg-gap) / 2);
justify-content: var(--dlg-menu-jc, flex-end);
margin: 0;
padding: 0;
}
:where([data-component*="dialog"] menu button) {
background-color: var(--dlg-button-bgc);
border: 0;
border-radius: var(--dlg-bdrs, 0.25em);
color: var(--dlg-button-c);
font-size: var(--dlg-button-fz, 0.8em);
padding: var(--dlg-button-p, 0.65em 1.5em);
}
:where([data-component*="dialog"] [data-ref="accept"]) {
--dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%));
--dlg-button-c: var(--dlg-accept-c, #fff);
}
:where([data-component*="dialog"] [data-ref="cancel"]) {
--dlg-button-bgc: var(--dlg-cancel-bgc, transparent);
--dlg-button-c: var(--dlg-cancel-c, inherit);
}
:where([data-component*="dialog"] [data-ref="fieldset"]) {
border: 0;
margin: unset;
padding: unset;
}
:where([data-component*="dialog"] [data-ref="message"]) {
font-size: var(--dlg-message-fz, 1.25em);
margin-block-end: var(--dlg-gap);
}
:where([data-component*="dialog"] [data-ref="template"]:not(:empty)) {
margin-block-end: var(--dlg-gap);
width: 100%;
}
/* hack for Firefox */
@-moz-document url-prefix() {
[data-component="no-dialog"]:not([hidden]) {
inset-inline-start: 0;
transform: none;
}
}
/* added to `body` when browser do not support `<dialog>` */
.dialog-open {
background-color: rgba(0, 0, 0, .1);
overflow: hidden;
}
/* FOR DEMO */
[name="prompt"] {
border: 1px solid silver;
padding: .6em 1em;
width: 100%;
}
.custom {
--dlg-accept-bgc: hsl(159, 65%, 75%);
--dlg-accept-c: #000;
--dlg-bg: linear-gradient(to bottom right,#00F5A0,#00D9F5);
--dlg-button-p: 0.75em 2em;
--dlg-outline-c: #00D9F5;
}
.custom input {
background-color: rgba(255, 255, 255, .5);
border-radius: .25em;
border: 0;
display: block;
margin-block: .5em 1em;
padding: .75em 1em;
width: 100%;
}
.custom label {
display: block;
font-size: small;
}
button[id] {
background-color: rgb(239, 239, 239);
border: 1px solid rgb(118, 118, 118);
border-radius: .25em;
font-size: .8rem;
margin-inline-end: .25em;
padding: 1em 2em;
}
button[id]:hover {
background-color: rgb(250, 250, 250);
border-color: rgb(0, 0, 0);
color: rgb(0, 0, 0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment