diff --git a/.upgradenotes/MDL-78325-2025110608544631.yml b/.upgradenotes/MDL-78325-2025110608544631.yml new file mode 100644 index 00000000000..6361b3f925f --- /dev/null +++ b/.upgradenotes/MDL-78325-2025110608544631.yml @@ -0,0 +1,10 @@ +issueNumber: MDL-78325 +notes: + core_grades: + - message: >- + In Moodle 4.2, the legacy Gradebook base widget from 4.1 has been removed and + replaced with a simpler class-based system due to a breaking change and + excessive complexity in the old pattern. The files `core/grades/basewidget.js` and + templates in `grade/templates/searchwidget/` have been deleted, + with minimal expected third-party impact. + type: removed diff --git a/public/grade/amd/build/searchwidget/basewidget.min.js b/public/grade/amd/build/searchwidget/basewidget.min.js deleted file mode 100644 index c71e4cd6169..00000000000 --- a/public/grade/amd/build/searchwidget/basewidget.min.js +++ /dev/null @@ -1,10 +0,0 @@ -define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core_grades/searchwidget/selectors","core/notification","core/log"],(function(_exports,_utils,Templates,Selectors,_notification,_log){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} -/** - * A widget to search users or grade items within the gradebook. - * - * @module core_grades/searchwidget/basewidget - * @copyright 2022 Mathew May - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=_interopRequireDefault(_notification),_log=_interopRequireDefault(_log);_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,afterSelect=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;_log.default.debug("The core_grades/searchwidget/basewidget component is deprecated. Please refer to core/search_combobox() instead."),bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc,afterSelect)})).catch(_notification.default.exception)};const registerListenerEvents=function(widgetContentContainer,data,searchFunc){let afterSelect=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}));const inputElement=document.getElementById(searchInput.dataset.inputElement);inputElement&&afterSelect&&inputElement.addEventListener("change",(e=>{widgetContentContainer.querySelector(Selectors.elements.getSearchWidgetSelectOption(searchInput))&&afterSelect(e.target.value)})),widgetContentContainer.addEventListener("click",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption)if(inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}})),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||"Enter"===e.key))if(e.preventDefault(),inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{container.innerHTML="";const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);if(await Templates.replaceNodeContents(searchResultsContainer,html,js),"listbox"!==searchResultsContainer.getAttribute("role")){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")}};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}})); - -//# sourceMappingURL=basewidget.min.js.map \ No newline at end of file diff --git a/public/grade/amd/build/searchwidget/basewidget.min.js.map b/public/grade/amd/build/searchwidget/basewidget.min.js.map deleted file mode 100644 index 28e60d9cf19..00000000000 --- a/public/grade/amd/build/searchwidget/basewidget.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"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 .\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 \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
  • tags without an afterSelect callback function being provided (old js).\n // - When we have 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
  • tags without an afterSelect callback function being provided.\n // - We have 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
  • tags without an afterSelect callback function being provided (old js).\n // - When we have 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
  • tags without an afterSelect callback function being provided.\n // - We have 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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"} \ No newline at end of file diff --git a/public/grade/amd/src/searchwidget/basewidget.js b/public/grade/amd/src/searchwidget/basewidget.js deleted file mode 100644 index 7f7750e9b31..00000000000 --- a/public/grade/amd/src/searchwidget/basewidget.js +++ /dev/null @@ -1,261 +0,0 @@ -// 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 . - -/** - * A widget to search users or grade items within the gradebook. - * - * @module core_grades/searchwidget/basewidget - * @copyright 2022 Mathew May - * @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
  • tags without an afterSelect callback function being provided (old js). - // - When we have 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
  • tags without an afterSelect callback function being provided. - // - We have 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
  • tags without an afterSelect callback function being provided (old js). - // - When we have 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
  • tags without an afterSelect callback function being provided. - // - We have 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}; -}; diff --git a/public/grade/templates/searchwidget/error.mustache b/public/grade/templates/searchwidget/error.mustache deleted file mode 100644 index c4dc44fb8a1..00000000000 --- a/public/grade/templates/searchwidget/error.mustache +++ /dev/null @@ -1,39 +0,0 @@ -{{! - 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 . -}} -{{! - @template core_grades/searchwidget/error - - Chooser error template. - - Variables required for this template: - * errormessage - The error message - - Example context (json): - { - "errormessage": "Error" - } -}} -
    - -
    diff --git a/public/grade/templates/searchwidget/loading.mustache b/public/grade/templates/searchwidget/loading.mustache deleted file mode 100644 index 7625998f023..00000000000 --- a/public/grade/templates/searchwidget/loading.mustache +++ /dev/null @@ -1,27 +0,0 @@ -{{! - 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 . -}} -{{! - @template core_grades/searchwidget/loading - - This template renders the loading placeholder for the search widget. - - Example context (json): - {} -}} -
    - {{> core/loading }} -
    diff --git a/public/grade/templates/searchwidget/readme.txt b/public/grade/templates/searchwidget/readme.txt deleted file mode 100644 index f2191eadb01..00000000000 --- a/public/grade/templates/searchwidget/readme.txt +++ /dev/null @@ -1,7 +0,0 @@ -NOTE: Templates inside this directory have been deprecated since Moodle 4.3 and will be removed in 4.7. - -They are only retained so that plugins using the now-deprecated searchwidget located in: - -'grade/amd/src/searchwidget/basewidget.js' - -can continue to use this widget in a working state during the deprecation period. diff --git a/public/grade/templates/searchwidget/searchitem.mustache b/public/grade/templates/searchwidget/searchitem.mustache deleted file mode 100644 index 9399309968a..00000000000 --- a/public/grade/templates/searchwidget/searchitem.mustache +++ /dev/null @@ -1,59 +0,0 @@ -{{! - 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 . -}} -{{! - @template core_grades/searchwidget/searchitem - - Search result line items. - - Example context (json): - { - "id": "1", - "name": "Quiz 1", - "fullname": "Cameron Greeve" - } -}} -
  • diff --git a/public/grade/templates/searchwidget/searchresults.mustache b/public/grade/templates/searchwidget/searchresults.mustache deleted file mode 100644 index 140c2529f5c..00000000000 --- a/public/grade/templates/searchwidget/searchresults.mustache +++ /dev/null @@ -1,45 +0,0 @@ -{{! - 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 . -}} -{{! - @template core_grades/searchwidget/searchresults - - The wrapper in which our search results will be rendered - - Example context (json): - { - "searchresults": [ - { - "id": "1", - "name": "Quiz 1", - "fullname": "Cameron Greeve", - "sendmessage": "http://foo.bar/message/index.php?id=25", - "addcontact": "http://foo.bar/message/index.php?user1=2&user2=14&addcontact=14&sesskey=XXXXX", - "currentuser": "2" - } - ] - } -}} -