diff --git a/grade/report/grader/amd/build/collapse.min.js b/grade/report/grader/amd/build/collapse.min.js
index f1e987797eb..2ad991dec49 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",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")}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==expandButton||expandButton.classList.add("d-none")):(element.classList.add("collapsed"),content.classList.remove("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.add("d-none")})),null==expandButton||expandButton.classList.remove("d-none"))}))}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}));
+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",selectors_cellMenuButton=".cellmenubtn",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 pendingPromise=new _pending.default("gradereport_grader/collapse:docClickHandler:hide"),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(),await this.nodesUpdate(desiredToHide),pendingPromise.resolve()}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 pendingPromise=new _pending.default("gradereport_grader/collapse:docClickHandler:expand"),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(),await this.nodesUpdate(null===(_e$target$closest6=e.target.closest(selectors_colVal))||void 0===_e$target$closest6?void 0:_e$target$closest6.dataset.col),await this.nodesUpdate(null===(_e$target$closest7=e.target.closest(selectors_colVal))||void 0===_e$target$closest7?void 0:_e$target$closest7.dataset.itemid),pendingPromise.resolve()}}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()}))}async nodesUpdate(item){const elements=[...[...document.querySelectorAll('[data-col="'.concat(item,'"]'))],...[...document.querySelectorAll('[data-itemid="'.concat(item,'"]'))]];if(elements&&elements.length){const pendingPromise=new _pending.default("gradereport_grader/collapse:nodesUpdate:"+item);this.updateDisplay(elements).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)}}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:""}})))}async updateDisplay(elements){const promises=[];elements.forEach((element=>{promises.push(this.updateDisplayForElement(element))})),await Promise.all(promises)}async updateDisplayForElement(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),cellMenuButton=element.querySelector(selectors_cellMenuButton),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")}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==expandButton||expandButton.classList.add("d-none"),null==cellMenuButton||cellMenuButton.focus()):(element.classList.add("collapsed"),content.classList.remove("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.add("d-none")})),null==expandButton||expandButton.classList.remove("d-none"))}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 8f85fe49dbd..6b35dc67733 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 <http://www.gnu.org/licenses/>.\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 <mathew.solutions>\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<void>}\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<void>}\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                } 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                    });\n                    expandButton?.classList.add('d-none');\n                } else {\n                    element.classList.add('collapsed');\n                    content.classList.remove('d-flex');\n                    nodeSet.forEach(node => {\n                        node?.classList.add('d-none');\n                    });\n                    expandButton?.classList.remove('d-none');\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<string,*>} 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<void>}\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<void>}\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","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
+{"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 <http://www.gnu.org/licenses/>.\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 <mathew.solutions>\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    cellMenuButton: '.cellmenubtn',\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<void>}\n     */\n    async docClickHandler(e) {\n        if (e.target.dataset.hider === selectors.hider) {\n            e.preventDefault();\n            const pendingPromise = new Pending('gradereport_grader/collapse:docClickHandler:hide');\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            await this.nodesUpdate(desiredToHide);\n            pendingPromise.resolve();\n        }\n\n        if (e.target.closest('button')?.dataset.hider === selectors.expand) {\n            e.preventDefault();\n            const pendingPromise = new Pending('gradereport_grader/collapse:docClickHandler:expand');\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            await this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);\n            await this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);\n            pendingPromise.resolve();\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    async nodesUpdate(item) {\n        const colNodesToHide = [...document.querySelectorAll(`[data-col=\"${item}\"]`)];\n        const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid=\"${item}\"]`)];\n        const elements = [...colNodesToHide, ...itemIDNodesToHide];\n        if (elements && elements.length) {\n            const pendingPromise = new Pending('gradereport_grader/collapse:nodesUpdate:' + item);\n            this.updateDisplay(elements).then(() => pendingPromise.resolve()).catch(Notification.exception);\n        }\n    }\n\n    /**\n     * Update the user preferences, count display then render the results.\n     *\n     * @returns {Promise<void>}\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     * @param {Array} elements The elements to update.\n     */\n    async updateDisplay(elements) {\n        const promises = [];\n        elements.forEach((element) => {\n            promises.push(this.updateDisplayForElement(element));\n        });\n\n        await Promise.all(promises);\n    }\n\n    /**\n     * Update display for given element, switch its classes and values.\n     *\n     * @param {HTMLElement} element The element to update.\n     */\n    async updateDisplayForElement(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 cellMenuButton = element.querySelector(selectors.cellMenuButton);\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            } 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                });\n                expandButton?.classList.add('d-none');\n                cellMenuButton?.focus();\n            } else {\n                element.classList.add('collapsed');\n                content.classList.remove('d-flex');\n                nodeSet.forEach(node => {\n                    node?.classList.add('d-none');\n                });\n                expandButton?.classList.remove('d-none');\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<string,*>} 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<void>}\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<void>}\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","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","promises","element","updateDisplayForElement","Promise","all","content","sort","expandButton","rangeRowCell","avgRowCell","cellMenuButton","nodeSet","contains","location","rowCell","toggle","childNodes","node","focus","textContent","getDatasetSize","parentElement","filterDataset","html","js","instance","updateNodes","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/src/collapse.js b/grade/report/grader/amd/src/collapse.js
index 2435fb66617..4f0c1030c0c 100644
--- a/grade/report/grader/amd/src/collapse.js
+++ b/grade/report/grader/amd/src/collapse.js
@@ -57,6 +57,7 @@ const selectors = {
     placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
     fullDropdown: '.collapsecolumndropdown',
     searchResultContainer: '.searchresultitemscontainer',
+    cellMenuButton: '.cellmenubtn',
 };
 
 const countIndicator = document.querySelector(selectors.count);
@@ -196,6 +197,7 @@ export default class ColumnSearch extends search_combobox {
     async docClickHandler(e) {
         if (e.target.dataset.hider === selectors.hider) {
             e.preventDefault();
+            const pendingPromise = new Pending('gradereport_grader/collapse:docClickHandler:hide');
             const desiredToHide = e.target.closest(selectors.colVal) ?
                 e.target.closest(selectors.colVal)?.dataset.col :
                 e.target.closest(selectors.itemVal)?.dataset.itemid;
@@ -205,11 +207,13 @@ export default class ColumnSearch extends search_combobox {
             }
             await this.prefcountpipe();
 
-            this.nodesUpdate(desiredToHide);
+            await this.nodesUpdate(desiredToHide);
+            pendingPromise.resolve();
         }
 
         if (e.target.closest('button')?.dataset.hider === selectors.expand) {
             e.preventDefault();
+            const pendingPromise = new Pending('gradereport_grader/collapse:docClickHandler:expand');
             const desiredToHide = e.target.closest(selectors.colVal) ?
                 e.target.closest(selectors.colVal)?.dataset.col :
                 e.target.closest(selectors.itemVal)?.dataset.itemid;
@@ -218,8 +222,9 @@ export default class ColumnSearch extends search_combobox {
 
             await this.prefcountpipe();
 
-            this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);
-            this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);
+            await this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);
+            await this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);
+            pendingPromise.resolve();
         }
     }
 
@@ -331,11 +336,14 @@ export default class ColumnSearch extends search_combobox {
         });
     }
 
