Commit | Line | Data |
---|---|---|
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 |
16 | import Pending from 'core/pending'; |
17 | import Log from 'core/log'; | |
18 | ||
19 | let currentContextId = M.cfg.contextid; | |
20 | ||
21 | const notificationTypes = { | |
22 | success: 'core/notification_success', | |
23 | info: 'core/notification_info', | |
24 | warning: 'core/notification_warning', | |
25 | error: 'core/notification_error', | |
26 | }; | |
27 | ||
28 | const notificationRegionId = 'user-notifications'; | |
29 | ||
30 | const Selectors = { | |
31 | notificationRegion: `#${notificationRegionId}`, | |
32 | fallbackRegionParents: [ | |
33 | '#region-main', | |
34 | '[role="main"]', | |
35 | 'body', | |
36 | ], | |
37 | }; | |
38 | ||
39 | const 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 |
66 | export 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 | */ | |
84 | const 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 | */ | |
107 | export 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 |
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 | } | |
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 | 152 | const 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 | */ | |
162 | export 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 | */ | |
195 | export 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 | */ | |
208 | export 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 | */ | |
245 | export 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 | 297 | export 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. | |
314 | export default { | |
315 | init, | |
316 | fetchNotifications, | |
317 | addNotification, | |
318 | alert, | |
319 | confirm, | |
320 | saveCancel, | |
321 | exception, | |
322 | }; |