Source rapidcontext/ui/msg.mjs

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15/**
16 * Provides common message dialogs.
17 * @namespace RapidContext.UI.Msg
18 */
19
20/**
21 * The message actions (i.e. buttons) to show in the message dialog. Each
22 * action corresponds to a result value returned by the message promise.
23 * The action values `cancel` and `reload` are handled specially, as they
24 * either reject the promise or reload the page altogether.
25 *
26 * Each action button can either use default styles, or be configured with
27 * an options object. By default, the action key is used as both the
28 * default button type (e.g. `primary`, `danger`, `info` or `success`), and
29 * the returned action value.
30 *
31 * @param {string|Object} * the action text or options object
32 * @param {string} [*.text] the button text to show
33 * @param {string} [*.icon] the button icon to show
34 * @param {string} [*.css] the button CSS classes or styles, uses action key by default
35 * @param {string} [*.action] the action return value, uses action key by default
36 * @param {string} [*.href] the URL to follow if selected
37 * @param {string} [*.target] the link target (e.g. `_blank`)
38 * @typedef {Object} Actions
39 * @memberof RapidContext.UI.Msg
40 *
41 * @example
42 * {
43 *    // Actions can use either default styles or configuration
44 *    danger: "Yes, I know what I'm doing",
45 *    custom: { text: "Ok", icon: "fa fa-lg fa-check", css: 'primary' },
46 *
47 *    // The special 'cancel' action rejects the returned promise
48 *    cancel: { text: "Cancel", icon: "fa fa-lg fa-times" },
49 *
50 *    // The 'reload' action triggers a reload of the current page
51 *    reload: { text: "Reload page" }
52 * }
53 */
54const TYPES = {
55    'error': {
56        title: 'Error',
57        css: 'danger',
58        text: 'An error message',
59        icon: 'fa fa-minus-circle color-danger',
60        actions: {
61            danger: { icon: 'fa fa-lg fa-bolt', text: 'Ok, I understand' }
62        }
63    },
64    'warning': {
65        title: 'Warning',
66        css: 'warning',
67        text: 'A warning message',
68        icon: 'fa fa-exclamation-triangle color-warning',
69        actions: {
70            warning: { icon: 'fa fa-lg fa-check', text: 'Ok, I understand' }
71        }
72    },
73    'success': {
74        title: 'Success',
75        css: 'success',
76        text: 'A success message',
77        icon: 'fa fa-check color-success',
78        actions: {
79            success: { icon: 'fa fa-lg fa-check', text: 'Ok, got it' }
80        }
81    },
82    'info': {
83        title: 'Information',
84        css: 'info',
85        text: 'An information message',
86        icon: 'fa fa-info-circle color-info',
87        actions: {
88            info: { icon: 'fa fa-lg fa-check', text: 'Ok, got it' }
89        }
90    }
91};
92
93const XML = `
94    <Dialog system='true' closeable='false' class='position-relative' style='width: 25rem;'>
95      <i class='ui-msg-icon position-absolute top-0 left-0 fa fa-fw fa-3x px-1 py-2'></i>
96      <div class='ui-msg-text' style='padding: 0.5em 0 1em 4.5em;'>
97      </div>
98      <div class='ui-msg-btns text-right mt-1'>
99      </div>
100    </Dialog>
101`.trim();
102
103function isObject(o) {
104    return Object.prototype.toString.call(o) === '[object Object]';
105}
106
107function show(type, cfg) {
108    cfg = Object.assign({}, TYPES[type], isObject(cfg) ? cfg : { text: String(cfg) });
109    let dlg = RapidContext.UI.buildUI(XML);
110    dlg.setAttrs({ title: cfg.title });
111    if (isObject(cfg.css) || /:/.test(cfg.css)) {
112        dlg.setAttrs({ style: cfg.css });
113    } else {
114        dlg.classList.add(...cfg.css.split(/\s+/g));
115    }
116    dlg.querySelector('.ui-msg-icon').classList.add(...cfg.icon.split(/\s+/g));
117    if (cfg.html && cfg.html.nodeType > 0) {
118        // FIXME: Use replaceChildren(..) instead
119        dlg.querySelector('.ui-msg-text').innerHTML = '';
120        dlg.querySelector('.ui-msg-text').append(cfg.html);
121    } else if (cfg.html) {
122        dlg.querySelector('.ui-msg-text').innerHTML = cfg.html;
123    } else {
124        dlg.querySelector('.ui-msg-text').innerText = cfg.text;
125    }
126    for (let key in cfg.actions) {
127        let act = normalizeAction(key, cfg.actions[key]);
128        let btn = createButton(act);
129        if (act.css.includes(':')) {
130            btn.style.cssText = act.css;
131        } else {
132            btn.classList.add(...act.css.split(/\s+/g));
133        }
134        btn.setAttribute('data-action', act.action);
135        if (act.icon || act.href) {
136            let icon = document.createElement('i');
137            icon.className = act.icon || 'fa fa-arrow-right';
138            btn.append(icon);
139        }
140        if (act.text) {
141            let span = document.createElement('span');
142            span.innerText = act.text;
143            btn.append(span);
144        }
145        dlg.querySelector('.ui-msg-btns').append(btn);
146    }
147    let promise = createEventHandler(dlg);
148    promise.dialog = dlg;
149    promise.content = dlg.querySelector('.ui-msg-text');
150    document.body.append(dlg);
151    dlg.show();
152    return promise;
153}
154
155function normalizeAction(key, val) {
156    val = isObject(val) ? val : { text: String(val) };
157    if (key === 'reload' || val.action === 'reload') {
158        let href = 'javascript:window.location.reload();';
159        let css = val.css || 'danger';
160        Object.assign(val, { href, target: null, css, icon: 'fa fa-refresh' });
161    }
162    val.css = val.css || key;
163    val.action = val.action || key;
164    return val;
165}
166
167function createButton(act) {
168    if (act.href) {
169        let el = document.createElement('a');
170        el.setAttribute('href', act.href);
171        if (act.target != null && act.target !== false) {
172            el.setAttribute('target', act.target || '_blank');
173        }
174        el.classList.add('btn');
175        return el;
176    } else {
177        return document.createElement('button');
178    }
179}
180
181function createEventHandler(dlg) {
182    return new Promise(function (resolve, reject) {
183        let handler = (evt) => {
184            let action = evt.delegateTarget.dataset.action;
185            if (action) {
186                dlg.hide();
187                RapidContext.Widget.destroyWidget(dlg);
188                if (action === 'cancel') {
189                    reject(new Error('operation cancelled'));
190                } else {
191                    resolve(action);
192                }
193            }
194        };
195        dlg.once('click', '.ui-msg-btns [data-action]', handler);
196    });
197}
198
199/**
200 * Displays a modal error dialog, similar to `alert()`. By default a
201 * single action is shown to close the dialog, but use the `actions`
202 * options to configure otherwise. The returned promise resolves with the
203 * action selected by the user.
204 *
205 * @param {string|Object} msg the message text or message configuration options
206 * @param {string} [msg.title="Error"] the dialog title
207 * @param {string} [msg.text="An error message"] the message text
208 * @param {string|Node} [msg.html] the message in HTML format
209 * @param {string} [msg.css] the dialog css
210 * @param {Actions} [msg.actions] the action buttons to show
211 * @return {Promise} a promise that resolves with the selected action
212 * @function RapidContext.UI.Msg.error
213 *
214 * @example
215 * try {
216 *     ...
217 * } catch (e) {
218 *     await RapidContext.UI.Msg.error(e.toString());
219 * }
220 */
221export const error = show.bind(null, 'error');
222
223/**
224 * Displays a modal warning dialog, similar to `alert()`. By default a
225 * single action is shown to close the dialog, but use the `actions`
226 * options to configure otherwise. The returned promise resolves with the
227 * action selected by the user.
228 *
229 * @param {string|Object} msg the message text or message configuration options
230 * @param {string} [msg.title="Warning"] the dialog title
231 * @param {string} [msg.text="A warning message"] the message text
232 * @param {string|Node} [msg.html] the message in HTML format
233 * @param {string} [msg.css] the dialog css
234 * @param {Actions} [msg.actions] the action buttons to show
235 * @return {Promise} a promise that resolves with the selected action
236 * @function RapidContext.UI.Msg.warning
237 *
238 * @example
239 * let text = "This will take some time to complete. Are you sure?";
240 * let actions = {
241 *     cancel: "No, I've changed my mind",
242 *     warning: "Yes, I understand"
243 * }
244 * RapidContext.UI.Msg.warning({ text, actions }).then(...);
245 */
246export const warning = show.bind(null, 'warning');
247
248/**
249 * Displays a modal success dialog, similar to `alert()`. By default a
250 * single action is shown to close the dialog, but use the `actions`
251 * options to configure otherwise. The returned promise resolves with the
252 * action selected by the user.
253 *
254 * @param {string|Object} msg the message text or message configuration options
255 * @param {string} [msg.title="Success"] the dialog title
256 * @param {string} [msg.text="A success message"] the message text
257 * @param {string|Node} [msg.html] the message in HTML format
258 * @param {string} [msg.css] the dialog css
259 * @param {Actions} [msg.actions] the action buttons to show
260 * @return {Promise} a promise that resolves with the selected action
261 * @function RapidContext.UI.Msg.success
262 *
263 * @example
264 * RapidContext.UI.Msg.success("Everything is proceeding as planned.");
265 */
266export const success = show.bind(null, 'success');
267
268/**
269 * Displays a modal info dialog, similar to `alert()`. By default a
270 * single action is shown to close the dialog, but use the `actions`
271 * options to configure otherwise. The returned promise resolves with the
272 * action selected by the user.
273 *
274 * @param {string|Object} msg the message text or message configuration options
275 * @param {string} [msg.title="Information"] the dialog title
276 * @param {string} [msg.text="An information message"] the message text
277 * @param {string|Node} [msg.html] the message in HTML format
278 * @param {string} [msg.css] the dialog css
279 * @param {Actions} [msg.actions] the action buttons to show
280 * @return {Promise} a promise that resolves with the selected action
281 * @function RapidContext.UI.Msg.info
282 *
283 * @example
284 * await RapidContext.UI.Msg.info("System will now reboot. Please wait.");
285 * ...
286 */
287export const info = show.bind(null, 'info');
288
289Object.assign(error, {
290    loggedOut() {
291        return error({
292            title: 'Logged out',
293            text: 'You\'ve been logged out. Please reload the page to login again.',
294            actions: {
295                reload: 'Reload page'
296            }
297        });
298    }
299});
300
301Object.assign(warning, {
302    /**
303     * Displays an object removal warning, similar to `confirm()`. This is
304     * a simplified preset that uses the generic `warning` function.
305     *
306     * @param {string} type the object type (e.g. "connection")
307     * @param {string} id the object name or id
308     * @return {Promise} a promise that resolves when confirmed, or
309     *                   rejects if cancelled
310     * @function RapidContext.UI.Msg.warning:remove
311     *
312     * @example
313     * try {
314     *     await RapidContext.UI.Msg.warning.remove("person", "123");
315     *     ...
316     * } catch (e) {
317     *     // cancelled
318     * }
319     */
320    remove(type, id) {
321        return warning({
322            title: `Remove ${type}`,
323            html: `Do you really want to remove the ${type} <code>${id}</code>?`,
324            actions: {
325                cancel: 'Cancel',
326                warning: {
327                    icon: 'fa fa-lg fa-minus-circle',
328                    text: `Yes, remove ${type}`
329                }
330            }
331        });
332    }
333});
334
335export default { error, warning, success, info };
336