Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

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