message/output/popup/amd/src/notification_popover_controller.js

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