MDL-68788 core_notification: Check if the user is logged in
[moodle.git] / lib / amd / src / notification.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle 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.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 import Pending from 'core/pending';
17 import Log from 'core/log';
19 let currentContextId = M.cfg.contextid;
21 const notificationTypes = {
22     success:  'core/notification_success',
23     info:     'core/notification_info',
24     warning:  'core/notification_warning',
25     error:    'core/notification_error',
26 };
28 const notificationRegionId = 'user-notifications';
30 const Selectors = {
31     notificationRegion: `#${notificationRegionId}`,
32     fallbackRegionParents: [
33         '#region-main',
34         '[role="main"]',
35         'body',
36     ],
37 };
39 const setupTargetRegion = () => {
40     let targetRegion = getNotificationRegion();
41     if (targetRegion) {
42         return false;
43     }
45     const newRegion = document.createElement('span');
46     newRegion.id = notificationRegionId;
48     return Selectors.fallbackRegionParents.some(selector => {
49         const targetRegion = document.querySelector(selector);
51         if (targetRegion) {
52             targetRegion.prepend(newRegion);
53             return true;
54         }
56         return false;
57     });
58 };
61 /**
62  * Poll the server for any new notifications.
63  *
64  * @returns {Promise}
65  */
66 export const fetchNotifications = async() => {
67     const Ajax = await import('core/ajax');
69     return Ajax.call([{
70         methodname: 'core_fetch_notifications',
71         args: {
72             contextid: currentContextId
73         }
74     }])[0]
75     .then(addNotifications);
76 };
78 /**
79  * Add all of the supplied notifications.
80  *
81  * @param {Array} notifications The list of notificaitons
82  * @returns {Promise}
83  */
84 const addNotifications = notifications => {
85     if (!notifications.length) {
86         return Promise.resolve();
87     }
89     const pendingPromise = new Pending('core/notification:addNotifications');
90     notifications.forEach(notification => renderNotification(notification.template, notification.variables));
92     return pendingPromise.resolve();
93 };
95 /**
96  * Add a notification to the page.
97  *
98  * Note: This does not cause the notification to be added to the session.
99  *
100  * @param {Object}  notification                The notification to add.
101  * @param {string}  notification.message        The body of the notification
102  * @param {string}  notification.type           The type of notification to add (error, warning, info, success).
103  * @param {Boolean} notification.closebutton    Whether to show the close button.
104  * @param {Boolean} notification.announce       Whether to announce to screen readers.
105  * @returns {Promise}
106  */
107 export const addNotification = notification => {
108     const pendingPromise = new Pending('core/notification:addNotifications');
110     let template = notificationTypes.error;
112     notification = {
113         closebutton:    true,
114         announce:       true,
115         type:           'error',
116         ...notification,
117     };
119     if (notification.template) {
120         template = notification.template;
121         delete notification.template;
122     } else if (notification.type) {
123         if (typeof notificationTypes[notification.type] !== 'undefined') {
124             template = notificationTypes[notification.type];
125         }
126         delete notification.type;
127     }
129     return renderNotification(template, notification)
130     .then(pendingPromise.resolve);
131 };
133 const renderNotification = async(template, variables) => {
134     if (typeof variables.message === 'undefined' || !variables.message) {
135         Log.debug('Notification received without content. Skipping.');
136         return;
137     }
139     const pendingPromise = new Pending('core/notification:renderNotification');
140     const Templates = await import('core/templates');
142     Templates.renderForPromise(template, variables)
143     .then(({html, js = ''}) => {
144         Templates.prependNodeContents(getNotificationRegion(), html, js);
146         return;
147     })
148     .then(pendingPromise.resolve)
149     .catch(exception);
150 };
152 const getNotificationRegion = () => document.querySelector(Selectors.notificationRegion);
154 /**
155  * Alert dialogue.
156  *
157  * @param {String|Promise} title
158  * @param {String|Promise} message
159  * @param {String|Promise} cancelText
160  * @returns {Promise}
161  */
162 export const alert = async(title, message, cancelText) => {
163     var pendingPromise = new Pending('core/notification:alert');
165     const ModalFactory = await import('core/modal_factory');
167     return ModalFactory.create({
168         type: ModalFactory.types.ALERT,
169         body: message,
170         title: title,
171         buttons: {
172             cancel: cancelText,
173         },
174         removeOnClose: true,
175     })
176     .then(function(modal) {
177         modal.show();
179         pendingPromise.resolve();
180         return modal;
181     });
182 };
184 /**
185  * The confirm has now been replaced with a save and cancel dialogue.
186  *
187  * @param {String|Promise} title
188  * @param {String|Promise} question
189  * @param {String|Promise} saveLabel
190  * @param {String|Promise} noLabel
191  * @param {String|Promise} saveCallback
192  * @param {String|Promise} cancelCallback
193  * @returns {Promise}
194  */
195 export const confirm = (title, question, saveLabel, noLabel, saveCallback, cancelCallback) =>
196         saveCancel(title, question, saveLabel, saveCallback, cancelCallback);
198 /**
199  * The Save and Cancel dialogue helper.
200  *
201  * @param {String|Promise} title
202  * @param {String|Promise} question
203  * @param {String|Promise} saveLabel
204  * @param {String|Promise} saveCallback
205  * @param {String|Promise} cancelCallback
206  * @returns {Promise}
207  */
208 export const saveCancel = async(title, question, saveLabel, saveCallback, cancelCallback) => {
209     const pendingPromise = new Pending('core/notification:confirm');
211     const [
212         ModalFactory,
213         ModalEvents,
214     ] = await Promise.all([
215         import('core/modal_factory'),
216         import('core/modal_events'),
217     ]);
219     return ModalFactory.create({
220         type: ModalFactory.types.SAVE_CANCEL,
221         title: title,
222         body: question,
223         buttons: {
224             // Note: The noLabel is no longer supported.
225             save: saveLabel,
226         },
227         removeOnClose: true,
228     })
229     .then(function(modal) {
230         modal.show();
232         modal.getRoot().on(ModalEvents.save, saveCallback);
233         modal.getRoot().on(ModalEvents.cancel, cancelCallback);
234         pendingPromise.resolve();
236         return modal;
237     });
238 };
240 /**
241  * Wrap M.core.exception.
242  *
243  * @param {Error} ex
244  */
245 export const exception = async ex => {
246     const pendingPromise = new Pending('core/notification:displayException');
248     // Fudge some parameters.
249     if (!ex.stack) {
250         ex.stack = '';
251     }
253     if (ex.debuginfo) {
254         ex.stack += ex.debuginfo + '\n';
255     }
257     if (!ex.backtrace && ex.stacktrace) {
258         ex.backtrace = ex.stacktrace;
259     }
261     if (ex.backtrace) {
262         ex.stack += ex.backtrace;
263         const ln = ex.backtrace.match(/line ([^ ]*) of/);
264         const fn = ex.backtrace.match(/ of ([^:]*): /);
265         if (ln && ln[1]) {
266             ex.lineNumber = ln[1];
267         }
268         if (fn && fn[1]) {
269             ex.fileName = fn[1];
270             if (ex.fileName.length > 30) {
271                 ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 27);
272             }
273         }
274     }
276     if (typeof ex.name === 'undefined' && ex.errorcode) {
277         ex.name = ex.errorcode;
278     }
280     const Y = await import('core/yui');
281     Y.use('moodle-core-notification-exception', function() {
282         var modal = new M.core.exception(ex);
284         modal.show();
286         pendingPromise.resolve();
287     });
288 };
290 /**
291  * Initialise the page for the suppled context, and displaying the supplied notifications.
292  *
293  * @param {Number} contextId
294  * @param {Array} notificationList
295  * @param {Boolean} userLoggedIn
296  */
297 export const init = (contextId, notificationList, userLoggedIn) => {
298     currentContextId = contextId;
300     // Setup the message target region if it isn't setup already
301     setupTargetRegion();
303     // Add provided notifications.
304     addNotifications(notificationList);
306     // If the user is not logged in then we can not fetch anything for them.
307     if (userLoggedIn) {
308         // Perform an initial poll for any new notifications.
309         fetchNotifications();
310     }
311 };
313 // To maintain backwards compatability we export default here.
314 export default {
315     init,
316     fetchNotifications,
317     addNotification,
318     alert,
319     confirm,
320     saveCancel,
321     exception,
322 };