grade/amd/src/searchwidget/basewidget.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 widget to search users or grade items within the gradebook.
  17. *
  18. * @module core_grades/searchwidget/basewidget
  19. * @copyright 2022 Mathew May <mathew.solutions>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import {debounce} from 'core/utils';
  23. import * as Templates from 'core/templates';
  24. import * as Selectors from 'core_grades/searchwidget/selectors';
  25. import Notification from 'core/notification';
  26. /**
  27. * Build the base searching widget.
  28. *
  29. * @method init
  30. * @param {HTMLElement} widgetContentContainer The selector for the widget container element.
  31. * @param {Promise} bodyPromise The promise from the callee of the contents to place in the widget container.
  32. * @param {Array} data An array of all the data generated by the callee.
  33. * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
  34. * @param {string|null} unsearchableContent The content rendered in a non-searchable area.
  35. */
  36. export const init = async(widgetContentContainer, bodyPromise, data, searchFunc, unsearchableContent = null) => {
  37. bodyPromise.then(async(bodyContent) => {
  38. // Render the body content.
  39. widgetContentContainer.innerHTML = bodyContent;
  40. // Render the unsearchable content if defined.
  41. if (unsearchableContent) {
  42. const unsearchableContentContainer = widgetContentContainer.querySelector(Selectors.regions.unsearchableContent);
  43. unsearchableContentContainer.innerHTML += unsearchableContent;
  44. }
  45. const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
  46. // Display a loader until the search results are rendered.
  47. await showLoader(searchResultsContainer);
  48. // Render the search results.
  49. await renderSearchResults(searchResultsContainer, data);
  50. registerListenerEvents(widgetContentContainer, data, searchFunc);
  51. }).catch(Notification.exception);
  52. };
  53. /**
  54. * Register the event listeners for the search widget.
  55. *
  56. * @method registerListenerEvents
  57. * @param {HTMLElement} widgetContentContainer The selector for the widget container element.
  58. * @param {Array} data An array of all the data generated by the callee.
  59. * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
  60. */
  61. export const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {
  62. const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
  63. const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);
  64. if (!searchInput) {
  65. // Too late. The widget is already closed and its content is empty.
  66. return;
  67. }
  68. // We want to focus on the first known user interable element within the dropdown.
  69. searchInput.focus();
  70. const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);
  71. // The search input is triggered.
  72. searchInput.addEventListener('input', debounce(async() => {
  73. // If search query is present display the 'clear search' button, otherwise hide it.
  74. if (searchInput.value.length > 0) {
  75. clearSearchButton.classList.remove('d-none');
  76. } else {
  77. clearSearchButton.classList.add('d-none');
  78. }
  79. // Display the search results.
  80. await renderSearchResults(
  81. searchResultsContainer,
  82. debounceCallee(
  83. searchInput.value,
  84. data,
  85. searchFunc()
  86. )
  87. );
  88. }, 300));
  89. // Clear search is triggered.
  90. clearSearchButton.addEventListener('click', async(e) => {
  91. e.stopPropagation();
  92. // Clear the entered search query in the search bar.
  93. searchInput.value = "";
  94. searchInput.focus();
  95. clearSearchButton.classList.add('d-none');
  96. // Display all results.
  97. await renderSearchResults(
  98. searchResultsContainer,
  99. debounceCallee(
  100. searchInput.value,
  101. data,
  102. searchFunc()
  103. )
  104. );
  105. });
  106. };
  107. /**
  108. * Renders the loading placeholder for the search widget.
  109. *
  110. * @method showLoader
  111. * @param {HTMLElement} container The DOM node where we'll render the loading placeholder.
  112. */
  113. export const showLoader = async(container) => {
  114. const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/loading', {});
  115. Templates.replaceNodeContents(container, html, js);
  116. };
  117. /**
  118. * We have a small helper that'll call the curried search function allowing callers to filter
  119. * the data set however we want rather than defining how data must be filtered.
  120. *
  121. * @method debounceCallee
  122. * @param {String} searchValue The input from the user that we'll search against.
  123. * @param {Array} data An array of all the data generated by the callee.
  124. * @param {Function} searchFunction Partially applied function we need to manage search the passed dataset.
  125. * @return {Array} The filtered subset of the provided data that we'll then render into the results.
  126. */
  127. const debounceCallee = (searchValue, data, searchFunction) => {
  128. if (searchValue.length > 0) { // Search query is present.
  129. return searchFunction(data, searchValue);
  130. }
  131. return data;
  132. };
  133. /**
  134. * Given the output of the callers' search function, render out the results into the search results container.
  135. *
  136. * @method renderSearchResults
  137. * @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results.
  138. * @param {Array} searchResultsData The filtered subset of the provided data that we'll then render into the results.
  139. */
  140. const renderSearchResults = async(searchResultsContainer, searchResultsData) => {
  141. const templateData = {
  142. 'searchresults': searchResultsData,
  143. };
  144. // Build up the html & js ready to place into the help section.
  145. const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/searchresults', templateData);
  146. await Templates.replaceNodeContents(searchResultsContainer, html, js);
  147. };
  148. /**
  149. * We want to create the basic promises and hooks that the caller will implement, so we can build the search widget
  150. * ahead of time and allow the caller to resolve their promises once complete.
  151. *
  152. * @method promisesAndResolvers
  153. * @returns {{bodyPromise: Promise, bodyPromiseResolver}}
  154. */
  155. export const promisesAndResolvers = () => {
  156. // We want to show the widget instantly but loading whilst waiting for our data.
  157. let bodyPromiseResolver;
  158. const bodyPromise = new Promise(resolve => {
  159. bodyPromiseResolver = resolve;
  160. });
  161. return {bodyPromiseResolver, bodyPromise};
  162. };