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":"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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 .\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 }} - -
-
    - {{>gradereport_grader/collapse/collapseresults}} -
+
+ {{#str}} aria:dropdowncolumns, gradereport_grader {{/str}} +
    + {{>gradereport_grader/collapse/collapseresults}} +
+
-
diff --git a/grade/report/grader/templates/collapse/collapseresultitems.mustache b/grade/report/grader/templates/collapse/collapseresultitems.mustache index d883c1ff02b..ff567358c89 100644 --- a/grade/report/grader/templates/collapse/collapseresultitems.mustache +++ b/grade/report/grader/templates/collapse/collapseresultitems.mustache @@ -26,9 +26,9 @@ "category": "Hitchhikers grade category" } }} -
  • - -
  • +
  • - - {{$content}} - - {{name}} - - {{/content}} - +
  • diff --git a/lib/templates/local/comboboxsearch/resultset.mustache b/lib/templates/local/comboboxsearch/resultset.mustache index e80444afabc..455c768fa02 100644 --- a/lib/templates/local/comboboxsearch/resultset.mustache +++ b/lib/templates/local/comboboxsearch/resultset.mustache @@ -17,23 +17,22 @@ Wrapping template for returned result items. Context variables required for this template: + * instance - The instance ID of the combo box. * results - Our returned results to render. * searchterm - The entered text to find these results. * hasresult - Allow the handling where no results exist for the returned search term. - * noresults - Our fall through case if nothing matches. Example context (json): { + "instance": 25, "results": [ { "id": 2, - "name": "Foo bar", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "name": "Foo bar" }, { "id": 3, - "name": "Bar Foo", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + "name": "Bar Foo" } ], "searchterm": "Foo", @@ -41,17 +40,22 @@ } }}
    - {{#hasresults}} -
      +
        + {{#hasresults}} {{$results}} {{#results}} {{>core/local/comboboxsearch/resultitem}} {{/results}} {{/results}} {{$selectall}}{{/selectall}} -
      - {{/hasresults}} + {{/hasresults}} +
    {{^hasresults}} - {{#str}} noresultsfor, core, {{searchterm}}{{/str}} + {{#str}} noresultsfor, core, {{searchterm}}{{/str}} {{/hasresults}}
    diff --git a/lib/templates/local/comboboxsearch/searchbody.mustache b/lib/templates/local/comboboxsearch/searchbody.mustache index 70d2f9aefb0..c4ad31508d2 100644 --- a/lib/templates/local/comboboxsearch/searchbody.mustache +++ b/lib/templates/local/comboboxsearch/searchbody.mustache @@ -28,39 +28,22 @@ }}
    - - {{#currentvalue}} - {{< core/search_input_auto }} - {{$label}}{{#str}} - searchitems, core - {{/str}}{{/label}} - {{$value}}{{currentvalue}}{{/value}} - {{$additionalattributes}} - role="combobox" - aria-expanded="true" - aria-controls="list-result-listbox" - aria-autocomplete="list" - data-input-element="result-input-{{uniqid}}" - {{/additionalattributes}} - {{/ core/search_input_auto }} - {{/currentvalue}} - {{^currentvalue}} - {{< core/search_input_auto }} - {{$label}}{{#str}} - searchitems, core - {{/str}}{{/label}} - {{$placeholder}}{{#str}} - searchitems, core - {{/str}}{{/placeholder}} - {{$additionalattributes}} - role="combobox" - aria-expanded="true" - aria-controls="list-result-listbox" - aria-autocomplete="list" - data-input-element="result-input-{{uniqid}}" - {{/additionalattributes}} - {{/ core/search_input_auto }} - {{/currentvalue}} - + {{< core/search_input_auto }} + {{$label}}{{#str}} + searchitems, core + {{/str}}{{/label}} + {{$placeholder}}{{#str}} + searchitems, core + {{/str}}{{/placeholder}} + {{$value}}{{currentvalue}}{{/value}} + {{$additionalattributes}} + role="combobox" + aria-expanded="true" + aria-controls="list-{{instance}}-result-listbox" + aria-autocomplete="list" + data-input-element="result-input-{{uniqid}}-{{instance}}" + {{/additionalattributes}} + {{/ core/search_input_auto }} +
    diff --git a/lib/upgrade.txt b/lib/upgrade.txt index d94d7131981..355f45b352e 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -99,6 +99,7 @@ information provided here is intended especially for developers. to bool if the original functionality is desired. * core\hook\manager::phpunit_get_instance() now sets self::$instance to the mocked instance if the optional $persist argument is true, so future calls to ::get_instance() will return it. +* The triggerSelector method in the `core/comboboxsearch/search_combobox` JS module is deprecated. It was not used. === 4.3 === diff --git a/theme/boost/amd/build/aria.min.js b/theme/boost/amd/build/aria.min.js index 677391beb29..f742bd778b3 100644 --- a/theme/boost/amd/build/aria.min.js +++ b/theme/boost/amd/build/aria.min.js @@ -1,10 +1,10 @@ -define("theme_boost/aria",["exports","jquery","core/pending"],(function(_exports,_jquery,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("theme_boost/aria",["exports","jquery","core/pending","core/local/aria/focuslock"],(function(_exports,_jquery,_pending,FocusLockManager){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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Enhancements to Bootstrap components for accessibility. * * @module theme_boost/aria * @copyright 2018 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const dropdownFix=()=>{let focusEnd=!1;const setFocusEnd=function(){let end=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];focusEnd=end},shiftFocus=function(element){let focusCheck=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const pendingPromise=new _pending.default("core/aria:delayed-focus");setTimeout((()=>{focusCheck&&!focusCheck()||element.focus(),pendingPromise.resolve()}),50)},handleMenuButton=e=>{const trigger=e.key;let fixFocus=!1;if(" "!==trigger&&"Enter"!==trigger||(fixFocus=!0,e.preventDefault(),e.target.click()),"ArrowUp"!==trigger&&"ArrowDown"!==trigger||(fixFocus=!0),!fixFocus)return;const menu=e.target.parentElement.querySelector('[role="menu"]');let menuItems=!1,foundMenuItem=!1,textInput=!1;menu&&(menuItems=menu.querySelectorAll('[role="menuitem"]'),textInput=e.target.parentElement.querySelector('[data-action="search"]')),menuItems&&menuItems.length>0&&("ArrowUp"===trigger?setFocusEnd():setFocusEnd(!1),foundMenuItem=(()=>{const result=focusEnd;return focusEnd=!1,result})()?menuItems[menuItems.length-1]:menuItems[0]),textInput&&shiftFocus(textInput),foundMenuItem&&null===textInput&&shiftFocus(foundMenuItem)};document.addEventListener("keypress",(e=>{if(e.target.matches('.dropdown [role="menu"] [role="menuitem"]')){const menu=e.target.closest('[role="menu"]');if(!menu)return;const menuItems=menu.querySelectorAll('[role="menuitem"]');if(!menuItems)return;const trigger=e.key.toLowerCase();for(let i=0;i{if(e.target.matches('[data-toggle="dropdown"]')&&handleMenuButton(e),e.target.matches('.dropdown [role="menu"] [role="menuitem"]')){const trigger=e.key;let next=!1;const menu=e.target.closest('[role="menu"]');if(!menu)return;const menuItems=menu.querySelectorAll('[role="menuitem"]');if(!menuItems)return;if("ArrowDown"==trigger){for(let i=0;i{const trigger=e.target.querySelector('[data-toggle="dropdown"]'),focused=document.activeElement!=document.body?document.activeElement:null;trigger&&focused&&e.target.contains(focused)&&shiftFocus(trigger,(()=>document.activeElement===document.body||e.target.contains(document.activeElement)))}))},tabElementFix=()=>{document.addEventListener("keydown",(e=>{["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End"].includes(e.key)&&e.target.matches('[role="tablist"] [role="tab"]')&&(e=>{const tabList=e.target.closest('[role="tablist"]'),vertical="vertical"==tabList.getAttribute("aria-orientation"),rtl=window.right_to_left(),arrowNext=vertical?"ArrowDown":rtl?"ArrowLeft":"ArrowRight",arrowPrevious=vertical?"ArrowUp":rtl?"ArrowRight":"ArrowLeft",tabs=Array.prototype.filter.call(tabList.querySelectorAll('[role="tab"]'),(tab=>!!tab.offsetHeight));for(let i=0;i{if(e.target.matches('[role="tablist"] [data-toggle="tab"], [role="tablist"] [data-toggle="pill"]')){const tabs=e.target.closest('[role="tablist"]').querySelectorAll('[data-toggle="tab"], [data-toggle="pill"]');e.preventDefault(),(0,_jquery.default)(e.target).tab("show"),tabs.forEach((tab=>{tab.tabIndex=-1})),e.target.tabIndex=0}}))};_exports.init=()=>{dropdownFix(),(()=>{(0,_jquery.default)(document).on("show.bs.dropdown",(e=>{if(e.relatedTarget.matches('[role="combobox"]')){const combobox=e.relatedTarget,listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]'));if(listbox){const selectedOption=listbox.querySelector('[role="option"][aria-selected="true"]');setTimeout((()=>{if(selectedOption)selectedOption.classList.add("active"),combobox.setAttribute("aria-activedescendant",selectedOption.id);else{const firstOption=listbox.querySelector('[role="option"]');firstOption.setAttribute("aria-selected","true"),firstOption.classList.add("active"),combobox.setAttribute("aria-activedescendant",firstOption.id)}}),0)}}})),(0,_jquery.default)(document).on("hidden.bs.dropdown",(e=>{if(e.relatedTarget.matches('[role="combobox"]')){const combobox=e.relatedTarget,listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]'));combobox.removeAttribute("aria-activedescendant"),listbox&&setTimeout((()=>{listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")}))}),0)}})),document.addEventListener("keydown",(e=>{if(e.target.matches('[role="combobox"][aria-controls]:not([aria-haspopup=dialog])')){const combobox=e.target,trigger=e.key;let next=null;const listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]')),options=listbox.querySelectorAll('[role="option"]'),activeOption=listbox.querySelector('.active[role="option"]'),editable=combobox.hasAttribute("aria-autocomplete");if(options&&(activeOption||editable)){if("ArrowDown"==trigger){for(let i=0;i{const option=e.target.closest('[role="listbox"] [role="option"]');if(option){const listbox=option.closest('[role="listbox"]'),combobox=document.querySelector('[role="combobox"][aria-controls="'.concat(listbox.id,'"]'));combobox&&(combobox.focus(),selectOption(combobox,option))}})),document.addEventListener("change",(e=>{if(e.target.matches('input[type="hidden"][id]')){const combobox=document.querySelector('[role="combobox"][data-input-element="'.concat(e.target.id,'"]')),option=e.target.parentElement.querySelector('[role="option"][data-value="'.concat(e.target.value,'"]'));combobox&&option&&selectOption(combobox,option)}}));const selectOption=(combobox,option)=>{const oldSelectedOption=option.closest('[role="listbox"]').querySelector('[role="option"][aria-selected="true"]');if(oldSelectedOption!=option&&(oldSelectedOption&&oldSelectedOption.removeAttribute("aria-selected"),option.setAttribute("aria-selected","true")),combobox.hasAttribute("value")?combobox.value=option.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim():combobox.textContent=option.textContent,combobox.dataset.inputElement){const inputElement=document.getElementById(combobox.dataset.inputElement);inputElement&&inputElement.value!=option.dataset.value&&(inputElement.value=option.dataset.value,inputElement.dispatchEvent(new Event("change",{bubbles:!0})))}}})(),window.addEventListener("load",(()=>{const alerts=document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');Array.prototype.forEach.call(alerts,(autofocusElement=>{autofocusElement.innerHTML+=" ",autofocusElement.removeAttribute("data-aria-autofocus")}))})),tabElementFix(),document.addEventListener("keydown",(e=>{e.target.matches('[data-toggle="collapse"]')&&" "===e.key&&(e.preventDefault(),e.target.click())}))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),FocusLockManager=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}(FocusLockManager);const dropdownFix=()=>{let focusEnd=!1;const setFocusEnd=function(){let end=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];focusEnd=end},shiftFocus=function(element){let focusCheck=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const pendingPromise=new _pending.default("core/aria:delayed-focus");setTimeout((()=>{focusCheck&&!focusCheck()||element.focus(),pendingPromise.resolve()}),50)},handleMenuButton=e=>{const trigger=e.key;let fixFocus=!1;if(" "!==trigger&&"Enter"!==trigger||(fixFocus=!0,e.preventDefault(),e.target.click()),"ArrowUp"!==trigger&&"ArrowDown"!==trigger||(fixFocus=!0),!fixFocus)return;const menu=e.target.parentElement.querySelector('[role="menu"]');let menuItems=!1,foundMenuItem=!1;menu&&(menuItems=menu.querySelectorAll('[role="menuitem"]')),menuItems&&menuItems.length>0&&("ArrowUp"===trigger?setFocusEnd():setFocusEnd(!1),foundMenuItem=(()=>{const result=focusEnd;return focusEnd=!1,result})()?menuItems[menuItems.length-1]:menuItems[0]),foundMenuItem&&shiftFocus(foundMenuItem)};document.addEventListener("keypress",(e=>{if(e.target.matches('.dropdown [role="menu"] [role="menuitem"]')){const menu=e.target.closest('[role="menu"]');if(!menu)return;const menuItems=menu.querySelectorAll('[role="menuitem"]');if(!menuItems)return;const trigger=e.key.toLowerCase();for(let i=0;i{if(e.target.matches('[data-toggle="dropdown"]')&&handleMenuButton(e),e.target.matches('.dropdown [role="menu"] [role="menuitem"]')){const trigger=e.key;let next=!1;const menu=e.target.closest('[role="menu"]');if(!menu)return;const menuItems=menu.querySelectorAll('[role="menuitem"]');if(!menuItems)return;if("ArrowDown"==trigger){for(let i=0;i{const dialog=e.target.querySelector("#".concat(e.relatedTarget.getAttribute("aria-controls"),'[role="dialog"]'));dialog&&setTimeout((()=>{FocusLockManager.trapFocus(dialog)}))})),(0,_jquery.default)(".dropdown").on("hidden.bs.dropdown",(e=>{var _e$clickEvent;e.target.querySelector("#".concat(e.relatedTarget.getAttribute("aria-controls"),'[role="dialog"]'))&&FocusLockManager.untrapFocus();const trigger=e.target.querySelector('[data-toggle="dropdown"]'),focused=(null===(_e$clickEvent=e.clickEvent)||void 0===_e$clickEvent?void 0:_e$clickEvent.target)||(document.activeElement!==document.body?document.activeElement:null);trigger&&focused&&e.target.contains(focused)&&shiftFocus(trigger,(()=>document.activeElement===document.body||e.target.contains(document.activeElement)))}))},tabElementFix=()=>{document.addEventListener("keydown",(e=>{["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End"].includes(e.key)&&e.target.matches('[role="tablist"] [role="tab"]')&&(e=>{const tabList=e.target.closest('[role="tablist"]'),vertical="vertical"==tabList.getAttribute("aria-orientation"),rtl=window.right_to_left(),arrowNext=vertical?"ArrowDown":rtl?"ArrowLeft":"ArrowRight",arrowPrevious=vertical?"ArrowUp":rtl?"ArrowRight":"ArrowLeft",tabs=Array.prototype.filter.call(tabList.querySelectorAll('[role="tab"]'),(tab=>!!tab.offsetHeight));for(let i=0;i{if(e.target.matches('[role="tablist"] [data-toggle="tab"], [role="tablist"] [data-toggle="pill"]')){const tabs=e.target.closest('[role="tablist"]').querySelectorAll('[data-toggle="tab"], [data-toggle="pill"]');e.preventDefault(),(0,_jquery.default)(e.target).tab("show"),tabs.forEach((tab=>{tab.tabIndex=-1})),e.target.tabIndex=0}}))};_exports.init=()=>{dropdownFix(),(()=>{(0,_jquery.default)(document).on("show.bs.dropdown",(e=>{if(e.relatedTarget.matches('[role="combobox"]')){const combobox=e.relatedTarget,listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]'));if(listbox){const selectedOption=listbox.querySelector('[role="option"][aria-selected="true"]');setTimeout((()=>{if(selectedOption)selectedOption.classList.add("active"),combobox.setAttribute("aria-activedescendant",selectedOption.id);else{const firstOption=listbox.querySelector('[role="option"]');firstOption.setAttribute("aria-selected","true"),firstOption.classList.add("active"),combobox.setAttribute("aria-activedescendant",firstOption.id)}}),0)}}})),(0,_jquery.default)(document).on("hidden.bs.dropdown",(e=>{if(e.relatedTarget.matches('[role="combobox"]')){const combobox=e.relatedTarget,listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]'));combobox.removeAttribute("aria-activedescendant"),listbox&&setTimeout((()=>{listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")}))}),0)}})),document.addEventListener("keydown",(e=>{if(e.target.matches('[role="combobox"][aria-controls]:not([aria-haspopup=dialog])')){const combobox=e.target,trigger=e.key;let next=null;const listbox=document.querySelector("#".concat(combobox.getAttribute("aria-controls"),'[role="listbox"]')),options=listbox.querySelectorAll('[role="option"]'),activeOption=listbox.querySelector('.active[role="option"]'),editable=combobox.hasAttribute("aria-autocomplete");if(options&&(activeOption||editable)){if("ArrowDown"==trigger){for(let i=0;i{const option=e.target.closest('[role="listbox"] [role="option"]');if(option){const listbox=option.closest('[role="listbox"]'),combobox=document.querySelector('[role="combobox"][aria-controls="'.concat(listbox.id,'"]'));combobox&&selectOption(combobox,option)}})),document.addEventListener("change",(e=>{if(e.target.matches('input[type="hidden"][id]')){const combobox=document.querySelector('[role="combobox"][data-input-element="'.concat(e.target.id,'"]')),option=e.target.parentElement.querySelector('[role="option"][data-value="'.concat(e.target.value,'"]'));combobox&&option&&selectOption(combobox,option)}}));const selectOption=(combobox,option)=>{const oldSelectedOption=option.closest('[role="listbox"]').querySelector('[role="option"][aria-selected="true"]');if(oldSelectedOption!=option&&(oldSelectedOption&&oldSelectedOption.removeAttribute("aria-selected"),option.setAttribute("aria-selected","true")),combobox.hasAttribute("value")?combobox.value=option.dataset.shortText||option.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim():combobox.textContent=option.dataset.shortText||option.textContent,combobox.dataset.inputElement){const inputElement=document.getElementById(combobox.dataset.inputElement);inputElement&&inputElement.value!=option.dataset.value&&(inputElement.value=option.dataset.value,inputElement.dispatchEvent(new Event("change",{bubbles:!0})))}}})(),window.addEventListener("load",(()=>{const alerts=document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');Array.prototype.forEach.call(alerts,(autofocusElement=>{autofocusElement.innerHTML+=" ",autofocusElement.removeAttribute("data-aria-autofocus")}))})),tabElementFix(),document.addEventListener("keydown",(e=>{e.target.matches('[data-toggle="collapse"]')&&" "===e.key&&(e.preventDefault(),e.target.click())}))}})); //# sourceMappingURL=aria.min.js.map \ No newline at end of file diff --git a/theme/boost/amd/build/aria.min.js.map b/theme/boost/amd/build/aria.min.js.map index 397c1eaff7e..e09ca507e69 100644 --- a/theme/boost/amd/build/aria.min.js.map +++ b/theme/boost/amd/build/aria.min.js.map @@ -1 +1 @@ -{"version":3,"file":"aria.min.js","sources":["../src/aria.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 * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n const setFocusEnd = (end = true) => {\n focusEnd = end;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = (element, focusCheck = null) => {\n const pendingPromise = new Pending('core/aria:delayed-focus');\n setTimeout(() => {\n if (!focusCheck || focusCheck()) {\n element.focus();\n }\n\n pendingPromise.resolve();\n }, 50);\n };\n\n // Event handling for the dropdown menu button.\n const handleMenuButton = e => {\n const trigger = e.key;\n let fixFocus = false;\n\n // Space key or Enter key opens the menu.\n if (trigger === ' ' || trigger === 'Enter') {\n fixFocus = true;\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n\n // Up and Down keys also open the menu.\n if (trigger === 'ArrowUp' || trigger === 'ArrowDown') {\n fixFocus = true;\n }\n\n if (!fixFocus) {\n // No need to fix the focus. Return early.\n return;\n }\n\n // Fix the focus on the menu items when the menu is opened.\n const menu = e.target.parentElement.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n let textInput = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n textInput = e.target.parentElement.querySelector('[data-action=\"search\"]');\n }\n\n if (menuItems && menuItems.length > 0) {\n // Up key opens the menu at the end.\n if (trigger === 'ArrowUp') {\n setFocusEnd();\n } else {\n setFocusEnd(false);\n }\n\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n\n if (textInput) {\n shiftFocus(textInput);\n }\n if (foundMenuItem && textInput === null) {\n shiftFocus(foundMenuItem);\n }\n };\n\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n\n // We only want to set focus when users access the dropdown via keyboard as per\n // guidelines defined in w3 aria practices 1.1 menu-button.\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n handleMenuButton(e);\n }\n\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n }\n\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n const focused = document.activeElement != document.body ? document.activeElement : null;\n if (trigger && focused && e.target.contains(focused)) {\n shiftFocus(trigger, () => {\n if (document.activeElement === document.body) {\n // If the focus is currently on the body, then we can safely assume that the focus needs to be updated.\n return true;\n }\n\n // If the focus is on a child of the clicked element still, then update the focus.\n return e.target.contains(document.activeElement);\n });\n }\n });\n};\n\n/**\n * A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.\n */\nconst comboboxFix = () => {\n $(document).on('show.bs.dropdown', e => {\n if (e.relatedTarget.matches('[role=\"combobox\"]')) {\n const combobox = e.relatedTarget;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n\n if (listbox) {\n const selectedOption = listbox.querySelector('[role=\"option\"][aria-selected=\"true\"]');\n\n // To make sure ArrowDown doesn't move the active option afterwards.\n setTimeout(() => {\n if (selectedOption) {\n selectedOption.classList.add('active');\n combobox.setAttribute('aria-activedescendant', selectedOption.id);\n } else {\n const firstOption = listbox.querySelector('[role=\"option\"]');\n firstOption.setAttribute('aria-selected', 'true');\n firstOption.classList.add('active');\n combobox.setAttribute('aria-activedescendant', firstOption.id);\n }\n }, 0);\n }\n }\n });\n\n $(document).on('hidden.bs.dropdown', e => {\n if (e.relatedTarget.matches('[role=\"combobox\"]')) {\n const combobox = e.relatedTarget;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n\n combobox.removeAttribute('aria-activedescendant');\n\n if (listbox) {\n setTimeout(() => {\n // Undo all previously highlighted options.\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n }, 0);\n }\n }\n });\n\n // Handling keyboard events for both navigating through and selecting options.\n document.addEventListener('keydown', e => {\n if (e.target.matches('[role=\"combobox\"][aria-controls]:not([aria-haspopup=dialog])')) {\n const combobox = e.target;\n const trigger = e.key;\n let next = null;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n const options = listbox.querySelectorAll('[role=\"option\"]');\n const activeOption = listbox.querySelector('.active[role=\"option\"]');\n const editable = combobox.hasAttribute('aria-autocomplete');\n\n // Under the special case that the dropdown menu is being shown as a result of the key press (like when the user\n // presses ArrowDown or Enter or ... to open the dropdown menu), activeOption is not set yet.\n // It's because of a race condition with show.bs.dropdown event handler.\n if (options && (activeOption || editable)) {\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < options.length - 1; i++) {\n if (options[i] == activeOption) {\n next = options[i + 1];\n break;\n }\n }\n if (editable && !next) {\n next = options[0];\n }\n } if (trigger == 'ArrowUp') {\n for (let i = 1; i < options.length; i++) {\n if (options[i] == activeOption) {\n next = options[i - 1];\n break;\n }\n }\n if (editable && !next) {\n next = options[options.length - 1];\n }\n } else if (trigger == 'Home') {\n next = options[0];\n } else if (trigger == 'End') {\n next = options[options.length - 1];\n } else if ((trigger == ' ' && !editable) || trigger == 'Enter') {\n e.preventDefault();\n selectOption(combobox, activeOption);\n } else if (!editable) {\n // Search for options by finding the first option that has\n // text starting with the typed character (case insensitive).\n for (let i = 0; i < options.length; i++) {\n const option = options[i];\n const optionText = option.textContent.trim().toLowerCase();\n const keyPressed = e.key.toLowerCase();\n if (optionText.indexOf(keyPressed) == 0) {\n next = option;\n break;\n }\n }\n }\n\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n if (activeOption) {\n activeOption.classList.remove('active');\n }\n next.classList.add('active');\n combobox.setAttribute('aria-activedescendant', next.id);\n next.scrollIntoView({block: 'nearest'});\n }\n }\n }\n });\n\n document.addEventListener('click', e => {\n const option = e.target.closest('[role=\"listbox\"] [role=\"option\"]');\n if (option) {\n const listbox = option.closest('[role=\"listbox\"]');\n const combobox = document.querySelector(`[role=\"combobox\"][aria-controls=\"${listbox.id}\"]`);\n if (combobox) {\n combobox.focus();\n selectOption(combobox, option);\n }\n }\n });\n\n // In case some code somewhere else changes the value of the combobox.\n document.addEventListener('change', e => {\n if (e.target.matches('input[type=\"hidden\"][id]')) {\n const combobox = document.querySelector(`[role=\"combobox\"][data-input-element=\"${e.target.id}\"]`);\n const option = e.target.parentElement.querySelector(`[role=\"option\"][data-value=\"${e.target.value}\"]`);\n\n if (combobox && option) {\n selectOption(combobox, option);\n }\n }\n });\n\n const selectOption = (combobox, option) => {\n const listbox = option.closest('[role=\"listbox\"]');\n const oldSelectedOption = listbox.querySelector('[role=\"option\"][aria-selected=\"true\"]');\n\n if (oldSelectedOption != option) {\n if (oldSelectedOption) {\n oldSelectedOption.removeAttribute('aria-selected');\n }\n option.setAttribute('aria-selected', 'true');\n }\n\n if (combobox.hasAttribute('value')) {\n combobox.value = option.textContent.replace(/[\\n\\r]+|[\\s]{2,}/g, ' ').trim();\n } else {\n combobox.textContent = option.textContent;\n }\n\n if (combobox.dataset.inputElement) {\n const inputElement = document.getElementById(combobox.dataset.inputElement);\n if (inputElement && (inputElement.value != option.dataset.value)) {\n inputElement.value = option.dataset.value;\n inputElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n }\n };\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => !!tab.offsetHeight); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [data-toggle=\"tab\"], [role=\"tablist\"] [data-toggle=\"pill\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[data-toggle=\"tab\"], [data-toggle=\"pill\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\n/**\n * Fix keyboard interaction with Bootstrap Collapse elements.\n *\n * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure|WAI-ARIA Authoring Practices 1.1 - Disclosure (Show/Hide)}\n */\nconst collapseFix = () => {\n document.addEventListener('keydown', e => {\n if (e.target.matches('[data-toggle=\"collapse\"]')) {\n // Pressing space should toggle expand/collapse.\n if (e.key === ' ') {\n e.preventDefault();\n e.target.click();\n }\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n comboboxFix();\n autoFocus();\n tabElementFix();\n collapseFix();\n};\n"],"names":["dropdownFix","focusEnd","setFocusEnd","end","shiftFocus","element","focusCheck","pendingPromise","Pending","setTimeout","focus","resolve","handleMenuButton","e","trigger","key","fixFocus","preventDefault","target","click","menu","parentElement","querySelector","menuItems","foundMenuItem","textInput","querySelectorAll","length","result","getFocusEnd","document","addEventListener","matches","closest","toLowerCase","i","item","text","trim","indexOf","next","on","focused","activeElement","body","contains","tabElementFix","includes","tabList","vertical","getAttribute","rtl","window","right_to_left","arrowNext","arrowPrevious","tabs","Array","prototype","filter","call","tab","offsetHeight","index","undefined","updateTabFocus","forEach","tabIndex","relatedTarget","combobox","listbox","selectedOption","classList","add","setAttribute","id","firstOption","removeAttribute","option","remove","options","activeOption","editable","hasAttribute","selectOption","optionText","textContent","keyPressed","scrollIntoView","block","value","oldSelectedOption","replace","dataset","inputElement","getElementById","dispatchEvent","Event","bubbles","comboboxFix","alerts","autofocusElement","innerHTML"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"aria.min.js","sources":["../src/aria.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 * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\nimport * as FocusLockManager from 'core/local/aria/focuslock';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n const setFocusEnd = (end = true) => {\n focusEnd = end;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = (element, focusCheck = null) => {\n const pendingPromise = new Pending('core/aria:delayed-focus');\n setTimeout(() => {\n if (!focusCheck || focusCheck()) {\n element.focus();\n }\n\n pendingPromise.resolve();\n }, 50);\n };\n\n // Event handling for the dropdown menu button.\n const handleMenuButton = e => {\n const trigger = e.key;\n let fixFocus = false;\n\n // Space key or Enter key opens the menu.\n if (trigger === ' ' || trigger === 'Enter') {\n fixFocus = true;\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n\n // Up and Down keys also open the menu.\n if (trigger === 'ArrowUp' || trigger === 'ArrowDown') {\n fixFocus = true;\n }\n\n if (!fixFocus) {\n // No need to fix the focus. Return early.\n return;\n }\n\n // Fix the focus on the menu items when the menu is opened.\n const menu = e.target.parentElement.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n }\n if (menuItems && menuItems.length > 0) {\n // Up key opens the menu at the end.\n if (trigger === 'ArrowUp') {\n setFocusEnd();\n } else {\n setFocusEnd(false);\n }\n\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n\n if (foundMenuItem) {\n shiftFocus(foundMenuItem);\n }\n };\n\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n\n // We only want to set focus when users access the dropdown via keyboard as per\n // guidelines defined in w3 aria practices 1.1 menu-button.\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n handleMenuButton(e);\n }\n\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n }\n\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('shown.bs.dropdown', e => {\n const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role=\"dialog\"]`);\n if (dialog) {\n // Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling.\n setTimeout(() => {\n FocusLockManager.trapFocus(dialog);\n });\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role=\"dialog\"]`);\n if (dialog) {\n FocusLockManager.untrapFocus();\n }\n\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n // If it's a click event, then no element is focused because the clicked element is inside a closed dropdown.\n const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null);\n if (trigger && focused && e.target.contains(focused)) {\n shiftFocus(trigger, () => {\n if (document.activeElement === document.body) {\n // If the focus is currently on the body, then we can safely assume that the focus needs to be updated.\n return true;\n }\n\n // If the focus is on a child of the clicked element still, then update the focus.\n return e.target.contains(document.activeElement);\n });\n }\n });\n};\n\n/**\n * A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.\n */\nconst comboboxFix = () => {\n $(document).on('show.bs.dropdown', e => {\n if (e.relatedTarget.matches('[role=\"combobox\"]')) {\n const combobox = e.relatedTarget;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n\n if (listbox) {\n const selectedOption = listbox.querySelector('[role=\"option\"][aria-selected=\"true\"]');\n\n // To make sure ArrowDown doesn't move the active option afterwards.\n setTimeout(() => {\n if (selectedOption) {\n selectedOption.classList.add('active');\n combobox.setAttribute('aria-activedescendant', selectedOption.id);\n } else {\n const firstOption = listbox.querySelector('[role=\"option\"]');\n firstOption.setAttribute('aria-selected', 'true');\n firstOption.classList.add('active');\n combobox.setAttribute('aria-activedescendant', firstOption.id);\n }\n }, 0);\n }\n }\n });\n\n $(document).on('hidden.bs.dropdown', e => {\n if (e.relatedTarget.matches('[role=\"combobox\"]')) {\n const combobox = e.relatedTarget;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n\n combobox.removeAttribute('aria-activedescendant');\n\n if (listbox) {\n setTimeout(() => {\n // Undo all previously highlighted options.\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n }, 0);\n }\n }\n });\n\n // Handling keyboard events for both navigating through and selecting options.\n document.addEventListener('keydown', e => {\n if (e.target.matches('[role=\"combobox\"][aria-controls]:not([aria-haspopup=dialog])')) {\n const combobox = e.target;\n const trigger = e.key;\n let next = null;\n const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role=\"listbox\"]`);\n const options = listbox.querySelectorAll('[role=\"option\"]');\n const activeOption = listbox.querySelector('.active[role=\"option\"]');\n const editable = combobox.hasAttribute('aria-autocomplete');\n\n // Under the special case that the dropdown menu is being shown as a result of the key press (like when the user\n // presses ArrowDown or Enter or ... to open the dropdown menu), activeOption is not set yet.\n // It's because of a race condition with show.bs.dropdown event handler.\n if (options && (activeOption || editable)) {\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < options.length - 1; i++) {\n if (options[i] == activeOption) {\n next = options[i + 1];\n break;\n }\n }\n if (editable && !next) {\n next = options[0];\n }\n } if (trigger == 'ArrowUp') {\n for (let i = 1; i < options.length; i++) {\n if (options[i] == activeOption) {\n next = options[i - 1];\n break;\n }\n }\n if (editable && !next) {\n next = options[options.length - 1];\n }\n } else if (trigger == 'Home' && !editable) {\n next = options[0];\n } else if (trigger == 'End' && !editable) {\n next = options[options.length - 1];\n } else if ((trigger == ' ' && !editable) || trigger == 'Enter') {\n e.preventDefault();\n selectOption(combobox, activeOption);\n } else if (!editable) {\n // Search for options by finding the first option that has\n // text starting with the typed character (case insensitive).\n for (let i = 0; i < options.length; i++) {\n const option = options[i];\n const optionText = option.textContent.trim().toLowerCase();\n const keyPressed = e.key.toLowerCase();\n if (optionText.indexOf(keyPressed) == 0) {\n next = option;\n break;\n }\n }\n }\n\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n if (activeOption) {\n activeOption.classList.remove('active');\n }\n next.classList.add('active');\n combobox.setAttribute('aria-activedescendant', next.id);\n next.scrollIntoView({block: 'nearest'});\n }\n }\n }\n });\n\n document.addEventListener('click', e => {\n const option = e.target.closest('[role=\"listbox\"] [role=\"option\"]');\n if (option) {\n const listbox = option.closest('[role=\"listbox\"]');\n const combobox = document.querySelector(`[role=\"combobox\"][aria-controls=\"${listbox.id}\"]`);\n if (combobox) {\n selectOption(combobox, option);\n }\n }\n });\n\n // In case some code somewhere else changes the value of the combobox.\n document.addEventListener('change', e => {\n if (e.target.matches('input[type=\"hidden\"][id]')) {\n const combobox = document.querySelector(`[role=\"combobox\"][data-input-element=\"${e.target.id}\"]`);\n const option = e.target.parentElement.querySelector(`[role=\"option\"][data-value=\"${e.target.value}\"]`);\n\n if (combobox && option) {\n selectOption(combobox, option);\n }\n }\n });\n\n const selectOption = (combobox, option) => {\n const listbox = option.closest('[role=\"listbox\"]');\n const oldSelectedOption = listbox.querySelector('[role=\"option\"][aria-selected=\"true\"]');\n\n if (oldSelectedOption != option) {\n if (oldSelectedOption) {\n oldSelectedOption.removeAttribute('aria-selected');\n }\n option.setAttribute('aria-selected', 'true');\n }\n\n if (combobox.hasAttribute('value')) {\n combobox.value = option.dataset.shortText || option.textContent.replace(/[\\n\\r]+|[\\s]{2,}/g, ' ').trim();\n } else {\n combobox.textContent = option.dataset.shortText || option.textContent;\n }\n\n if (combobox.dataset.inputElement) {\n const inputElement = document.getElementById(combobox.dataset.inputElement);\n if (inputElement && (inputElement.value != option.dataset.value)) {\n inputElement.value = option.dataset.value;\n inputElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n }\n };\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => !!tab.offsetHeight); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [data-toggle=\"tab\"], [role=\"tablist\"] [data-toggle=\"pill\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[data-toggle=\"tab\"], [data-toggle=\"pill\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\n/**\n * Fix keyboard interaction with Bootstrap Collapse elements.\n *\n * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure|WAI-ARIA Authoring Practices 1.1 - Disclosure (Show/Hide)}\n */\nconst collapseFix = () => {\n document.addEventListener('keydown', e => {\n if (e.target.matches('[data-toggle=\"collapse\"]')) {\n // Pressing space should toggle expand/collapse.\n if (e.key === ' ') {\n e.preventDefault();\n e.target.click();\n }\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n comboboxFix();\n autoFocus();\n tabElementFix();\n collapseFix();\n};\n"],"names":["dropdownFix","focusEnd","setFocusEnd","end","shiftFocus","element","focusCheck","pendingPromise","Pending","setTimeout","focus","resolve","handleMenuButton","e","trigger","key","fixFocus","preventDefault","target","click","menu","parentElement","querySelector","menuItems","foundMenuItem","querySelectorAll","length","result","getFocusEnd","document","addEventListener","matches","closest","toLowerCase","i","item","text","trim","indexOf","next","on","dialog","relatedTarget","getAttribute","FocusLockManager","trapFocus","untrapFocus","focused","clickEvent","activeElement","body","contains","tabElementFix","includes","tabList","vertical","rtl","window","right_to_left","arrowNext","arrowPrevious","tabs","Array","prototype","filter","call","tab","offsetHeight","index","undefined","updateTabFocus","forEach","tabIndex","combobox","listbox","selectedOption","classList","add","setAttribute","id","firstOption","removeAttribute","option","remove","options","activeOption","editable","hasAttribute","selectOption","optionText","textContent","keyPressed","scrollIntoView","block","value","oldSelectedOption","dataset","shortText","replace","inputElement","getElementById","dispatchEvent","Event","bubbles","comboboxFix","alerts","autofocusElement","innerHTML"],"mappings":";;;;;;;01BA8BMA,YAAc,SACZC,UAAW,QACTC,YAAc,eAACC,+DACjBF,SAAWE,KASTC,WAAa,SAACC,aAASC,kEAAa,WAChCC,eAAiB,IAAIC,iBAAQ,2BACnCC,YAAW,KACFH,aAAcA,cACfD,QAAQK,QAGZH,eAAeI,YAChB,KAIDC,iBAAmBC,UACfC,QAAUD,EAAEE,QACdC,UAAW,KAGC,MAAZF,SAA+B,UAAZA,UACnBE,UAAW,EAEXH,EAAEI,iBAEFJ,EAAEK,OAAOC,SAIG,YAAZL,SAAqC,cAAZA,UACzBE,UAAW,IAGVA,sBAMCI,KAAOP,EAAEK,OAAOG,cAAcC,cAAc,qBAC9CC,WAAY,EACZC,eAAgB,EAEhBJ,OACAG,UAAYH,KAAKK,iBAAiB,sBAElCF,WAAaA,UAAUG,OAAS,IAEhB,YAAZZ,QACAZ,cAEAA,aAAY,GAIZsB,cA3DQ,YACVG,OAAS1B,gBACfA,UAAW,EACJ0B,QAuDCC,GACgBL,UAAUA,UAAUG,OAAS,GAG7BH,UAAU,IAI9BC,eACApB,WAAWoB,gBAMnBK,SAASC,iBAAiB,YAAYjB,OAC9BA,EAAEK,OAAOa,QAAQ,6CAA8C,OACzDX,KAAOP,EAAEK,OAAOc,QAAQ,qBACzBZ,kBAGCG,UAAYH,KAAKK,iBAAiB,yBACnCF,uBAICT,QAAUD,EAAEE,IAAIkB,kBAEjB,IAAIC,EAAI,EAAGA,EAAIX,UAAUG,OAAQQ,IAAK,OACjCC,KAAOZ,UAAUW,MAEU,GADhBC,KAAKC,KAAKC,OAAOJ,cACrBK,QAAQxB,SAAe,CAChCV,WAAW+B,kBAQ3BN,SAASC,iBAAiB,WAAWjB,OAI7BA,EAAEK,OAAOa,QAAQ,6BACjBnB,iBAAiBC,GAGjBA,EAAEK,OAAOa,QAAQ,oDACXjB,QAAUD,EAAEE,QACdwB,MAAO,QACLnB,KAAOP,EAAEK,OAAOc,QAAQ,qBAEzBZ,kBAGCG,UAAYH,KAAKK,iBAAiB,yBACnCF,oBAIU,aAAXT,QAAwB,KACnB,IAAIoB,EAAI,EAAGA,EAAIX,UAAUG,OAAS,EAAGQ,OAClCX,UAAUW,IAAMrB,EAAEK,OAAQ,CAC1BqB,KAAOhB,UAAUW,EAAI,SAIxBK,OAEDA,KAAOhB,UAAU,SAElB,GAAe,WAAXT,QAAsB,KAExB,IAAIoB,EAAI,EAAGA,EAAIX,UAAUG,OAAQQ,OAC9BX,UAAUW,IAAMrB,EAAEK,OAAQ,CAC1BqB,KAAOhB,UAAUW,EAAI,SAIxBK,OAEDA,KAAOhB,UAAUA,UAAUG,OAAS,QAEtB,QAAXZ,QAEPyB,KAAOhB,UAAU,GAEC,OAAXT,UAEPyB,KAAOhB,UAAUA,UAAUG,OAAS,IAIpCa,OACA1B,EAAEI,iBACFb,WAAWmC,oCAMrB,aAAaC,GAAG,qBAAqB3B,UAC7B4B,OAAS5B,EAAEK,OAAOI,yBAAkBT,EAAE6B,cAAcC,aAAa,qCACnEF,QAEAhC,YAAW,KACPmC,iBAAiBC,UAAUJ,kCAKrC,aAAaD,GAAG,sBAAsB3B,sBACrBA,EAAEK,OAAOI,yBAAkBT,EAAE6B,cAAcC,aAAa,sCAEnEC,iBAAiBE,oBAIfhC,QAAUD,EAAEK,OAAOI,cAAc,4BAEjCyB,+BAAUlC,EAAEmC,yDAAY9B,UAAWW,SAASoB,gBAAkBpB,SAASqB,KAAOrB,SAASoB,cAAgB,MACzGnC,SAAWiC,SAAWlC,EAAEK,OAAOiC,SAASJ,UACxC3C,WAAWU,SAAS,IACZe,SAASoB,gBAAkBpB,SAASqB,MAMjCrC,EAAEK,OAAOiC,SAAStB,SAASoB,qBA2O5CG,cAAgB,KAClBvB,SAASC,iBAAiB,WAAWjB,IAC7B,CAAC,UAAW,YAAa,YAAa,aAAc,OAAQ,OAAOwC,SAASxC,EAAEE,MAC1EF,EAAEK,OAAOa,QAAQ,kCA/CVlB,CAAAA,UACbyC,QAAUzC,EAAEK,OAAOc,QAAQ,oBAC3BuB,SAAuD,YAA5CD,QAAQX,aAAa,oBAChCa,IAAMC,OAAOC,gBACbC,UAAYJ,SAAW,YAAeC,IAAM,YAAc,aAC1DI,cAAgBL,SAAW,UAAaC,IAAM,aAAe,YAC7DK,KAAOC,MAAMC,UAAUC,OAAOC,KAChCX,QAAQ7B,iBAAiB,iBACzByC,OAASA,IAAIC,mBAEZ,IAAIjC,EAAI,EAAGA,EAAI2B,KAAKnC,OAAQQ,IAC7B2B,KAAK3B,GAAGkC,MAAQlC,SAGZrB,EAAEE,UACD4C,UACD9C,EAAEI,sBACqBoD,IAAnBxD,EAAEK,OAAOkD,OAAuBP,KAAKhD,EAAEK,OAAOkD,MAAQ,GACtDP,KAAKhD,EAAEK,OAAOkD,MAAQ,GAAG1D,QAEzBmD,KAAK,GAAGnD,mBAGXkD,cACD/C,EAAEI,sBACqBoD,IAAnBxD,EAAEK,OAAOkD,OAAuBP,KAAKhD,EAAEK,OAAOkD,MAAQ,GACtDP,KAAKhD,EAAEK,OAAOkD,MAAQ,GAAG1D,QAEzBmD,KAAKA,KAAKnC,OAAS,GAAGhB,kBAGzB,OACDG,EAAEI,iBACF4C,KAAK,GAAGnD,kBAEP,MACDG,EAAEI,iBACF4C,KAAKA,KAAKnC,OAAS,GAAGhB,UAWlB4D,CAAezD,MAK3BgB,SAASC,iBAAiB,SAASjB,OAC3BA,EAAEK,OAAOa,QAAQ,+EAAgF,OAC3F8B,KAAOhD,EAAEK,OAAOc,QAAQ,oBAAoBP,iBAAiB,6CACnEZ,EAAEI,qCACAJ,EAAEK,QAAQgD,IAAI,QAChBL,KAAKU,SAAQL,MACTA,IAAIM,UAAY,KAEpB3D,EAAEK,OAAOsD,SAAW,qBAsBZ,KAChBxE,cA1QgB,0BACd6B,UAAUW,GAAG,oBAAoB3B,OAC3BA,EAAE6B,cAAcX,QAAQ,qBAAsB,OACxC0C,SAAW5D,EAAE6B,cACbgC,QAAU7C,SAASP,yBAAkBmD,SAAS9B,aAAa,yCAE7D+B,QAAS,OACHC,eAAiBD,QAAQpD,cAAc,yCAG7Cb,YAAW,QACHkE,eACAA,eAAeC,UAAUC,IAAI,UAC7BJ,SAASK,aAAa,wBAAyBH,eAAeI,QAC3D,OACGC,YAAcN,QAAQpD,cAAc,mBAC1C0D,YAAYF,aAAa,gBAAiB,QAC1CE,YAAYJ,UAAUC,IAAI,UAC1BJ,SAASK,aAAa,wBAAyBE,YAAYD,OAEhE,4BAKblD,UAAUW,GAAG,sBAAsB3B,OAC7BA,EAAE6B,cAAcX,QAAQ,qBAAsB,OACxC0C,SAAW5D,EAAE6B,cACbgC,QAAU7C,SAASP,yBAAkBmD,SAAS9B,aAAa,sCAEjE8B,SAASQ,gBAAgB,yBAErBP,SACAjE,YAAW,KAEPiE,QAAQjD,iBAAiB,0BAA0B8C,SAAQW,SACvDA,OAAON,UAAUO,OAAO,eAE7B,OAMftD,SAASC,iBAAiB,WAAWjB,OAC7BA,EAAEK,OAAOa,QAAQ,gEAAiE,OAC5E0C,SAAW5D,EAAEK,OACbJ,QAAUD,EAAEE,QACdwB,KAAO,WACLmC,QAAU7C,SAASP,yBAAkBmD,SAAS9B,aAAa,sCAC3DyC,QAAUV,QAAQjD,iBAAiB,mBACnC4D,aAAeX,QAAQpD,cAAc,0BACrCgE,SAAWb,SAASc,aAAa,wBAKnCH,UAAYC,cAAgBC,UAAW,IACxB,aAAXxE,QAAwB,KACnB,IAAIoB,EAAI,EAAGA,EAAIkD,QAAQ1D,OAAS,EAAGQ,OAChCkD,QAAQlD,IAAMmD,aAAc,CAC5B9C,KAAO6C,QAAQlD,EAAI,SAIvBoD,WAAa/C,OACbA,KAAO6C,QAAQ,OAEN,WAAXtE,QAAsB,KACnB,IAAIoB,EAAI,EAAGA,EAAIkD,QAAQ1D,OAAQQ,OAC5BkD,QAAQlD,IAAMmD,aAAc,CAC5B9C,KAAO6C,QAAQlD,EAAI,SAIvBoD,WAAa/C,OACbA,KAAO6C,QAAQA,QAAQ1D,OAAS,SAEjC,GAAe,QAAXZ,SAAsBwE,SAE1B,GAAe,OAAXxE,SAAqBwE,UAEzB,GAAgB,KAAXxE,UAAmBwE,UAAwB,SAAXxE,QACxCD,EAAEI,iBACFuE,aAAaf,SAAUY,mBACpB,IAAKC,aAGH,IAAIpD,EAAI,EAAGA,EAAIkD,QAAQ1D,OAAQQ,IAAK,OAC/BgD,OAASE,QAAQlD,GACjBuD,WAAaP,OAAOQ,YAAYrD,OAAOJ,cACvC0D,WAAa9E,EAAEE,IAAIkB,iBACa,GAAlCwD,WAAWnD,QAAQqD,YAAkB,CACrCpD,KAAO2C,oBAZf3C,KAAO6C,QAAQA,QAAQ1D,OAAS,QAFhCa,KAAO6C,QAAQ,GAqBf7C,OACA1B,EAAEI,iBACEoE,cACAA,aAAaT,UAAUO,OAAO,UAElC5C,KAAKqC,UAAUC,IAAI,UACnBJ,SAASK,aAAa,wBAAyBvC,KAAKwC,IACpDxC,KAAKqD,eAAe,CAACC,MAAO,kBAM5ChE,SAASC,iBAAiB,SAASjB,UACzBqE,OAASrE,EAAEK,OAAOc,QAAQ,uCAC5BkD,OAAQ,OACFR,QAAUQ,OAAOlD,QAAQ,oBACzByC,SAAW5C,SAASP,yDAAkDoD,QAAQK,UAChFN,UACAe,aAAaf,SAAUS,YAMnCrD,SAASC,iBAAiB,UAAUjB,OAC5BA,EAAEK,OAAOa,QAAQ,4BAA6B,OACxC0C,SAAW5C,SAASP,8DAAuDT,EAAEK,OAAO6D,UACpFG,OAASrE,EAAEK,OAAOG,cAAcC,oDAA6CT,EAAEK,OAAO4E,aAExFrB,UAAYS,QACZM,aAAaf,SAAUS,kBAK7BM,aAAe,CAACf,SAAUS,gBAEtBa,kBADUb,OAAOlD,QAAQ,oBACGV,cAAc,4CAE5CyE,mBAAqBb,SACjBa,mBACAA,kBAAkBd,gBAAgB,iBAEtCC,OAAOJ,aAAa,gBAAiB,SAGrCL,SAASc,aAAa,SACtBd,SAASqB,MAAQZ,OAAOc,QAAQC,WAAaf,OAAOQ,YAAYQ,QAAQ,oBAAqB,KAAK7D,OAElGoC,SAASiB,YAAcR,OAAOc,QAAQC,WAAaf,OAAOQ,YAG1DjB,SAASuB,QAAQG,aAAc,OACzBA,aAAetE,SAASuE,eAAe3B,SAASuB,QAAQG,cAC1DA,cAAiBA,aAAaL,OAASZ,OAAOc,QAAQF,QACtDK,aAAaL,MAAQZ,OAAOc,QAAQF,MACpCK,aAAaE,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,SA8GrEC,GApGA/C,OAAO3B,iBAAiB,QAAQ,WACtB2E,OAAS5E,SAASJ,iBAAiB,8CACzCqC,MAAMC,UAAUQ,QAAQN,KAAKwC,QAAQC,mBAGjCA,iBAAiBC,WAAa,IAC9BD,iBAAiBzB,gBAAgB,6BAgGzC7B,gBAfAvB,SAASC,iBAAiB,WAAWjB,IAC7BA,EAAEK,OAAOa,QAAQ,6BAEH,MAAVlB,EAAEE,MACFF,EAAEI,iBACFJ,EAAEK,OAAOC"} \ No newline at end of file diff --git a/theme/boost/amd/src/aria.js b/theme/boost/amd/src/aria.js index 6793b0fb44a..0b42157cde2 100644 --- a/theme/boost/amd/src/aria.js +++ b/theme/boost/amd/src/aria.js @@ -23,6 +23,7 @@ import $ from 'jquery'; import Pending from 'core/pending'; +import * as FocusLockManager from 'core/local/aria/focuslock'; /** * Drop downs from bootstrap don't support keyboard accessibility by default. @@ -78,13 +79,10 @@ const dropdownFix = () => { const menu = e.target.parentElement.querySelector('[role="menu"]'); let menuItems = false; let foundMenuItem = false; - let textInput = false; if (menu) { menuItems = menu.querySelectorAll('[role="menuitem"]'); - textInput = e.target.parentElement.querySelector('[data-action="search"]'); } - if (menuItems && menuItems.length > 0) { // Up key opens the menu at the end. if (trigger === 'ArrowUp') { @@ -101,10 +99,7 @@ const dropdownFix = () => { } } - if (textInput) { - shiftFocus(textInput); - } - if (foundMenuItem && textInput === null) { + if (foundMenuItem) { shiftFocus(foundMenuItem); } }; @@ -198,10 +193,26 @@ const dropdownFix = () => { } }); + $('.dropdown').on('shown.bs.dropdown', e => { + const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`); + if (dialog) { + // Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling. + setTimeout(() => { + FocusLockManager.trapFocus(dialog); + }); + } + }); + $('.dropdown').on('hidden.bs.dropdown', e => { + const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`); + if (dialog) { + FocusLockManager.untrapFocus(); + } + // We need to focus on the menu trigger. const trigger = e.target.querySelector('[data-toggle="dropdown"]'); - const focused = document.activeElement != document.body ? document.activeElement : null; + // If it's a click event, then no element is focused because the clicked element is inside a closed dropdown. + const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null); if (trigger && focused && e.target.contains(focused)) { shiftFocus(trigger, () => { if (document.activeElement === document.body) { @@ -297,9 +308,9 @@ const comboboxFix = () => { if (editable && !next) { next = options[options.length - 1]; } - } else if (trigger == 'Home') { + } else if (trigger == 'Home' && !editable) { next = options[0]; - } else if (trigger == 'End') { + } else if (trigger == 'End' && !editable) { next = options[options.length - 1]; } else if ((trigger == ' ' && !editable) || trigger == 'Enter') { e.preventDefault(); @@ -338,7 +349,6 @@ const comboboxFix = () => { const listbox = option.closest('[role="listbox"]'); const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`); if (combobox) { - combobox.focus(); selectOption(combobox, option); } } @@ -368,9 +378,9 @@ const comboboxFix = () => { } if (combobox.hasAttribute('value')) { - combobox.value = option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim(); + combobox.value = option.dataset.shortText || option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim(); } else { - combobox.textContent = option.textContent; + combobox.textContent = option.dataset.shortText || option.textContent; } if (combobox.dataset.inputElement) { diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 158e53fd0f1..250b73bb607 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -3121,14 +3121,16 @@ blockquote { .usersearchdropdown, .gradesearchdropdown, .groupsearchdropdown { - max-width: 350px; - .searchresultitemscontainer { - max-height: 170px; - overflow: auto; - /* stylelint-disable declaration-no-important */ - img { - height: 48px !important; - width: 48px !important; + &.dropdown-menu { + width: 350px; + .searchresultitemscontainer { + max-height: 170px; + overflow: auto; + /* stylelint-disable declaration-no-important */ + img { + height: 48px !important; + width: 48px !important; + } } } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 859e8c8875d..9a190efab09 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -26026,21 +26026,21 @@ blockquote { } /* Combobox search dropdowns */ -.usersearchdropdown, -.gradesearchdropdown, -.groupsearchdropdown { - max-width: 350px; +.usersearchdropdown.dropdown-menu, +.gradesearchdropdown.dropdown-menu, +.groupsearchdropdown.dropdown-menu { + width: 350px; } -.usersearchdropdown .searchresultitemscontainer, -.gradesearchdropdown .searchresultitemscontainer, -.groupsearchdropdown .searchresultitemscontainer { +.usersearchdropdown.dropdown-menu .searchresultitemscontainer, +.gradesearchdropdown.dropdown-menu .searchresultitemscontainer, +.groupsearchdropdown.dropdown-menu .searchresultitemscontainer { max-height: 170px; overflow: auto; /* stylelint-disable declaration-no-important */ } -.usersearchdropdown .searchresultitemscontainer img, -.gradesearchdropdown .searchresultitemscontainer img, -.groupsearchdropdown .searchresultitemscontainer img { +.usersearchdropdown.dropdown-menu .searchresultitemscontainer img, +.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img, +.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img { height: 48px !important; width: 48px !important; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 175cea99e78..2ce2cd99c84 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -26026,21 +26026,21 @@ blockquote { } /* Combobox search dropdowns */ -.usersearchdropdown, -.gradesearchdropdown, -.groupsearchdropdown { - max-width: 350px; +.usersearchdropdown.dropdown-menu, +.gradesearchdropdown.dropdown-menu, +.groupsearchdropdown.dropdown-menu { + width: 350px; } -.usersearchdropdown .searchresultitemscontainer, -.gradesearchdropdown .searchresultitemscontainer, -.groupsearchdropdown .searchresultitemscontainer { +.usersearchdropdown.dropdown-menu .searchresultitemscontainer, +.gradesearchdropdown.dropdown-menu .searchresultitemscontainer, +.groupsearchdropdown.dropdown-menu .searchresultitemscontainer { max-height: 170px; overflow: auto; /* stylelint-disable declaration-no-important */ } -.usersearchdropdown .searchresultitemscontainer img, -.gradesearchdropdown .searchresultitemscontainer img, -.groupsearchdropdown .searchresultitemscontainer img { +.usersearchdropdown.dropdown-menu .searchresultitemscontainer img, +.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img, +.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img { height: 48px !important; width: 48px !important; } diff --git a/user/amd/build/comboboxsearch/user.min.js b/user/amd/build/comboboxsearch/user.min.js index 887542d2775..49a56ffb080 100644 --- a/user/amd/build/comboboxsearch/user.min.js +++ b/user/amd/build/comboboxsearch/user.min.js @@ -1,3 +1,3 @@ -define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery","core/notification"],(function(_exports,_search_combobox,_str,_templates,_jquery,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}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,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()})),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}triggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e).catch(_notification.default.exception),e.target.closest(this.selectors.component)&&e.stopImmediatePropagation(),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(this.selectors.resetPageButton)&&(window.location=e.target.closest(this.selectors.resetPageButton).href)}keyHandler(e){switch(super.keyHandler(e),e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(document.activeElement===this.getHTMLElements().searchInput&&"Enter"===e.key&&null!==this.selectAllResultsLink()&&(window.location=this.selectAllResultsLink()),document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(this.selectors.resetPageButton)){window.location=e.target.closest(this.selectors.resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll&&!e.shiftKey?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch())}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.component.setAttribute("aria-expanded","true")):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.component.setAttribute("aria-expanded","false"))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default})); +define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery"],(function(_exports,_search_combobox,_str,_templates,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}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,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),["click","focus"].forEach((eventType=>{document.addEventListener(eventType,(e=>{this.searchDropdown.classList.contains("show")&&!this.combobox.contains(e.target)&&this.toggleDropdown()}),!0)})),this.component.addEventListener("keydown",this.keyHandler.bind(this)),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'},this.courseID=this.component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid,this.instance=this.component.querySelector(this.selectors.instance).dataset.instance,this.renderDefault()}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,instance:this.instance,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){if(this.getPreppedSearchTerm()){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}return[]}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")");break}}return user})))}changeHandler(e){this.toggleDropdown(),"0"===e.target.value?window.location=this.selectAllResultsLink():window.location=this.selectOneLink(e.target.value)}keyHandler(e){switch(e.key){case"ArrowUp":case"ArrowDown":""!==this.getSearchTerm()&&!this.searchDropdown.classList.contains("show")&&e.target.contains(this.combobox)&&this.renderAndShow();break;case"Enter":case" ":if(e.target.closest(this.selectors.resetPageButton)){e.stopPropagation(),window.location=e.target.closest(this.selectors.resetPageButton).href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0})}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","true"),this.searchInput.focus({preventScroll:!0})):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","false"),this.searchInput.removeAttribute("aria-activedescendant"),this.searchDropdown.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default})); //# sourceMappingURL=user.min.js.map \ No newline at end of file diff --git a/user/amd/build/comboboxsearch/user.min.js.map b/user/amd/build/comboboxsearch/user.min.js.map index 87c9b6b9690..c7c46bf45b4 100644 --- a/user/amd/build/comboboxsearch/user.min.js.map +++ b/user/amd/build/comboboxsearch/user.min.js.map @@ -1 +1 @@ -{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_user/comboboxsearch/user\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 {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\nimport Notification from 'core/notification';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\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 const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[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 * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\n }\n }\n return user;\n })\n );\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).catch(Notification.exception);\n if (e.target.closest(this.selectors.component)) {\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 if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).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\n if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {\n window.location = this.selectAllResultsLink();\n }\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Enter':\n case ' ':\n if (document.activeElement === this.getHTMLElements().searchInput) {\n if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {\n window.location = this.selectAllResultsLink();\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n if (e.target.closest('.dropdown-item')) {\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll && !e.shiftKey) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.component.setAttribute('aria-expanded', 'true');\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n this.component.setAttribute('aria-expanded', 'false');\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\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 */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\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 getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'fullname',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","document","addEventListener","e","target","closest","this","selectors","component","searchDropdown","classList","contains","toggleDropdown","courseid","groupid","resetPageButton","querySelector","componentSelector","courseID","dataset","groupID","_document$querySelect","_document$querySelect2","dropdownSelector","triggerSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","fetchDataset","Error","name","filterableData","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","getPreppedSearchTerm","setMatchedResults","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","matchingField","email","link","selectOneLink","id","clickHandler","catch","Notification","exception","stopImmediatePropagation","currentViewAll","button","window","location","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","focus","preventScroll","clearSearch","shiftKey","add","show","setAttribute","remove","hide","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_user/comboboxsearch/user\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 {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a couple of events onto the document since we need to check if they are moving off the component.\n ['click', 'focus'].forEach(eventType => {\n // Since we are handling dropdowns manually, ensure we can close it when moving off.\n document.addEventListener(eventType, e => {\n if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {\n this.toggleDropdown();\n }\n }, true);\n });\n\n // Register keyboard events.\n this.component.addEventListener('keydown', this.keyHandler.bind(this));\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;\n\n // We need to render some content by default for ARIA purposes.\n this.renderDefault();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, 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\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\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 if (this.getPreppedSearchTerm()) {\n const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n } else {\n return [];\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 * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n break;\n }\n }\n return user;\n })\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 this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n if (e.target.value === '0') {\n window.location = this.selectAllResultsLink();\n } else {\n window.location = this.selectOneLink(e.target.value);\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 // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n case 'ArrowDown':\n if (\n this.getSearchTerm() !== ''\n && !this.searchDropdown.classList.contains('show')\n && e.target.contains(this.combobox)\n ) {\n this.renderAndShow();\n }\n break;\n case 'Enter':\n case ' ':\n if (e.target.closest(this.selectors.resetPageButton)) {\n e.stopPropagation();\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n this.searchInput.focus({preventScroll: true});\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n\n // As we are manually handling the dropdown, we need to do some housekeeping manually.\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false');\n this.searchInput.removeAttribute('aria-activedescendant');\n this.searchDropdown.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\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} userID The ID of the user selected.\n */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\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 getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'fullname',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","forEach","eventType","document","addEventListener","e","this","searchDropdown","classList","contains","combobox","target","toggleDropdown","component","keyHandler","bind","selectors","courseid","groupid","resetPageButton","courseID","querySelector","dataset","groupID","_document$querySelect","_document$querySelect2","instance","renderDefault","componentSelector","dropdownSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchInput","removeAttribute","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","fetchDataset","Error","name","filterableData","getPreppedSearchTerm","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","matchingField","email","changeHandler","window","location","selectOneLink","renderAndShow","closest","stopPropagation","href","focus","preventScroll","add","show","setAttribute","remove","hide","querySelectorAll","option","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"} \ No newline at end of file diff --git a/user/amd/src/comboboxsearch/user.js b/user/amd/src/comboboxsearch/user.js index ce8655de4c0..c1350902980 100644 --- a/user/amd/src/comboboxsearch/user.js +++ b/user/amd/src/comboboxsearch/user.js @@ -24,7 +24,6 @@ import search_combobox from 'core/comboboxsearch/search_combobox'; import {getStrings} from 'core/str'; import {renderForPromise, replaceNodeContents} from 'core/templates'; import $ from 'jquery'; -import Notification from 'core/notification'; export default class UserSearch extends search_combobox { @@ -36,14 +35,19 @@ export default class UserSearch extends search_combobox { constructor() { super(); - // Register a small click event onto the document since we need to check if they are clicking off the component. - document.addEventListener('click', (e) => { - // Since we are handling dropdowns manually, ensure we can close it when clicking off. - if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) { - this.toggleDropdown(); - } + // Register a couple of events onto the document since we need to check if they are moving off the component. + ['click', 'focus'].forEach(eventType => { + // Since we are handling dropdowns manually, ensure we can close it when moving off. + document.addEventListener(eventType, e => { + if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) { + this.toggleDropdown(); + } + }, true); }); + // Register keyboard events. + this.component.addEventListener('keydown', this.keyHandler.bind(this)); + // Define our standard lookups. this.selectors = {...this.selectors, courseid: '[data-region="courseid"]', @@ -51,9 +55,12 @@ export default class UserSearch extends search_combobox { resetPageButton: '[data-action="resetpage"]', }; - const component = document.querySelector(this.componentSelector()); - this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; + this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid; this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid; + this.instance = this.component.querySelector(this.selectors.instance).dataset.instance; + + // We need to render some content by default for ARIA purposes. + this.renderDefault(); } static init() { @@ -78,15 +85,6 @@ export default class UserSearch extends search_combobox { return '.usersearchdropdown'; } - /** - * The triggering div that contains the searching widget. - * - * @returns {string} - */ - triggerSelector() { - return '.usersearchwidget'; - } - /** * Build the content then replace the node. */ @@ -94,11 +92,24 @@ export default class UserSearch extends search_combobox { const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', { users: this.getMatchedResults().slice(0, 5), hasresults: this.getMatchedResults().length > 0, + instance: this.instance, matches: this.getMatchedResults().length, searchterm: this.getSearchTerm(), selectall: this.selectAllResultsLink(), }); replaceNodeContents(this.getHTMLElements().searchDropdown, html, js); + // Remove aria-activedescendant when the available options change. + this.searchInput.removeAttribute('aria-activedescendant'); + } + + /** + * Build the content then replace the node by default we want our form to exist. + */ + async renderDefault() { + this.setMatchedResults(await this.filterDataset(await this.getDataset())); + this.filterMatchDataset(); + + await this.renderDropdown(); } /** @@ -117,13 +128,17 @@ export default class UserSearch extends search_combobox { * @returns {Array} The users that match the given criteria. */ async filterDataset(filterableData) { - const stringMap = await this.getStringMap(); - return filterableData.filter((user) => Object.keys(user).some((key) => { - if (user[key] === "" || user[key] === null || !stringMap.get(key)) { - return false; - } - return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm()); - })); + if (this.getPreppedSearchTerm()) { + const stringMap = await this.getStringMap(); + return filterableData.filter((user) => Object.keys(user).some((key) => { + if (user[key] === "" || user[key] === null || !stringMap.get(key)) { + return false; + } + return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm()); + })); + } else { + return []; + } } /** @@ -158,7 +173,6 @@ export default class UserSearch extends search_combobox { ); user.matchingField = `${escapedMatchingField} (${user.email})`; - user.link = this.selectOneLink(user.id); break; } } @@ -168,22 +182,17 @@ export default class UserSearch extends search_combobox { } /** - * The handler for when a user interacts with the component. + * The handler for when a user changes the value of the component (selects an option from the dropdown). * - * @param {MouseEvent} e The triggering event that we are working with. + * @param {Event} e The change event. */ - clickHandler(e) { - super.clickHandler(e).catch(Notification.exception); - if (e.target.closest(this.selectors.component)) { - // 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(); - } - if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) { + changeHandler(e) { + this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard. + + if (e.target.value === '0') { window.location = this.selectAllResultsLink(); - } - if (e.target.closest(this.selectors.resetPageButton)) { - window.location = e.target.closest(this.selectors.resetPageButton).href; + } else { + window.location = this.selectOneLink(e.target.value); } } @@ -193,50 +202,30 @@ export default class UserSearch extends search_combobox { * @param {KeyboardEvent} e The triggering event that we are working with. */ keyHandler(e) { - super.keyHandler(e); - - if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) { - window.location = this.selectAllResultsLink(); - } - // Switch the key presses to handle keyboard nav. switch (e.key) { + case 'ArrowUp': + case 'ArrowDown': + if ( + this.getSearchTerm() !== '' + && !this.searchDropdown.classList.contains('show') + && e.target.contains(this.combobox) + ) { + this.renderAndShow(); + } + break; case 'Enter': case ' ': - if (document.activeElement === this.getHTMLElements().searchInput) { - if (e.key === 'Enter' && this.selectAllResultsLink() !== null) { - window.location = this.selectAllResultsLink(); - } - } - if (document.activeElement === this.getHTMLElements().clearSearchButton) { - this.closeSearch(true); - break; - } if (e.target.closest(this.selectors.resetPageButton)) { + e.stopPropagation(); window.location = e.target.closest(this.selectors.resetPageButton).href; break; } - if (e.target.closest('.dropdown-item')) { - e.preventDefault(); - window.location = e.target.closest('.dropdown-item').href; - break; - } break; case 'Escape': this.toggleDropdown(); this.searchInput.focus({preventScroll: true}); break; - case 'Tab': - // If the current focus is on clear search, then check if viewall exists then around tab to it. - if (e.target.closest(this.selectors.clearSearch)) { - if (this.currentViewAll && !e.shiftKey) { - e.preventDefault(); - this.currentViewAll.focus({preventScroll: true}); - } else { - this.closeSearch(); - } - } - break; } } @@ -249,11 +238,18 @@ export default class UserSearch extends search_combobox { if (on) { this.searchDropdown.classList.add('show'); $(this.searchDropdown).show(); - this.component.setAttribute('aria-expanded', 'true'); + this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true'); + this.searchInput.focus({preventScroll: true}); } else { this.searchDropdown.classList.remove('show'); $(this.searchDropdown).hide(); - this.component.setAttribute('aria-expanded', 'false'); + + // As we are manually handling the dropdown, we need to do some housekeeping manually. + this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false'); + this.searchInput.removeAttribute('aria-activedescendant'); + this.searchDropdown.querySelectorAll('.active[role="option"]').forEach(option => { + option.classList.remove('active'); + }); } } @@ -266,6 +262,7 @@ export default class UserSearch 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} userID The ID of the user selected. */ diff --git a/user/templates/comboboxsearch/resultitem.mustache b/user/templates/comboboxsearch/resultitem.mustache index f91ead9bee4..7e1ddcf2375 100644 --- a/user/templates/comboboxsearch/resultitem.mustache +++ b/user/templates/comboboxsearch/resultitem.mustache @@ -22,7 +22,6 @@ * profileimageurl - Link for the users' large profile image. * matchingField - The field in the user object that matched the search criteria. * matchingFieldName - The name of the field that was matched upon for A11y purposes. - * link - The link used to redirect upon self to show only this specific user. Example context (json): { @@ -30,12 +29,12 @@ "fullname": "Foo bar", "profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630", "matchingField": "Foo bar", - "matchingFieldName": "Fullname", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "matchingFieldName": "Fullname" } }} {{ {{#profileimageurl}} diff --git a/user/templates/comboboxsearch/resultset.mustache b/user/templates/comboboxsearch/resultset.mustache index 1d6a1c6f3dc..b0c4bb2c88b 100644 --- a/user/templates/comboboxsearch/resultset.mustache +++ b/user/templates/comboboxsearch/resultset.mustache @@ -17,35 +17,35 @@ Wrapping template for returned result items. Context variables required for this template: + * instance - The instance ID of the combo box. * users - Our returned users to render. * found - Count of the found users. * total - Total count of users within this report. - * selectall - The created link that allows users to select all of the results. + * selectall - Whether to show the select all option. * searchterm - The entered text to find these results. * hasusers - Allow the handling where no users exist for the returned search term. Example context (json): { + "instance": 25, "users": [ { "id": 2, "fullname": "Foo bar", "profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630", "matchingField": "Foo bar", - "matchingFieldName": "Fullname", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "matchingFieldName": "Fullname" }, { "id": 3, "fullname": "Bar Foo", "profileimageurl": "http://foo.bar/pluginfile.php/80/user/icon/boost/f1?rev=7631", "matchingField": "Bar Foo", - "matchingFieldName": "Fullname", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + "matchingFieldName": "Fullname" } ], "matches": 20, - "selectall": "https://foo.bar/grade/report/grader/index.php?id=2&searchvalue=abe", + "selectall": true, "searchterm": "Foo", "hasresults": true } @@ -59,10 +59,15 @@ {{/results}} {{$selectall}} {{#selectall}} -
  • - - {{#str}}viewallresults, core, {{matches}}{{/str}} - +
  • {{/selectall}} {{/selectall}} diff --git a/user/templates/comboboxsearch/user_selector.mustache b/user/templates/comboboxsearch/user_selector.mustache index 7edd4a633e7..c59f1b514e2 100644 --- a/user/templates/comboboxsearch/user_selector.mustache +++ b/user/templates/comboboxsearch/user_selector.mustache @@ -20,6 +20,8 @@ The user selector trigger element. Context variables required for this template: + * name - The name of the input element representing the user search combobox. + * value - The value of the input element representing the user search combobox. * currentvalue - If the user has already searched, set the value to that. * courseid - The course ID. * group - The group ID. @@ -27,41 +29,33 @@ Example context (json): { + "name": "input-1", + "value": "0", "currentvalue": "bar", "courseid": 2, "group": 25, "resetlink": "grade/report/grader/index.php?id=2" } }} - - + + + +{{< core/search_input_auto }} + {{$label}}{{#str}}searchusers, core{{/str}}{{/label}} + {{$placeholder}}{{#str}}searchusers, core{{/str}}{{/placeholder}} + {{$value}}{{currentvalue}}{{/value}} + {{$additionalattributes}} + role="combobox" + aria-expanded="false" + aria-controls="user-{{instance}}-result-listbox" + aria-autocomplete="list" + aria-haspopup="listbox" + data-input-element="user-input-{{uniqid}}-{{instance}}" + {{/additionalattributes}} +{{/ core/search_input_auto }} {{#currentvalue}} - {{< core/search_input_auto }} - {{$label}}{{#str}} - searchusers, core - {{/str}}{{/label}} - {{$value}}{{currentvalue}}{{/value}} - {{$additionalattributes}} - aria-autocomplete="list" - data-input-element="user-input-{{uniqid}}" - {{/additionalattributes}} - {{/ core/search_input_auto }} {{#str}}clear{{/str}} {{/currentvalue}} -{{^currentvalue}} - {{< core/search_input_auto }} - {{$label}}{{#str}} - searchusers, core - {{/str}}{{/label}} - {{$placeholder}}{{#str}} - searchusers, core - {{/str}}{{/placeholder}} - {{$additionalattributes}} - aria-autocomplete="list" - data-input-element="user-input-{{uniqid}}" - {{/additionalattributes}} - {{/ core/search_input_auto }} -{{/currentvalue}} - +