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