blocks/timeline/amd/src/view_nav.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. * Manage the timeline view navigation for the timeline block.
  17. *
  18. * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
  19. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  20. */
  21. import $ from 'jquery';
  22. import * as CustomEvents from 'core/custom_interaction_events';
  23. import * as View from 'block_timeline/view';
  24. import * as Notification from 'core/notification';
  25. import * as Utils from 'core/utils';
  26. import * as UserRepository from 'core_user/repository';
  27. const SELECTORS = {
  28. TIMELINE_DAY_FILTER: '[data-region="day-filter"]',
  29. TIMELINE_DAY_FILTER_OPTION: '[data-from]',
  30. TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]',
  31. DATA_DAYS_OFFSET: '[data-days-offset]',
  32. DATA_DAYS_LIMIT: '[data-days-limit]',
  33. TIMELINE_SEARCH_INPUT: '[data-action="search"]',
  34. TIMELINE_SEARCH_CLEAR_ICON: '[data-action="clearsearch"]',
  35. NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
  36. };
  37. /**
  38. * Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
  39. *
  40. * @param {object} root The root element for the timeline block
  41. * @param {object} timelineViewRoot The root element for the timeline view
  42. */
  43. const registerTimelineDaySelector = function(root, timelineViewRoot) {
  44. const timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);
  45. CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);
  46. timelineDaySelectorContainer.on(
  47. CustomEvents.events.activate,
  48. SELECTORS.TIMELINE_DAY_FILTER_OPTION,
  49. function(e, data) {
  50. // Update the user preference
  51. var filtername = $(e.currentTarget).data('filtername');
  52. var type = 'block_timeline_user_filter_preference';
  53. UserRepository.setUserPreference(type, filtername)
  54. .catch(Notification.exception);
  55. var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
  56. if (option.attr('aria-current') == 'true') {
  57. // If it's already active then we don't need to do anything.
  58. return;
  59. }
  60. var daysOffset = option.attr('data-from');
  61. var daysLimit = option.attr('data-to');
  62. var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);
  63. elementsWithDaysOffset.attr('data-days-offset', daysOffset);
  64. if (daysLimit != undefined) {
  65. elementsWithDaysOffset.attr('data-days-limit', daysLimit);
  66. } else {
  67. elementsWithDaysOffset.removeAttr('data-days-limit');
  68. }
  69. if (option.attr('data-filtername') === 'overdue') {
  70. elementsWithDaysOffset.attr('data-filter-overdue', true);
  71. } else {
  72. elementsWithDaysOffset.removeAttr('data-filter-overdue');
  73. }
  74. // Reset the views to reinitialise the event lists now that we've
  75. // updated the day limits.
  76. View.reset(timelineViewRoot);
  77. data.originalEvent.preventDefault();
  78. }
  79. );
  80. };
  81. /**
  82. * Event listener for the "sort" button in the timeline navigation that allows for
  83. * changing between the timeline dates and courses views.
  84. *
  85. * On a view change we tell the timeline view module that the view has been shown
  86. * so that it can handle how to display the appropriate view.
  87. *
  88. * @param {object} root The root element for the timeline block
  89. * @param {object} timelineViewRoot The root element for the timeline view
  90. */
  91. const registerViewSelector = function(root, timelineViewRoot) {
  92. const viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);
  93. // Listen for when the user changes tab so that we can show the first set of courses
  94. // and load their events when they request the sort by courses view for the first time.
  95. viewSelector.on('shown shown.bs.tab', function(e) {
  96. View.shown(timelineViewRoot);
  97. $(e.target).removeClass('active');
  98. });
  99. // Event selector for user_sort
  100. CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
  101. viewSelector.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
  102. var filtername = $(e.currentTarget).data('filtername');
  103. var type = 'block_timeline_user_sort_preference';
  104. UserRepository.setUserPreference(type, filtername)
  105. .catch(Notification.exception);
  106. });
  107. };
  108. /**
  109. * Event listener for the "search" input field in the timeline navigation that allows for
  110. * searching the activity name, course name and activity type.
  111. *
  112. * @param {object} root The root element for the timeline block
  113. * @param {object} timelineViewRoot The root element for the timeline view
  114. */
  115. const registerSearch = (root, timelineViewRoot) => {
  116. const searchInput = root.find(SELECTORS.TIMELINE_SEARCH_INPUT);
  117. const clearSearchIcon = root.find(SELECTORS.TIMELINE_SEARCH_CLEAR_ICON);
  118. searchInput.on('input', Utils.debounce(() => {
  119. if (searchInput.val() !== '') {
  120. activeSearchState(clearSearchIcon, timelineViewRoot);
  121. } else {
  122. clearSearchState(clearSearchIcon, timelineViewRoot);
  123. }
  124. }, 1000));
  125. clearSearchIcon.on('click', () => {
  126. searchInput.val('');
  127. clearSearchState(clearSearchIcon, timelineViewRoot);
  128. searchInput.focus();
  129. });
  130. };
  131. /**
  132. * Show the clear search icon.
  133. *
  134. * @param {object} clearSearchIcon Clear search icon element.
  135. * @param {object} timelineViewRoot The root element for the timeline view
  136. */
  137. const activeSearchState = (clearSearchIcon, timelineViewRoot) => {
  138. clearSearchIcon.removeClass('d-none');
  139. View.reset(timelineViewRoot);
  140. };
  141. /**
  142. * Hide the clear search icon.
  143. *
  144. * @param {object} clearSearchIcon Clear search icon element.
  145. * @param {object} timelineViewRoot The root element for the timeline view
  146. */
  147. const clearSearchState = (clearSearchIcon, timelineViewRoot) => {
  148. clearSearchIcon.addClass('d-none');
  149. View.reset(timelineViewRoot);
  150. };
  151. /**
  152. * Initialise the timeline view navigation by adding event listeners to
  153. * the navigation elements.
  154. *
  155. * @param {jQuery|HTMLElement} root The root element for the timeline block
  156. * @param {object} timelineViewRoot The root element for the timeline view
  157. */
  158. export const init = function(root, timelineViewRoot) {
  159. root = $(root);
  160. registerViewSelector(root, timelineViewRoot);
  161. // Only need to handle filtering if the user is actively enrolled in a course.
  162. if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
  163. registerTimelineDaySelector(root, timelineViewRoot);
  164. registerSearch(root, timelineViewRoot);
  165. }
  166. };