Files
jsl/dialog.js

199 lines
6.3 KiB
JavaScript
Raw Normal View History

2025-09-20 22:59:18 +02:00
/*
* 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 = {};
2025-09-20 22:59:18 +02:00
/**
* 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) {
2025-09-20 22:59:18 +02:00
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;
2025-09-20 22:59:18 +02:00
}
/**
* 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;
}
2025-09-20 22:59:18 +02:00
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,
2025-09-20 22:59:18 +02:00
]));
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); });
}
2025-09-20 22:59:18 +02:00
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) {
2025-09-20 22:59:18 +02:00
let dialog;
switch (type) {
case 'msgbox':
title = title ?? "MessageBox";
actions = actions ?? { 'ok': 'Ok' };
dialog = new UserDialog('msgbox', title, message, actions, options);
2025-09-20 22:59:18 +02:00
break;
case 'information':
case 'info':
title = title ?? "Information";
actions = actions ?? { 'ok': 'Ok' };
dialog = new UserDialog('info', title, message, actions, options);
2025-09-20 22:59:18 +02:00
break;
case 'error':
title = title ?? "Error";
actions = actions ?? { 'ok': 'Ok' };
dialog = new UserDialog('error', title, message, actions, options);
2025-09-20 22:59:18 +02:00
break;
case 'warning':
title = title ?? "Warning";
actions = actions ?? { 'ok': 'Ok' };
dialog = new UserDialog('warning', title, message, actions, options);
2025-09-20 22:59:18 +02:00
break;
case 'confirm':
title = title ?? "Confirmation";
actions = actions ?? { 'ok': 'Ok', 'cancel': 'Cancel' };
dialog = new UserDialog('confirm', title, message, actions, options);
2025-09-20 22:59:18 +02:00
break;
default:
title = title ?? "Dialog";
actions = actions ?? { 'ok': 'Ok' };
dialog = new UserDialog(type, title, message, actions, options);
2025-09-20 22:59:18 +02:00
}
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);
2025-09-20 22:59:18 +02:00
}
});
export {
factory,
dialog,
UserDialog
};