message/output/popup/amd/src/notification_area_control_area.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 area on the notification page.
  17. *
  18. * @module message_popup/notification_area_control_area
  19. * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. define(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events',
  23. 'message_popup/notification_repository', 'message_popup/notification_area_events'],
  24. function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) {
  25. var SELECTORS = {
  26. CONTAINER: '[data-region="notification-area"]',
  27. CONTENT: '[data-region="content"]',
  28. NOTIFICATION: '[data-region="notification-content-item-container"]',
  29. CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
  30. };
  31. var TEMPLATES = {
  32. NOTIFICATION: 'message_popup/notification_content_item',
  33. };
  34. /**
  35. * Constructor for ControlArea
  36. *
  37. * @class
  38. * @param {object} root The root element for the content area
  39. * @param {int} userId The user id of the current user
  40. */
  41. var ControlArea = function(root, userId) {
  42. this.root = $(root);
  43. this.container = this.root.closest(SELECTORS.CONTAINER);
  44. this.userId = userId;
  45. this.content = this.root.find(SELECTORS.CONTENT);
  46. this.offset = 0;
  47. this.limit = 20;
  48. this.initialLoad = false;
  49. this.isLoading = false;
  50. this.loadedAll = false;
  51. this.notifications = {};
  52. this.registerEventListeners();
  53. };
  54. /**
  55. * Get the root element.
  56. *
  57. * @method getRoot
  58. * @return {object} jQuery element
  59. */
  60. ControlArea.prototype.getRoot = function() {
  61. return this.root;
  62. };
  63. /**
  64. * Get the container element (which the control area is within).
  65. *
  66. * @method getContainer
  67. * @return {object} jQuery element
  68. */
  69. ControlArea.prototype.getContainer = function() {
  70. return this.container;
  71. };
  72. /**
  73. * Get the user id.
  74. *
  75. * @method getUserId
  76. * @return {int}
  77. */
  78. ControlArea.prototype.getUserId = function() {
  79. return this.userId;
  80. };
  81. /**
  82. * Get the control area content element.
  83. *
  84. * @method getContent
  85. * @return {object} jQuery element
  86. */
  87. ControlArea.prototype.getContent = function() {
  88. return this.content;
  89. };
  90. /**
  91. * Get the offset value for paginated loading of the
  92. * notifications.
  93. *
  94. * @method getOffset
  95. * @return {int}
  96. */
  97. ControlArea.prototype.getOffset = function() {
  98. return this.offset;
  99. };
  100. /**
  101. * Get the limit value for the paginated loading of the
  102. * notifications.
  103. *
  104. * @method getLimit
  105. * @return {int}
  106. */
  107. ControlArea.prototype.getLimit = function() {
  108. return this.limit;
  109. };
  110. /**
  111. * Set the offset value for the paginated loading of the
  112. * notifications.
  113. *
  114. * @method setOffset
  115. * @param {int} value The new offset value
  116. */
  117. ControlArea.prototype.setOffset = function(value) {
  118. this.offset = value;
  119. };
  120. /**
  121. * Set the limit value for the paginated loading of the
  122. * notifications.
  123. *
  124. * @method setLimit
  125. * @param {int} value The new limit value
  126. */
  127. ControlArea.prototype.setLimit = function(value) {
  128. this.limit = value;
  129. };
  130. /**
  131. * Increment the offset by the limit amount.
  132. *
  133. * @method incrementOffset
  134. */
  135. ControlArea.prototype.incrementOffset = function() {
  136. this.offset += this.limit;
  137. };
  138. /**
  139. * Flag the control area as loading.
  140. *
  141. * @method startLoading
  142. */
  143. ControlArea.prototype.startLoading = function() {
  144. this.isLoading = true;
  145. this.getRoot().addClass('loading');
  146. };
  147. /**
  148. * Remove the loading flag from the control area.
  149. *
  150. * @method stopLoading
  151. */
  152. ControlArea.prototype.stopLoading = function() {
  153. this.isLoading = false;
  154. this.getRoot().removeClass('loading');
  155. };
  156. /**
  157. * Check if the first load of notifications has been triggered.
  158. *
  159. * @method hasDoneInitialLoad
  160. * @return {bool} true if first notification loaded, false otherwise
  161. */
  162. ControlArea.prototype.hasDoneInitialLoad = function() {
  163. return this.initialLoad;
  164. };
  165. /**
  166. * Check if all of the notifications have been loaded.
  167. *
  168. * @method hasLoadedAllContent
  169. * @return {bool}
  170. */
  171. ControlArea.prototype.hasLoadedAllContent = function() {
  172. return this.loadedAll;
  173. };
  174. /**
  175. * Set the state of the loaded all content property.
  176. *
  177. * @method setLoadedAllContent
  178. * @param {bool} val True if all content is loaded, false otherwise
  179. */
  180. ControlArea.prototype.setLoadedAllContent = function(val) {
  181. this.loadedAll = val;
  182. };
  183. /**
  184. * Save a notification in the cache.
  185. *
  186. * @method setCacheNotification
  187. * @param {object} notification A notification returned by a webservice
  188. */
  189. ControlArea.prototype.setCacheNotification = function(notification) {
  190. this.notifications[notification.id] = notification;
  191. };
  192. /**
  193. * Retrieve a notification from the cache.
  194. *
  195. * @method getCacheNotification
  196. * @param {int} id The id for the notification you wish to retrieve
  197. * @return {object} A notification (as returned by a webservice)
  198. */
  199. ControlArea.prototype.getCacheNotification = function(id) {
  200. return this.notifications[id];
  201. };
  202. /**
  203. * Find the notification element in the control area for the given id.
  204. *
  205. * @method getNotificationElement
  206. * @param {int} id The notification id
  207. * @return {(object|null)} jQuery element or null
  208. */
  209. ControlArea.prototype.getNotificationElement = function(id) {
  210. var element = this.getRoot().find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]');
  211. return element.length == 1 ? element : null;
  212. };
  213. /**
  214. * Scroll the notification element into view within the control area, if it
  215. * isn't already visible.
  216. *
  217. * @method scrollNotificationIntoView
  218. * @param {object} notificationElement The jQuery notification element
  219. */
  220. ControlArea.prototype.scrollNotificationIntoView = function(notificationElement) {
  221. var position = notificationElement.position();
  222. var container = this.getRoot();
  223. var relativeTop = position.top - container.scrollTop();
  224. // If the element isn't in the view window.
  225. if (relativeTop > container.innerHeight()) {
  226. var height = notificationElement.outerHeight();
  227. // Offset enough to make sure the notification will be in view.
  228. height = height * 4;
  229. var scrollTo = position.top - height;
  230. container.scrollTop(scrollTo);
  231. }
  232. };
  233. /**
  234. * Show the full notification for the given notification element. The notification
  235. * context is retrieved from the cache and send as data with an event to be
  236. * rendered in the content area.
  237. *
  238. * @method showNotification
  239. * @param {(int|object)} notificationElement The notification id or jQuery notification element
  240. */
  241. ControlArea.prototype.showNotification = function(notificationElement) {
  242. if (typeof notificationElement !== 'object') {
  243. // Assume it's an ID if it's not an object.
  244. notificationElement = this.getNotificationElement(notificationElement);
  245. }
  246. if (notificationElement && notificationElement.length) {
  247. this.getRoot().find(SELECTORS.NOTIFICATION).removeClass('selected');
  248. notificationElement.addClass('selected').find(SELECTORS.CAN_RECEIVE_FOCUS).focus();
  249. var notificationId = notificationElement.attr('data-id');
  250. var notification = this.getCacheNotification(notificationId);
  251. this.scrollNotificationIntoView(notificationElement);
  252. // Create a new version of the notification to send with the notification so
  253. // this copy isn't modified.
  254. this.getContainer().trigger(NotificationAreaEvents.showNotification, [$.extend({}, notification)]);
  255. }
  256. };
  257. /**
  258. * Send a request to mark the notification as read in the server and remove the unread
  259. * status from the element.
  260. *
  261. * @method markNotificationAsRead
  262. * @param {object} notificationElement The jQuery notification element
  263. * @return {object} jQuery promise
  264. */
  265. ControlArea.prototype.markNotificationAsRead = function(notificationElement) {
  266. return NotificationRepo.markAsRead(notificationElement.attr('data-id')).done(function() {
  267. notificationElement.removeClass('unread');
  268. });
  269. };
  270. /**
  271. * Render the notification data with the appropriate template and add it to the DOM.
  272. *
  273. * @method renderNotifications
  274. * @param {array} notifications Array of notification data
  275. * @return {object} jQuery promise that is resolved when all notifications have been
  276. * rendered and added to the DOM
  277. */
  278. ControlArea.prototype.renderNotifications = function(notifications) {
  279. var promises = [];
  280. var container = this.getContent();
  281. $.each(notifications, function(index, notification) {
  282. // Need to remove the contexturl so the item isn't rendered
  283. // as a link.
  284. var contextUrl = notification.contexturl;
  285. delete notification.contexturl;
  286. var promise = Templates.render(TEMPLATES.NOTIFICATION, notification)
  287. .then(function(html, js) {
  288. // Restore it for the cache.
  289. notification.contexturl = contextUrl;
  290. this.setCacheNotification(notification);
  291. // Pass the Rendered content out.
  292. return {html: html, js: js};
  293. }.bind(this));
  294. promises.push(promise);
  295. }.bind(this));
  296. return $.when.apply($, promises).then(function() {
  297. // Each of the promises in the when will pass its results as an argument to the function.
  298. // The order of the arguments will be the order that the promises are passed to when()
  299. // i.e. the first promise's results will be in the first argument.
  300. $.each(arguments, function(index, argument) {
  301. container.append(argument.html);
  302. Templates.runTemplateJS(argument.js);
  303. });
  304. return;
  305. });
  306. };
  307. /**
  308. * Load notifications from the server and render them.
  309. *
  310. * @method loadMoreNotifications
  311. * @return {object} jQuery promise
  312. */
  313. ControlArea.prototype.loadMoreNotifications = function() {
  314. if (this.isLoading || this.hasLoadedAllContent()) {
  315. return $.Deferred().resolve();
  316. }
  317. this.startLoading();
  318. var request = {
  319. limit: this.getLimit(),
  320. offset: this.getOffset(),
  321. useridto: this.getUserId(),
  322. };
  323. if (!this.initialLoad) {
  324. // If this is the first load we may have been given a non-zero offset,
  325. // in which case we need to load all notifications preceeding that offset
  326. // to make sure the full list is rendered.
  327. request.limit = this.getOffset() + this.getLimit();
  328. request.offset = 0;
  329. }
  330. var promise = NotificationRepo.query(request).then(function(result) {
  331. var notifications = result.notifications;
  332. this.unreadCount = result.unreadcount;
  333. this.setLoadedAllContent(!notifications.length || notifications.length < this.getLimit());
  334. this.initialLoad = true;
  335. if (notifications.length) {
  336. this.incrementOffset();
  337. return this.renderNotifications(notifications);
  338. }
  339. return false;
  340. }.bind(this))
  341. .always(function() {
  342. this.stopLoading();
  343. }.bind(this));
  344. return promise;
  345. };
  346. /**
  347. * Create the event listeners for the control area.
  348. *
  349. * @method registerEventListeners
  350. */
  351. ControlArea.prototype.registerEventListeners = function() {
  352. CustomEvents.define(this.getRoot(), [
  353. CustomEvents.events.activate,
  354. CustomEvents.events.scrollBottom,
  355. CustomEvents.events.scrollLock,
  356. CustomEvents.events.up,
  357. CustomEvents.events.down,
  358. ]);
  359. this.getRoot().on(CustomEvents.events.scrollBottom, function() {
  360. this.loadMoreNotifications();
  361. }.bind(this));
  362. this.getRoot().on(CustomEvents.events.activate, SELECTORS.NOTIFICATION, function(e) {
  363. var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
  364. this.showNotification(notificationElement);
  365. }.bind(this));
  366. // Show the previous notification in the list.
  367. this.getRoot().on(CustomEvents.events.up, SELECTORS.NOTIFICATION, function(e, data) {
  368. var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
  369. this.showNotification(notificationElement.prev());
  370. data.originalEvent.preventDefault();
  371. }.bind(this));
  372. // Show the next notification in the list.
  373. this.getRoot().on(CustomEvents.events.down, SELECTORS.NOTIFICATION, function(e, data) {
  374. var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);
  375. this.showNotification(notificationElement.next());
  376. data.originalEvent.preventDefault();
  377. }.bind(this));
  378. this.getContainer().on(NotificationAreaEvents.notificationShown, function(e, notification) {
  379. if (!notification.read) {
  380. var element = this.getNotificationElement(notification.id);
  381. if (element) {
  382. this.markNotificationAsRead(element);
  383. }
  384. var cachedNotification = this.getCacheNotification(notification.id);
  385. if (cachedNotification) {
  386. cachedNotification.read = true;
  387. }
  388. }
  389. }.bind(this));
  390. };
  391. return ControlArea;
  392. });