diff --git a/grade/amd/build/searchwidget/basewidget.min.js b/grade/amd/build/searchwidget/basewidget.min.js index 572ebd934cb..64fbb503d1a 100644 --- a/grade/amd/build/searchwidget/basewidget.min.js +++ b/grade/amd/build/searchwidget/basewidget.min.js @@ -1,10 +1,10 @@ -define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core/aria","core_grades/searchwidget/selectors","core/notification"],(function(_exports,_utils,Templates,_aria,Selectors,_notification){var 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} +define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core_grades/searchwidget/selectors","core/notification"],(function(_exports,_utils,Templates,Selectors,_notification){var 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=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;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)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search),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"),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"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))})),(0,_aria.comboBox)(searchInput)};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{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);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;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)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);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"),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"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{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);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_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/grade/amd/build/searchwidget/basewidget.min.js.map b/grade/amd/build/searchwidget/basewidget.min.js.map index 0a24d4c0875..4d136840e37 100644 --- a/grade/amd/build/searchwidget/basewidget.min.js.map +++ b/grade/amd/build/searchwidget/basewidget.min.js.map @@ -1 +1 @@ -{"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 {comboBox} from 'core/aria';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\nimport Notification from 'core/notification';\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 */\nexport const init = async(widgetContentContainer, bodyPromise, data, searchFunc, unsearchableContent = null) => {\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);\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 */\nexport const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {\n const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);\n const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);\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 // 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 // Display all results.\n await renderSearchResults(\n searchResultsContainer,\n debounceCallee(\n searchInput.value,\n data,\n searchFunc()\n )\n );\n });\n\n // Trigger event handling for the results in line with aria guidelines.\n comboBox(searchInput);\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 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\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","then","innerHTML","bodyContent","querySelector","Selectors","regions","searchResultsContainer","searchResults","showLoader","renderSearchResults","registerListenerEvents","catch","Notification","exception","searchInput","actions","search","clearSearchButton","clearSearch","addEventListener","value","length","classList","remove","add","debounceCallee","e","stopPropagation","focus","html","js","Templates","renderForPromise","replaceNodeContents","container","searchValue","searchFunction","searchResultsData","templateData","bodyPromiseResolver","Promise","resolve"],"mappings":";;;;;;;gVAsCoBA,eAAMC,uBAAwBC,YAAaC,KAAMC,gBAAYC,2EAAsB,KACnGH,YAAYI,MAAKN,MAAAA,iBAEbC,uBAAuBM,UAAYC,YAG/BH,oBAAqB,CACgBJ,uBAAuBQ,cAAcC,UAAUC,QAAQN,qBAC/DE,WAAaF,0BAGxCO,uBAAyBX,uBAAuBQ,cAAcC,UAAUC,QAAQE,qBAEhFC,WAAWF,8BAEXG,oBAAoBH,uBAAwBT,MAElDa,uBAAuBf,uBAAwBE,KAAMC,eAEtDa,MAAMC,sBAAaC,kBAWbH,uBAAyB,CAACf,uBAAwBE,KAAMC,oBAC3DQ,uBAAyBX,uBAAuBQ,cAAcC,UAAUC,QAAQE,eAChFO,YAAcnB,uBAAuBQ,cAAcC,UAAUW,QAAQC,QACrEC,kBAAoBtB,uBAAuBQ,cAAcC,UAAUW,QAAQG,aAGjFJ,YAAYK,iBAAiB,SAAS,oBAASzB,UAEvCoB,YAAYM,MAAMC,OAAS,EAC3BJ,kBAAkBK,UAAUC,OAAO,UAEnCN,kBAAkBK,UAAUE,IAAI,gBAG9Bf,oBACFH,uBACAmB,eACIX,YAAYM,MACZvB,KACAC,iBAGT,MAGHmB,kBAAkBE,iBAAiB,SAASzB,MAAAA,IACxCgC,EAAEC,kBAEFb,YAAYM,MAAQ,GACpBN,YAAYc,QACZX,kBAAkBK,UAAUE,IAAI,gBAG1Bf,oBACFH,uBACAmB,eACIX,YAAYM,MACZvB,KACAC,qCAMHgB,2EASAN,WAAad,MAAAA,kBAChBmC,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAAiB,mCAAoC,IACxFD,UAAUE,oBAAoBC,UAAWL,KAAMC,0CAa7CL,eAAiB,CAACU,YAAatC,KAAMuC,iBACnCD,YAAYd,OAAS,EACde,eAAevC,KAAMsC,aAEzBtC,KAULY,oBAAsBf,MAAMY,uBAAwB+B,2BAChDC,aAAe,eACAD,oBAGfR,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAAiB,yCAA0CM,oBACxFP,UAAUE,oBAAoB3B,uBAAwBuB,KAAMC,mCAUlC,SAE5BS,0BACE3C,YAAc,IAAI4C,SAAQC,UAC5BF,oBAAsBE,iBAGnB,CAACF,oBAAAA,oBAAqB3C,YAAAA"} \ No newline at end of file +{"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';\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 */\nexport const init = async(widgetContentContainer, bodyPromise, data, searchFunc, unsearchableContent = null) => {\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);\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 */\nexport const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {\n const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);\n const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);\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 // 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 // Display all results.\n await renderSearchResults(\n searchResultsContainer,\n debounceCallee(\n searchInput.value,\n data,\n searchFunc()\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 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\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","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","debounceCallee","e","stopPropagation","html","js","Templates","renderForPromise","replaceNodeContents","container","searchValue","searchFunction","searchResultsData","templateData","bodyPromiseResolver","Promise","resolve"],"mappings":";;;;;;;gVAqCoBA,eAAMC,uBAAwBC,YAAaC,KAAMC,gBAAYC,2EAAsB,KACnGH,YAAYI,MAAKN,MAAAA,iBAEbC,uBAAuBM,UAAYC,YAG/BH,oBAAqB,CACgBJ,uBAAuBQ,cAAcC,UAAUC,QAAQN,qBAC/DE,WAAaF,0BAGxCO,uBAAyBX,uBAAuBQ,cAAcC,UAAUC,QAAQE,qBAEhFC,WAAWF,8BAEXG,oBAAoBH,uBAAwBT,MAElDa,uBAAuBf,uBAAwBE,KAAMC,eAEtDa,MAAMC,sBAAaC,kBAWbH,uBAAyB,CAACf,uBAAwBE,KAAMC,oBAC3DQ,uBAAyBX,uBAAuBQ,cAAcC,UAAUC,QAAQE,eAChFO,YAAcnB,uBAAuBQ,cAAcC,UAAUW,QAAQC,QAE3EF,YAAYG,cACNC,kBAAoBvB,uBAAuBQ,cAAcC,UAAUW,QAAQI,aAGjFL,YAAYM,iBAAiB,SAAS,oBAAS1B,UAEvCoB,YAAYO,MAAMC,OAAS,EAC3BJ,kBAAkBK,UAAUC,OAAO,UAEnCN,kBAAkBK,UAAUE,IAAI,gBAG9BhB,oBACFH,uBACAoB,eACIZ,YAAYO,MACZxB,KACAC,iBAGT,MAGHoB,kBAAkBE,iBAAiB,SAAS1B,MAAAA,IACxCiC,EAAEC,kBAEFd,YAAYO,MAAQ,GACpBP,YAAYG,QACZC,kBAAkBK,UAAUE,IAAI,gBAG1BhB,oBACFH,uBACAoB,eACIZ,YAAYO,MACZxB,KACAC,gFAYHU,WAAad,MAAAA,kBAChBmC,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAAiB,mCAAoC,IACxFD,UAAUE,oBAAoBC,UAAWL,KAAMC,0CAa7CJ,eAAiB,CAACS,YAAatC,KAAMuC,iBACnCD,YAAYb,OAAS,EACdc,eAAevC,KAAMsC,aAEzBtC,KAULY,oBAAsBf,MAAMY,uBAAwB+B,2BAChDC,aAAe,eACAD,oBAGfR,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAAiB,yCAA0CM,oBACxFP,UAAUE,oBAAoB3B,uBAAwBuB,KAAMC,mCAUlC,SAE5BS,0BACE3C,YAAc,IAAI4C,SAAQC,UAC5BF,oBAAsBE,iBAGnB,CAACF,oBAAAA,oBAAqB3C,YAAAA"} \ No newline at end of file diff --git a/grade/amd/src/searchwidget/basewidget.js b/grade/amd/src/searchwidget/basewidget.js index e9cb47cbcae..6f66768e62a 100644 --- a/grade/amd/src/searchwidget/basewidget.js +++ b/grade/amd/src/searchwidget/basewidget.js @@ -22,7 +22,6 @@ */ import {debounce} from 'core/utils'; import * as Templates from 'core/templates'; -import {comboBox} from 'core/aria'; import * as Selectors from 'core_grades/searchwidget/selectors'; import Notification from 'core/notification'; @@ -69,6 +68,8 @@ export const init = async(widgetContentContainer, bodyPromise, data, searchFunc, export const registerListenerEvents = (widgetContentContainer, data, searchFunc) => { const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults); const searchInput = widgetContentContainer.querySelector(Selectors.actions.search); + // 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. @@ -108,9 +109,6 @@ export const registerListenerEvents = (widgetContentContainer, data, searchFunc) ) ); }); - - // Trigger event handling for the results in line with aria guidelines. - comboBox(searchInput); }; /** diff --git a/grade/classes/external/get_enrolled_users_for_search_widget.php b/grade/classes/external/get_enrolled_users_for_search_widget.php index 37bfc2b0a6f..91e16f6de88 100644 --- a/grade/classes/external/get_enrolled_users_for_search_widget.php +++ b/grade/classes/external/get_enrolled_users_for_search_widget.php @@ -105,6 +105,7 @@ class get_enrolled_users_for_search_widget extends external_api { $userpicture->size = 1; $user->profileimage = $userpicture->get_url($PAGE)->out(false); $user->email = $guiuser->email; + $user->active = false; // @TODO MDL-76246 $users[] = $user; } @@ -151,6 +152,7 @@ class get_enrolled_users_for_search_widget extends external_api { core_user::get_property_type('email'), 'An email address - allow email as root@localhost', VALUE_OPTIONAL), + 'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED) ]; return new external_single_structure($userfields); } diff --git a/grade/classes/external/get_groups_for_search_widget.php b/grade/classes/external/get_groups_for_search_widget.php index 9e3d527c973..6424318a873 100644 --- a/grade/classes/external/get_groups_for_search_widget.php +++ b/grade/classes/external/get_groups_for_search_widget.php @@ -116,8 +116,10 @@ class get_groups_for_search_widget extends external_api { 'group' => $group->id ]); return (object) [ + 'id' => $group->id, 'name' => $group->name, 'url' => $url->out(false), + 'active' => false // @TODO MDL-76246 ]; }, $groupsmenu); } @@ -147,8 +149,10 @@ class get_groups_for_search_widget extends external_api { */ public static function group_description(): external_description { $groupfields = [ + 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED), 'url' => new external_value(PARAM_URL, 'The link that applies the group action', VALUE_REQUIRED), 'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED), + 'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED) ]; return new external_single_structure($groupfields); } diff --git a/grade/report/singleview/classes/external/singleview.php b/grade/report/singleview/classes/external/singleview.php index 6f73bbbe57d..6a1dd25472b 100644 --- a/grade/report/singleview/classes/external/singleview.php +++ b/grade/report/singleview/classes/external/singleview.php @@ -85,7 +85,7 @@ class singleview extends core_course_external { $gradeitems = array_map(function ($gradeitem) use ($PAGE, $USER, $params) { $item = new \stdClass(); - $item->gid = $gradeitem->id; + $item->id = $gradeitem->id; $url = new moodle_url('/grade/report/singleview/index.php', [ 'id' => $params['courseid'], 'itemid' => $gradeitem->id, @@ -94,6 +94,7 @@ class singleview extends core_course_external { ); $item->name = $gradeitem->get_name(true); $item->url = $url->out(false); + $item->active = false; // @TODO MDL-76246 return $item; }, $gradeableitems); @@ -114,13 +115,14 @@ class singleview extends core_course_external { return new external_single_structure([ 'gradeitems' => new external_multiple_structure( new external_single_structure([ - 'gid' => new external_value(PARAM_INT, 'ID of the grade item', VALUE_OPTIONAL), + 'id' => new external_value(PARAM_INT, 'ID of the grade item', VALUE_OPTIONAL), 'url' => new external_value( PARAM_URL, 'The link to the grade report', VALUE_OPTIONAL ), 'name' => new external_value(PARAM_TEXT, 'The full name of the grade item', VALUE_OPTIONAL), + 'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED) ]) ), 'warnings' => new external_warnings(), diff --git a/grade/report/singleview/templates/zero_state.mustache b/grade/report/singleview/templates/zero_state.mustache index 9a918c4646a..38797640f65 100644 --- a/grade/report/singleview/templates/zero_state.mustache +++ b/grade/report/singleview/templates/zero_state.mustache @@ -27,7 +27,7 @@ } }}
- {{#str}}pluginname, gradereport_singleview{{/str}} +

{{#str}}pluginname, gradereport_singleview{{/str}}

{{#str}}viewsingleuserorgradeitem, gradereport_singleview{{/str}}

diff --git a/grade/report/singleview/templates/zero_state_grade.mustache b/grade/report/singleview/templates/zero_state_grade.mustache index 955415cfce8..3cf1aadba3e 100644 --- a/grade/report/singleview/templates/zero_state_grade.mustache +++ b/grade/report/singleview/templates/zero_state_grade.mustache @@ -25,7 +25,7 @@ } }}
- {{#str}}viewsinglegradeitem, gradereport_singleview{{/str}} +

{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}

{{#str}}singleviewdescription, gradereport_singleview{{/str}}

diff --git a/grade/report/singleview/templates/zero_state_user.mustache b/grade/report/singleview/templates/zero_state_user.mustache index e3d8c2c898f..8af52294598 100644 --- a/grade/report/singleview/templates/zero_state_user.mustache +++ b/grade/report/singleview/templates/zero_state_user.mustache @@ -25,7 +25,7 @@ } }}
- {{#str}}viewsingleuser, gradereport_singleview{{/str}} +

{{#str}}viewsingleuser, gradereport_singleview{{/str}}

{{#str}}singleviewdescription, gradereport_singleview{{/str}}

diff --git a/grade/report/user/amd/build/user.min.js b/grade/report/user/amd/build/user.min.js index fdf30495d3f..e5018892cf3 100644 --- a/grade/report/user/amd/build/user.min.js +++ b/grade/report/user/amd/build/user.min.js @@ -5,6 +5,6 @@ define("gradereport_user/user",["exports","core/pending","core/templates","core_ * @module gradereport_user/user * @copyright 2022 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("user"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,groupId=e.relatedTarget.dataset.groupid,actionBaseUrl=_url.default.relativeUrl("/grade/report/user/index.php",{},!1);await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("core_grades/searchwidget/searchitem",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.users,searchUsers(),allUsersOption),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0}))})),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("user"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,groupId=e.relatedTarget.dataset.groupid,actionBaseUrl=_url.default.relativeUrl("/grade/report/user/index.php",{},!1);await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("gradereport_user/all_users_item",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.users,searchUsers(),allUsersOption),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0}))})),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}})); //# sourceMappingURL=user.min.js.map \ No newline at end of file diff --git a/grade/report/user/amd/build/user.min.js.map b/grade/report/user/amd/build/user.min.js.map index 47dec3c23c7..64792d3b973 100644 --- a/grade/report/user/amd/build/user.min.js.map +++ b/grade/report/user/amd/build/user.min.js.map @@ -1 +1 @@ -{"version":3,"file":"user.min.js","sources":["../src/user.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 within the gradebook.\n *\n * @module gradereport_user/user\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport {get_string as getString} from 'core/str';\nimport Url from 'core/url';\nimport $ from 'jquery';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\n\n/**\n * Our entry point into starting to build the search widget.\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n};\n\n/**\n * Register user search widget related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user'));\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(Selectors.elements.getSearchWidgetSelector('user')).on('show.bs.dropdown', async(e) => {\n const courseID = e.relatedTarget.dataset.courseid;\n const groupId = e.relatedTarget.dataset.groupid;\n const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false);\n // Display a loading icon in the dropdown menu container until the body promise is resolved.\n await WidgetBase.showLoader(dropdownMenuContainer);\n\n // If an error occurs while fetching the data, display the error within the dropdown menu.\n const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n\n // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.\n const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);\n const allUsersOption = await Templates.render('core_grades/searchwidget/searchitem', {\n id: 0,\n name: allUsersOptionName,\n url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),\n });\n\n await WidgetBase.init(\n dropdownMenuContainer,\n bodyPromise,\n data.users,\n searchUsers(),\n allUsersOption\n );\n\n // Resolvers for passed functions in the dropdown menu creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}\n ));\n });\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(Selectors.elements.getSearchWidgetSelector('user')).on('hide.bs.dropdown', () => {\n // Reset the state once the user menu dropdown is closed.\n dropdownMenuContainer.innerHTML = '';\n });\n};\n\n/**\n * Define how we want to search and filter users when the user decides to input a search value.\n *\n * @method registerListenerEvents\n * @returns {function(): function(*, *): (*)}\n */\nconst searchUsers = () => {\n return () => {\n return (users, searchTerm) => {\n if (searchTerm === '') {\n return users;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n users.forEach((user) => {\n const userName = user.fullname.toLowerCase();\n if (userName.includes(searchTerm)) {\n searchResults.push(user);\n }\n });\n return searchResults;\n };\n };\n};\n"],"names":["pendingPromise","Pending","registerListenerEvents","resolve","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","dropdownMenuContainer","document","querySelector","Selectors","elements","getSearchWidgetDropdownSelector","getSearchWidgetSelector","on","async","courseID","e","relatedTarget","dataset","courseid","groupId","groupid","actionBaseUrl","Url","relativeUrl","showLoader","data","Repository","userFetch","catch","errorTemplateData","message","Templates","render","allUsersOptionName","users","length","allUsersOption","id","name","url","userid","init","searchUsers","displayunsearchablecontent","innerHTML","searchTerm","toLowerCase","searchResults","forEach","user","fullname","includes","push"],"mappings":";;;;;;;4YAsCoB,WACVA,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,iBAQbD,uBAAyB,SACvBE,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,6BAC9CC,sBAAwBC,SAASC,cAAcC,UAAUC,SAASC,gCAAgC,6BAGtGF,UAAUC,SAASE,wBAAwB,SAASC,GAAG,oBAAoBC,MAAAA,UACnEC,SAAWC,EAAEC,cAAcC,QAAQC,SACnCC,QAAUJ,EAAEC,cAAcC,QAAQG,QAClCC,cAAgBC,aAAIC,YAAY,+BAAgC,IAAI,SAEpEpB,WAAWqB,WAAWnB,6BAGtBoB,WAAaC,WAAWC,UAAUb,SAAUO,cAAeF,SAASS,OAAMf,MAAAA,UACtEgB,kBAAoB,cACNd,EAAEe,SAEtB7B,0BACU8B,UAAUC,OAAO,iCAAkCH,0BAK7DJ,OAAS,gBAKPQ,yBAA2B,mBAAU,cAAe,mBAAoBR,KAAKS,MAAMC,QACnFC,qBAAuBL,UAAUC,OAAO,sCAAuC,CACjFK,GAAI,EACJC,KAAML,mBACNM,IAAKjB,aAAIC,YAAY,+BAAgC,CAACc,GAAIvB,SAAU0B,OAAQ,IAAI,WAG9ErC,WAAWsC,KACbpC,sBACAH,YACAuB,KAAKS,MACLQ,cACAN,gBAIJnC,oBAAoB8B,UAAUC,OAC1B,gDAAiD,CAACW,4BAA4B,4BAKpFnC,UAAUC,SAASE,wBAAwB,SAASC,GAAG,oBAAoB,KAEzEP,sBAAsBuC,UAAY,OAUpCF,YAAc,IACT,IACI,CAACR,MAAOW,iBACQ,KAAfA,kBACOX,MAEXW,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBb,MAAMc,SAASC,OACMA,KAAKC,SAASJ,cAClBK,SAASN,aAClBE,cAAcK,KAAKH,SAGpBF"} \ No newline at end of file +{"version":3,"file":"user.min.js","sources":["../src/user.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 within the gradebook.\n *\n * @module gradereport_user/user\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport {get_string as getString} from 'core/str';\nimport Url from 'core/url';\nimport $ from 'jquery';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\n\n/**\n * Our entry point into starting to build the search widget.\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n};\n\n/**\n * Register user search widget related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user'));\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(Selectors.elements.getSearchWidgetSelector('user')).on('show.bs.dropdown', async(e) => {\n const courseID = e.relatedTarget.dataset.courseid;\n const groupId = e.relatedTarget.dataset.groupid;\n const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false);\n // Display a loading icon in the dropdown menu container until the body promise is resolved.\n await WidgetBase.showLoader(dropdownMenuContainer);\n\n // If an error occurs while fetching the data, display the error within the dropdown menu.\n const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n\n // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.\n const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);\n const allUsersOption = await Templates.render('gradereport_user/all_users_item', {\n id: 0,\n name: allUsersOptionName,\n url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),\n });\n\n await WidgetBase.init(\n dropdownMenuContainer,\n bodyPromise,\n data.users,\n searchUsers(),\n allUsersOption\n );\n\n // Resolvers for passed functions in the dropdown menu creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}\n ));\n });\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(Selectors.elements.getSearchWidgetSelector('user')).on('hide.bs.dropdown', () => {\n // Reset the state once the user menu dropdown is closed.\n dropdownMenuContainer.innerHTML = '';\n });\n};\n\n/**\n * Define how we want to search and filter users when the user decides to input a search value.\n *\n * @method registerListenerEvents\n * @returns {function(): function(*, *): (*)}\n */\nconst searchUsers = () => {\n return () => {\n return (users, searchTerm) => {\n if (searchTerm === '') {\n return users;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n users.forEach((user) => {\n const userName = user.fullname.toLowerCase();\n if (userName.includes(searchTerm)) {\n searchResults.push(user);\n }\n });\n return searchResults;\n };\n };\n};\n"],"names":["pendingPromise","Pending","registerListenerEvents","resolve","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","dropdownMenuContainer","document","querySelector","Selectors","elements","getSearchWidgetDropdownSelector","getSearchWidgetSelector","on","async","courseID","e","relatedTarget","dataset","courseid","groupId","groupid","actionBaseUrl","Url","relativeUrl","showLoader","data","Repository","userFetch","catch","errorTemplateData","message","Templates","render","allUsersOptionName","users","length","allUsersOption","id","name","url","userid","init","searchUsers","displayunsearchablecontent","innerHTML","searchTerm","toLowerCase","searchResults","forEach","user","fullname","includes","push"],"mappings":";;;;;;;4YAsCoB,WACVA,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,iBAQbD,uBAAyB,SACvBE,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,6BAC9CC,sBAAwBC,SAASC,cAAcC,UAAUC,SAASC,gCAAgC,6BAGtGF,UAAUC,SAASE,wBAAwB,SAASC,GAAG,oBAAoBC,MAAAA,UACnEC,SAAWC,EAAEC,cAAcC,QAAQC,SACnCC,QAAUJ,EAAEC,cAAcC,QAAQG,QAClCC,cAAgBC,aAAIC,YAAY,+BAAgC,IAAI,SAEpEpB,WAAWqB,WAAWnB,6BAGtBoB,WAAaC,WAAWC,UAAUb,SAAUO,cAAeF,SAASS,OAAMf,MAAAA,UACtEgB,kBAAoB,cACNd,EAAEe,SAEtB7B,0BACU8B,UAAUC,OAAO,iCAAkCH,0BAK7DJ,OAAS,gBAKPQ,yBAA2B,mBAAU,cAAe,mBAAoBR,KAAKS,MAAMC,QACnFC,qBAAuBL,UAAUC,OAAO,kCAAmC,CAC7EK,GAAI,EACJC,KAAML,mBACNM,IAAKjB,aAAIC,YAAY,+BAAgC,CAACc,GAAIvB,SAAU0B,OAAQ,IAAI,WAG9ErC,WAAWsC,KACbpC,sBACAH,YACAuB,KAAKS,MACLQ,cACAN,gBAIJnC,oBAAoB8B,UAAUC,OAC1B,gDAAiD,CAACW,4BAA4B,4BAKpFnC,UAAUC,SAASE,wBAAwB,SAASC,GAAG,oBAAoB,KAEzEP,sBAAsBuC,UAAY,OAUpCF,YAAc,IACT,IACI,CAACR,MAAOW,iBACQ,KAAfA,kBACOX,MAEXW,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBb,MAAMc,SAASC,OACMA,KAAKC,SAASJ,cAClBK,SAASN,aAClBE,cAAcK,KAAKH,SAGpBF"} \ No newline at end of file diff --git a/grade/report/user/amd/src/user.js b/grade/report/user/amd/src/user.js index 54a753e7bd0..df87e19ac3c 100644 --- a/grade/report/user/amd/src/user.js +++ b/grade/report/user/amd/src/user.js @@ -76,7 +76,7 @@ const registerListenerEvents = () => { // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget. const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length); - const allUsersOption = await Templates.render('core_grades/searchwidget/searchitem', { + const allUsersOption = await Templates.render('gradereport_user/all_users_item', { id: 0, name: allUsersOptionName, url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false), diff --git a/grade/report/user/templates/all_users_item.mustache b/grade/report/user/templates/all_users_item.mustache new file mode 100644 index 00000000000..7c2d29bf2f6 --- /dev/null +++ b/grade/report/user/templates/all_users_item.mustache @@ -0,0 +1,33 @@ +{{! + 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 gradereport_user/all_users_item + + Search result line items. + + Example context (json): + { + "id": "0", + "name": "All users(10)", + "url": "http://foo.bar/gradereport/?userid=0&id=2" + } +}} + + + {{name}} + + diff --git a/grade/report/user/templates/zero_state.mustache b/grade/report/user/templates/zero_state.mustache index 69b9cb174e4..496d90bd76c 100644 --- a/grade/report/user/templates/zero_state.mustache +++ b/grade/report/user/templates/zero_state.mustache @@ -25,7 +25,7 @@ } }}
- {{#str}}selectuser, gradereport_user{{/str}} +

{{#str}}selectuser, gradereport_user{{/str}}

{{#str}}selectuserinstructions, gradereport_user{{/str}}

diff --git a/grade/templates/searchwidget/searchitem.mustache b/grade/templates/searchwidget/searchitem.mustache index b90203ace5a..931d65f298f 100644 --- a/grade/templates/searchwidget/searchitem.mustache +++ b/grade/templates/searchwidget/searchitem.mustache @@ -24,23 +24,26 @@ "id": "1", "name": "Quiz 1", "url": "http://foo.bar/gradereport/?userid=25", - "fullname": "Cameron Greeve" + "fullname": "Cameron Greeve", + "active": true } }} -{{#name}} - - - {{name}} - - -{{/name}} -{{^name}} - - - {{fullname}} - - - -{{/name}} +
  • + {{#name}} + + + {{name}} + + + {{/name}} + {{^name}} + + + {{fullname}} + + + + {{/name}} +
  • diff --git a/grade/templates/searchwidget/searchresults.mustache b/grade/templates/searchwidget/searchresults.mustache index 40d40a5c527..81fc63fa959 100644 --- a/grade/templates/searchwidget/searchresults.mustache +++ b/grade/templates/searchwidget/searchresults.mustache @@ -34,13 +34,13 @@ ] } }} -
    -
    + +
    diff --git a/grade/templates/searchwidget/user/usersearch_body.mustache b/grade/templates/searchwidget/user/usersearch_body.mustache index 9f5be04520a..2a775f5d4e2 100644 --- a/grade/templates/searchwidget/user/usersearch_body.mustache +++ b/grade/templates/searchwidget/user/usersearch_body.mustache @@ -32,5 +32,5 @@
    {{#displayunsearchablecontent}} -
    + {{/displayunsearchablecontent}} diff --git a/lib/amd/build/aria.min.js b/lib/amd/build/aria.min.js index 38718343f09..7565c92b927 100644 --- a/lib/amd/build/aria.min.js +++ b/lib/amd/build/aria.min.js @@ -1,3 +1,3 @@ -define("core/aria",["exports","./local/aria/aria-hidden","./local/aria/aria-combobox"],(function(_exports,_ariaHidden,_ariaCombobox){Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"comboBox",{enumerable:!0,get:function(){return _ariaCombobox.comboBox}}),Object.defineProperty(_exports,"hide",{enumerable:!0,get:function(){return _ariaHidden.hide}}),Object.defineProperty(_exports,"hideSiblings",{enumerable:!0,get:function(){return _ariaHidden.hideSiblings}}),Object.defineProperty(_exports,"unhide",{enumerable:!0,get:function(){return _ariaHidden.unhide}}),Object.defineProperty(_exports,"unhideSiblings",{enumerable:!0,get:function(){return _ariaHidden.unhideSiblings}})})); +define("core/aria",["exports","./local/aria/aria-hidden"],(function(_exports,_ariaHidden){Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"hide",{enumerable:!0,get:function(){return _ariaHidden.hide}}),Object.defineProperty(_exports,"hideSiblings",{enumerable:!0,get:function(){return _ariaHidden.hideSiblings}}),Object.defineProperty(_exports,"unhide",{enumerable:!0,get:function(){return _ariaHidden.unhide}}),Object.defineProperty(_exports,"unhideSiblings",{enumerable:!0,get:function(){return _ariaHidden.unhideSiblings}})})); //# sourceMappingURL=aria.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/aria/aria-combobox.min.js b/lib/amd/build/local/aria/aria-combobox.min.js deleted file mode 100644 index 278f1348f10..00000000000 --- a/lib/amd/build/local/aria/aria-combobox.min.js +++ /dev/null @@ -1,3 +0,0 @@ -define("core/local/aria/aria-combobox",["exports","core/key_codes"],(function(_exports,_key_codes){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.comboBox=void 0;_exports.comboBox=comboInput=>{registerEventListeners(comboInput)};const registerEventListeners=comboInput=>{document.addEventListener("keydown",(e=>{if(e.target===comboInput){let next=null;const comboResultArea=document.querySelector('[data-region="search-result-items-container"]'),resultRows=Array.from(comboResultArea.querySelectorAll('[role="row"]')),resultCells=Array.from(comboResultArea.querySelectorAll('[role="gridcell"]')),activeResultRow=comboResultArea.querySelector('.active[role="row"]'),activeResultCell=comboResultArea.querySelector('.focused-cell[role="gridcell"]');switch(e.keyCode){case _key_codes.arrowUp:if(null===activeResultRow)next=setFirstActiveRow(next,resultRows,comboInput,resultRows.length-1);else for(let i=0;i((next=resultRows[val]).setAttribute("aria-selected","true"),next.classList.add("active"),comboInput.setAttribute("aria-activedescendant",next.id),next.querySelector(".result-cell").classList.add("focused-cell"),next),nextHandler=(next,e,activeResultRow,comboInput)=>{next&&(e.preventDefault(),null!==activeResultRow&&(activeResultRow.classList.remove("active"),activeResultRow.querySelector(".result-cell").classList.remove("focused-cell")),next.classList.add("active"),next.querySelector(".result-cell").classList.add("focused-cell"),comboInput.setAttribute("aria-activedescendant",next.id))}})); - -//# sourceMappingURL=aria-combobox.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/aria/aria-combobox.min.js.map b/lib/amd/build/local/aria/aria-combobox.min.js.map deleted file mode 100644 index dbd42599c5d..00000000000 --- a/lib/amd/build/local/aria/aria-combobox.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"aria-combobox.min.js","sources":["../../../src/local/aria/aria-combobox.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\nimport {end, arrowLeft, arrowRight, arrowUp, arrowDown, home, enter, space} from 'core/key_codes';\n\n/**\n * ARIA helpers related to the combobox role.\n *\n * @module core/local/aria/aria-combobox.\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Our entry point into adding accessibility handling for comboboxes.\n *\n * @param {Element} comboInput The combobox area to add aria handling to.\n */\nexport const comboBox = (comboInput) => {\n registerEventListeners(comboInput);\n};\n\n/**\n * Event management of the provided combobox.\n *\n * @param {Element} comboInput The combobox area to add aria handling to.\n */\nconst registerEventListeners = (comboInput) => {\n document.addEventListener('keydown', (e) => {\n if (e.target === comboInput) {\n let next = null;\n const comboResultArea = document.querySelector('[data-region=\"search-result-items-container\"]');\n const resultRows = Array.from(comboResultArea.querySelectorAll('[role=\"row\"]'));\n const resultCells = Array.from(comboResultArea.querySelectorAll('[role=\"gridcell\"]'));\n const activeResultRow = comboResultArea.querySelector('.active[role=\"row\"]');\n const activeResultCell = comboResultArea.querySelector('.focused-cell[role=\"gridcell\"]');\n switch (e.keyCode) {\n case arrowUp: {\n // TODO: Handle the wrapping.\n if (activeResultRow === null) {\n next = setFirstActiveRow(next, resultRows, comboInput, resultRows.length - 1);\n } else {\n for (let i = 0; i < resultRows.length; i++) {\n if (resultRows[i].id === activeResultRow.id) {\n next = resultRows[i - 1];\n break;\n }\n }\n }\n break;\n }\n case arrowDown: {\n if (activeResultRow === null) {\n next = setFirstActiveRow(next, resultRows, comboInput, 0);\n } else {\n for (let i = 0; i < resultRows.length - 1; i++) {\n if (resultRows[i].id === activeResultRow.id) {\n next = resultRows[i + 1];\n break;\n }\n }\n }\n break;\n }\n case home: {\n next = resultRows[0];\n break;\n }\n case end: {\n next = resultRows[resultRows.length - 1];\n break;\n }\n case enter || space: {\n // Redirect the user to the appropriate link.\n // TODO: Space does not work, special handler on the cell itself?\n window.location = activeResultCell.href;\n break;\n }\n case arrowLeft: {\n if (activeResultRow === null) {\n next = setFirstActiveRow(next, resultRows, comboInput, 0);\n } else {\n for (let i = 0; i < resultCells.length; i++) {\n if (resultCells[i].id === activeResultCell.id) {\n if (resultCells[i - 1] === undefined) {\n resultCells[i].classList.remove('focused-cell');\n resultCells[resultCells.length - 1].classList.add('focused-cell');\n break;\n } else {\n resultCells[i].classList.remove('focused-cell');\n resultCells[i - 1].classList.add('focused-cell');\n break;\n }\n }\n }\n }\n break;\n }\n case arrowRight: {\n if (activeResultRow === null) {\n next = setFirstActiveRow(next, resultRows, comboInput, 0);\n } else {\n for (let i = 0; i < resultCells.length - 1; i++) {\n if (resultCells[i].id === activeResultCell.id) {\n resultCells[i].classList.remove('focused-cell');\n resultCells[i + 1].classList.add('focused-cell');\n break;\n }\n if (resultCells[i + 2] === undefined) {\n resultCells[i + 1].classList.remove('focused-cell');\n resultCells[0].classList.add('focused-cell');\n break;\n }\n }\n }\n break;\n }\n default: {\n window.console.log('nothing to see here!');\n break;\n }\n }\n // Variable next is set if we do want to act on the keypress.\n nextHandler(next, e, activeResultRow, comboInput);\n }\n });\n};\n\n/**\n * With search, we can't automatically set aria elements in the results field, so we do it here.\n *\n * @param {Element} next\n * @param {Array} resultRows\n * @param {Element} comboInput\n * @param {Number} val\n * @returns {Element}\n */\nconst setFirstActiveRow = (next, resultRows, comboInput, val) => {\n // Set first option as active.\n next = resultRows[val];\n next.setAttribute('aria-selected', 'true');\n next.classList.add('active');\n comboInput.setAttribute('aria-activedescendant', next.id);\n next.querySelector('.result-cell').classList.add('focused-cell');\n return next;\n};\n\n/**\n * Given we have a value to next set active, handle some of the basic handling.\n *\n * @param {Element} next\n * @param {Event} e\n * @param {Element} activeResultRow\n * @param {Element} comboInput\n */\nconst nextHandler = (next, e, activeResultRow, comboInput) => {\n if (next) {\n e.preventDefault();\n if (activeResultRow !== null) {\n activeResultRow.classList.remove('active');\n activeResultRow.querySelector('.result-cell').classList.remove('focused-cell');\n }\n next.classList.add('active');\n // Find whatever the first result cell is to add the class.\n next.querySelector('.result-cell').classList.add('focused-cell');\n comboInput.setAttribute('aria-activedescendant', next.id);\n }\n};\n"],"names":["comboInput","registerEventListeners","document","addEventListener","e","target","next","comboResultArea","querySelector","resultRows","Array","from","querySelectorAll","resultCells","activeResultRow","activeResultCell","keyCode","arrowUp","setFirstActiveRow","length","i","id","arrowDown","home","end","enter","space","window","location","href","arrowLeft","undefined","classList","remove","add","arrowRight","console","log","nextHandler","val","setAttribute","preventDefault"],"mappings":"sMA8ByBA,aACrBC,uBAAuBD,mBAQrBC,uBAA0BD,aAC5BE,SAASC,iBAAiB,WAAYC,OAC9BA,EAAEC,SAAWL,WAAY,KACrBM,KAAO,WACLC,gBAAkBL,SAASM,cAAc,iDACzCC,WAAaC,MAAMC,KAAKJ,gBAAgBK,iBAAiB,iBACzDC,YAAcH,MAAMC,KAAKJ,gBAAgBK,iBAAiB,sBAC1DE,gBAAkBP,gBAAgBC,cAAc,uBAChDO,iBAAmBR,gBAAgBC,cAAc,yCAC/CJ,EAAEY,cACDC,sBAEuB,OAApBH,gBACAR,KAAOY,kBAAkBZ,KAAMG,WAAYT,WAAYS,WAAWU,OAAS,YAEtE,IAAIC,EAAI,EAAGA,EAAIX,WAAWU,OAAQC,OAC/BX,WAAWW,GAAGC,KAAOP,gBAAgBO,GAAI,CACzCf,KAAOG,WAAWW,EAAI,oBAOjCE,wBACuB,OAApBR,gBACAR,KAAOY,kBAAkBZ,KAAMG,WAAYT,WAAY,YAElD,IAAIoB,EAAI,EAAGA,EAAIX,WAAWU,OAAS,EAAGC,OACnCX,WAAWW,GAAGC,KAAOP,gBAAgBO,GAAI,CACzCf,KAAOG,WAAWW,EAAI,oBAOjCG,gBACDjB,KAAOG,WAAW,cAGjBe,eACDlB,KAAOG,WAAWA,WAAWU,OAAS,cAGrCM,kBAASC,iBAGVC,OAAOC,SAAWb,iBAAiBc,gBAGlCC,wBACuB,OAApBhB,gBACAR,KAAOY,kBAAkBZ,KAAMG,WAAYT,WAAY,YAElD,IAAIoB,EAAI,EAAGA,EAAIP,YAAYM,OAAQC,OAChCP,YAAYO,GAAGC,KAAON,iBAAiBM,GAAI,SAChBU,IAAvBlB,YAAYO,EAAI,GAAkB,CAClCP,YAAYO,GAAGY,UAAUC,OAAO,gBAChCpB,YAAYA,YAAYM,OAAS,GAAGa,UAAUE,IAAI,sBAGlDrB,YAAYO,GAAGY,UAAUC,OAAO,gBAChCpB,YAAYO,EAAI,GAAGY,UAAUE,IAAI,iCAQhDC,yBACuB,OAApBrB,gBACAR,KAAOY,kBAAkBZ,KAAMG,WAAYT,WAAY,YAElD,IAAIoB,EAAI,EAAGA,EAAIP,YAAYM,OAAS,EAAGC,IAAK,IACzCP,YAAYO,GAAGC,KAAON,iBAAiBM,GAAI,CAC3CR,YAAYO,GAAGY,UAAUC,OAAO,gBAChCpB,YAAYO,EAAI,GAAGY,UAAUE,IAAI,8BAGVH,IAAvBlB,YAAYO,EAAI,GAAkB,CAClCP,YAAYO,EAAI,GAAGY,UAAUC,OAAO,gBACpCpB,YAAY,GAAGmB,UAAUE,IAAI,qCAQzCP,OAAOS,QAAQC,IAAI,wBAK3BC,YAAYhC,KAAMF,EAAGU,gBAAiBd,iBAc5CkB,kBAAoB,CAACZ,KAAMG,WAAYT,WAAYuC,QAErDjC,KAAOG,WAAW8B,MACbC,aAAa,gBAAiB,QACnClC,KAAK0B,UAAUE,IAAI,UACnBlC,WAAWwC,aAAa,wBAAyBlC,KAAKe,IACtDf,KAAKE,cAAc,gBAAgBwB,UAAUE,IAAI,gBAC1C5B,MAWLgC,YAAc,CAAChC,KAAMF,EAAGU,gBAAiBd,cACvCM,OACAF,EAAEqC,iBACsB,OAApB3B,kBACAA,gBAAgBkB,UAAUC,OAAO,UACjCnB,gBAAgBN,cAAc,gBAAgBwB,UAAUC,OAAO,iBAEnE3B,KAAK0B,UAAUE,IAAI,UAEnB5B,KAAKE,cAAc,gBAAgBwB,UAAUE,IAAI,gBACjDlC,WAAWwC,aAAa,wBAAyBlC,KAAKe"} \ No newline at end of file diff --git a/lib/amd/src/aria.js b/lib/amd/src/aria.js index d65b05b5145..baa0c1a0d45 100644 --- a/lib/amd/src/aria.js +++ b/lib/amd/src/aria.js @@ -27,6 +27,3 @@ export { hideSiblings, unhideSiblings, } from './local/aria/aria-hidden'; -export { - comboBox -} from './local/aria/aria-combobox'; diff --git a/lib/amd/src/local/aria/aria-combobox.js b/lib/amd/src/local/aria/aria-combobox.js deleted file mode 100644 index 82e23af72a8..00000000000 --- a/lib/amd/src/local/aria/aria-combobox.js +++ /dev/null @@ -1,180 +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 . - -import {end, arrowLeft, arrowRight, arrowUp, arrowDown, home, enter, space} from 'core/key_codes'; - -/** - * ARIA helpers related to the combobox role. - * - * @module core/local/aria/aria-combobox. - * @copyright 2022 Mathew May - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * Our entry point into adding accessibility handling for comboboxes. - * - * @param {Element} comboInput The combobox area to add aria handling to. - */ -export const comboBox = (comboInput) => { - registerEventListeners(comboInput); -}; - -/** - * Event management of the provided combobox. - * - * @param {Element} comboInput The combobox area to add aria handling to. - */ -const registerEventListeners = (comboInput) => { - document.addEventListener('keydown', (e) => { - if (e.target === comboInput) { - let next = null; - const comboResultArea = document.querySelector('[data-region="search-result-items-container"]'); - const resultRows = Array.from(comboResultArea.querySelectorAll('[role="row"]')); - const resultCells = Array.from(comboResultArea.querySelectorAll('[role="gridcell"]')); - const activeResultRow = comboResultArea.querySelector('.active[role="row"]'); - const activeResultCell = comboResultArea.querySelector('.focused-cell[role="gridcell"]'); - switch (e.keyCode) { - case arrowUp: { - // TODO: Handle the wrapping. - if (activeResultRow === null) { - next = setFirstActiveRow(next, resultRows, comboInput, resultRows.length - 1); - } else { - for (let i = 0; i < resultRows.length; i++) { - if (resultRows[i].id === activeResultRow.id) { - next = resultRows[i - 1]; - break; - } - } - } - break; - } - case arrowDown: { - if (activeResultRow === null) { - next = setFirstActiveRow(next, resultRows, comboInput, 0); - } else { - for (let i = 0; i < resultRows.length - 1; i++) { - if (resultRows[i].id === activeResultRow.id) { - next = resultRows[i + 1]; - break; - } - } - } - break; - } - case home: { - next = resultRows[0]; - break; - } - case end: { - next = resultRows[resultRows.length - 1]; - break; - } - case enter || space: { - // Redirect the user to the appropriate link. - // TODO: Space does not work, special handler on the cell itself? - window.location = activeResultCell.href; - break; - } - case arrowLeft: { - if (activeResultRow === null) { - next = setFirstActiveRow(next, resultRows, comboInput, 0); - } else { - for (let i = 0; i < resultCells.length; i++) { - if (resultCells[i].id === activeResultCell.id) { - if (resultCells[i - 1] === undefined) { - resultCells[i].classList.remove('focused-cell'); - resultCells[resultCells.length - 1].classList.add('focused-cell'); - break; - } else { - resultCells[i].classList.remove('focused-cell'); - resultCells[i - 1].classList.add('focused-cell'); - break; - } - } - } - } - break; - } - case arrowRight: { - if (activeResultRow === null) { - next = setFirstActiveRow(next, resultRows, comboInput, 0); - } else { - for (let i = 0; i < resultCells.length - 1; i++) { - if (resultCells[i].id === activeResultCell.id) { - resultCells[i].classList.remove('focused-cell'); - resultCells[i + 1].classList.add('focused-cell'); - break; - } - if (resultCells[i + 2] === undefined) { - resultCells[i + 1].classList.remove('focused-cell'); - resultCells[0].classList.add('focused-cell'); - break; - } - } - } - break; - } - default: { - window.console.log('nothing to see here!'); - break; - } - } - // Variable next is set if we do want to act on the keypress. - nextHandler(next, e, activeResultRow, comboInput); - } - }); -}; - -/** - * With search, we can't automatically set aria elements in the results field, so we do it here. - * - * @param {Element} next - * @param {Array} resultRows - * @param {Element} comboInput - * @param {Number} val - * @returns {Element} - */ -const setFirstActiveRow = (next, resultRows, comboInput, val) => { - // Set first option as active. - next = resultRows[val]; - next.setAttribute('aria-selected', 'true'); - next.classList.add('active'); - comboInput.setAttribute('aria-activedescendant', next.id); - next.querySelector('.result-cell').classList.add('focused-cell'); - return next; -}; - -/** - * Given we have a value to next set active, handle some of the basic handling. - * - * @param {Element} next - * @param {Event} e - * @param {Element} activeResultRow - * @param {Element} comboInput - */ -const nextHandler = (next, e, activeResultRow, comboInput) => { - if (next) { - e.preventDefault(); - if (activeResultRow !== null) { - activeResultRow.classList.remove('active'); - activeResultRow.querySelector('.result-cell').classList.remove('focused-cell'); - } - next.classList.add('active'); - // Find whatever the first result cell is to add the class. - next.querySelector('.result-cell').classList.add('focused-cell'); - comboInput.setAttribute('aria-activedescendant', next.id); - } -}; diff --git a/lib/templates/search_input_auto.mustache b/lib/templates/search_input_auto.mustache index 9dad3e20cc2..2d9e3fe2f82 100644 --- a/lib/templates/search_input_auto.mustache +++ b/lib/templates/search_input_auto.mustache @@ -43,6 +43,7 @@