/* * This file is part of NoccyLabs JavaScript Standard Library (JSL) * Copyright (c) 2025, NoccyLabs. Licensed under GNU GPL v2 or later * * */ import { dom, el } from './dom.js'; /** * A friendly user dialog class * */ class UserDialog { #dialog; #resolver; #style = "msgbox"; #title = "MessageBox"; #message; #actions = { "ok":"Ok" }; #options = {}; /** * Dialog constructor * * @param {string} style The dialog style * @param {string} title The dialog title * @param {string} message The dialog message * @param {object} actions The dialog actions */ constructor(style, title, message, actions, options) { if (style) this.#style = style; if (title) this.#title = title; if (message) this.#message = message; if (actions && typeof actions == 'object') this.#actions = actions; if (options && typeof options == 'object') this.#options = options; } /** * Build and show the dialog. Promise will resolve with the selected action key, * or false if closed via the X. * * @returns Promise */ show() { let promise = new Promise((resolve) => { this.#resolver = resolve; }); this.#buildDialog(); this.#showDialog(); return promise; } #buildDialog() { if (this.#dialog) throw new Error("#buildDialog should not be called with an active #dialog"); const dialog = el.dialog({ class: 'jsl-dialog' }); if (this.#options.width) { dialog.style.width = this.#options.width; } dialog.addEventListener('close', event => { if (this.#resolver) { this.#resolver(false); this.#resolver = null; } this.#destroyDialog(); }); dialog.className += ' -style-' + this.#style; let cbtn; dom.append(dialog, el.div({ class: '-header' }, [ el.div({ class: '-title' }, this.#title), cbtn = (this.#options.showClose ?? false) ? el.button({ class: '-close-btn' }, '✕') : null, ])); if (this.#options.body ?? null) { dom.append(dialog, el.div({ class: '-content' }, this.#options.body)); } else { dom.append(dialog, el.div({ class: '-content' }, this.#message)); } if (this.#options.showClose ?? false) { cbtn.addEventListener('click', () => { this.#handleButton(false); }); } const buttons = el.div({ class: '-buttons' }); let tabindex = 1; Object.keys(this.#actions).forEach(action => { let btn; dom.append(buttons, btn = el.button({ class: 'action', tabIndex: tabindex++ }, this.#actions[action])); // if (action.match(/\*$/)) { // btn.style.outline = 'solid 1px #222'; // btn.class += ' default-action'; // } btn.addEventListener('click', () => { this.#handleButton(action); }); }); dom.append(dialog, buttons); this.#dialog = dialog; } #destroyDialog() { document.body.removeChild(this.#dialog); this.#dialog = null; } #showDialog() { if (!this.#dialog) throw new Error("#showDialog should be called after #buildDialog with an active #dialog"); document.body.appendChild(this.#dialog); this.#dialog.showModal(); setTimeout(() => { let act = this.#dialog.querySelector('button.default-action'); if (act) { act.focus(); return; } act = this.#dialog.querySelector('button.action'); if (!act) { console.warning("No buttons found? What to focus?"); return; } act.focus(); }, 200); } #handleButton(id) { this.#resolver(id); this.#resolver = null; this.#destroyDialog(); } } class UserDialogFactory { /** * Create a dialog, and return a promise that will resolve with the key of the * selected button, or false if closed via the X. * * @param {string} type The dialog type, ex msgbox, information, error * @param {string} title The dialog title * @param {string} message The message to display * @param {object} actions The buttons to present at the bottom of the dialog * @returns Promise */ showDialog(type, title, message, actions, options) { let dialog; switch (type) { case 'msgbox': title = title ?? "MessageBox"; actions = actions ?? { 'ok': 'Ok' }; dialog = new UserDialog('msgbox', title, message, actions, options); break; case 'information': case 'info': title = title ?? "Information"; actions = actions ?? { 'ok': 'Ok' }; dialog = new UserDialog('info', title, message, actions, options); break; case 'error': title = title ?? "Error"; actions = actions ?? { 'ok': 'Ok' }; dialog = new UserDialog('error', title, message, actions, options); break; case 'warning': title = title ?? "Warning"; actions = actions ?? { 'ok': 'Ok' }; dialog = new UserDialog('warning', title, message, actions, options); break; case 'confirm': title = title ?? "Confirmation"; actions = actions ?? { 'ok': 'Ok', 'cancel': 'Cancel' }; dialog = new UserDialog('confirm', title, message, actions, options); break; default: title = title ?? "Dialog"; actions = actions ?? { 'ok': 'Ok' }; dialog = new UserDialog(type, title, message, actions, options); } return dialog.show(); } } // The dialog factory const factory = new UserDialogFactory(); // A proxy to invoke various types via dialog. const dialog = new Proxy(factory, { get(target, name) { return name in target ? target[name] : (message, title = null, actions = null, options = {}) => target.showDialog(name, title, message, actions, options); } }); export { factory, dialog, UserDialog };