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