MDL-68788 core_notification: Check if the user is logged in
[moodle.git] / lib / amd / src / notification.js
CommitLineData
9bdcf579
DW
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/>.
15
e8df743b
AN
16import Pending from 'core/pending';
17import Log from 'core/log';
18
19let currentContextId = M.cfg.contextid;
20
21const notificationTypes = {
22 success: 'core/notification_success',
23 info: 'core/notification_info',
24 warning: 'core/notification_warning',
25 error: 'core/notification_error',
26};
27
28const notificationRegionId = 'user-notifications';
29
30const Selectors = {
31 notificationRegion: `#${notificationRegionId}`,
32 fallbackRegionParents: [
33 '#region-main',
34 '[role="main"]',
35 'body',
36 ],
37};
38
39const setupTargetRegion = () => {
40 let targetRegion = getNotificationRegion();
41 if (targetRegion) {
42 return false;
43 }
44
45 const newRegion = document.createElement('span');
46 newRegion.id = notificationRegionId;
47
48 return Selectors.fallbackRegionParents.some(selector => {
49 const targetRegion = document.querySelector(selector);
50
51 if (targetRegion) {
52 targetRegion.prepend(newRegion);
53 return true;
54 }
55
56 return false;
57 });
58};
59
60
9bdcf579 61/**
e8df743b 62 * Poll the server for any new notifications.
0346323c 63 *
e8df743b 64 * @returns {Promise}
9bdcf579 65 */
e8df743b
AN
66export const fetchNotifications = async() => {
67 const Ajax = await import('core/ajax');
9bdcf579 68
e8df743b
AN
69 return Ajax.call([{
70 methodname: 'core_fetch_notifications',
71 args: {
72 contextid: currentContextId
73 }
74 }])[0]
75 .then(addNotifications);
76};
0346323c 77
e8df743b
AN
78/**
79 * Add all of the supplied notifications.
80 *
81 * @param {Array} notifications The list of notificaitons
82 * @returns {Promise}
83 */
84const addNotifications = notifications => {
85 if (!notifications.length) {
86 return Promise.resolve();
87 }
0bc0fa87 88
e8df743b
AN
89 const pendingPromise = new Pending('core/notification:addNotifications');
90 notifications.forEach(notification => renderNotification(notification.template, notification.variables));
0346323c 91
e8df743b
AN
92 return pendingPromise.resolve();
93};
0bc0fa87 94
e8df743b
AN
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 */
107export const addNotification = notification => {
108 const pendingPromise = new Pending('core/notification:addNotifications');
0346323c 109
e8df743b 110 let template = notificationTypes.error;
0346323c 111
e8df743b
AN
112 notification = {
113 closebutton: true,
114 announce: true,
115 type: 'error',
116 ...notification,
117 };
0346323c 118
e8df743b
AN
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 }
0346323c 128
e8df743b
AN
129 return renderNotification(template, notification)
130 .then(pendingPromise.resolve);
131};
0346323c 132
e8df743b
AN
133const renderNotification = async(template, variables) => {
134 if (typeof variables.message === 'undefined' || !variables.message) {
135 Log.debug('Notification received without content. Skipping.');
136 return;
137 }
0346323c 138
e8df743b
AN
139 const pendingPromise = new Pending('core/notification:renderNotification');
140 const Templates = await import('core/templates');
0346323c 141
e8df743b
AN
142 Templates.renderForPromise(template, variables)
143 .then(({html, js = ''}) => {
144 Templates.prependNodeContents(getNotificationRegion(), html, js);
0bc0fa87 145
e8df743b
AN
146 return;
147 })
148 .then(pendingPromise.resolve)
149 .catch(exception);
150};
0346323c 151
e8df743b 152const getNotificationRegion = () => document.querySelector(Selectors.notificationRegion);
0bc0fa87 153
e8df743b
AN
154/**
155 * Alert dialogue.
156 *
157 * @param {String|Promise} title
158 * @param {String|Promise} message
159 * @param {String|Promise} cancelText
160 * @returns {Promise}
161 */
162export const alert = async(title, message, cancelText) => {
163 var pendingPromise = new Pending('core/notification:alert');
0bc0fa87 164
e8df743b 165 const ModalFactory = await import('core/modal_factory');
0bc0fa87 166
e8df743b
AN
167 return ModalFactory.create({
168 type: ModalFactory.types.ALERT,
169 body: message,
170 title: title,
171 buttons: {
172 cancel: cancelText,
0346323c 173 },
e8df743b
AN
174 removeOnClose: true,
175 })
176 .then(function(modal) {
177 modal.show();
9bdcf579 178
e8df743b
AN
179 pendingPromise.resolve();
180 return modal;
181 });
182};
9bdcf579 183
e8df743b
AN
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
9050f3f1
AN
190 * @param {String|Promise} noLabel
191 * @param {String|Promise} saveCallback
192 * @param {String|Promise} cancelCallback
193 * @returns {Promise}
194 */
195export const confirm = (title, question, saveLabel, noLabel, saveCallback, cancelCallback) =>
196 saveCancel(title, question, saveLabel, saveCallback, cancelCallback);
197
198/**
199 * The Save and Cancel dialogue helper.
200 *
201 * @param {String|Promise} title
202 * @param {String|Promise} question
203 * @param {String|Promise} saveLabel
e8df743b
AN
204 * @param {String|Promise} saveCallback
205 * @param {String|Promise} cancelCallback
206 * @returns {Promise}
207 */
208export const saveCancel = async(title, question, saveLabel, saveCallback, cancelCallback) => {
209 const pendingPromise = new Pending('core/notification:confirm');
210
211 const [
212 ModalFactory,
213 ModalEvents,
214 ] = await Promise.all([
215 import('core/modal_factory'),
216 import('core/modal_events'),
217 ]);
218
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,
9bdcf579 226 },
e8df743b
AN
227 removeOnClose: true,
228 })
229 .then(function(modal) {
230 modal.show();
9bdcf579 231
e8df743b
AN
232 modal.getRoot().on(ModalEvents.save, saveCallback);
233 modal.getRoot().on(ModalEvents.cancel, cancelCallback);
234 pendingPromise.resolve();
9bdcf579 235
e8df743b
AN
236 return modal;
237 });
238};
0bc0fa87 239
e8df743b
AN
240/**
241 * Wrap M.core.exception.
242 *
243 * @param {Error} ex
244 */
245export const exception = async ex => {
246 const pendingPromise = new Pending('core/notification:displayException');
247
248 // Fudge some parameters.
249 if (!ex.stack) {
250 ex.stack = '';
251 }
252
253 if (ex.debuginfo) {
254 ex.stack += ex.debuginfo + '\n';
255 }
256
257 if (!ex.backtrace && ex.stacktrace) {
258 ex.backtrace = ex.stacktrace;
259 }
260
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);
9bdcf579 272 }
9bdcf579 273 }
e8df743b 274 }
0346323c 275
e8df743b
AN
276 if (typeof ex.name === 'undefined' && ex.errorcode) {
277 ex.name = ex.errorcode;
278 }
0346323c 279
e8df743b
AN
280 const Y = await import('core/yui');
281 Y.use('moodle-core-notification-exception', function() {
282 var modal = new M.core.exception(ex);
0346323c 283
e8df743b 284 modal.show();
0346323c 285
e8df743b
AN
286 pendingPromise.resolve();
287 });
288};
0346323c 289
e8df743b
AN
290/**
291 * Initialise the page for the suppled context, and displaying the supplied notifications.
292 *
293 * @param {Number} contextId
294 * @param {Array} notificationList
eff7ef2c 295 * @param {Boolean} userLoggedIn
e8df743b 296 */
eff7ef2c 297export const init = (contextId, notificationList, userLoggedIn) => {
e8df743b
AN
298 currentContextId = contextId;
299
300 // Setup the message target region if it isn't setup already
301 setupTargetRegion();
302
303 // Add provided notifications.
304 addNotifications(notificationList);
305
eff7ef2c
MM
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 }
e8df743b
AN
311};
312
313// To maintain backwards compatability we export default here.
314export default {
315 init,
316 fetchNotifications,
317 addNotification,
318 alert,
319 confirm,
320 saveCancel,
321 exception,
322};