grade/amd/src/searchwidget/initials.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. * A small dropdown to filter users within the gradebook.
  17. *
  18. * @module core_grades/searchwidget/initials
  19. * @copyright 2022 Mathew May <mathew.solutions>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. * @deprecated since Moodle 4.5 - please use core_course/actionbar/initials instead.
  22. * @todo Final deprecation in Moodle 6.0. See MDL-82421.
  23. */
  24. import Pending from 'core/pending';
  25. import * as Url from 'core/url';
  26. import CustomEvents from "core/custom_interaction_events";
  27. import Dropdown from 'theme_boost/bootstrap/dropdown';
  28. /**
  29. * Whether the event listener has already been registered for this module.
  30. *
  31. * @type {boolean}
  32. */
  33. let registered = false;
  34. // Contain our selectors within this file until they could be of use elsewhere.
  35. const selectors = {
  36. pageListItem: 'page-item',
  37. pageClickableItem: '.page-link',
  38. activeItem: 'active',
  39. formDropdown: '.initialsdropdownform',
  40. parentDomNode: '.initials-selector',
  41. firstInitial: 'firstinitial',
  42. lastInitial: 'lastinitial',
  43. initialBars: '.initialbar', // Both first and last name use this class.
  44. targetButton: 'initialswidget',
  45. formItems: {
  46. type: 'submit',
  47. save: 'save',
  48. cancel: 'cancel'
  49. }
  50. };
  51. /**
  52. * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.
  53. *
  54. * @param {String} callingLink The link to redirect upon form submission.
  55. * @param {Null|Number} gpr_userid The user id to filter by.
  56. * @param {Null|String} gpr_search The search value to filter by.
  57. */
  58. export const init = (callingLink, gpr_userid = null, gpr_search = null) => {
  59. if (registered) {
  60. return;
  61. }
  62. const pendingPromise = new Pending();
  63. registerListenerEvents(callingLink, gpr_userid, gpr_search);
  64. // BS events always bubble so, we need to listen for the event higher up the chain.
  65. document.querySelector(selectors.parentDomNode).addEventListener('shown.bs.dropdown', () => {
  66. document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});
  67. });
  68. pendingPromise.resolve();
  69. registered = true;
  70. };
  71. /**
  72. * Register event listeners.
  73. *
  74. * @param {String} callingLink The link to redirect upon form submission.
  75. * @param {Null|Number} gpr_userid The user id to filter by.
  76. * @param {Null|String} gpr_search The search value to filter by.
  77. */
  78. const registerListenerEvents = (callingLink, gpr_userid = null, gpr_search = null) => {
  79. const events = [
  80. 'click',
  81. CustomEvents.events.activate,
  82. CustomEvents.events.keyboardActivate
  83. ];
  84. CustomEvents.define(document, events);
  85. // Register events.
  86. events.forEach((event) => {
  87. document.addEventListener(event, (e) => {
  88. // Always fetch the latest information when we click as state is a fickle thing.
  89. let {firstActive, lastActive, sifirst, silast} = onClickVariables();
  90. let itemToReset = '';
  91. // Prevent the usual form behaviour.
  92. if (e.target.closest(selectors.formDropdown)) {
  93. e.preventDefault();
  94. }
  95. // Handle the state of active initials before form submission.
  96. if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {
  97. // Ensure the li items don't cause weird clicking emptying out the form.
  98. if (e.target.classList.contains(selectors.pageListItem)) {
  99. return;
  100. }
  101. const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.
  102. // We want to find the current active item in the menu area the user selected.
  103. // We also want to fetch the raw item out of the array for instant manipulation.
  104. if (initialsBar.classList.contains(selectors.firstInitial)) {
  105. sifirst = e.target;
  106. itemToReset = firstActive;
  107. } else {
  108. silast = e.target;
  109. itemToReset = lastActive;
  110. }
  111. swapActiveItems(itemToReset, e);
  112. }
  113. // Handle form submissions.
  114. if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) {
  115. if (e.target.dataset.action === selectors.formItems.save) {
  116. // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.
  117. // Then we will redirect the user back onto the page with new filters applied.
  118. const params = {
  119. 'id': e.target.closest(selectors.formDropdown).dataset.courseid,
  120. 'gpr_search': gpr_search !== null ? gpr_search : '',
  121. 'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,
  122. 'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,
  123. };
  124. if (gpr_userid !== null) {
  125. params.gpr_userid = gpr_userid;
  126. }
  127. window.location = Url.relativeUrl(callingLink, params);
  128. }
  129. if (e.target.dataset.action === selectors.formItems.cancel) {
  130. Dropdown.getOrCreateInstance(document.querySelector(`.${selectors.targetButton}`)).toggle();
  131. }
  132. }
  133. });
  134. });
  135. };
  136. /**
  137. * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.
  138. *
  139. * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}
  140. */
  141. const onClickVariables = () => {
  142. // Ensure we have an up-to-date initials bar.
  143. const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];
  144. const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];
  145. const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];
  146. const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];
  147. // Ensure we retain both of the selections from a previous instance.
  148. let sifirst = firstActive.querySelector(selectors.pageClickableItem);
  149. let silast = lastActive.querySelector(selectors.pageClickableItem);
  150. return {firstActive, lastActive, sifirst, silast};
  151. };
  152. /**
  153. * Given we are provided the old li and current click event, swap around the active properties.
  154. *
  155. * @param {HTMLElement} itemToReset
  156. * @param {Event} e
  157. */
  158. const swapActiveItems = (itemToReset, e) => {
  159. itemToReset.classList.remove(selectors.activeItem);
  160. itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;
  161. // Set the select item as the current item.
  162. const itemToSetActive = e.target.parentElement;
  163. itemToSetActive.classList.add(selectors.activeItem);
  164. e.target.ariaCurrent = true;
  165. };