Commit | Line | Data |
---|---|---|
a0e358a6 RW |
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 | ||
16 | /** | |
17 | * Controls the notification popover in the nav bar. | |
18 | * | |
7d69958e | 19 | * See template: message_popup/notification_popover |
a0e358a6 | 20 | * |
7d69958e | 21 | * @module message_popup/notification_popover_controller |
a0e358a6 | 22 | * @class notification_popover_controller |
7d69958e | 23 | * @package message_popup |
a0e358a6 RW |
24 | * @copyright 2016 Ryan Wyllie <ryan@moodle.com> |
25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
a0e358a6 | 26 | */ |
963ba889 | 27 | define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', |
d4555a3d | 28 | 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller', |
641b36e2 | 29 | 'message_popup/notification_repository', 'message_popup/notification_area_events'], |
963ba889 | 30 | function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents, |
a038fcf5 | 31 | PopoverController, NotificationRepo, NotificationAreaEvents) { |
a0e358a6 RW |
32 | |
33 | var SELECTORS = { | |
6af2bd09 | 34 | MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]', |
6af2bd09 | 35 | ALL_NOTIFICATIONS_CONTAINER: '[data-region="all-notifications"]', |
0b19d048 RW |
36 | NOTIFICATION: '[data-region="notification-content-item-container"]', |
37 | UNREAD_NOTIFICATION: '[data-region="notification-content-item-container"].unread', | |
eeee7bca | 38 | NOTIFICATION_LINK: '[data-action="content-item-link"]', |
6af2bd09 | 39 | EMPTY_MESSAGE: '[data-region="empty-message"]', |
6af2bd09 | 40 | COUNT_CONTAINER: '[data-region="count-container"]', |
a0e358a6 RW |
41 | }; |
42 | ||
43 | /** | |
44 | * Constructor for the NotificationPopoverController. | |
d4555a3d | 45 | * Extends PopoverRegionController. |
a0e358a6 | 46 | * |
7b55aaa1 | 47 | * @param {object} element jQuery object root element of the popover |
a0e358a6 RW |
48 | */ |
49 | var NotificationPopoverController = function(element) { | |
50 | // Initialise base class. | |
195a683b | 51 | PopoverController.call(this, element); |
a0e358a6 RW |
52 | |
53 | this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON); | |
54 | this.unreadCount = 0; | |
6af2bd09 | 55 | this.userId = this.root.attr('data-userid'); |
0b19d048 RW |
56 | this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER); |
57 | this.limit = 20; | |
58 | this.offset = 0; | |
59 | this.loadedAll = false; | |
60 | this.initialLoad = false; | |
a0e358a6 RW |
61 | |
62 | // Let's find out how many unread notifications there are. | |
84e03ed8 | 63 | this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html(); |
a0e358a6 RW |
64 | }; |
65 | ||
66 | /** | |
67 | * Clone the parent prototype. | |
68 | */ | |
195a683b | 69 | NotificationPopoverController.prototype = Object.create(PopoverController.prototype); |
a0e358a6 | 70 | |
34eb5fcb RW |
71 | /** |
72 | * Make sure the constructor is set correctly. | |
73 | */ | |
74 | NotificationPopoverController.prototype.constructor = NotificationPopoverController; | |
75 | ||
a0e358a6 RW |
76 | /** |
77 | * Set the correct aria label on the menu toggle button to be read out by screen | |
78 | * readers. The message will indicate the state of the unread notifications. | |
79 | * | |
80 | * @method updateButtonAriaLabel | |
81 | */ | |
82 | NotificationPopoverController.prototype.updateButtonAriaLabel = function() { | |
83 | if (this.isMenuOpen()) { | |
34eb5fcb | 84 | Str.get_string('hidenotificationwindow', 'message').done(function(string) { |
a0e358a6 RW |
85 | this.menuToggle.attr('aria-label', string); |
86 | }.bind(this)); | |
87 | } else { | |
88 | if (this.unreadCount) { | |
34eb5fcb | 89 | Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) { |
a0e358a6 RW |
90 | this.menuToggle.attr('aria-label', string); |
91 | }.bind(this)); | |
92 | } else { | |
34eb5fcb | 93 | Str.get_string('shownotificationwindownonew', 'message').done(function(string) { |
a0e358a6 RW |
94 | this.menuToggle.attr('aria-label', string); |
95 | }.bind(this)); | |
96 | } | |
97 | } | |
98 | }; | |
99 | ||
100 | /** | |
101 | * Return the jQuery element with the content. This will return either | |
102 | * the unread notification container or the all notification container | |
103 | * depending on which is currently visible. | |
104 | * | |
105 | * @method getContent | |
7b55aaa1 | 106 | * @return {object} jQuery object currently visible content contianer |
a0e358a6 RW |
107 | */ |
108 | NotificationPopoverController.prototype.getContent = function() { | |
0b19d048 | 109 | return this.container; |
a0e358a6 RW |
110 | }; |
111 | ||
112 | /** | |
113 | * Get the offset value for the current state of the popover in order | |
114 | * to sent to the backend to correctly paginate the notifications. | |
115 | * | |
116 | * @method getOffset | |
7b55aaa1 | 117 | * @return {int} current offset |
a0e358a6 RW |
118 | */ |
119 | NotificationPopoverController.prototype.getOffset = function() { | |
0b19d048 | 120 | return this.offset; |
a0e358a6 RW |
121 | }; |
122 | ||
123 | /** | |
124 | * Increment the offset for the current state, if required. | |
125 | * | |
126 | * @method incrementOffset | |
127 | */ | |
128 | NotificationPopoverController.prototype.incrementOffset = function() { | |
0b19d048 | 129 | this.offset += this.limit; |
a0e358a6 RW |
130 | }; |
131 | ||
132 | /** | |
133 | * Check if the first load of notification has been triggered for the current | |
134 | * state of the popover. | |
135 | * | |
136 | * @method hasDoneInitialLoad | |
7b55aaa1 | 137 | * @return {bool} true if first notification loaded, false otherwise |
a0e358a6 RW |
138 | */ |
139 | NotificationPopoverController.prototype.hasDoneInitialLoad = function() { | |
0b19d048 | 140 | return this.initialLoad; |
a0e358a6 RW |
141 | }; |
142 | ||
143 | /** | |
144 | * Check if we've loaded all of the notifications for the current popover | |
145 | * state. | |
146 | * | |
147 | * @method hasLoadedAllContent | |
7b55aaa1 | 148 | * @return {bool} true if all notifications loaded, false otherwise |
a0e358a6 RW |
149 | */ |
150 | NotificationPopoverController.prototype.hasLoadedAllContent = function() { | |
0b19d048 | 151 | return this.loadedAll; |
a0e358a6 RW |
152 | }; |
153 | ||
154 | /** | |
155 | * Set the state of the loaded all content property for the current state | |
156 | * of the popover. | |
157 | * | |
158 | * @method setLoadedAllContent | |
7b55aaa1 | 159 | * @param {bool} val True if all content is loaded, false otherwise |
a0e358a6 RW |
160 | */ |
161 | NotificationPopoverController.prototype.setLoadedAllContent = function(val) { | |
0b19d048 | 162 | this.loadedAll = val; |
a0e358a6 RW |
163 | }; |
164 | ||
165 | /** | |
166 | * Show the unread notification count badge on the menu toggle if there | |
167 | * are unread notifications, otherwise hide it. | |
168 | * | |
169 | * @method renderUnreadCount | |
170 | */ | |
171 | NotificationPopoverController.prototype.renderUnreadCount = function() { | |
6af2bd09 | 172 | var element = this.root.find(SELECTORS.COUNT_CONTAINER); |
a0e358a6 RW |
173 | |
174 | if (this.unreadCount) { | |
175 | element.text(this.unreadCount); | |
176 | element.removeClass('hidden'); | |
177 | } else { | |
178 | element.addClass('hidden'); | |
179 | } | |
180 | }; | |
181 | ||
182 | /** | |
183 | * Hide the unread notification count badge on the menu toggle. | |
184 | * | |
185 | * @method hideUnreadCount | |
186 | */ | |
187 | NotificationPopoverController.prototype.hideUnreadCount = function() { | |
6af2bd09 | 188 | this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden'); |
a0e358a6 RW |
189 | }; |
190 | ||
a038fcf5 RW |
191 | /** |
192 | * Find the notification element for the given id. | |
193 | * | |
194 | * @param {int} id | |
195 | * @method getNotificationElement | |
196 | * @return {object|null} The notification element | |
197 | */ | |
198 | NotificationPopoverController.prototype.getNotificationElement = function(id) { | |
199 | var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]'); | |
200 | return element.length == 1 ? element : null; | |
201 | }; | |
202 | ||
a0e358a6 RW |
203 | /** |
204 | * Render the notification data with the appropriate template and add it to the DOM. | |
205 | * | |
206 | * @method renderNotifications | |
7b55aaa1 MN |
207 | * @param {array} notifications Notification data |
208 | * @param {object} container jQuery object the container to append the rendered notifications | |
209 | * @return {object} jQuery promise that is resolved when all notifications have been | |
210 | * rendered and added to the DOM | |
a0e358a6 RW |
211 | */ |
212 | NotificationPopoverController.prototype.renderNotifications = function(notifications, container) { | |
213 | var promises = []; | |
a038fcf5 | 214 | |
877d997f DP |
215 | $.each(notifications, function(index, notification) { |
216 | // Determine what the offset was when loading this notification. | |
217 | var offset = this.getOffset() - this.limit; | |
218 | // Update the view more url to contain the offset to allow the notifications | |
219 | // page to load to the correct position in the list of notifications. | |
220 | notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', { | |
221 | notificationid: notification.id, | |
222 | offset: offset, | |
223 | }); | |
30aac24d | 224 | |
105974cd MH |
225 | // Link to mark read page before loading the actual link. |
226 | notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', { | |
105974cd | 227 | notificationid: notification.id, |
4f6cb2ee | 228 | redirecturl: notification.contexturl |
105974cd MH |
229 | }); |
230 | ||
5b40aaa5 NM |
231 | var promise = Templates.render('message_popup/notification_content_item', notification) |
232 | .then(function(html, js) { | |
233 | return {html: html, js: js}; | |
234 | }); | |
877d997f DP |
235 | promises.push(promise); |
236 | }.bind(this)); | |
a0e358a6 | 237 | |
a5fbe27d NM |
238 | return $.when.apply($, promises).then(function() { |
239 | // Each of the promises in the when will pass its results as an argument to the function. | |
240 | // The order of the arguments will be the order that the promises are passed to when() | |
241 | // i.e. the first promise's results will be in the first argument. | |
5b40aaa5 NM |
242 | $.each(arguments, function(index, argument) { |
243 | container.append(argument.html); | |
244 | Templates.runTemplateJS(argument.js); | |
a5fbe27d NM |
245 | }); |
246 | return; | |
247 | }); | |
a0e358a6 RW |
248 | }; |
249 | ||
250 | /** | |
251 | * Send a request for more notifications from the server, if we aren't already | |
252 | * loading some and haven't already loaded all of them. | |
253 | * | |
254 | * Takes into account the current mode of the popover and will request only | |
255 | * unread notifications if required. | |
256 | * | |
257 | * All notifications are marked as read by the server when they are returned. | |
258 | * | |
259 | * @method loadMoreNotifications | |
7b55aaa1 | 260 | * @return {object} jQuery promise that is resolved when notifications have been |
a0e358a6 RW |
261 | * retrieved and added to the DOM |
262 | */ | |
263 | NotificationPopoverController.prototype.loadMoreNotifications = function() { | |
264 | if (this.isLoading || this.hasLoadedAllContent()) { | |
265 | return $.Deferred().resolve(); | |
266 | } | |
267 | ||
268 | this.startLoading(); | |
269 | var request = { | |
270 | limit: this.limit, | |
271 | offset: this.getOffset(), | |
272 | useridto: this.userId, | |
a0e358a6 RW |
273 | }; |
274 | ||
a0e358a6 | 275 | var container = this.getContent(); |
7b55aaa1 | 276 | return NotificationRepo.query(request).then(function(result) { |
a0e358a6 RW |
277 | var notifications = result.notifications; |
278 | this.unreadCount = result.unreadcount; | |
279 | this.setLoadedAllContent(!notifications.length || notifications.length < this.limit); | |
0b19d048 | 280 | this.initialLoad = true; |
a0e358a6 RW |
281 | this.updateButtonAriaLabel(); |
282 | ||
283 | if (notifications.length) { | |
284 | this.incrementOffset(); | |
285 | return this.renderNotifications(notifications, container); | |
286 | } | |
a0e358a6 | 287 | |
7b55aaa1 MN |
288 | return false; |
289 | }.bind(this)) | |
290 | .always(function() { | |
291 | this.stopLoading(); | |
292 | }.bind(this)); | |
a0e358a6 RW |
293 | }; |
294 | ||
295 | /** | |
296 | * Send a request to the server to mark all unread notifications as read and update | |
297 | * the unread count and unread notification elements appropriately. | |
298 | * | |
7b55aaa1 | 299 | * @return {Promise} |
a0e358a6 RW |
300 | * @method markAllAsRead |
301 | */ | |
302 | NotificationPopoverController.prototype.markAllAsRead = function() { | |
303 | this.markAllReadButton.addClass('loading'); | |
304 | ||
195a683b | 305 | return NotificationRepo.markAllAsRead({useridto: this.userId}) |
a0e358a6 RW |
306 | .then(function() { |
307 | this.unreadCount = 0; | |
0b19d048 | 308 | this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread'); |
a0e358a6 | 309 | }.bind(this)) |
7b55aaa1 MN |
310 | .always(function() { |
311 | this.markAllReadButton.removeClass('loading'); | |
312 | }.bind(this)); | |
a0e358a6 RW |
313 | }; |
314 | ||
a0e358a6 RW |
315 | /** |
316 | * Add all of the required event listeners for this notification popover. | |
317 | * | |
318 | * @method registerEventListeners | |
319 | */ | |
320 | NotificationPopoverController.prototype.registerEventListeners = function() { | |
195a683b RW |
321 | CustomEvents.define(this.root, [ |
322 | CustomEvents.events.activate, | |
a0e358a6 RW |
323 | ]); |
324 | ||
a0e358a6 | 325 | // Mark all notifications read if the user activates the mark all as read button. |
b999cee9 | 326 | this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) { |
a0e358a6 RW |
327 | this.markAllAsRead(); |
328 | e.stopPropagation(); | |
b999cee9 | 329 | data.originalEvent.preventDefault(); |
a0e358a6 RW |
330 | }.bind(this)); |
331 | ||
0b19d048 RW |
332 | // Mark individual notification read if the user activates it. |
333 | this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) { | |
334 | var element = $(e.target).closest(SELECTORS.NOTIFICATION); | |
105974cd MH |
335 | |
336 | if (element.hasClass('unread')) { | |
337 | this.unreadCount--; | |
338 | element.removeClass('unread'); | |
339 | } | |
340 | ||
0b19d048 | 341 | e.stopPropagation(); |
a0e358a6 RW |
342 | }.bind(this)); |
343 | ||
344 | // Update the notification information when the menu is opened. | |
345 | this.root.on(this.events().menuOpened, function() { | |
346 | this.hideUnreadCount(); | |
347 | this.updateButtonAriaLabel(); | |
348 | ||
349 | if (!this.hasDoneInitialLoad()) { | |
350 | this.loadMoreNotifications(); | |
351 | } | |
352 | }.bind(this)); | |
353 | ||
354 | // Update the unread notification count when the menu is closed. | |
355 | this.root.on(this.events().menuClosed, function() { | |
356 | this.renderUnreadCount(); | |
a0e358a6 RW |
357 | this.updateButtonAriaLabel(); |
358 | }.bind(this)); | |
359 | ||
360 | // Set aria attributes when popover is loading. | |
361 | this.root.on(this.events().startLoading, function() { | |
362 | this.getContent().attr('aria-busy', 'true'); | |
363 | }.bind(this)); | |
364 | ||
365 | // Set aria attributes when popover is finished loading. | |
366 | this.root.on(this.events().stopLoading, function() { | |
367 | this.getContent().attr('aria-busy', 'false'); | |
368 | }.bind(this)); | |
369 | ||
370 | // Load more notifications if the user has scrolled to the end of content | |
371 | // item list. | |
195a683b | 372 | this.getContentContainer().on(CustomEvents.events.scrollBottom, function() { |
a0e358a6 RW |
373 | if (!this.isLoading && !this.hasLoadedAllContent()) { |
374 | this.loadMoreNotifications(); | |
375 | } | |
376 | }.bind(this)); | |
99c7f0a7 RW |
377 | |
378 | // Stop mouse scroll from propagating to the window element and | |
379 | // scrolling the page. | |
380 | CustomEvents.define(this.getContentContainer(), [ | |
381 | CustomEvents.events.scrollLock | |
382 | ]); | |
a038fcf5 RW |
383 | |
384 | // Listen for when a notification is shown in the notifications page and mark | |
385 | // it as read, if it's unread. | |
386 | $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) { | |
387 | if (!notification.read) { | |
388 | var element = this.getNotificationElement(notification.id); | |
389 | ||
390 | if (element) { | |
391 | element.removeClass('unread'); | |
392 | } | |
393 | ||
394 | this.unreadCount--; | |
395 | this.renderUnreadCount(); | |
396 | } | |
397 | }.bind(this)); | |
a0e358a6 RW |
398 | }; |
399 | ||
400 | return NotificationPopoverController; | |
401 | }); |