MDL-62658 message_popup: fixed issue with malformed URL being generated
[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.
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});