AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** A widget to search users or grade items within the gradebook.** @module core_grades/searchwidget/basewidget* @copyright 2022 Mathew May <mathew.solutions>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/import {debounce} from 'core/utils';import * as Templates from 'core/templates';import * as Selectors from 'core_grades/searchwidget/selectors';import Notification from 'core/notification';import Log from 'core/log';/*** Build the base searching widget.** @method init* @param {HTMLElement} widgetContentContainer The selector for the widget container element.* @param {Promise} bodyPromise The promise from the callee of the contents to place in the widget container.* @param {Array} data An array of all the data generated by the callee.* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.* @param {string|null} unsearchableContent The content rendered in a non-searchable area.* @param {Function|null} afterSelect Callback executed after an item is selected.*/export const init = async(widgetContentContainer,bodyPromise,data,searchFunc,unsearchableContent = null,afterSelect = null,) => {Log.debug('The core_grades/searchwidget/basewidget component is deprecated. Please refer to core/search_combobox() instead.');bodyPromise.then(async(bodyContent) => {// Render the body content.widgetContentContainer.innerHTML = bodyContent;// Render the unsearchable content if defined.if (unsearchableContent) {const unsearchableContentContainer = widgetContentContainer.querySelector(Selectors.regions.unsearchableContent);unsearchableContentContainer.innerHTML += unsearchableContent;}const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);// Display a loader until the search results are rendered.await showLoader(searchResultsContainer);// Render the search results.await renderSearchResults(searchResultsContainer, data);registerListenerEvents(widgetContentContainer, data, searchFunc, afterSelect);}).catch(Notification.exception);};/*** Register the event listeners for the search widget.** @method registerListenerEvents* @param {HTMLElement} widgetContentContainer The selector for the widget container element.* @param {Array} data An array of all the data generated by the callee.* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.* @param {Function|null} afterSelect Callback executed after an item is selected.*/export const registerListenerEvents = (widgetContentContainer, data, searchFunc, afterSelect = null) => {const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);if (!searchInput) {// Too late. The widget is already closed and its content is empty.return;}// We want to focus on the first known user interable element within the dropdown.searchInput.focus();const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);// The search input is triggered.searchInput.addEventListener('input', debounce(async() => {// If search query is present display the 'clear search' button, otherwise hide it.if (searchInput.value.length > 0) {clearSearchButton.classList.remove('d-none');} else {clearSearchButton.classList.add('d-none');}// Remove aria-activedescendant when the available options change.searchInput.removeAttribute('aria-activedescendant');// Display the search results.await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()));}, 300));// Clear search is triggered.clearSearchButton.addEventListener('click', async(e) => {e.stopPropagation();// Clear the entered search query in the search bar.searchInput.value = "";searchInput.focus();clearSearchButton.classList.add('d-none');// Remove aria-activedescendant when the available options change.searchInput.removeAttribute('aria-activedescendant');// Display all results.await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()));});const inputElement = document.getElementById(searchInput.dataset.inputElement);if (inputElement && afterSelect) {inputElement.addEventListener('change', e => {const selectedOption = widgetContentContainer.querySelector(Selectors.elements.getSearchWidgetSelectOption(searchInput),);if (selectedOption) {afterSelect(e.target.value);}});}// Backward compatibility. Handle the click event for the following cases:// - When we have <li> tags without an afterSelect callback function being provided (old js).// - When we have <a> tags without href (old template).widgetContentContainer.addEventListener('click', e => {const deprecatedOption = e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if (deprecatedOption) {// We are in one of these situations:// - We have <li> tags without an afterSelect callback function being provided.// - We have <a> tags without href.if (inputElement && afterSelect) {afterSelect(deprecatedOption.dataset.value);} else {const url = (data.find(object => object.id == deprecatedOption.dataset.value) || {url: ''}).url;location.href = url;}}});// Backward compatibility. Handle the keydown event for the following cases:// - When we have <li> tags without an afterSelect callback function being provided (old js).// - When we have <a> tags without href (old template).widgetContentContainer.addEventListener('keydown', e => {const deprecatedOption = e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if (deprecatedOption && (e.key === ' ' || e.key === 'Enter')) {// We are in one of these situations:// - We have <li> tags without an afterSelect callback function being provided.// - We have <a> tags without href.e.preventDefault();if (inputElement && afterSelect) {afterSelect(deprecatedOption.dataset.value);} else {const url = (data.find(object => object.id == deprecatedOption.dataset.value) || {url: ''}).url;location.href = url;}}});};/*** Renders the loading placeholder for the search widget.** @method showLoader* @param {HTMLElement} container The DOM node where we'll render the loading placeholder.*/export const showLoader = async(container) => {container.innerHTML = '';const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/loading', {});Templates.replaceNodeContents(container, html, js);};/*** We have a small helper that'll call the curried search function allowing callers to filter* the data set however we want rather than defining how data must be filtered.** @method debounceCallee* @param {String} searchValue The input from the user that we'll search against.* @param {Array} data An array of all the data generated by the callee.* @param {Function} searchFunction Partially applied function we need to manage search the passed dataset.* @return {Array} The filtered subset of the provided data that we'll then render into the results.*/const debounceCallee = (searchValue, data, searchFunction) => {if (searchValue.length > 0) { // Search query is present.return searchFunction(data, searchValue);}return data;};/*** Given the output of the callers' search function, render out the results into the search results container.** @method renderSearchResults* @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results.* @param {Array} searchResultsData The filtered subset of the provided data that we'll then render into the results.*/const renderSearchResults = async(searchResultsContainer, searchResultsData) => {const templateData = {'searchresults': searchResultsData,};// Build up the html & js ready to place into the help section.const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/searchresults', templateData);await Templates.replaceNodeContents(searchResultsContainer, html, js);// Backward compatibility.if (searchResultsContainer.getAttribute('role') !== 'listbox') {const deprecatedOptions = searchResultsContainer.querySelectorAll('a.dropdown-item[role="menuitem"][href=""], .dropdown-item[role="option"]:not([href])');for (const option of deprecatedOptions) {option.tabIndex = 0;option.removeAttribute('href');}}};/*** We want to create the basic promises and hooks that the caller will implement, so we can build the search widget* ahead of time and allow the caller to resolve their promises once complete.** @method promisesAndResolvers* @returns {{bodyPromise: Promise, bodyPromiseResolver}}*/export const promisesAndResolvers = () => {// We want to show the widget instantly but loading whilst waiting for our data.let bodyPromiseResolver;const bodyPromise = new Promise(resolve => {bodyPromiseResolver = resolve;});return {bodyPromiseResolver, bodyPromise};};