1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 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.create(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
RapidContext
Access · Discovery · Insight
www.rapidcontext.com