-    nodesUpdate(item) {
+    async nodesUpdate(item) {
         const colNodesToHide = [...document.querySelectorAll(`[data-col="${item}"]`)];
         const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid="${item}"]`)];
-        this.nodes = [...colNodesToHide, ...itemIDNodesToHide];
-        this.updateDisplay();
+        const elements = [...colNodesToHide, ...itemIDNodesToHide];
+        if (elements && elements.length) {
+            const pendingPromise = new Pending('gradereport_grader/collapse:nodesUpdate:' + item);
+            this.updateDisplay(elements).then(() => pendingPromise.resolve()).catch(Notification.exception);
+        }
     }
 
     /**
@@ -400,52 +408,68 @@ export default class ColumnSearch extends search_combobox {
 
     /**
      * With an array of nodes, switch their classes and values.
+     *
+     * @param {Array} elements The elements to update.
      */
-    updateDisplay() {
-        this.nodes.forEach((element) => {
-            const content = element.querySelector(selectors.content);
-            const sort = element.querySelector(selectors.sort);
-            const expandButton = element.querySelector(selectors.expandbutton);
-            const rangeRowCell = element.querySelector(selectors.rangerowcell);
-            const avgRowCell = element.querySelector(selectors.avgrowcell);
-            const nodeSet = [
-                element.querySelector(selectors.menu),
-                element.querySelector(selectors.icons),
-                content
-            ];
-
-            // This can be further improved to reduce redundant similar calls.
-            if (element.classList.contains('cell')) {
-                // The column is actively being sorted, lets reset that and reload the page.
-                if (sort !== null) {
-                    window.location = this.defaultSort;
-                }
-                if (content === null) {
-                    // If it's not a content cell, it must be an overall average or a range cell.
-                    const rowCell = avgRowCell ?? rangeRowCell;
-
-                    rowCell?.classList.toggle('d-none');
-                } else if (content.classList.contains('d-none')) {
-                    // We should always have content but some cells do not contain menus or other actions.
-                    element.classList.remove('collapsed');
-                    // If there are many nodes, apply the following.
-                    if (content.childNodes.length > 1) {
-                        content.classList.add('d-flex');
-                    }
-                    nodeSet.forEach(node => {
-                        node?.classList.remove('d-none');
-                    });
-                    expandButton?.classList.add('d-none');
-                } else {
-                    element.classList.add('collapsed');
-                    content.classList.remove('d-flex');
-                    nodeSet.forEach(node => {
-                        node?.classList.add('d-none');
-                    });
-                    expandButton?.classList.remove('d-none');
-                }
-            }
+    async updateDisplay(elements) {
+        const promises = [];
+        elements.forEach((element) => {
+            promises.push(this.updateDisplayForElement(element));
         });
+
+        await Promise.all(promises);
+    }
+
+    /**
+     * Update display for given element, switch its classes and values.
+     *
+     * @param {HTMLElement} element The element to update.
+     */
+    async updateDisplayForElement(element) {
+        const content = element.querySelector(selectors.content);
+        const sort = element.querySelector(selectors.sort);
+        const expandButton = element.querySelector(selectors.expandbutton);
+        const rangeRowCell = element.querySelector(selectors.rangerowcell);
+        const avgRowCell = element.querySelector(selectors.avgrowcell);
+        const cellMenuButton = element.querySelector(selectors.cellMenuButton);
+        const nodeSet = [
+            element.querySelector(selectors.menu),
+            element.querySelector(selectors.icons),
+            content
+        ];
+
+        // This can be further improved to reduce redundant similar calls.
+        if (element.classList.contains('cell')) {
+            // The column is actively being sorted, lets reset that and reload the page.
+            if (sort !== null) {
+                window.location = this.defaultSort;
+            }
+            if (content === null) {
+                // If it's not a content cell, it must be an overall average or a range cell.
+                const rowCell = avgRowCell ?? rangeRowCell;
+
+                rowCell?.classList.toggle('d-none');
+            } else if (content.classList.contains('d-none')) {
+                // We should always have content but some cells do not contain menus or other actions.
+                element.classList.remove('collapsed');
+                // If there are many nodes, apply the following.
+                if (content.childNodes.length > 1) {
+                    content.classList.add('d-flex');
+                }
+                nodeSet.forEach(node => {
+                    node?.classList.remove('d-none');
+                });
+                expandButton?.classList.add('d-none');
+                cellMenuButton?.focus();
+            } else {
+                element.classList.add('collapsed');
+                content.classList.remove('d-flex');
+                nodeSet.forEach(node => {
+                    node?.classList.add('d-none');
+                });
+                expandButton?.classList.remove('d-none');
+            }
+        }
     }
 
     /**