1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2026 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 = { ...TYPES[type], ...(isObject(cfg) ? cfg : { text: String(cfg) }) };
109 const 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?.nodeType > 0) {
118 dlg.querySelector('.ui-msg-text').replaceChildren(cfg.html);
119 } else if (cfg.html) {
120 dlg.querySelector('.ui-msg-text').innerHTML = cfg.html;
121 } else {
122 dlg.querySelector('.ui-msg-text').innerText = cfg.text;
123 }
124 for (const key in cfg.actions) {
125 const act = normalizeAction(key, cfg.actions[key]);
126 const btn = createButton(act);
127 if (act.css.includes(':')) {
128 btn.style.cssText = act.css;
129 } else {
130 btn.classList.add(...act.css.split(/\s+/g));
131 }
132 btn.setAttribute('data-action', act.action);
133 if (act.icon || act.href) {
134 const icon = document.createElement('i');
135 icon.className = act.icon || 'fa fa-arrow-right';
136 btn.append(icon);
137 }
138 if (act.text) {
139 const span = document.createElement('span');
140 span.innerText = act.text;
141 btn.append(span);
142 }
143 dlg.querySelector('.ui-msg-btns').append(btn);
144 }
145 const promise = createEventHandler(dlg);
146 promise.dialog = dlg;
147 promise.content = dlg.querySelector('.ui-msg-text');
148 document.body.append(dlg);
149 dlg.show();
150 return promise;
151}
152
153function normalizeAction(key, val) {
154 val = isObject(val) ? val : { text: String(val) };
155 if (key === 'reload' || val.action === 'reload') {
156 const href = 'javascript:window.location.reload();';
157 const css = val.css || 'danger';
158 Object.assign(val, { href, target: null, css, icon: 'fa fa-refresh' });
159 }
160 val.css = val.css || key;
161 val.action = val.action || key;
162 return val;
163}
164
165function createButton(act) {
166 if (act.href) {
167 const el = document.createElement('a');
168 el.setAttribute('href', act.href);
169 if (act.target != null && act.target !== false) {
170 el.setAttribute('target', act.target || '_blank');
171 }
172 el.classList.add('btn');
173 return el;
174 } else {
175 return document.createElement('button');
176 }
177}
178
179function createEventHandler(dlg) {
180 return new Promise(function (resolve, reject) {
181 const handler = (evt) => {
182 const action = evt.delegateTarget.dataset.action;
183 if (action) {
184 dlg.hide();
185 RapidContext.Widget.destroyWidget(dlg);
186 if (action === 'cancel') {
187 reject(new Error('operation cancelled'));
188 } else {
189 resolve(action);
190 }
191 }
192 };
193 dlg.once('click', '.ui-msg-btns [data-action]', handler);
194 });
195}
196
197/**
198 * Displays a modal error dialog, similar to `alert()`. By default a
199 * single action is shown to close the dialog, but use the `actions`
200 * options to configure otherwise. The returned promise resolves with the
201 * action selected by the user.
202 *
203 * @param {string|Object} msg the message text or message configuration options
204 * @param {string} [msg.title="Error"] the dialog title
205 * @param {string} [msg.text="An error message"] the message text
206 * @param {string|Node} [msg.html] the message in HTML format
207 * @param {string} [msg.css] the dialog css
208 * @param {Actions} [msg.actions] the action buttons to show
209 * @return {Promise} a promise that resolves with the selected action
210 * @function RapidContext.UI.Msg.error
211 *
212 * @example
213 * try {
214 * ...
215 * } catch (e) {
216 * await RapidContext.UI.Msg.error(e.toString());
217 * }
218 */
219export const error = show.bind(null, 'error');
220
221/**
222 * Displays a modal warning dialog, similar to `alert()`. By default a
223 * single action is shown to close the dialog, but use the `actions`
224 * options to configure otherwise. The returned promise resolves with the
225 * action selected by the user.
226 *
227 * @param {string|Object} msg the message text or message configuration options
228 * @param {string} [msg.title="Warning"] the dialog title
229 * @param {string} [msg.text="A warning message"] the message text
230 * @param {string|Node} [msg.html] the message in HTML format
231 * @param {string} [msg.css] the dialog css
232 * @param {Actions} [msg.actions] the action buttons to show
233 * @return {Promise} a promise that resolves with the selected action
234 * @function RapidContext.UI.Msg.warning
235 *
236 * @example
237 * let text = "This will take some time to complete. Are you sure?";
238 * let actions = {
239 * cancel: "No, I've changed my mind",
240 * warning: "Yes, I understand"
241 * }
242 * RapidContext.UI.Msg.warning({ text, actions }).then(...);
243 */
244export const warning = show.bind(null, 'warning');
245
246/**
247 * Displays a modal success dialog, similar to `alert()`. By default a
248 * single action is shown to close the dialog, but use the `actions`
249 * options to configure otherwise. The returned promise resolves with the
250 * action selected by the user.
251 *
252 * @param {string|Object} msg the message text or message configuration options
253 * @param {string} [msg.title="Success"] the dialog title
254 * @param {string} [msg.text="A success message"] the message text
255 * @param {string|Node} [msg.html] the message in HTML format
256 * @param {string} [msg.css] the dialog css
257 * @param {Actions} [msg.actions] the action buttons to show
258 * @return {Promise} a promise that resolves with the selected action
259 * @function RapidContext.UI.Msg.success
260 *
261 * @example
262 * RapidContext.UI.Msg.success("Everything is proceeding as planned.");
263 */
264export const success = show.bind(null, 'success');
265
266/**
267 * Displays a modal info dialog, similar to `alert()`. By default a
268 * single action is shown to close the dialog, but use the `actions`
269 * options to configure otherwise. The returned promise resolves with the
270 * action selected by the user.
271 *
272 * @param {string|Object} msg the message text or message configuration options
273 * @param {string} [msg.title="Information"] the dialog title
274 * @param {string} [msg.text="An information message"] the message text
275 * @param {string|Node} [msg.html] the message in HTML format
276 * @param {string} [msg.css] the dialog css
277 * @param {Actions} [msg.actions] the action buttons to show
278 * @return {Promise} a promise that resolves with the selected action
279 * @function RapidContext.UI.Msg.info
280 *
281 * @example
282 * await RapidContext.UI.Msg.info("System will now reboot. Please wait.");
283 * ...
284 */
285export const info = show.bind(null, 'info');
286
287Object.assign(error, {
288 loggedOut() {
289 return error({
290 title: 'Logged out',
291 text: 'You\'ve been logged out. Please reload the page to login again.',
292 actions: {
293 reload: 'Reload page'
294 }
295 });
296 }
297});
298
299Object.assign(warning, {
300 /**
301 * Displays an object removal warning, similar to `confirm()`. This is
302 * a simplified preset that uses the generic `warning` function.
303 *
304 * @param {string} type the object type (e.g. "connection")
305 * @param {string} id the object name or id
306 * @return {Promise} a promise that resolves when confirmed, or
307 * rejects if cancelled
308 * @function RapidContext.UI.Msg.warning:remove
309 *
310 * @example
311 * try {
312 * await RapidContext.UI.Msg.warning.remove("person", "123");
313 * ...
314 * } catch (e) {
315 * // cancelled
316 * }
317 */
318 remove(type, id) {
319 return warning({
320 title: `Remove ${type}`,
321 html: `Do you really want to remove the ${type} <code>${id}</code>?`,
322 actions: {
323 cancel: 'Cancel',
324 warning: {
325 icon: 'fa fa-lg fa-minus-circle',
326 text: `Yes, remove ${type}`
327 }
328 }
329 });
330 }
331});
332
333Object.assign(info, {
334 updateAvailable() {
335 return info({
336 title: 'Update available',
337 text: 'An updated version is available. Reload the page to access the latest version.',
338 actions: {
339 close: 'Maybe later',
340 reload: { text: 'Reload page', css: 'info', action: 'info' }
341 }
342 });
343 }
344});
345
346export default { error, warning, success, info };
347
RapidContext
Access · Discovery · Insight
www.rapidcontext.com