Moodle release 3.5rc1
[moodle.git] / message / output / popup / amd / src / notification_popover_controller.js
CommitLineData
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 27define(['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.
63 this.loadUnreadNotificationCount();
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
191 /**
192 * Ask the server how many unread notifications are left, render the value
193 * as a badge on the menu toggle and update the aria labels on the menu
194 * toggle.
195 *
196 * @method loadUnreadNotificationCount
197 */
198 NotificationPopoverController.prototype.loadUnreadNotificationCount = function() {
195a683b 199 NotificationRepo.countUnread({useridto: this.userId}).then(function(count) {
a0e358a6
RW
200 this.unreadCount = count;
201 this.renderUnreadCount();
202 this.updateButtonAriaLabel();
877d997f 203 }.bind(this)).catch(DebugNotification.exception);
a0e358a6
RW
204 };
205
a038fcf5
RW
206 /**
207 * Find the notification element for the given id.
208 *
209 * @param {int} id
210 * @method getNotificationElement
211 * @return {object|null} The notification element
212 */
213 NotificationPopoverController.prototype.getNotificationElement = function(id) {
214 var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]');
215 return element.length == 1 ? element : null;
216 };
217
a0e358a6
RW
218 /**
219 * Render the notification data with the appropriate template and add it to the DOM.
220 *
221 * @method renderNotifications
7b55aaa1
MN
222 * @param {array} notifications Notification data
223 * @param {object} container jQuery object the container to append the rendered notifications
224 * @return {object} jQuery promise that is resolved when all notifications have been
225 * rendered and added to the DOM
a0e358a6
RW
226 */
227 NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {
228 var promises = [];
a038fcf5 229
877d997f
DP
230 $.each(notifications, function(index, notification) {
231 // Determine what the offset was when loading this notification.
232 var offset = this.getOffset() - this.limit;
233 // Update the view more url to contain the offset to allow the notifications
234 // page to load to the correct position in the list of notifications.
235 notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {
236 notificationid: notification.id,
237 offset: offset,
238 });
30aac24d 239
5b40aaa5
NM
240 var promise = Templates.render('message_popup/notification_content_item', notification)
241 .then(function(html, js) {
242 return {html: html, js: js};
243 });
877d997f
DP
244 promises.push(promise);
245 }.bind(this));
a0e358a6 246
a5fbe27d
NM
247 return $.when.apply($, promises).then(function() {
248 // Each of the promises in the when will pass its results as an argument to the function.
249 // The order of the arguments will be the order that the promises are passed to when()
250 // i.e. the first promise's results will be in the first argument.
5b40aaa5
NM
251 $.each(arguments, function(index, argument) {
252 container.append(argument.html);
253 Templates.runTemplateJS(argument.js);
a5fbe27d
NM
254 });
255 return;
256 });
a0e358a6
RW
257 };
258
259 /**
260 * Send a request for more notifications from the server, if we aren't already
261 * loading some and haven't already loaded all of them.
262 *
263 * Takes into account the current mode of the popover and will request only
264 * unread notifications if required.
265 *
266 * All notifications are marked as read by the server when they are returned.
267 *
268 * @method loadMoreNotifications
7b55aaa1 269 * @return {object} jQuery promise that is resolved when notifications have been
a0e358a6
RW
270 * retrieved and added to the DOM
271 */
272 NotificationPopoverController.prototype.loadMoreNotifications = function() {
273 if (this.isLoading || this.hasLoadedAllContent()) {
274 return $.Deferred().resolve();
275 }
276
277 this.startLoading();
278 var request = {
279 limit: this.limit,
280 offset: this.getOffset(),
281 useridto: this.userId,
a0e358a6
RW
282 };
283
a0e358a6 284 var container = this.getContent();
7b55aaa1 285 return NotificationRepo.query(request).then(function(result) {
a0e358a6
RW
286 var notifications = result.notifications;
287 this.unreadCount = result.unreadcount;
288 this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);
0b19d048 289 this.initialLoad = true;
a0e358a6
RW
290 this.updateButtonAriaLabel();
291
292 if (notifications.length) {
293 this.incrementOffset();
294 return this.renderNotifications(notifications, container);
295 }
a0e358a6 296
7b55aaa1
MN
297 return false;
298 }.bind(this))
299 .always(function() {
300 this.stopLoading();
301 }.bind(this));
a0e358a6
RW
302 };
303
304 /**
305 * Send a request to the server to mark all unread notifications as read and update
306 * the unread count and unread notification elements appropriately.
307 *
7b55aaa1 308 * @return {Promise}
a0e358a6
RW
309 * @method markAllAsRead
310 */
311 NotificationPopoverController.prototype.markAllAsRead = function() {
312 this.markAllReadButton.addClass('loading');
313
195a683b 314 return NotificationRepo.markAllAsRead({useridto: this.userId})
a0e358a6
RW
315 .then(function() {
316 this.unreadCount = 0;
0b19d048 317 this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');
a0e358a6 318 }.bind(this))
7b55aaa1
MN
319 .always(function() {
320 this.markAllReadButton.removeClass('loading');
321 }.bind(this));
a0e358a6
RW
322 };
323
324 /**
0b19d048
RW
325 * Send a request to the server to mark a single notification as read and update
326 * the unread count and unread notification elements appropriately.
c5dd16a1 327 *
7b55aaa1
MN
328 * @param {jQuery} element
329 * @return {Promise|boolean}
0b19d048 330 * @method markAllAsRead
c5dd16a1 331 */
0b19d048
RW
332 NotificationPopoverController.prototype.markNotificationAsRead = function(element) {
333 if (!element.hasClass('unread')) {
7b55aaa1 334 return false;
c5dd16a1
RW
335 }
336
0b19d048
RW
337 return NotificationRepo.markAsRead(element.attr('data-id'))
338 .then(function() {
339 this.unreadCount--;
340 element.removeClass('unread');
341 }.bind(this));
c5dd16a1
RW
342 };
343
a0e358a6
RW
344 /**
345 * Add all of the required event listeners for this notification popover.
346 *
347 * @method registerEventListeners
348 */
349 NotificationPopoverController.prototype.registerEventListeners = function() {
195a683b
RW
350 CustomEvents.define(this.root, [
351 CustomEvents.events.activate,
a0e358a6
RW
352 ]);
353
a0e358a6 354 // Mark all notifications read if the user activates the mark all as read button.
b999cee9 355 this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {
a0e358a6
RW
356 this.markAllAsRead();
357 e.stopPropagation();
b999cee9 358 data.originalEvent.preventDefault();
a0e358a6
RW
359 }.bind(this));
360
0b19d048
RW
361 // Mark individual notification read if the user activates it.
362 this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {
363 var element = $(e.target).closest(SELECTORS.NOTIFICATION);
364 this.markNotificationAsRead(element);
365 e.stopPropagation();
a0e358a6
RW
366 }.bind(this));
367
368 // Update the notification information when the menu is opened.
369 this.root.on(this.events().menuOpened, function() {
370 this.hideUnreadCount();
371 this.updateButtonAriaLabel();
372
373 if (!this.hasDoneInitialLoad()) {
374 this.loadMoreNotifications();
375 }
376 }.bind(this));
377
378 // Update the unread notification count when the menu is closed.
379 this.root.on(this.events().menuClosed, function() {
380 this.renderUnreadCount();
a0e358a6
RW
381 this.updateButtonAriaLabel();
382 }.bind(this));
383
384 // Set aria attributes when popover is loading.
385 this.root.on(this.events().startLoading, function() {
386 this.getContent().attr('aria-busy', 'true');
387 }.bind(this));
388
389 // Set aria attributes when popover is finished loading.
390 this.root.on(this.events().stopLoading, function() {
391 this.getContent().attr('aria-busy', 'false');
392 }.bind(this));
393
394 // Load more notifications if the user has scrolled to the end of content
395 // item list.
195a683b 396 this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {
a0e358a6
RW
397 if (!this.isLoading && !this.hasLoadedAllContent()) {
398 this.loadMoreNotifications();
399 }
400 }.bind(this));
99c7f0a7
RW
401
402 // Stop mouse scroll from propagating to the window element and
403 // scrolling the page.
404 CustomEvents.define(this.getContentContainer(), [
405 CustomEvents.events.scrollLock
406 ]);
a038fcf5
RW
407
408 // Listen for when a notification is shown in the notifications page and mark
409 // it as read, if it's unread.
410 $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {
411 if (!notification.read) {
412 var element = this.getNotificationElement(notification.id);
413
414 if (element) {
415 element.removeClass('unread');
416 }
417
418 this.unreadCount--;
419 this.renderUnreadCount();
420 }
421 }.bind(this));
a0e358a6
RW
422 };
423
424 return NotificationPopoverController;
425});