diff --git a/grade/amd/build/comboboxsearch/grade.min.js b/grade/amd/build/comboboxsearch/grade.min.js
index bba3b155790..3fd01ec6032 100644
--- a/grade/amd/build/comboboxsearch/grade.min.js
+++ b/grade/amd/build/comboboxsearch/grade.min.js
@@ -1,3 +1,3 @@
-define("core_grades/comboboxsearch/grade",["exports","core/comboboxsearch/search_combobox","core_grades/searchwidget/repository","core/templates","core/utils"],(function(_exports,_search_combobox,Repository,_templates,_utils){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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=(obj=_search_combobox)&&obj.__esModule?obj:{default:obj},Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class GradeItemSearch extends _search_combobox.default{constructor(){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"courseID",void 0),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.gradesearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault()}static init(){return new GradeItemSearch}componentSelector(){return".grade-search"}dropdownSelector(){return".gradesearchdropdown"}triggerSelector(){return".gradesearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core/local/comboboxsearch/resultset",{results:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0})}))}async fetchDataset(){return await Repository.gradeitemFetch(this.courseID).then((r=>r.gradeitems))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((grade=>Object.keys(grade).some((key=>""!==grade[key]&&grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((grade=>({id:grade.id,name:grade.name,link:this.selectOneLink(grade.id)}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){switch(super.keyHandler(e),e.key){case"Tab":e.target.closest(this.selectors.input)&&(e.preventDefault(),this.clearSearchButton.focus({preventScroll:!0}));break;case"Escape":if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(gradeID){throw new Error("selectOneLink(".concat(gradeID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GradeItemSearch,_exports.default}));
+define("core_grades/comboboxsearch/grade",["exports","core/comboboxsearch/search_combobox","core_grades/searchwidget/repository","core/templates","core/utils"],(function(_exports,_search_combobox,Repository,_templates,_utils){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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=(obj=_search_combobox)&&obj.__esModule?obj:{default:obj},Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class GradeItemSearch extends _search_combobox.default{constructor(){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"courseID",void 0),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.gradesearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=this.component.querySelector(this.selectors.instance).dataset.instance;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault()}static init(){return new GradeItemSearch}componentSelector(){return".grade-search"}dropdownSelector(){return".gradesearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core/local/comboboxsearch/resultset",{instance:this.instance,results:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents()}async fetchDataset(){return await Repository.gradeitemFetch(this.courseID).then((r=>r.gradeitems))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((grade=>Object.keys(grade).some((key=>""!==grade[key]&&grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((grade=>({id:grade.id,name:grade.name}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(gradeID){throw new Error("selectOneLink(".concat(gradeID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GradeItemSearch,_exports.default}));
//# sourceMappingURL=grade.min.js.map
\ No newline at end of file
diff --git a/grade/amd/build/comboboxsearch/grade.min.js.map b/grade/amd/build/comboboxsearch/grade.min.js.map
index 464efafb1a6..9b0ed4097ed 100644
--- a/grade/amd/build/comboboxsearch/grade.min.js.map
+++ b/grade/amd/build/comboboxsearch/grade.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"grade.min.js","sources":["../../src/comboboxsearch/grade.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 * Allow the user to search for grades within the grade area.\n *\n * @module core_grades/comboboxsearch/grade\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\n\nexport default class GradeItemSearch extends search_combobox {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {\n ...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.gradesearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n\n this.renderDefault();\n }\n\n static init() {\n return new GradeItemSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.grade-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.gradesearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.gradesearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {\n results: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n this.registerInputEvents();\n\n // Add a small BS listener so that we can set the focus correctly on open.\n this.$component.on('shown.bs.dropdown', () => {\n this.searchInput.focus({preventScroll: true});\n });\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await Repository.gradeitemFetch(this.courseID).then((r) => r.gradeitems);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((grade) => Object.keys(grade).some((key) => {\n if (grade[key] === \"\") {\n return false;\n }\n return grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((grade) => {\n return {\n id: grade.id,\n name: grade.name,\n link: this.selectOneLink(grade.id),\n };\n })\n );\n }\n\n /**\n * Handle any keyboard inputs.\n */\n registerInputEvents() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.searchInput.value === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.dropdown)) {\n // Forcibly prevent BS events so that we can control the open and close.\n // Really needed because by default input elements cant trigger a dropdown.\n e.stopImmediatePropagation();\n }\n this.clearSearchButton.addEventListener('click', async() => {\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n await this.filterrenderpipe();\n });\n // Prevent normal key presses activating this.\n if (e.target.closest('.dropdown-item') && e.button === 0) {\n window.location = e.target.closest('.dropdown-item').href;\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n super.keyHandler(e);\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Tab':\n if (e.target.closest(this.selectors.input)) {\n e.preventDefault();\n this.clearSearchButton.focus({preventScroll: true});\n }\n break;\n case 'Escape':\n if (document.activeElement.getAttribute('role') === 'option') {\n e.stopPropagation();\n this.searchInput.focus({preventScroll: true});\n } else if (e.target.closest(this.selectors.input)) {\n const trigger = this.component.querySelector(this.selectors.trigger);\n trigger.focus({preventScroll: true});\n }\n }\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} gradeID The ID of the grade item selected.\n */\n selectOneLink(gradeID) {\n throw new Error(`selectOneLink(${gradeID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GradeItemSearch","search_combobox","constructor","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","renderDefault","dropdownSelector","triggerSelector","html","js","results","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","registerInputEvents","$component","on","searchInput","focus","preventScroll","Repository","gradeitemFetch","then","r","gradeitems","filterableData","getPreppedSearchTerm","filter","grade","Object","keys","some","key","toString","toLowerCase","includes","map","id","name","link","selectOneLink","addEventListener","async","setSearchTerms","value","clearSearchButton","classList","add","remove","filterrenderpipe","e","target","closest","dropdown","stopImmediatePropagation","button","window","location","href","keyHandler","input","preventDefault","activeElement","getAttribute","stopPropagation","trigger","registerInputHandlers","gradeID","Error"],"mappings":"i0CA2BqBA,wBAAwBC,yBAIzCC,6LAISC,UAAY,IACVC,KAAKD,UACRE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,qCAIE,IAAIb,gBAQfU,0BACW,gBAQXI,yBACW,uBAQXC,wBACW,kDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,QAASd,KAAKe,oBACdC,WAAYhB,KAAKe,oBAAoBE,OAAS,EAC9CC,WAAYlB,KAAKmB,qDAEDnB,KAAKD,UAAUG,YAAaU,KAAMC,+BAOjDO,wBAAwBpB,KAAKqB,oBAAoBrB,KAAKsB,oBACtDC,2BAECvB,KAAKwB,sBAENC,mBACAC,2BAGAC,WAAWC,GAAG,qBAAqB,UAC/BC,YAAYC,MAAM,CAACC,eAAe,yCAU9BC,WAAWC,eAAejC,KAAKO,UAAU2B,MAAMC,GAAMA,EAAEC,iCASpDC,sBAEoB,KAAhCrC,KAAKsC,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,MAGHJ,MAAMI,KAAKC,WAAWC,cAAcC,SAAS/C,KAAKsC,4BAOjEf,0BACSH,kBACDpB,KAAKe,oBAAoBiC,KAAKR,QACnB,CACHS,GAAIT,MAAMS,GACVC,KAAMV,MAAMU,KACZC,KAAMnD,KAAKoD,cAAcZ,MAAMS,SAS/CvB,2BAESG,YAAYwB,iBAAiB,SAAS,oBAASC,eAC3CC,eAAevD,KAAK6B,YAAY2B,OAEN,KAA3BxD,KAAK6B,YAAY2B,WAEZC,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,gBAGtC5D,KAAK6D,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQhE,KAAKD,UAAUkE,WAGhCH,EAAEI,gCAEDT,kBAAkBJ,iBAAiB,SAASC,eACxCzB,YAAY2B,MAAQ,QACpBD,eAAevD,KAAK6B,YAAY2B,aAC/BxD,KAAK6D,sBAGXC,EAAEC,OAAOC,QAAQ,mBAAkC,IAAbF,EAAEK,SACxCC,OAAOC,SAAWP,EAAEC,OAAOC,QAAQ,kBAAkBM,MAS7DC,WAAWT,gBACDS,WAAWT,GAETA,EAAElB,SACD,MACGkB,EAAEC,OAAOC,QAAQhE,KAAKD,UAAUyE,SAChCV,EAAEW,sBACGhB,kBAAkB3B,MAAM,CAACC,eAAe,eAGhD,YACmD,WAAhD3B,SAASsE,cAAcC,aAAa,QACpCb,EAAEc,uBACG/C,YAAYC,MAAM,CAACC,eAAe,SACpC,GAAI+B,EAAEC,OAAOC,QAAQhE,KAAKD,UAAUyE,OAAQ,CAC/BxE,KAAKG,UAAUE,cAAcL,KAAKD,UAAU8E,SACpD/C,MAAM,CAACC,eAAe,MAQ9C+C,6BAESjD,YAAYwB,iBAAiB,SAAS,oBAAS,UAC3CE,eAAevD,KAAK6B,YAAY2B,OAER,KAAzBxD,KAAKmB,qBAEAsC,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,YAE7C,MAQPR,cAAc2B,eACJ,IAAIC,8BAAuBD,4CAAmC/E,KAAKF,YAAYoD"}
\ No newline at end of file
+{"version":3,"file":"grade.min.js","sources":["../../src/comboboxsearch/grade.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 * Allow the user to search for grades within the grade area.\n *\n * @module core_grades/comboboxsearch/grade\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\n\nexport default class GradeItemSearch extends search_combobox {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {\n ...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.gradesearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault();\n }\n\n static init() {\n return new GradeItemSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.grade-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.gradesearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {\n instance: this.instance,\n results: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n this.registerInputEvents();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await Repository.gradeitemFetch(this.courseID).then((r) => r.gradeitems);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((grade) => Object.keys(grade).some((key) => {\n if (grade[key] === \"\") {\n return false;\n }\n return grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((grade) => {\n return {\n id: grade.id,\n name: grade.name,\n };\n })\n );\n }\n\n /**\n * Handle any keyboard inputs.\n */\n registerInputEvents() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.searchInput.value === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} gradeID The ID of the grade item selected.\n */\n selectOneLink(gradeID) {\n throw new Error(`selectOneLink(${gradeID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GradeItemSearch","search_combobox","constructor","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","dropdownSelector","html","js","results","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","registerInputEvents","Repository","gradeitemFetch","then","r","gradeitems","filterableData","getPreppedSearchTerm","filter","grade","Object","keys","some","key","toString","toLowerCase","includes","map","id","name","async","setSearchTerms","clearSearchButton","add","filterrenderpipe","e","target","closest","clearSearch","stopPropagation","focus","changeHandler","window","location","selectOneLink","registerInputHandlers","gradeID","Error"],"mappings":"i0CA2BqBA,wBAAwBC,yBAIzCC,6LAISC,UAAY,IACVC,KAAKD,UACRE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cACpEQ,SAAWT,KAAKG,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,eAExEC,mBAAqBV,KAAKG,UAAUE,yBAAkBL,KAAKW,YAAYH,QAAQI,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAef,KAAKG,UAAUE,yBAAkBL,KAAKgB,SAASR,QAAQI,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,WAAWC,GAAG,oBAAoB,UAC9BX,YAAYY,gBAAgB,+BAE3BC,QAAUpB,SAASC,yBAAkBL,KAAKW,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BhC,KAAKW,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,qCAIE,IAAIrC,gBAQfU,0BACW,gBAQX4B,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7E3B,SAAUT,KAAKS,SACf4B,QAASrC,KAAKsC,oBACdC,WAAYvC,KAAKsC,oBAAoBE,OAAS,EAC9CC,WAAYzC,KAAK0C,qDAED1C,KAAKD,UAAUG,YAAaiC,KAAMC,SAEjDzB,YAAYY,gBAAgB,oDAO5BoB,wBAAwB3C,KAAK4C,oBAAoB5C,KAAK6C,oBACtDC,2BAEC9C,KAAK+C,sBAENC,mBACAC,wDASQC,WAAWC,eAAenD,KAAKO,UAAU6C,MAAMC,GAAMA,EAAEC,iCASpDC,sBAEoB,KAAhCvD,KAAKwD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,MAGHJ,MAAMI,KAAKC,WAAWC,cAAcC,SAASjE,KAAKwD,4BAOjEV,0BACSH,kBACD3C,KAAKsC,oBAAoB4B,KAAKR,QACnB,CACHS,GAAIT,MAAMS,GACVC,KAAMV,MAAMU,UAS5BnB,2BAEStC,YAAYE,iBAAiB,SAAS,oBAASwD,eAC3CC,eAAetE,KAAKW,YAAYM,OAEN,KAA3BjB,KAAKW,YAAYM,WAEZsD,kBAAkB1C,UAAU2C,IAAI,eAGhCD,kBAAkB1C,UAAUC,OAAO,gBAGtC9B,KAAKyE,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQ5E,KAAKD,UAAU8E,eAChCH,EAAEI,uBAEGnE,YAAYM,MAAQ,QACpBqD,eAAetE,KAAKW,YAAYM,YAChCN,YAAYoE,aACZR,kBAAkB1C,UAAU2C,IAAI,gBAE/BxE,KAAKyE,oBASnBO,cAAcN,GACVO,OAAOC,SAAWlF,KAAKmF,cAAcT,EAAEC,OAAO1D,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAAS,UAC3CyD,eAAetE,KAAKW,YAAYM,OAER,KAAzBjB,KAAK0C,qBAEA6B,kBAAkB1C,UAAU2C,IAAI,eAGhCD,kBAAkB1C,UAAUC,OAAO,YAE7C,MASPqD,cAAcE,eACJ,IAAIC,8BAAuBD,4CAAmCrF,KAAKF,YAAYsE"}
\ No newline at end of file
diff --git a/grade/amd/src/comboboxsearch/grade.js b/grade/amd/src/comboboxsearch/grade.js
index 93172d4924d..611258173a7 100644
--- a/grade/amd/src/comboboxsearch/grade.js
+++ b/grade/amd/src/comboboxsearch/grade.js
@@ -40,6 +40,38 @@ export default class GradeItemSearch extends search_combobox {
};
const component = document.querySelector(this.componentSelector());
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
+ this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;
+
+ const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);
+ searchValueElement.addEventListener('change', () => {
+ this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
+
+ const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);
+ if (valueElement.value !== searchValueElement.value) {
+ valueElement.value = searchValueElement.value;
+ valueElement.dispatchEvent(new Event('change', {bubbles: true}));
+ }
+
+ searchValueElement.value = '';
+ });
+
+ this.$component.on('hide.bs.dropdown', () => {
+ this.searchInput.removeAttribute('aria-activedescendant');
+
+ const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role="listbox"]`);
+ listbox.querySelectorAll('.active[role="option"]').forEach(option => {
+ option.classList.remove('active');
+ });
+ listbox.scrollTop = 0;
+
+ // Use setTimeout to make sure the following code is executed after the click event is handled.
+ setTimeout(() => {
+ if (this.searchInput.value !== '') {
+ this.searchInput.value = '';
+ this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
+ }
+ });
+ });
this.renderDefault();
}
@@ -66,25 +98,19 @@ export default class GradeItemSearch extends search_combobox {
return '.gradesearchdropdown';
}
- /**
- * The triggering div that contains the searching widget.
- *
- * @returns {string}
- */
- triggerSelector() {
- return '.gradesearchwidget';
- }
-
/**
* Build the content then replace the node.
*/
async renderDropdown() {
const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {
+ instance: this.instance,
results: this.getMatchedResults(),
hasresults: this.getMatchedResults().length > 0,
searchterm: this.getSearchTerm(),
});
replaceNodeContents(this.selectors.placeholder, html, js);
+ // Remove aria-activedescendant when the available options change.
+ this.searchInput.removeAttribute('aria-activedescendant');
}
/**
@@ -98,11 +124,6 @@ export default class GradeItemSearch extends search_combobox {
this.updateNodes();
this.registerInputEvents();
-
- // Add a small BS listener so that we can set the focus correctly on open.
- this.$component.on('shown.bs.dropdown', () => {
- this.searchInput.focus({preventScroll: true});
- });
}
/**
@@ -142,7 +163,6 @@ export default class GradeItemSearch extends search_combobox {
return {
id: grade.id,
name: grade.name,
- link: this.selectOneLink(grade.id),
};
})
);
@@ -174,46 +194,25 @@ export default class GradeItemSearch extends search_combobox {
* @param {MouseEvent} e The triggering event that we are working with.
*/
async clickHandler(e) {
- if (e.target.closest(this.selectors.dropdown)) {
- // Forcibly prevent BS events so that we can control the open and close.
- // Really needed because by default input elements cant trigger a dropdown.
- e.stopImmediatePropagation();
- }
- this.clearSearchButton.addEventListener('click', async() => {
+ if (e.target.closest(this.selectors.clearSearch)) {
+ e.stopPropagation();
+ // Clear the entered search query in the search bar.
this.searchInput.value = '';
this.setSearchTerms(this.searchInput.value);
+ this.searchInput.focus();
+ this.clearSearchButton.classList.add('d-none');
+ // Display results.
await this.filterrenderpipe();
- });
- // Prevent normal key presses activating this.
- if (e.target.closest('.dropdown-item') && e.button === 0) {
- window.location = e.target.closest('.dropdown-item').href;
}
}
/**
- * The handler for when a user presses a key within the component.
+ * The handler for when a user changes the value of the component (selects an option from the dropdown).
*
- * @param {KeyboardEvent} e The triggering event that we are working with.
+ * @param {Event} e The change event.
*/
- keyHandler(e) {
- super.keyHandler(e);
- // Switch the key presses to handle keyboard nav.
- switch (e.key) {
- case 'Tab':
- if (e.target.closest(this.selectors.input)) {
- e.preventDefault();
- this.clearSearchButton.focus({preventScroll: true});
- }
- break;
- case 'Escape':
- if (document.activeElement.getAttribute('role') === 'option') {
- e.stopPropagation();
- this.searchInput.focus({preventScroll: true});
- } else if (e.target.closest(this.selectors.input)) {
- const trigger = this.component.querySelector(this.selectors.trigger);
- trigger.focus({preventScroll: true});
- }
- }
+ changeHandler(e) {
+ window.location = this.selectOneLink(e.target.value);
}
/**
@@ -236,6 +235,7 @@ export default class GradeItemSearch extends search_combobox {
/**
* Build up the view all link that is dedicated to a particular result.
+ * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.
*
* @param {Number} gradeID The ID of the grade item selected.
*/
diff --git a/grade/renderer.php b/grade/renderer.php
index 202843e24a2..8beccd2549f 100644
--- a/grade/renderer.php
+++ b/grade/renderer.php
@@ -45,12 +45,18 @@ class core_grades_renderer extends plugin_renderer_base {
* Renders the group selector trigger element.
*
* @param object $course The course object.
- * @param string|null $groupactionbaseurl The base URL for the group action.
+ * @param string|null $groupactionbaseurl This parameter has been deprecated since 4.4 and should not be used anymore.
* @return string|null The raw HTML to render.
*/
public function group_selector(object $course, ?string $groupactionbaseurl = null): ?string {
global $USER;
+ if ($groupactionbaseurl !== null) {
+ debugging(
+ 'The $groupactionbaseurl argument has been deprecated. Please remove it from your method calls.',
+ DEBUG_DEVELOPER,
+ );
+ }
// Make sure that group mode is enabled.
if (!$groupmode = $course->groupmode) {
return null;
@@ -59,17 +65,12 @@ class core_grades_renderer extends plugin_renderer_base {
$sbody = $this->render_from_template('core_group/comboboxsearch/searchbody', [
'courseid' => $course->id,
'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS),
+ 'instance' => rand(),
]);
- $label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') :
- get_string('selectgroupsseparate');
+ $label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') : get_string('selectgroupsseparate');
- $data = [
- 'name' => 'group',
- 'label' => $label,
- 'courseid' => $course->id,
- 'groupactionbaseurl' => $groupactionbaseurl
- ];
+ $buttondata = ['label' => $label];
$context = context_course::instance($course->id);
@@ -80,22 +81,27 @@ class core_grades_renderer extends plugin_renderer_base {
}
$activegroup = groups_get_course_group($course, true, $allowedgroups);
- $data['group'] = $activegroup;
+ $buttondata['group'] = $activegroup;
if ($activegroup) {
$group = groups_get_group($activegroup);
- $data['selectedgroup'] = format_string($group->name, true, ['context' => $context]);
+ $buttondata['selectedgroup'] = format_string($group->name, true, ['context' => $context]);
} else if ($activegroup === 0) {
- $data['selectedgroup'] = get_string('allparticipants');
+ $buttondata['selectedgroup'] = get_string('allparticipants');
}
$groupdropdown = new comboboxsearch(
false,
- $this->render_from_template('core_group/comboboxsearch/group_selector', $data),
+ $this->render_from_template('core_group/comboboxsearch/group_selector', $buttondata),
$sbody,
'group-search',
'groupsearchwidget',
- 'groupsearchdropdown overflow-auto w-100',
+ 'groupsearchdropdown overflow-auto',
+ null,
+ true,
+ $label,
+ 'group',
+ $activegroup
);
return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this));
}
diff --git a/grade/report/grader/amd/build/collapse.min.js b/grade/report/grader/amd/build/collapse.min.js
index d465a83330e..feb982860fd 100644
--- a/grade/report/grader/amd/build/collapse.min.js
+++ b/grade/report/grader/amd/build/collapse.min.js
@@ -1,3 +1,3 @@
-define("gradereport_grader/collapse",["exports","gradereport_grader/collapse/repository","core/comboboxsearch/search_combobox","core/templates","core/utils","jquery","core/str","core/custom_interaction_events","core/localstorage","core/loadingicon","core/notification","core/pending"],(function(_exports,Repository,_search_combobox,_templates,_utils,_jquery,_str,_custom_interaction_events,_localstorage,_loadingicon,_notification,_pending){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 _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository),_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_localstorage=_interopRequireDefault(_localstorage),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const selectors_component=".collapse-columns",selectors_formDropdown=".columnsdropdownform",selectors_formItems={cancel:"cancel",save:"save",checked:'input[type="checkbox"]:checked',currentlyUnchecked:'input[type="checkbox"]:not([data-action="selectall"])'},selectors_hider="hide",selectors_expand="expand",selectors_colVal="[data-col]",selectors_itemVal="[data-itemid]",selectors_content='[data-collapse="content"]',selectors_sort='[data-collapse="sort"]',selectors_expandbutton='[data-collapse="expandbutton"]',selectors_rangerowcell='[data-collapse="rangerowcell"]',selectors_avgrowcell='[data-collapse="avgrowcell"]',selectors_menu='[data-collapse="menu"]',selectors_icons=".data-collapse_gradeicons",selectors_count='[data-collapse="count"]',selectors_placeholder='.collapsecolumndropdown [data-region="placeholder"]',selectors_fullDropdown=".collapsecolumndropdown",countIndicator=document.querySelector(selectors_count);class ColumnSearch extends _search_combobox.default{static init(userID,courseID,defaultSort){return new ColumnSearch(userID,courseID,defaultSort)}constructor(userID,courseID,defaultSort){super(),_defineProperty(this,"userID",-1),_defineProperty(this,"courseID",null),_defineProperty(this,"defaultSort",""),_defineProperty(this,"nodes",[]),_defineProperty(this,"gradeStrings",null),_defineProperty(this,"userStrings",null),_defineProperty(this,"stringMap",[]),this.userID=userID,this.courseID=courseID,this.defaultSort=defaultSort,this.component=document.querySelector(selectors_component);const pendingPromise=new _pending.default;(0,_loadingicon.addIconToContainer)(document.querySelector(".gradeparent")).then((loader=>{setTimeout((()=>{this.getDataset().forEach((item=>{this.nodesUpdate(item)})),this.renderDefault(),loader.remove(),document.querySelector(".gradereport-grader-table").classList.remove("d-none")}),10)})).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)}componentSelector(){return".collapse-columns"}dropdownSelector(){return".searchresultitemscontainer"}triggerSelector(){return".collapsecolumn"}getDataset(){if(!this.dataset){const cols=this.fetchDataset();this.dataset=JSON.parse(cols)?JSON.parse(cols).split(","):[]}return this.datasetSize=this.dataset.length,this.dataset}fetchDataset(){return _localstorage.default.get("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID))}setPreferences(){_localstorage.default.set("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID),JSON.stringify(this.getDataset().join(",")))}registerClickHandlers(){this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",this.docClickHandler.bind(this))}clickHandler(e){super.clickHandler(e),e.target.closest(selectors_fullDropdown)&&e.stopPropagation()}async docClickHandler(e){var _e$target$closest3;if(e.target.dataset.hider===selectors_hider){var _e$target$closest,_e$target$closest2;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest=e.target.closest(selectors_colVal))||void 0===_e$target$closest?void 0:_e$target$closest.dataset.col:null===(_e$target$closest2=e.target.closest(selectors_itemVal))||void 0===_e$target$closest2?void 0:_e$target$closest2.dataset.itemid;-1===this.getDataset().indexOf(desiredToHide)&&this.getDataset().push(desiredToHide),await this.prefcountpipe(),this.nodesUpdate(desiredToHide)}if((null===(_e$target$closest3=e.target.closest("button"))||void 0===_e$target$closest3?void 0:_e$target$closest3.dataset.hider)===selectors_expand){var _e$target$closest4,_e$target$closest5,_e$target$closest6,_e$target$closest7;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest4=e.target.closest(selectors_colVal))||void 0===_e$target$closest4?void 0:_e$target$closest4.dataset.col:null===(_e$target$closest5=e.target.closest(selectors_itemVal))||void 0===_e$target$closest5?void 0:_e$target$closest5.dataset.itemid,idx=this.getDataset().indexOf(desiredToHide);this.getDataset().splice(idx,1),await this.prefcountpipe(),this.nodesUpdate(null===(_e$target$closest6=e.target.closest(selectors_colVal))||void 0===_e$target$closest6?void 0:_e$target$closest6.dataset.col),this.nodesUpdate(null===(_e$target$closest7=e.target.closest(selectors_colVal))||void 0===_e$target$closest7?void 0:_e$target$closest7.dataset.itemid)}}async keyHandler(e){if(super.keyHandler(e),"Tab"===e.key)e.target.closest(this.selectors.input)&&(e.preventDefault(),this.clearSearchButton.focus({preventScroll:!0}))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{if(this.getSearchTerm()===this.searchInput.value&&this.searchResultsVisible())return void window.console.warn("Search term matches input value - skipping");this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none");const pendingPromise=new _pending.default;await this.filterrenderpipe().then((()=>(pendingPromise.resolve(),!0)))}),300,{pending:!0}))}registerFormEvents(){const form=this.component.querySelector(selectors_formDropdown),events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);const selectall=form.querySelector('[data-action="selectall"]');events.forEach((event=>{const submitBtn=form.querySelector('[data-action="'.concat(selectors_formItems.save,'"'));form.addEventListener(event,(e=>{e.stopPropagation();const input=e.target.closest("input");if(input){selectall.checked&&!input.checked&&(selectall.checked=!1);const checkedCount=Array.from(form.querySelectorAll(selectors_formItems.checked)).length;submitBtn.disabled=checkedCount<=0}}),!1),this.searchInput.addEventListener(event,(e=>e.stopPropagation())),this.clearSearchButton.addEventListener(event,(async e=>{e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),selectall.addEventListener(event,(e=>{if(e.stopPropagation(),selectall.checked){Array.from(form.querySelectorAll(selectors_formItems.currentlyUnchecked)).forEach((item=>{item.checked=!0})),submitBtn.disabled=!1}else{Array.from(form.querySelectorAll(selectors_formItems.checked)).forEach((item=>{item.checked=!1})),submitBtn.disabled=!0}}))})),form.addEventListener("submit",(async e=>{if(e.preventDefault(),e.submitter.dataset.action===selectors_formItems.cancel)return void(0,_jquery.default)(this.component).dropdown("toggle");[...form.elements].filter((item=>item.checked)).forEach((item=>{const idx=this.getDataset().indexOf(item.dataset.collapse);this.getDataset().splice(idx,1),this.nodesUpdate(item.dataset.collapse)})),selectall.checked=!1,e.submitter.disabled=!0,await this.prefcountpipe()}))}nodesUpdate(item){const colNodesToHide=[...document.querySelectorAll('[data-col="'.concat(item,'"]'))],itemIDNodesToHide=[...document.querySelectorAll('[data-itemid="'.concat(item,'"]'))];this.nodes=[...colNodesToHide,...itemIDNodesToHide],this.updateDisplay()}async prefcountpipe(){this.setPreferences(),this.countUpdate(),await this.filterrenderpipe()}async filterDataset(filterableData){const stringUserMap=await this.fetchRequiredUserStrings(),stringGradeMap=await this.fetchRequiredGradeStrings(),customFieldMap=this.fetchCustomFieldValues();this.stringMap=new Map([...stringGradeMap,...stringUserMap,...customFieldMap]);const searching=filterableData.map((s=>{var _mapObj$itemname,_mapObj$category;const mapObj=this.stringMap.get(s);return void 0===mapObj?{key:s,string:s}:{key:s,string:null!==(_mapObj$itemname=mapObj.itemname)&&void 0!==_mapObj$itemname?_mapObj$itemname:this.stringMap.get(s),category:null!==(_mapObj$category=mapObj.category)&&void 0!==_mapObj$category?_mapObj$category:""}}));return""===this.getPreppedSearchTerm()?searching:searching.filter((col=>col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm())))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((column=>{var _column$string,_column$category;return{name:column.key,displayName:null!==(_column$string=column.string)&&void 0!==_column$string?_column$string:column.key,category:null!==(_column$category=column.category)&&void 0!==_column$category?_column$category:""}})))}updateDisplay(){this.nodes.forEach((element=>{const content=element.querySelector(selectors_content),sort=element.querySelector(selectors_sort),expandButton=element.querySelector(selectors_expandbutton),rangeRowCell=element.querySelector(selectors_rangerowcell),avgRowCell=element.querySelector(selectors_avgrowcell),nodeSet=[element.querySelector(selectors_menu),element.querySelector(selectors_icons),content];if(element.classList.contains("cell"))if(null!==sort&&(window.location=this.defaultSort),null===content){const rowCell=null!=avgRowCell?avgRowCell:rangeRowCell;null==rowCell||rowCell.classList.toggle("d-none"),null==rowCell||rowCell.setAttribute("aria-hidden",null!=rowCell&&rowCell.classList.contains("d-none")?"true":"false")}else content.classList.contains("d-none")?(element.classList.remove("collapsed"),content.childNodes.length>1&&content.classList.add("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.remove("d-none"),null==node||node.setAttribute("aria-hidden","false")})),null==expandButton||expandButton.classList.add("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","true")):(element.classList.add("collapsed"),content.classList.remove("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.add("d-none"),null==node||node.setAttribute("aria-hidden","true")})),null==expandButton||expandButton.classList.remove("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","false"))}))}countUpdate(){countIndicator.textContent=this.getDatasetSize(),this.getDatasetSize()>0?(this.component.parentElement.classList.add("d-flex"),this.component.parentElement.classList.remove("d-none")):(this.component.parentElement.classList.remove("d-flex"),this.component.parentElement.classList.add("d-none"))}async renderDefault(){this.setMatchedResults(await this.filterDataset(this.getDataset())),this.filterMatchDataset(),this.countUpdate();const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapsebody",{results:this.getMatchedResults(),userid:this.userID});(0,_templates.replaceNode)(selectors_placeholder,html,js),this.updateNodes(),this.registerFormEvents(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0})}))}async renderDropdown(){const selectall=this.component.querySelector(selectors_formDropdown).querySelector('[data-action="selectall"]'),{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapseresults",{results:this.getMatchedResults(),searchTerm:this.getSearchTerm()});selectall.disabled=0===this.getMatchedResults().length,(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchCustomFieldValues(){return[...document.querySelectorAll("[data-collapse-name]")].map((field=>[field.parentElement.dataset.col,field.dataset.collapseName]))}fetchRequiredUserStrings(){if(!this.userStrings){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.userStrings=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.userStrings}fetchRequiredGradeStrings(){return this.gradeStrings||(this.gradeStrings=Repository.gradeItems(this.courseID).then((result=>new Map(result.gradeItems.map((key=>[key.id,key])))))),this.gradeStrings}}return _exports.default=ColumnSearch,_exports.default}));
+define("gradereport_grader/collapse",["exports","gradereport_grader/collapse/repository","core/comboboxsearch/search_combobox","core/templates","core/utils","jquery","core/str","core/custom_interaction_events","core/localstorage","core/loadingicon","core/notification","core/pending"],(function(_exports,Repository,_search_combobox,_templates,_utils,_jquery,_str,_custom_interaction_events,_localstorage,_loadingicon,_notification,_pending){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 _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository),_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_localstorage=_interopRequireDefault(_localstorage),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const selectors_component=".collapse-columns",selectors_formDropdown=".columnsdropdownform",selectors_formItems={cancel:"cancel",save:"save",checked:'input[type="checkbox"]:checked',currentlyUnchecked:'input[type="checkbox"]:not([data-action="selectall"])'},selectors_hider="hide",selectors_expand="expand",selectors_colVal="[data-col]",selectors_itemVal="[data-itemid]",selectors_content='[data-collapse="content"]',selectors_sort='[data-collapse="sort"]',selectors_expandbutton='[data-collapse="expandbutton"]',selectors_rangerowcell='[data-collapse="rangerowcell"]',selectors_avgrowcell='[data-collapse="avgrowcell"]',selectors_menu='[data-collapse="menu"]',selectors_icons=".data-collapse_gradeicons",selectors_count='[data-collapse="count"]',selectors_placeholder='.collapsecolumndropdown [data-region="placeholder"]',selectors_fullDropdown=".collapsecolumndropdown",selectors_searchResultContainer=".searchresultitemscontainer",countIndicator=document.querySelector(selectors_count);class ColumnSearch extends _search_combobox.default{static init(userID,courseID,defaultSort){return new ColumnSearch(userID,courseID,defaultSort)}constructor(userID,courseID,defaultSort){super(),_defineProperty(this,"userID",-1),_defineProperty(this,"courseID",null),_defineProperty(this,"defaultSort",""),_defineProperty(this,"nodes",[]),_defineProperty(this,"gradeStrings",null),_defineProperty(this,"userStrings",null),_defineProperty(this,"stringMap",[]),this.userID=userID,this.courseID=courseID,this.defaultSort=defaultSort,this.component=document.querySelector(selectors_component);const pendingPromise=new _pending.default;(0,_loadingicon.addIconToContainer)(document.querySelector(".gradeparent")).then((loader=>{setTimeout((()=>{this.getDataset().forEach((item=>{this.nodesUpdate(item)})),this.renderDefault(),loader.remove(),document.querySelector(".gradereport-grader-table").classList.remove("d-none")}),10)})).then((()=>pendingPromise.resolve())).catch(_notification.default.exception),this.$component.on("hide.bs.dropdown",(()=>{this.component.querySelector(selectors_searchResultContainer).scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))}))}componentSelector(){return".collapse-columns"}dropdownSelector(){return".searchresultitemscontainer"}getDataset(){if(!this.dataset){const cols=this.fetchDataset();this.dataset=JSON.parse(cols)?JSON.parse(cols).split(","):[]}return this.datasetSize=this.dataset.length,this.dataset}fetchDataset(){return _localstorage.default.get("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID))}setPreferences(){_localstorage.default.set("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID),JSON.stringify(this.getDataset().join(",")))}registerClickHandlers(){this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",this.docClickHandler.bind(this))}clickHandler(e){super.clickHandler(e),e.target.closest(selectors_fullDropdown)&&e.stopPropagation()}async docClickHandler(e){var _e$target$closest3;if(e.target.dataset.hider===selectors_hider){var _e$target$closest,_e$target$closest2;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest=e.target.closest(selectors_colVal))||void 0===_e$target$closest?void 0:_e$target$closest.dataset.col:null===(_e$target$closest2=e.target.closest(selectors_itemVal))||void 0===_e$target$closest2?void 0:_e$target$closest2.dataset.itemid;-1===this.getDataset().indexOf(desiredToHide)&&this.getDataset().push(desiredToHide),await this.prefcountpipe(),this.nodesUpdate(desiredToHide)}if((null===(_e$target$closest3=e.target.closest("button"))||void 0===_e$target$closest3?void 0:_e$target$closest3.dataset.hider)===selectors_expand){var _e$target$closest4,_e$target$closest5,_e$target$closest6,_e$target$closest7;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest4=e.target.closest(selectors_colVal))||void 0===_e$target$closest4?void 0:_e$target$closest4.dataset.col:null===(_e$target$closest5=e.target.closest(selectors_itemVal))||void 0===_e$target$closest5?void 0:_e$target$closest5.dataset.itemid,idx=this.getDataset().indexOf(desiredToHide);this.getDataset().splice(idx,1),await this.prefcountpipe(),this.nodesUpdate(null===(_e$target$closest6=e.target.closest(selectors_colVal))||void 0===_e$target$closest6?void 0:_e$target$closest6.dataset.col),this.nodesUpdate(null===(_e$target$closest7=e.target.closest(selectors_colVal))||void 0===_e$target$closest7?void 0:_e$target$closest7.dataset.itemid)}}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{if(this.getSearchTerm()===this.searchInput.value&&this.searchResultsVisible())return void window.console.warn("Search term matches input value - skipping");this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none");const pendingPromise=new _pending.default;await this.filterrenderpipe().then((()=>(pendingPromise.resolve(),!0)))}),300,{pending:!0}))}registerFormEvents(){const form=this.component.querySelector(selectors_formDropdown),events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);const selectall=form.querySelector('[data-action="selectall"]');events.forEach((event=>{const submitBtn=form.querySelector('[data-action="'.concat(selectors_formItems.save,'"'));form.addEventListener(event,(e=>{e.stopPropagation();const input=e.target.closest("input");if(input){selectall.checked&&!input.checked&&(selectall.checked=!1);const checkedCount=Array.from(form.querySelectorAll(selectors_formItems.checked)).length;submitBtn.disabled=checkedCount<=0}}),!1),this.searchInput.addEventListener(event,(e=>e.stopPropagation())),this.clearSearchButton.addEventListener(event,(async e=>{e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),selectall.addEventListener(event,(e=>{if(e.stopPropagation(),selectall.checked){Array.from(form.querySelectorAll(selectors_formItems.currentlyUnchecked)).forEach((item=>{item.checked=!0})),submitBtn.disabled=!1}else{Array.from(form.querySelectorAll(selectors_formItems.checked)).forEach((item=>{item.checked=!1})),submitBtn.disabled=!0}}))})),form.addEventListener("submit",(async e=>{if(e.preventDefault(),e.submitter.dataset.action===selectors_formItems.cancel)return void(0,_jquery.default)(this.component).dropdown("toggle");[...form.elements].filter((item=>item.checked)).forEach((item=>{const idx=this.getDataset().indexOf(item.dataset.collapse);this.getDataset().splice(idx,1),this.nodesUpdate(item.dataset.collapse)})),selectall.checked=!1,e.submitter.disabled=!0,await this.prefcountpipe()}))}nodesUpdate(item){const colNodesToHide=[...document.querySelectorAll('[data-col="'.concat(item,'"]'))],itemIDNodesToHide=[...document.querySelectorAll('[data-itemid="'.concat(item,'"]'))];this.nodes=[...colNodesToHide,...itemIDNodesToHide],this.updateDisplay()}async prefcountpipe(){this.setPreferences(),this.countUpdate(),await this.filterrenderpipe()}async filterDataset(filterableData){const stringUserMap=await this.fetchRequiredUserStrings(),stringGradeMap=await this.fetchRequiredGradeStrings(),customFieldMap=this.fetchCustomFieldValues();this.stringMap=new Map([...stringGradeMap,...stringUserMap,...customFieldMap]);const searching=filterableData.map((s=>{var _mapObj$itemname,_mapObj$category;const mapObj=this.stringMap.get(s);return void 0===mapObj?{key:s,string:s}:{key:s,string:null!==(_mapObj$itemname=mapObj.itemname)&&void 0!==_mapObj$itemname?_mapObj$itemname:this.stringMap.get(s),category:null!==(_mapObj$category=mapObj.category)&&void 0!==_mapObj$category?_mapObj$category:""}}));return""===this.getPreppedSearchTerm()?searching:searching.filter((col=>col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm())))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((column=>{var _column$string,_column$category;return{name:column.key,displayName:null!==(_column$string=column.string)&&void 0!==_column$string?_column$string:column.key,category:null!==(_column$category=column.category)&&void 0!==_column$category?_column$category:""}})))}updateDisplay(){this.nodes.forEach((element=>{const content=element.querySelector(selectors_content),sort=element.querySelector(selectors_sort),expandButton=element.querySelector(selectors_expandbutton),rangeRowCell=element.querySelector(selectors_rangerowcell),avgRowCell=element.querySelector(selectors_avgrowcell),nodeSet=[element.querySelector(selectors_menu),element.querySelector(selectors_icons),content];if(element.classList.contains("cell"))if(null!==sort&&(window.location=this.defaultSort),null===content){const rowCell=null!=avgRowCell?avgRowCell:rangeRowCell;null==rowCell||rowCell.classList.toggle("d-none"),null==rowCell||rowCell.setAttribute("aria-hidden",null!=rowCell&&rowCell.classList.contains("d-none")?"true":"false")}else content.classList.contains("d-none")?(element.classList.remove("collapsed"),content.childNodes.length>1&&content.classList.add("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.remove("d-none"),null==node||node.setAttribute("aria-hidden","false")})),null==expandButton||expandButton.classList.add("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","true")):(element.classList.add("collapsed"),content.classList.remove("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.add("d-none"),null==node||node.setAttribute("aria-hidden","true")})),null==expandButton||expandButton.classList.remove("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","false"))}))}countUpdate(){countIndicator.textContent=this.getDatasetSize(),this.getDatasetSize()>0?(this.component.parentElement.classList.add("d-flex"),this.component.parentElement.classList.remove("d-none")):(this.component.parentElement.classList.remove("d-flex"),this.component.parentElement.classList.add("d-none"))}async renderDefault(){this.setMatchedResults(await this.filterDataset(this.getDataset())),this.filterMatchDataset(),this.countUpdate();const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapsebody",{instance:this.instance,results:this.getMatchedResults(),userid:this.userID});(0,_templates.replaceNode)(selectors_placeholder,html,js),this.updateNodes(),this.registerFormEvents(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0}),this.selectallEnable()}))}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapseresults",{instance:this.instance,results:this.getMatchedResults(),searchTerm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.selectallEnable();this.component.querySelector(selectors_formDropdown).querySelector('[data-action="'.concat(selectors_formItems.save,'"')).disabled=!0}selectallEnable(){this.component.querySelector(selectors_formDropdown).querySelector('[data-action="selectall"]').disabled=0===this.getMatchedResults().length}fetchCustomFieldValues(){return[...document.querySelectorAll("[data-collapse-name]")].map((field=>[field.parentElement.dataset.col,field.dataset.collapseName]))}fetchRequiredUserStrings(){if(!this.userStrings){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.userStrings=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.userStrings}fetchRequiredGradeStrings(){return this.gradeStrings||(this.gradeStrings=Repository.gradeItems(this.courseID).then((result=>new Map(result.gradeItems.map((key=>[key.id,key])))))),this.gradeStrings}}return _exports.default=ColumnSearch,_exports.default}));
//# sourceMappingURL=collapse.min.js.map
\ No newline at end of file
diff --git a/grade/report/grader/amd/build/collapse.min.js.map b/grade/report/grader/amd/build/collapse.min.js.map
index 877f26953a4..0530684ac1c 100644
--- a/grade/report/grader/amd/build/collapse.min.js.map
+++ b/grade/report/grader/amd/build/collapse.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"collapse.min.js","sources":["../src/collapse.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 * Allow the user to show and hide columns of the report at will.\n *\n * @module gradereport_grader/collapse\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Repository from 'gradereport_grader/collapse/repository';\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport $ from 'jquery';\nimport {getStrings} from 'core/str';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport storage from 'core/localstorage';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n component: '.collapse-columns',\n formDropdown: '.columnsdropdownform',\n formItems: {\n cancel: 'cancel',\n save: 'save',\n checked: 'input[type=\"checkbox\"]:checked',\n currentlyUnchecked: 'input[type=\"checkbox\"]:not([data-action=\"selectall\"])',\n },\n hider: 'hide',\n expand: 'expand',\n colVal: '[data-col]',\n itemVal: '[data-itemid]',\n content: '[data-collapse=\"content\"]',\n sort: '[data-collapse=\"sort\"]',\n expandbutton: '[data-collapse=\"expandbutton\"]',\n rangerowcell: '[data-collapse=\"rangerowcell\"]',\n avgrowcell: '[data-collapse=\"avgrowcell\"]',\n menu: '[data-collapse=\"menu\"]',\n icons: '.data-collapse_gradeicons',\n count: '[data-collapse=\"count\"]',\n placeholder: '.collapsecolumndropdown [data-region=\"placeholder\"]',\n fullDropdown: '.collapsecolumndropdown',\n};\n\nconst countIndicator = document.querySelector(selectors.count);\n\nexport default class ColumnSearch extends search_combobox {\n\n userID = -1;\n courseID = null;\n defaultSort = '';\n\n nodes = [];\n\n gradeStrings = null;\n userStrings = null;\n stringMap = [];\n\n static init(userID, courseID, defaultSort) {\n return new ColumnSearch(userID, courseID, defaultSort);\n }\n\n constructor(userID, courseID, defaultSort) {\n super();\n this.userID = userID;\n this.courseID = courseID;\n this.defaultSort = defaultSort;\n this.component = document.querySelector(selectors.component);\n\n const pendingPromise = new Pending();\n // Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user).\n addIconToContainer(document.querySelector('.gradeparent')).then((loader) => {\n setTimeout(() => {\n // Get the users' checked columns to change.\n this.getDataset().forEach((item) => {\n this.nodesUpdate(item);\n });\n this.renderDefault();\n\n // Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.\n loader.remove();\n document.querySelector('.gradereport-grader-table').classList.remove('d-none');\n }, 10);\n }).then(() => pendingPromise.resolve()).catch(Notification.exception);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.collapse-columns';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.searchresultitemscontainer';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.collapsecolumn';\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Array}\n */\n getDataset() {\n if (!this.dataset) {\n const cols = this.fetchDataset();\n this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : [];\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {string}\n */\n fetchDataset() {\n return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`);\n }\n\n /**\n * Given a user performs an action, update the users' preferences.\n */\n setPreferences() {\n storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`,\n JSON.stringify(this.getDataset().join(','))\n );\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\n document.addEventListener('click', this.docClickHandler.bind(this));\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e);\n // Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form.\n if (e.target.closest(selectors.fullDropdown)) {\n e.stopPropagation();\n }\n }\n\n /**\n * Externally defined click function to improve memory handling.\n *\n * @param {MouseEvent} e\n * @returns {Promise}\n */\n async docClickHandler(e) {\n if (e.target.dataset.hider === selectors.hider) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n if (idx === -1) {\n this.getDataset().push(desiredToHide);\n }\n await this.prefcountpipe();\n\n this.nodesUpdate(desiredToHide);\n }\n\n if (e.target.closest('button')?.dataset.hider === selectors.expand) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n this.getDataset().splice(idx, 1);\n\n await this.prefcountpipe();\n\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n async keyHandler(e) {\n super.keyHandler(e);\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Tab':\n if (e.target.closest(this.selectors.input)) {\n e.preventDefault();\n this.clearSearchButton.focus({preventScroll: true});\n }\n break;\n }\n }\n\n /**\n * Handle any keyboard inputs.\n */\n registerInputEvents() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n if (this.getSearchTerm() === this.searchInput.value && this.searchResultsVisible()) {\n window.console.warn(`Search term matches input value - skipping`);\n // Debounce can happen multiple times quickly.\n return;\n }\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.searchInput.value === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n const pendingPromise = new Pending();\n // User has given something for us to filter against.\n await this.filterrenderpipe().then(() => {\n pendingPromise.resolve();\n return true;\n });\n }, 300, {pending: true}));\n }\n\n /**\n * Handle the form submission within the dropdown.\n */\n registerFormEvents() {\n const form = this.component.querySelector(selectors.formDropdown);\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n const selectall = form.querySelector('[data-action=\"selectall\"]');\n\n // Register clicks & keyboard form handling.\n events.forEach((event) => {\n const submitBtn = form.querySelector(`[data-action=\"${selectors.formItems.save}\"`);\n form.addEventListener(event, (e) => {\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n const input = e.target.closest('input');\n if (input) {\n // If the user is unchecking an item, we need to uncheck the select all if it's checked.\n if (selectall.checked && !input.checked) {\n selectall.checked = false;\n }\n const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length;\n // Check if any are clicked or not then change disabled.\n submitBtn.disabled = checkedCount <= 0;\n }\n }, false);\n\n // Stop Bootstrap from being clever.\n this.searchInput.addEventListener(event, e => e.stopPropagation());\n this.clearSearchButton.addEventListener(event, async(e) => {\n e.stopPropagation();\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n await this.filterrenderpipe();\n });\n selectall.addEventListener(event, (e) => {\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n if (!selectall.checked) {\n const touncheck = Array.from(form.querySelectorAll(selectors.formItems.checked));\n touncheck.forEach(item => {\n item.checked = false;\n });\n submitBtn.disabled = true;\n } else {\n const currentUnchecked = Array.from(form.querySelectorAll(selectors.formItems.currentlyUnchecked));\n currentUnchecked.forEach(item => {\n item.checked = true;\n });\n submitBtn.disabled = false;\n }\n });\n });\n\n form.addEventListener('submit', async(e) => {\n e.preventDefault();\n if (e.submitter.dataset.action === selectors.formItems.cancel) {\n $(this.component).dropdown('toggle');\n return;\n }\n // Get the users' checked columns to change.\n const checkedItems = [...form.elements].filter(item => item.checked);\n checkedItems.forEach((item) => {\n const idx = this.getDataset().indexOf(item.dataset.collapse);\n this.getDataset().splice(idx, 1);\n this.nodesUpdate(item.dataset.collapse);\n });\n // Reset the check all & submit to false just in case.\n selectall.checked = false;\n e.submitter.disabled = true;\n await this.prefcountpipe();\n });\n }\n\n nodesUpdate(item) {\n const colNodesToHide = [...document.querySelectorAll(`[data-col=\"${item}\"]`)];\n const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid=\"${item}\"]`)];\n this.nodes = [...colNodesToHide, ...itemIDNodesToHide];\n this.updateDisplay();\n }\n\n /**\n * Update the user preferences, count display then render the results.\n *\n * @returns {Promise}\n */\n async prefcountpipe() {\n this.setPreferences();\n this.countUpdate();\n await this.filterrenderpipe();\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} An array of objects containing the system reference and the user readable value.\n */\n async filterDataset(filterableData) {\n const stringUserMap = await this.fetchRequiredUserStrings();\n const stringGradeMap = await this.fetchRequiredGradeStrings();\n // Custom user profile fields are not in our string map and need a bit of extra love.\n const customFieldMap = this.fetchCustomFieldValues();\n this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]);\n\n const searching = filterableData.map(s => {\n const mapObj = this.stringMap.get(s);\n if (mapObj === undefined) {\n return {key: s, string: s};\n }\n return {\n key: s,\n string: mapObj.itemname ?? this.stringMap.get(s),\n category: mapObj.category ?? '',\n };\n });\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return searching;\n }\n // Other times we want to actually filter the content.\n return searching.filter((col) => {\n return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm());\n });\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((column) => {\n return {\n name: column.key,\n displayName: column.string ?? column.key,\n category: column.category ?? '',\n };\n })\n );\n }\n\n /**\n * With an array of nodes, switch their classes and values.\n */\n updateDisplay() {\n this.nodes.forEach((element) => {\n const content = element.querySelector(selectors.content);\n const sort = element.querySelector(selectors.sort);\n const expandButton = element.querySelector(selectors.expandbutton);\n const rangeRowCell = element.querySelector(selectors.rangerowcell);\n const avgRowCell = element.querySelector(selectors.avgrowcell);\n const nodeSet = [\n element.querySelector(selectors.menu),\n element.querySelector(selectors.icons),\n content\n ];\n\n // This can be further improved to reduce redundant similar calls.\n if (element.classList.contains('cell')) {\n // The column is actively being sorted, lets reset that and reload the page.\n if (sort !== null) {\n window.location = this.defaultSort;\n }\n if (content === null) {\n // If it's not a content cell, it must be an overall average or a range cell.\n const rowCell = avgRowCell ?? rangeRowCell;\n\n rowCell?.classList.toggle('d-none');\n rowCell?.setAttribute('aria-hidden',\n rowCell?.classList.contains('d-none') ? 'true' : 'false');\n } else if (content.classList.contains('d-none')) {\n // We should always have content but some cells do not contain menus or other actions.\n element.classList.remove('collapsed');\n // If there are many nodes, apply the following.\n if (content.childNodes.length > 1) {\n content.classList.add('d-flex');\n }\n nodeSet.forEach(node => {\n node?.classList.remove('d-none');\n node?.setAttribute('aria-hidden', 'false');\n });\n expandButton?.classList.add('d-none');\n expandButton?.setAttribute('aria-hidden', 'true');\n } else {\n element.classList.add('collapsed');\n content.classList.remove('d-flex');\n nodeSet.forEach(node => {\n node?.classList.add('d-none');\n node?.setAttribute('aria-hidden', 'true');\n });\n expandButton?.classList.remove('d-none');\n expandButton?.setAttribute('aria-hidden', 'false');\n }\n }\n });\n }\n\n /**\n * Update the visual count of collapsed columns or hide the count all together.\n */\n countUpdate() {\n countIndicator.textContent = this.getDatasetSize();\n if (this.getDatasetSize() > 0) {\n this.component.parentElement.classList.add('d-flex');\n this.component.parentElement.classList.remove('d-none');\n } else {\n this.component.parentElement.classList.remove('d-flex');\n this.component.parentElement.classList.add('d-none');\n }\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(this.getDataset()));\n this.filterMatchDataset();\n\n // Update the collapsed button pill.\n this.countUpdate();\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {\n 'results': this.getMatchedResults(),\n 'userid': this.userID,\n });\n replaceNode(selectors.placeholder, html, js);\n this.updateNodes();\n\n // Given we now have the body, we can set up more triggers.\n this.registerFormEvents();\n this.registerInputEvents();\n\n // Add a small BS listener so that we can set the focus correctly on open.\n this.$component.on('shown.bs.dropdown', () => {\n this.searchInput.focus({preventScroll: true});\n });\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const form = this.component.querySelector(selectors.formDropdown);\n const selectall = form.querySelector('[data-action=\"selectall\"]');\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {\n 'results': this.getMatchedResults(),\n 'searchTerm': this.getSearchTerm(),\n });\n selectall.disabled = this.getMatchedResults().length === 0;\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * If we have any custom user profile fields, grab their system & readable names to add to our string map.\n *\n * @returns {array} An array of associated string arrays ready for our map.\n */\n fetchCustomFieldValues() {\n const customFields = document.querySelectorAll('[data-collapse-name]');\n // Cast from NodeList to array to grab all the values.\n return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredUserStrings() {\n if (!this.userStrings) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.userStrings = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.userStrings;\n }\n\n /**\n * Given the set of gradable items we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredGradeStrings() {\n if (!this.gradeStrings) {\n this.gradeStrings = Repository.gradeItems(this.courseID)\n .then((result) => new Map(\n result.gradeItems.map(key => ([key.id, key]))\n ));\n }\n return this.gradeStrings;\n }\n}\n"],"names":["selectors","cancel","save","checked","currentlyUnchecked","countIndicator","document","querySelector","ColumnSearch","search_combobox","userID","courseID","defaultSort","constructor","component","pendingPromise","Pending","then","loader","setTimeout","getDataset","forEach","item","nodesUpdate","renderDefault","remove","classList","resolve","catch","Notification","exception","componentSelector","dropdownSelector","triggerSelector","this","dataset","cols","fetchDataset","JSON","parse","split","datasetSize","length","storage","get","setPreferences","set","stringify","join","registerClickHandlers","addEventListener","clickHandler","bind","docClickHandler","e","target","closest","stopPropagation","hider","preventDefault","desiredToHide","_e$target$closest","col","_e$target$closest2","itemid","indexOf","push","prefcountpipe","_e$target$closest4","_e$target$closest5","idx","splice","_e$target$closest6","_e$target$closest7","keyHandler","key","input","clearSearchButton","focus","preventScroll","registerInputEvents","searchInput","async","getSearchTerm","value","searchResultsVisible","window","console","warn","setSearchTerms","add","filterrenderpipe","pending","registerFormEvents","form","events","CustomEvents","activate","keyboardActivate","define","selectall","event","submitBtn","checkedCount","Array","from","querySelectorAll","disabled","submitter","action","dropdown","elements","filter","collapse","colNodesToHide","itemIDNodesToHide","nodes","updateDisplay","countUpdate","filterableData","stringUserMap","fetchRequiredUserStrings","stringGradeMap","fetchRequiredGradeStrings","customFieldMap","fetchCustomFieldValues","stringMap","Map","searching","map","s","mapObj","undefined","string","itemname","category","getPreppedSearchTerm","toString","toLowerCase","includes","filterMatchDataset","setMatchedResults","getMatchedResults","column","name","displayName","element","content","sort","expandButton","rangeRowCell","avgRowCell","nodeSet","contains","location","rowCell","toggle","setAttribute","childNodes","node","textContent","getDatasetSize","parentElement","filterDataset","html","js","updateNodes","$component","on","getHTMLElements","searchDropdown","field","collapseName","userStrings","requiredStrings","stringArray","index","gradeStrings","Repository","gradeItems","result","id"],"mappings":"8/DAmCMA,oBACS,oBADTA,uBAEY,uBAFZA,oBAGS,CACPC,OAAQ,SACRC,KAAM,OACNC,QAAS,iCACTC,mBAAoB,yDAPtBJ,gBASK,OATLA,iBAUM,SAVNA,iBAWM,aAXNA,kBAYO,gBAZPA,kBAaO,4BAbPA,eAcI,yBAdJA,uBAeY,iCAfZA,uBAgBY,iCAhBZA,qBAiBU,+BAjBVA,eAkBI,yBAlBJA,gBAmBK,4BAnBLA,gBAoBK,0BApBLA,sBAqBW,sDArBXA,uBAsBY,0BAGZK,eAAiBC,SAASC,cAAcP,uBAEzBQ,qBAAqBC,qCAY1BC,OAAQC,SAAUC,oBACnB,IAAIJ,aAAaE,OAAQC,SAAUC,aAG9CC,YAAYH,OAAQC,SAAUC,oDAdpB,mCACC,yCACG,iCAEN,wCAEO,yCACD,uCACF,SAQHF,OAASA,YACTC,SAAWA,cACXC,YAAcA,iBACdE,UAAYR,SAASC,cAAcP,2BAElCe,eAAiB,IAAIC,qDAERV,SAASC,cAAc,iBAAiBU,MAAMC,SAC7DC,YAAW,UAEFC,aAAaC,SAASC,YAClBC,YAAYD,cAEhBE,gBAGLN,OAAOO,SACPnB,SAASC,cAAc,6BAA6BmB,UAAUD,OAAO,YACtE,OACJR,MAAK,IAAMF,eAAeY,YAAWC,MAAMC,sBAAaC,WAQ/DC,0BACW,oBAQXC,yBACW,8BAQXC,wBACW,kBAQXb,iBACSc,KAAKC,QAAS,OACTC,KAAOF,KAAKG,oBACbF,QAAUG,KAAKC,MAAMH,MAAQE,KAAKC,MAAMH,MAAMI,MAAM,KAAO,eAE/DC,YAAcP,KAAKC,QAAQO,OACzBR,KAAKC,QAQhBE,sBACWM,sBAAQC,gDAAyCV,KAAKvB,qBAAYuB,KAAKxB,SAMlFmC,uCACYC,gDAAyCZ,KAAKvB,qBAAYuB,KAAKxB,QACnE4B,KAAKS,UAAUb,KAAKd,aAAa4B,KAAK,OAO9CC,6BAESnC,UAAUoC,iBAAiB,QAAShB,KAAKiB,aAAaC,KAAKlB,OAEhE5B,SAAS4C,iBAAiB,QAAShB,KAAKmB,gBAAgBD,KAAKlB,OAQjEiB,aAAaG,SACHH,aAAaG,GAEfA,EAAEC,OAAOC,QAAQxD,yBACjBsD,EAAEG,wCAUYH,6BACdA,EAAEC,OAAOpB,QAAQuB,QAAU1D,gBAAiB,0CAC5CsD,EAAEK,uBACIC,cAAgBN,EAAEC,OAAOC,QAAQxD,4CACnCsD,EAAEC,OAAOC,QAAQxD,sDAAjB6D,kBAAoC1B,QAAQ2B,+BAC5CR,EAAEC,OAAOC,QAAQxD,wDAAjB+D,mBAAqC5B,QAAQ6B,QAEpC,IADD9B,KAAKd,aAAa6C,QAAQL,qBAE7BxC,aAAa8C,KAAKN,qBAErB1B,KAAKiC,qBAEN5C,YAAYqC,8CAGjBN,EAAEC,OAAOC,QAAQ,kEAAWrB,QAAQuB,SAAU1D,iBAAkB,iFAChEsD,EAAEK,uBACIC,cAAgBN,EAAEC,OAAOC,QAAQxD,6CACnCsD,EAAEC,OAAOC,QAAQxD,uDAAjBoE,mBAAoCjC,QAAQ2B,+BAC5CR,EAAEC,OAAOC,QAAQxD,wDAAjBqE,mBAAqClC,QAAQ6B,OAC3CM,IAAMpC,KAAKd,aAAa6C,QAAQL,oBACjCxC,aAAamD,OAAOD,IAAK,SAExBpC,KAAKiC,qBAEN5C,uCAAY+B,EAAEC,OAAOC,QAAQxD,uDAAjBwE,mBAAoCrC,QAAQ2B,UACxDvC,uCAAY+B,EAAEC,OAAOC,QAAQxD,uDAAjByE,mBAAoCtC,QAAQ6B,0BASpDV,YACPoB,WAAWpB,GAIR,QADDA,EAAEqB,IAEErB,EAAEC,OAAOC,QAAQtB,KAAKlC,UAAU4E,SAChCtB,EAAEK,sBACGkB,kBAAkBC,MAAM,CAACC,eAAe,KAS7DC,2BAESC,YAAY/B,iBAAiB,SAAS,oBAASgC,aAC5ChD,KAAKiD,kBAAoBjD,KAAK+C,YAAYG,OAASlD,KAAKmD,mCACxDC,OAAOC,QAAQC,wDAIdC,eAAevD,KAAK+C,YAAYG,OAEN,KAA3BlD,KAAK+C,YAAYG,WAEZP,kBAAkBnD,UAAUgE,IAAI,eAGhCb,kBAAkBnD,UAAUD,OAAO,gBAEtCV,eAAiB,IAAIC,uBAErBkB,KAAKyD,mBAAmB1E,MAAK,KAC/BF,eAAeY,WACR,OAEZ,IAAK,CAACiE,SAAS,KAMtBC,2BACUC,KAAO5D,KAAKpB,UAAUP,cAAcP,wBACpC+F,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAO7F,SAAUyF,cAExBK,UAAYN,KAAKvF,cAAc,6BAGrCwF,OAAO1E,SAASgF,cACNC,UAAYR,KAAKvF,sCAA+BP,oBAAoBE,WAC1E4F,KAAK5C,iBAAiBmD,OAAQ/C,IAE1BA,EAAEG,wBACImB,MAAQtB,EAAEC,OAAOC,QAAQ,YAC3BoB,MAAO,CAEHwB,UAAUjG,UAAYyE,MAAMzE,UAC5BiG,UAAUjG,SAAU,SAElBoG,aAAeC,MAAMC,KAAKX,KAAKY,iBAAiB1G,oBAAoBG,UAAUuC,OAEpF4D,UAAUK,SAAWJ,cAAgB,MAE1C,QAGEtB,YAAY/B,iBAAiBmD,OAAO/C,GAAKA,EAAEG,yBAC3CoB,kBAAkB3B,iBAAiBmD,OAAOnB,MAAAA,IAC3C5B,EAAEG,uBACGwB,YAAYG,MAAQ,QACpBK,eAAevD,KAAK+C,YAAYG,aAC/BlD,KAAKyD,sBAEfS,UAAUlD,iBAAiBmD,OAAQ/C,OAE/BA,EAAEG,kBACG2C,UAAUjG,QAMR,CACsBqG,MAAMC,KAAKX,KAAKY,iBAAiB1G,oBAAoBI,qBAC7DiB,SAAQC,OACrBA,KAAKnB,SAAU,KAEnBmG,UAAUK,UAAW,MAXD,CACFH,MAAMC,KAAKX,KAAKY,iBAAiB1G,oBAAoBG,UAC7DkB,SAAQC,OACdA,KAAKnB,SAAU,KAEnBmG,UAAUK,UAAW,SAWjCb,KAAK5C,iBAAiB,UAAUgC,MAAAA,OAC5B5B,EAAEK,iBACEL,EAAEsD,UAAUzE,QAAQ0E,SAAW7G,oBAAoBC,sCACjDiC,KAAKpB,WAAWgG,SAAS,UAIV,IAAIhB,KAAKiB,UAAUC,QAAO1F,MAAQA,KAAKnB,UAC/CkB,SAASC,aACZgD,IAAMpC,KAAKd,aAAa6C,QAAQ3C,KAAKa,QAAQ8E,eAC9C7F,aAAamD,OAAOD,IAAK,QACzB/C,YAAYD,KAAKa,QAAQ8E,aAGlCb,UAAUjG,SAAU,EACpBmD,EAAEsD,UAAUD,UAAW,QACjBzE,KAAKiC,mBAInB5C,YAAYD,YACF4F,eAAiB,IAAI5G,SAASoG,sCAA+BpF,aAC7D6F,kBAAoB,IAAI7G,SAASoG,yCAAkCpF,kBACpE8F,MAAQ,IAAIF,kBAAmBC,wBAC/BE,2CASAxE,sBACAyE,oBACCpF,KAAKyD,uCASK4B,sBACVC,oBAAsBtF,KAAKuF,2BAC3BC,qBAAuBxF,KAAKyF,4BAE5BC,eAAiB1F,KAAK2F,8BACvBC,UAAY,IAAIC,IAAI,IAAIL,kBAAmBF,iBAAkBI,uBAE5DI,UAAYT,eAAeU,KAAIC,gDAC3BC,OAASjG,KAAK4F,UAAUlF,IAAIsF,eACnBE,IAAXD,OACO,CAACxD,IAAKuD,EAAGG,OAAQH,GAErB,CACHvD,IAAKuD,EACLG,gCAAQF,OAAOG,sDAAYpG,KAAK4F,UAAUlF,IAAIsF,GAC9CK,kCAAUJ,OAAOI,sDAAY,aAID,KAAhCrG,KAAKsG,uBACER,UAGJA,UAAUhB,QAAQlD,KACdA,IAAIuE,OAAOI,WAAWC,cAAcC,SAASzG,KAAKsG,0BAOjEI,0BACSC,kBACD3G,KAAK4G,oBAAoBb,KAAKc,mDACnB,CACHC,KAAMD,OAAOpE,IACbsE,mCAAaF,OAAOV,gDAAUU,OAAOpE,IACrC4D,kCAAUQ,OAAOR,sDAAY,QAS7ClB,qBACSD,MAAM/F,SAAS6H,gBACVC,QAAUD,QAAQ3I,cAAcP,mBAChCoJ,KAAOF,QAAQ3I,cAAcP,gBAC7BqJ,aAAeH,QAAQ3I,cAAcP,wBACrCsJ,aAAeJ,QAAQ3I,cAAcP,wBACrCuJ,WAAaL,QAAQ3I,cAAcP,sBACnCwJ,QAAU,CACZN,QAAQ3I,cAAcP,gBACtBkJ,QAAQ3I,cAAcP,iBACtBmJ,YAIAD,QAAQxH,UAAU+H,SAAS,WAEd,OAATL,OACA9D,OAAOoE,SAAWxH,KAAKtB,aAEX,OAAZuI,QAAkB,OAEZQ,QAAUJ,MAAAA,WAAAA,WAAcD,aAE9BK,MAAAA,SAAAA,QAASjI,UAAUkI,OAAO,UAC1BD,MAAAA,SAAAA,QAASE,aAAa,cAClBF,MAAAA,SAAAA,QAASjI,UAAU+H,SAAS,UAAY,OAAS,cAC9CN,QAAQzH,UAAU+H,SAAS,WAElCP,QAAQxH,UAAUD,OAAO,aAErB0H,QAAQW,WAAWpH,OAAS,GAC5ByG,QAAQzH,UAAUgE,IAAI,UAE1B8D,QAAQnI,SAAQ0I,OACZA,MAAAA,MAAAA,KAAMrI,UAAUD,OAAO,UACvBsI,MAAAA,MAAAA,KAAMF,aAAa,cAAe,YAEtCR,MAAAA,cAAAA,aAAc3H,UAAUgE,IAAI,UAC5B2D,MAAAA,cAAAA,aAAcQ,aAAa,cAAe,UAE1CX,QAAQxH,UAAUgE,IAAI,aACtByD,QAAQzH,UAAUD,OAAO,UACzB+H,QAAQnI,SAAQ0I,OACZA,MAAAA,MAAAA,KAAMrI,UAAUgE,IAAI,UACpBqE,MAAAA,MAAAA,KAAMF,aAAa,cAAe,WAEtCR,MAAAA,cAAAA,aAAc3H,UAAUD,OAAO,UAC/B4H,MAAAA,cAAAA,aAAcQ,aAAa,cAAe,aAS1DvC,cACIjH,eAAe2J,YAAc9H,KAAK+H,iBAC9B/H,KAAK+H,iBAAmB,QACnBnJ,UAAUoJ,cAAcxI,UAAUgE,IAAI,eACtC5E,UAAUoJ,cAAcxI,UAAUD,OAAO,iBAEzCX,UAAUoJ,cAAcxI,UAAUD,OAAO,eACzCX,UAAUoJ,cAAcxI,UAAUgE,IAAI,sCAQ1CmD,wBAAwB3G,KAAKiI,cAAcjI,KAAKd,oBAChDwH,0BAGAtB,oBACC8C,KAACA,KAADC,GAAOA,UAAY,+BAAiB,2CAA4C,SACvEnI,KAAK4G,2BACN5G,KAAKxB,oCAEPV,sBAAuBoK,KAAMC,SACpCC,mBAGAzE,0BACAb,2BAGAuF,WAAWC,GAAG,qBAAqB,UAC/BvF,YAAYH,MAAM,CAACC,eAAe,oCASrCqB,UADOlE,KAAKpB,UAAUP,cAAcP,wBACnBO,cAAc,8BAC/B6J,KAACA,KAADC,GAAOA,UAAY,+BAAiB,8CAA+C,SAC1EnI,KAAK4G,+BACF5G,KAAKiD,kBAEvBiB,UAAUO,SAA+C,IAApCzE,KAAK4G,oBAAoBpG,0CAC1BR,KAAKuI,kBAAkBC,eAAgBN,KAAMC,IAQrExC,+BAGW,IAFcvH,SAASoG,iBAAiB,yBAEtBuB,KAAI0C,OAAS,CAACA,MAAMT,cAAc/H,QAAQ2B,IAAK6G,MAAMxI,QAAQyI,gBAS1FnD,+BACSvF,KAAK2I,YAAa,OACbC,gBAAkB,CACpB,WACA,YACA,WACA,QACA,OACA,UACA,aACA,cACA,WACA,SACA,eAECD,aAAc,mBAAWC,gBAAgB7C,KAAKtD,OAAUA,IAAAA,SACxD1D,MAAM8J,aAAgB,IAAIhD,IACvB+C,gBAAgB7C,KAAI,CAACtD,IAAKqG,QAAW,CAACrG,IAAKoG,YAAYC,oBAG5D9I,KAAK2I,YAShBlD,mCACSzF,KAAK+I,oBACDA,aAAeC,WAAWC,WAAWjJ,KAAKvB,UAC1CM,MAAMmK,QAAW,IAAIrD,IAClBqD,OAAOD,WAAWlD,KAAItD,KAAQ,CAACA,IAAI0G,GAAI1G,WAG5CzC,KAAK+I"}
\ No newline at end of file
+{"version":3,"file":"collapse.min.js","sources":["../src/collapse.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 * Allow the user to show and hide columns of the report at will.\n *\n * @module gradereport_grader/collapse\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Repository from 'gradereport_grader/collapse/repository';\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport $ from 'jquery';\nimport {getStrings} from 'core/str';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport storage from 'core/localstorage';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n component: '.collapse-columns',\n formDropdown: '.columnsdropdownform',\n formItems: {\n cancel: 'cancel',\n save: 'save',\n checked: 'input[type=\"checkbox\"]:checked',\n currentlyUnchecked: 'input[type=\"checkbox\"]:not([data-action=\"selectall\"])',\n },\n hider: 'hide',\n expand: 'expand',\n colVal: '[data-col]',\n itemVal: '[data-itemid]',\n content: '[data-collapse=\"content\"]',\n sort: '[data-collapse=\"sort\"]',\n expandbutton: '[data-collapse=\"expandbutton\"]',\n rangerowcell: '[data-collapse=\"rangerowcell\"]',\n avgrowcell: '[data-collapse=\"avgrowcell\"]',\n menu: '[data-collapse=\"menu\"]',\n icons: '.data-collapse_gradeicons',\n count: '[data-collapse=\"count\"]',\n placeholder: '.collapsecolumndropdown [data-region=\"placeholder\"]',\n fullDropdown: '.collapsecolumndropdown',\n searchResultContainer: '.searchresultitemscontainer',\n};\n\nconst countIndicator = document.querySelector(selectors.count);\n\nexport default class ColumnSearch extends search_combobox {\n\n userID = -1;\n courseID = null;\n defaultSort = '';\n\n nodes = [];\n\n gradeStrings = null;\n userStrings = null;\n stringMap = [];\n\n static init(userID, courseID, defaultSort) {\n return new ColumnSearch(userID, courseID, defaultSort);\n }\n\n constructor(userID, courseID, defaultSort) {\n super();\n this.userID = userID;\n this.courseID = courseID;\n this.defaultSort = defaultSort;\n this.component = document.querySelector(selectors.component);\n\n const pendingPromise = new Pending();\n // Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user).\n addIconToContainer(document.querySelector('.gradeparent')).then((loader) => {\n setTimeout(() => {\n // Get the users' checked columns to change.\n this.getDataset().forEach((item) => {\n this.nodesUpdate(item);\n });\n this.renderDefault();\n\n // Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.\n loader.remove();\n document.querySelector('.gradereport-grader-table').classList.remove('d-none');\n }, 10);\n }).then(() => pendingPromise.resolve()).catch(Notification.exception);\n\n this.$component.on('hide.bs.dropdown', () => {\n const searchResultContainer = this.component.querySelector(selectors.searchResultContainer);\n searchResultContainer.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.collapse-columns';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.searchresultitemscontainer';\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Array}\n */\n getDataset() {\n if (!this.dataset) {\n const cols = this.fetchDataset();\n this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : [];\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {string}\n */\n fetchDataset() {\n return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`);\n }\n\n /**\n * Given a user performs an action, update the users' preferences.\n */\n setPreferences() {\n storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`,\n JSON.stringify(this.getDataset().join(','))\n );\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\n document.addEventListener('click', this.docClickHandler.bind(this));\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e);\n // Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form.\n if (e.target.closest(selectors.fullDropdown)) {\n e.stopPropagation();\n }\n }\n\n /**\n * Externally defined click function to improve memory handling.\n *\n * @param {MouseEvent} e\n * @returns {Promise}\n */\n async docClickHandler(e) {\n if (e.target.dataset.hider === selectors.hider) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n if (idx === -1) {\n this.getDataset().push(desiredToHide);\n }\n await this.prefcountpipe();\n\n this.nodesUpdate(desiredToHide);\n }\n\n if (e.target.closest('button')?.dataset.hider === selectors.expand) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n this.getDataset().splice(idx, 1);\n\n await this.prefcountpipe();\n\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);\n }\n }\n\n /**\n * Handle any keyboard inputs.\n */\n registerInputEvents() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n if (this.getSearchTerm() === this.searchInput.value && this.searchResultsVisible()) {\n window.console.warn(`Search term matches input value - skipping`);\n // Debounce can happen multiple times quickly.\n return;\n }\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.searchInput.value === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n const pendingPromise = new Pending();\n // User has given something for us to filter against.\n await this.filterrenderpipe().then(() => {\n pendingPromise.resolve();\n return true;\n });\n }, 300, {pending: true}));\n }\n\n /**\n * Handle the form submission within the dropdown.\n */\n registerFormEvents() {\n const form = this.component.querySelector(selectors.formDropdown);\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n const selectall = form.querySelector('[data-action=\"selectall\"]');\n\n // Register clicks & keyboard form handling.\n events.forEach((event) => {\n const submitBtn = form.querySelector(`[data-action=\"${selectors.formItems.save}\"`);\n form.addEventListener(event, (e) => {\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n const input = e.target.closest('input');\n if (input) {\n // If the user is unchecking an item, we need to uncheck the select all if it's checked.\n if (selectall.checked && !input.checked) {\n selectall.checked = false;\n }\n const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length;\n // Check if any are clicked or not then change disabled.\n submitBtn.disabled = checkedCount <= 0;\n }\n }, false);\n\n // Stop Bootstrap from being clever.\n this.searchInput.addEventListener(event, e => e.stopPropagation());\n this.clearSearchButton.addEventListener(event, async(e) => {\n e.stopPropagation();\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n await this.filterrenderpipe();\n });\n selectall.addEventListener(event, (e) => {\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n if (!selectall.checked) {\n const touncheck = Array.from(form.querySelectorAll(selectors.formItems.checked));\n touncheck.forEach(item => {\n item.checked = false;\n });\n submitBtn.disabled = true;\n } else {\n const currentUnchecked = Array.from(form.querySelectorAll(selectors.formItems.currentlyUnchecked));\n currentUnchecked.forEach(item => {\n item.checked = true;\n });\n submitBtn.disabled = false;\n }\n });\n });\n\n form.addEventListener('submit', async(e) => {\n e.preventDefault();\n if (e.submitter.dataset.action === selectors.formItems.cancel) {\n $(this.component).dropdown('toggle');\n return;\n }\n // Get the users' checked columns to change.\n const checkedItems = [...form.elements].filter(item => item.checked);\n checkedItems.forEach((item) => {\n const idx = this.getDataset().indexOf(item.dataset.collapse);\n this.getDataset().splice(idx, 1);\n this.nodesUpdate(item.dataset.collapse);\n });\n // Reset the check all & submit to false just in case.\n selectall.checked = false;\n e.submitter.disabled = true;\n await this.prefcountpipe();\n });\n }\n\n nodesUpdate(item) {\n const colNodesToHide = [...document.querySelectorAll(`[data-col=\"${item}\"]`)];\n const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid=\"${item}\"]`)];\n this.nodes = [...colNodesToHide, ...itemIDNodesToHide];\n this.updateDisplay();\n }\n\n /**\n * Update the user preferences, count display then render the results.\n *\n * @returns {Promise}\n */\n async prefcountpipe() {\n this.setPreferences();\n this.countUpdate();\n await this.filterrenderpipe();\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} An array of objects containing the system reference and the user readable value.\n */\n async filterDataset(filterableData) {\n const stringUserMap = await this.fetchRequiredUserStrings();\n const stringGradeMap = await this.fetchRequiredGradeStrings();\n // Custom user profile fields are not in our string map and need a bit of extra love.\n const customFieldMap = this.fetchCustomFieldValues();\n this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]);\n\n const searching = filterableData.map(s => {\n const mapObj = this.stringMap.get(s);\n if (mapObj === undefined) {\n return {key: s, string: s};\n }\n return {\n key: s,\n string: mapObj.itemname ?? this.stringMap.get(s),\n category: mapObj.category ?? '',\n };\n });\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return searching;\n }\n // Other times we want to actually filter the content.\n return searching.filter((col) => {\n return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm());\n });\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((column) => {\n return {\n name: column.key,\n displayName: column.string ?? column.key,\n category: column.category ?? '',\n };\n })\n );\n }\n\n /**\n * With an array of nodes, switch their classes and values.\n */\n updateDisplay() {\n this.nodes.forEach((element) => {\n const content = element.querySelector(selectors.content);\n const sort = element.querySelector(selectors.sort);\n const expandButton = element.querySelector(selectors.expandbutton);\n const rangeRowCell = element.querySelector(selectors.rangerowcell);\n const avgRowCell = element.querySelector(selectors.avgrowcell);\n const nodeSet = [\n element.querySelector(selectors.menu),\n element.querySelector(selectors.icons),\n content\n ];\n\n // This can be further improved to reduce redundant similar calls.\n if (element.classList.contains('cell')) {\n // The column is actively being sorted, lets reset that and reload the page.\n if (sort !== null) {\n window.location = this.defaultSort;\n }\n if (content === null) {\n // If it's not a content cell, it must be an overall average or a range cell.\n const rowCell = avgRowCell ?? rangeRowCell;\n\n rowCell?.classList.toggle('d-none');\n rowCell?.setAttribute('aria-hidden',\n rowCell?.classList.contains('d-none') ? 'true' : 'false');\n } else if (content.classList.contains('d-none')) {\n // We should always have content but some cells do not contain menus or other actions.\n element.classList.remove('collapsed');\n // If there are many nodes, apply the following.\n if (content.childNodes.length > 1) {\n content.classList.add('d-flex');\n }\n nodeSet.forEach(node => {\n node?.classList.remove('d-none');\n node?.setAttribute('aria-hidden', 'false');\n });\n expandButton?.classList.add('d-none');\n expandButton?.setAttribute('aria-hidden', 'true');\n } else {\n element.classList.add('collapsed');\n content.classList.remove('d-flex');\n nodeSet.forEach(node => {\n node?.classList.add('d-none');\n node?.setAttribute('aria-hidden', 'true');\n });\n expandButton?.classList.remove('d-none');\n expandButton?.setAttribute('aria-hidden', 'false');\n }\n }\n });\n }\n\n /**\n * Update the visual count of collapsed columns or hide the count all together.\n */\n countUpdate() {\n countIndicator.textContent = this.getDatasetSize();\n if (this.getDatasetSize() > 0) {\n this.component.parentElement.classList.add('d-flex');\n this.component.parentElement.classList.remove('d-none');\n } else {\n this.component.parentElement.classList.remove('d-flex');\n this.component.parentElement.classList.add('d-none');\n }\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(this.getDataset()));\n this.filterMatchDataset();\n\n // Update the collapsed button pill.\n this.countUpdate();\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {\n 'instance': this.instance,\n 'results': this.getMatchedResults(),\n 'userid': this.userID,\n });\n replaceNode(selectors.placeholder, html, js);\n this.updateNodes();\n\n // Given we now have the body, we can set up more triggers.\n this.registerFormEvents();\n this.registerInputEvents();\n\n // Add a small BS listener so that we can set the focus correctly on open.\n this.$component.on('shown.bs.dropdown', () => {\n this.searchInput.focus({preventScroll: true});\n this.selectallEnable();\n });\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {\n instance: this.instance,\n 'results': this.getMatchedResults(),\n 'searchTerm': this.getSearchTerm(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n this.selectallEnable();\n // Reset the expand button to be disabled as we have re-rendered the dropdown.\n const form = this.component.querySelector(selectors.formDropdown);\n const expandButton = form.querySelector(`[data-action=\"${selectors.formItems.save}\"`);\n expandButton.disabled = true;\n }\n\n /**\n * Given we render the dropdown, Determine if we want to enable the select all checkbox.\n */\n selectallEnable() {\n const form = this.component.querySelector(selectors.formDropdown);\n const selectall = form.querySelector('[data-action=\"selectall\"]');\n selectall.disabled = this.getMatchedResults().length === 0;\n }\n\n /**\n * If we have any custom user profile fields, grab their system & readable names to add to our string map.\n *\n * @returns {array} An array of associated string arrays ready for our map.\n */\n fetchCustomFieldValues() {\n const customFields = document.querySelectorAll('[data-collapse-name]');\n // Cast from NodeList to array to grab all the values.\n return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredUserStrings() {\n if (!this.userStrings) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.userStrings = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.userStrings;\n }\n\n /**\n * Given the set of gradable items we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredGradeStrings() {\n if (!this.gradeStrings) {\n this.gradeStrings = Repository.gradeItems(this.courseID)\n .then((result) => new Map(\n result.gradeItems.map(key => ([key.id, key]))\n ));\n }\n return this.gradeStrings;\n }\n}\n"],"names":["selectors","cancel","save","checked","currentlyUnchecked","countIndicator","document","querySelector","ColumnSearch","search_combobox","userID","courseID","defaultSort","constructor","component","pendingPromise","Pending","then","loader","setTimeout","getDataset","forEach","item","nodesUpdate","renderDefault","remove","classList","resolve","catch","Notification","exception","$component","on","this","scrollTop","searchInput","value","dispatchEvent","Event","bubbles","componentSelector","dropdownSelector","dataset","cols","fetchDataset","JSON","parse","split","datasetSize","length","storage","get","setPreferences","set","stringify","join","registerClickHandlers","addEventListener","clickHandler","bind","docClickHandler","e","target","closest","stopPropagation","hider","preventDefault","desiredToHide","_e$target$closest","col","_e$target$closest2","itemid","indexOf","push","prefcountpipe","_e$target$closest4","_e$target$closest5","idx","splice","_e$target$closest6","_e$target$closest7","registerInputEvents","async","getSearchTerm","searchResultsVisible","window","console","warn","setSearchTerms","clearSearchButton","add","filterrenderpipe","pending","registerFormEvents","form","events","CustomEvents","activate","keyboardActivate","define","selectall","event","submitBtn","input","checkedCount","Array","from","querySelectorAll","disabled","submitter","action","dropdown","elements","filter","collapse","colNodesToHide","itemIDNodesToHide","nodes","updateDisplay","countUpdate","filterableData","stringUserMap","fetchRequiredUserStrings","stringGradeMap","fetchRequiredGradeStrings","customFieldMap","fetchCustomFieldValues","stringMap","Map","searching","map","s","mapObj","undefined","key","string","itemname","category","getPreppedSearchTerm","toString","toLowerCase","includes","filterMatchDataset","setMatchedResults","getMatchedResults","column","name","displayName","element","content","sort","expandButton","rangeRowCell","avgRowCell","nodeSet","contains","location","rowCell","toggle","setAttribute","childNodes","node","textContent","getDatasetSize","parentElement","filterDataset","html","js","instance","updateNodes","focus","preventScroll","selectallEnable","getHTMLElements","searchDropdown","field","collapseName","userStrings","requiredStrings","stringArray","index","gradeStrings","Repository","gradeItems","result","id"],"mappings":"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"}
\ No newline at end of file
diff --git a/grade/report/grader/amd/build/group.min.js.map b/grade/report/grader/amd/build/group.min.js.map
index bdde7d2115d..d3833d64d78 100644
--- a/grade/report/grader/amd/build/group.min.js.map
+++ b/grade/report/grader/amd/build/group.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the grader report.\n *\n * @module gradereport_grader/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"2WAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
\ No newline at end of file
+{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the grader report.\n *\n * @module gradereport_grader/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"2WAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
\ No newline at end of file
diff --git a/grade/report/grader/amd/build/user.min.js.map b/grade/report/grader/amd/build/user.min.js.map
index cc8b285891d..4da4df8ac4b 100644
--- a/grade/report/grader/amd/build/user.min.js.map
+++ b/grade/report/grader/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 * Allow the user to search for learners within the grader report.\n *\n * @module gradereport_grader/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport * as Repository from 'gradereport_grader/local/user/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(courseID).then((r) => r.users);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","User","UserSearch","constructor","fetchDataset","Repository","userFetch","then","r","users","selectAllResultsLink","Url","relativeUrl","id","gpr_search","this","getSearchTerm","selectOneLink","userID","gpr_userid"],"mappings":";;;;;;;q0BA2BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,eAEhDC,aAAaC,cAE9BC,2CAKW,IAAIF,KAQfG,sBACWC,WAAWC,UAAUV,UAAUW,MAAMC,GAAMA,EAAEC,QAQxDC,8BACWC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,kBAClB,GASPC,cAAcC,eACHP,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,gBACjBG,WAAYD,SACb"}
\ 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 * Allow the user to search for learners within the grader report.\n *\n * @module gradereport_grader/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport * as Repository from 'gradereport_grader/local/user/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(courseID).then((r) => r.users);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","User","UserSearch","constructor","fetchDataset","Repository","userFetch","then","r","users","selectAllResultsLink","Url","relativeUrl","id","gpr_search","this","getSearchTerm","selectOneLink","userID","gpr_userid"],"mappings":";;;;;;;q0BA2BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,eAEhDC,aAAaC,cAE9BC,2CAKW,IAAIF,KAQfG,sBACWC,WAAWC,UAAUV,UAAUW,MAAMC,GAAMA,EAAEC,QAQxDC,8BACWC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,kBAClB,GASPC,cAAcC,eACHP,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,gBACjBG,WAAYD,SACb"}
\ No newline at end of file
diff --git a/grade/report/grader/amd/src/collapse.js b/grade/report/grader/amd/src/collapse.js
index 7599203cef4..971819396af 100644
--- a/grade/report/grader/amd/src/collapse.js
+++ b/grade/report/grader/amd/src/collapse.js
@@ -56,6 +56,7 @@ const selectors = {
count: '[data-collapse="count"]',
placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
fullDropdown: '.collapsecolumndropdown',
+ searchResultContainer: '.searchresultitemscontainer',
};
const countIndicator = document.querySelector(selectors.count);
@@ -98,6 +99,19 @@ export default class ColumnSearch extends search_combobox {
document.querySelector('.gradereport-grader-table').classList.remove('d-none');
}, 10);
}).then(() => pendingPromise.resolve()).catch(Notification.exception);
+
+ this.$component.on('hide.bs.dropdown', () => {
+ const searchResultContainer = this.component.querySelector(selectors.searchResultContainer);
+ searchResultContainer.scrollTop = 0;
+
+ // Use setTimeout to make sure the following code is executed after the click event is handled.
+ setTimeout(() => {
+ if (this.searchInput.value !== '') {
+ this.searchInput.value = '';
+ this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
+ }
+ });
+ });
}
/**
@@ -118,15 +132,6 @@ export default class ColumnSearch extends search_combobox {
return '.searchresultitemscontainer';
}
- /**
- * The triggering div that contains the searching widget.
- *
- * @returns {string}
- */
- triggerSelector() {
- return '.collapsecolumn';
- }
-
/**
* Return the dataset that we will be searching upon.
*
@@ -218,25 +223,6 @@ export default class ColumnSearch extends search_combobox {
}
}
- /**
- * The handler for when a user presses a key within the component.
- *
- * @param {KeyboardEvent} e The triggering event that we are working with.
- */
- async keyHandler(e) {
- super.keyHandler(e);
-
- // Switch the key presses to handle keyboard nav.
- switch (e.key) {
- case 'Tab':
- if (e.target.closest(this.selectors.input)) {
- e.preventDefault();
- this.clearSearchButton.focus({preventScroll: true});
- }
- break;
- }
- }
-
/**
* Handle any keyboard inputs.
*/
@@ -492,6 +478,7 @@ export default class ColumnSearch extends search_combobox {
// Update the collapsed button pill.
this.countUpdate();
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {
+ 'instance': this.instance,
'results': this.getMatchedResults(),
'userid': this.userID,
});
@@ -505,6 +492,7 @@ export default class ColumnSearch extends search_combobox {
// Add a small BS listener so that we can set the focus correctly on open.
this.$component.on('shown.bs.dropdown', () => {
this.searchInput.focus({preventScroll: true});
+ this.selectallEnable();
});
}
@@ -512,14 +500,26 @@ export default class ColumnSearch extends search_combobox {
* Build the content then replace the node.
*/
async renderDropdown() {
- const form = this.component.querySelector(selectors.formDropdown);
- const selectall = form.querySelector('[data-action="selectall"]');
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {
+ instance: this.instance,
'results': this.getMatchedResults(),
'searchTerm': this.getSearchTerm(),
});
- selectall.disabled = this.getMatchedResults().length === 0;
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
+ this.selectallEnable();
+ // Reset the expand button to be disabled as we have re-rendered the dropdown.
+ const form = this.component.querySelector(selectors.formDropdown);
+ const expandButton = form.querySelector(`[data-action="${selectors.formItems.save}"`);
+ expandButton.disabled = true;
+ }
+
+ /**
+ * Given we render the dropdown, Determine if we want to enable the select all checkbox.
+ */
+ selectallEnable() {
+ const form = this.component.querySelector(selectors.formDropdown);
+ const selectall = form.querySelector('[data-action="selectall"]');
+ selectall.disabled = this.getMatchedResults().length === 0;
}
/**
diff --git a/grade/report/grader/amd/src/group.js b/grade/report/grader/amd/src/group.js
index 9e15167e3ca..1a22571c641 100644
--- a/grade/report/grader/amd/src/group.js
+++ b/grade/report/grader/amd/src/group.js
@@ -43,7 +43,7 @@ export default class Group extends GroupSearch {
}
/**
- * Build up the view all link that is dedicated to a particular result.
+ * Build up the link that is dedicated to a particular result.
*
* @param {Number} groupID The ID of the group selected.
* @returns {string|*}
diff --git a/grade/report/grader/amd/src/user.js b/grade/report/grader/amd/src/user.js
index ab375e751d5..82c5992d760 100644
--- a/grade/report/grader/amd/src/user.js
+++ b/grade/report/grader/amd/src/user.js
@@ -64,7 +64,7 @@ export default class User extends UserSearch {
}
/**
- * Build up the view all link that is dedicated to a particular result.
+ * Build up the link that is dedicated to a particular result.
*
* @param {Number} userID The ID of the user selected.
* @returns {string|*}
diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php
index ceed096d53c..240ef44face 100644
--- a/grade/report/grader/classes/output/action_bar.php
+++ b/grade/report/grader/classes/output/action_bar.php
@@ -32,6 +32,9 @@ class action_bar extends \core_grades\output\action_bar {
/** @var string $usersearch The content that the current user is looking for. */
protected string $usersearch = '';
+ /** @var int $userid The ID of the user that the current user is looking for. */
+ protected int $userid = 0;
+
/**
* The class constructor.
*
@@ -40,7 +43,13 @@ class action_bar extends \core_grades\output\action_bar {
public function __construct(\context_course $context) {
parent::__construct($context);
+ $this->userid = optional_param('gpr_userid', 0, PARAM_INT);
$this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS);
+
+ if ($this->userid) {
+ $user = \core_user::get_user($this->userid);
+ $this->usersearch = fullname($user);
+ }
}
/**
@@ -80,6 +89,10 @@ class action_bar extends \core_grades\output\action_bar {
$this->context,
'/grade/report/grader/index.php'
);
+
+ $firstnameinitial = $SESSION->gradereport["filterfirstname-{$this->context->id}"] ?? '';
+ $lastnameinitial = $SESSION->gradereport["filtersurname-{$this->context->id}"] ?? '';
+
$initialselector = new comboboxsearch(
false,
$initialscontent->buttoncontent,
@@ -88,6 +101,13 @@ class action_bar extends \core_grades\output\action_bar {
'initialswidget',
'initialsdropdown',
$initialscontent->buttonheader,
+ true,
+ get_string('filterbyname', 'core_grades'),
+ 'nameinitials',
+ json_encode([
+ 'first' => $firstnameinitial,
+ 'last' => $lastnameinitial,
+ ])
);
$data['initialselector'] = $initialselector->export_for_template($output);
$data['groupselector'] = $gradesrenderer->group_selector($course);
@@ -96,14 +116,20 @@ class action_bar extends \core_grades\output\action_bar {
$searchinput = $OUTPUT->render_from_template('core_user/comboboxsearch/user_selector', [
'currentvalue' => $this->usersearch,
'courseid' => $courseid,
+ 'instance' => rand(),
'resetlink' => $resetlink->out(false),
'group' => 0,
+ 'name' => 'usersearch',
+ 'value' => json_encode([
+ 'userid' => $this->userid,
+ 'search' => $this->usersearch,
+ ]),
]);
$searchdropdown = new comboboxsearch(
true,
$searchinput,
null,
- 'user-search dropdown d-flex',
+ 'user-search d-flex',
null,
'usersearchdropdown overflow-auto',
null,
@@ -123,6 +149,8 @@ class action_bar extends \core_grades\output\action_bar {
'collapsecolumndropdown p-3 flex-column ' . $collapsemenudirection,
null,
true,
+ get_string('aria:dropdowncolumns', 'gradereport_grader'),
+ 'collapsedcolumns'
);
$data['collapsedcolumns'] = [
'classes' => 'd-none',
@@ -135,10 +163,12 @@ class action_bar extends \core_grades\output\action_bar {
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
}
- if (!empty($SESSION->gradereport["filterfirstname-{$this->context->id}"]) ||
- !empty($SESSION->gradereport["filterlastname-{$this->context->id}"]) ||
+ if (
+ $firstnameinitial ||
+ $lastnameinitial ||
groups_get_course_group($course, true, $allowedgroups) ||
- $this->usersearch) {
+ $this->usersearch
+ ) {
$reset = new moodle_url('/grade/report/grader/index.php', [
'id' => $courseid,
'group' => 0,
diff --git a/grade/report/grader/templates/collapse/collapsebody.mustache b/grade/report/grader/templates/collapse/collapsebody.mustache
index a919154717a..e8983247496 100644
--- a/grade/report/grader/templates/collapse/collapsebody.mustache
+++ b/grade/report/grader/templates/collapse/collapsebody.mustache
@@ -18,6 +18,7 @@
Example context (json):
{
+ "instance": 25,
"results": [
{
"name": "42",
@@ -39,24 +40,18 @@
{{$placeholder}}{{#str}}
searchcollapsedcolumns, core_grades
{{/str}}{{/placeholder}}
- {{$additionalattributes}}
- role="combobox"
- aria-expanded="true"
- aria-controls="collapse-listbox"
- aria-autocomplete="list"
- data-input-element="collapse-input-{{uniqid}}"
- {{/additionalattributes}}
{{/ core/search_input_auto }}
-
-