From ee5c4f8f999df33bc8a262a7e823d8cdbd7fd97a Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Mon, 30 Oct 2023 07:39:12 +1100 Subject: [PATCH] MDL-78885 comboboxsearch: Reset status of filters to a zero state Most of this is automatically handled by aria.js for other comboboxes. But the search combobox inside the grade and group dropdowns are special cases because they are not the same as the toggle element of the dropdown. --- grade/amd/build/comboboxsearch/grade.min.js | 2 +- .../amd/build/comboboxsearch/grade.min.js.map | 2 +- grade/amd/src/comboboxsearch/grade.js | 18 ++++++++++++++++++ group/amd/build/comboboxsearch/group.min.js | 2 +- .../amd/build/comboboxsearch/group.min.js.map | 2 +- group/amd/src/comboboxsearch/group.js | 18 ++++++++++++++++++ 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/grade/amd/build/comboboxsearch/grade.min.js b/grade/amd/build/comboboxsearch/grade.min.js index 82c4451e4fe..df105889b78 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;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.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),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})); +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;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"}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),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 5f464bad09c..07346bba7ff 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 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.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 // 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 *\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","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","renderDefault","dropdownSelector","triggerSelector","html","js","results","getMatchedResults","hasresults","length","searchterm","getSearchTerm","removeAttribute","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","classList","add","remove","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,eAEnEQ,mBAAqBT,KAAKG,UAAUE,yBAAkBL,KAAKU,YAAYF,QAAQG,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAed,KAAKG,UAAUE,yBAAkBL,KAAKe,SAASP,QAAQG,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,qCAIE,IAAIxB,gBAQfU,0BACW,gBAQXe,yBACW,uBAQXC,wBACW,kDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,QAASzB,KAAK0B,oBACdC,WAAY3B,KAAK0B,oBAAoBE,OAAS,EAC9CC,WAAY7B,KAAK8B,qDAED9B,KAAKD,UAAUG,YAAaqB,KAAMC,SAEjDd,YAAYqB,gBAAgB,oDAO5BC,wBAAwBhC,KAAKiC,oBAAoBjC,KAAKkC,oBACtDC,2BAECnC,KAAKoC,sBAENC,mBACAC,wDASQC,WAAWC,eAAexC,KAAKO,UAAUkC,MAAMC,GAAMA,EAAEC,iCASpDC,sBAEoB,KAAhC5C,KAAK6C,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,MAGHJ,MAAMI,KAAKC,WAAWC,cAAcC,SAAStD,KAAK6C,4BAOjEV,0BACSH,kBACDhC,KAAK0B,oBAAoB6B,KAAKR,QACnB,CACHS,GAAIT,MAAMS,GACVC,KAAMV,MAAMU,UAS5BnB,2BAES5B,YAAYE,iBAAiB,SAAS,oBAAS8C,eAC3CC,eAAe3D,KAAKU,YAAYM,OAEN,KAA3BhB,KAAKU,YAAYM,WAEZ4C,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,gBAGtC/D,KAAKgE,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQnE,KAAKD,UAAUqE,eAChCH,EAAEI,uBAEG3D,YAAYM,MAAQ,QACpB2C,eAAe3D,KAAKU,YAAYM,YAChCN,YAAY4D,aACZV,kBAAkBC,UAAUC,IAAI,gBAE/B9D,KAAKgE,oBASnBO,cAAcN,GACVO,OAAOC,SAAWzE,KAAK0E,cAAcT,EAAEC,OAAOlD,OAMlD2D,6BAESjE,YAAYE,iBAAiB,SAAS,oBAAS,UAC3C+C,eAAe3D,KAAKU,YAAYM,OAER,KAAzBhB,KAAK8B,qBAEA8B,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,YAE7C,MAQPW,cAAcE,eACJ,IAAIC,8BAAuBD,4CAAmC5E,KAAKF,YAAY2D"} \ 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\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 * 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 // 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 *\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","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","triggerSelector","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,eAEnEQ,mBAAqBT,KAAKG,UAAUE,yBAAkBL,KAAKU,YAAYF,QAAQG,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAed,KAAKG,UAAUE,yBAAkBL,KAAKe,SAASP,QAAQG,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,QAAUnB,SAASC,yBAAkBL,KAAKU,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3B/B,KAAKU,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,qCAIE,IAAIpC,gBAQfU,0BACW,gBAQX2B,yBACW,uBAQXC,wBACW,kDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,QAASrC,KAAKsC,oBACdC,WAAYvC,KAAKsC,oBAAoBE,OAAS,EAC9CC,WAAYzC,KAAK0C,qDAED1C,KAAKD,UAAUG,YAAaiC,KAAMC,SAEjD1B,YAAYY,gBAAgB,oDAO5BqB,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,2BAESvC,YAAYE,iBAAiB,SAAS,oBAASyD,eAC3CC,eAAetE,KAAKU,YAAYM,OAEN,KAA3BhB,KAAKU,YAAYM,WAEZuD,kBAAkB3C,UAAU4C,IAAI,eAGhCD,kBAAkB3C,UAAUC,OAAO,gBAGtC7B,KAAKyE,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQ5E,KAAKD,UAAU8E,eAChCH,EAAEI,uBAEGpE,YAAYM,MAAQ,QACpBsD,eAAetE,KAAKU,YAAYM,YAChCN,YAAYqE,aACZR,kBAAkB3C,UAAU4C,IAAI,gBAE/BxE,KAAKyE,oBASnBO,cAAcN,GACVO,OAAOC,SAAWlF,KAAKmF,cAAcT,EAAEC,OAAO3D,OAMlDoE,6BAES1E,YAAYE,iBAAiB,SAAS,oBAAS,UAC3C0D,eAAetE,KAAKU,YAAYM,OAER,KAAzBhB,KAAK0C,qBAEA6B,kBAAkB3C,UAAU4C,IAAI,eAGhCD,kBAAkB3C,UAAUC,OAAO,YAE7C,MAQPsD,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 6b3582097bc..6b42f031698 100644 --- a/grade/amd/src/comboboxsearch/grade.js +++ b/grade/amd/src/comboboxsearch/grade.js @@ -54,6 +54,24 @@ export default class GradeItemSearch extends search_combobox { 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(); } diff --git a/group/amd/build/comboboxsearch/group.min.js b/group/amd/build/comboboxsearch/group.min.js index 003d49ca1d5..51ce4c01aa6 100644 --- a/group/amd/build/comboboxsearch/group.min.js +++ b/group/amd/build/comboboxsearch/group.min.js @@ -1,3 +1,3 @@ -define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_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),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid;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.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups: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(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}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(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); +define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_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),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid;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().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups: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(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}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(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/group/amd/build/comboboxsearch/group.min.js.map b/group/amd/build/comboboxsearch/group.min.js.map index 2ca49cd5001..2aa6fc1fb3d 100644 --- a/group/amd/build/comboboxsearch/group.min.js.map +++ b/group/amd/build/comboboxsearch/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_group/comboboxsearch/group\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 {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n constructor() {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\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.renderDefault().catch(Notification.exception);\n }\n\n static init() {\n return new GroupSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.groupsearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: 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 groupFetch(this.courseID).then((r) => r.groups);\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((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[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((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\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 *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","renderDefault","catch","Notification","exception","dropdownSelector","triggerSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","removeAttribute","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","registerInputEvents","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","async","setSearchTerms","clearSearchButton","classList","add","remove","filterrenderpipe","e","target","closest","clearSearch","stopPropagation","focus","changeHandler","window","location","selectOneLink","registerInputHandlers","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAKrCC,wGAFqB,CAAC,KAAM,OAAQ,uBAI3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,eAEnEQ,mBAAqBT,KAAKG,UAAUE,yBAAkBL,KAAKU,YAAYF,QAAQG,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAed,KAAKG,UAAUE,yBAAkBL,KAAKe,SAASP,QAAQG,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,gBAAgBC,MAAMC,sBAAaC,gCAIjC,IAAI3B,YAQfU,0BACW,gBAQXkB,yBACW,uBAQXC,wBACW,kDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQ5B,KAAK6B,oBACbC,WAAY9B,KAAK6B,oBAAoBE,OAAS,EAC9CC,WAAYhC,KAAKiC,qDAEDjC,KAAKD,UAAUG,YAAawB,KAAMC,SAEjDjB,YAAYwB,gBAAgB,oDAO5BC,wBAAwBnC,KAAKoC,oBAAoBpC,KAAKqC,oBACtDC,2BAECtC,KAAKuC,sBAENC,mBACAC,wDASQ,0BAAWzC,KAAKO,UAAUmC,MAAMC,GAAMA,EAAEf,6BASrCgB,sBAEoB,KAAhC5C,KAAK6C,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAenD,KAAKoD,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAASrD,KAAK6C,4BAOjEP,0BACSH,kBACDnC,KAAK6B,oBAAoB2B,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,mBASrClB,2BAES/B,YAAYE,iBAAiB,SAAS,oBAASgD,eAC3CC,eAAe7D,KAAKU,YAAYM,OAEN,KAA3BhB,KAAKU,YAAYM,WAEZ8C,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,gBAGtCjE,KAAKkE,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQrE,KAAKD,UAAUuE,eAChCH,EAAEI,uBAEG7D,YAAYM,MAAQ,QACpB6C,eAAe7D,KAAKU,YAAYM,YAChCN,YAAY8D,aACZV,kBAAkBC,UAAUC,IAAI,gBAE/BhE,KAAKkE,oBASnBO,cAAcN,GACVO,OAAOC,SAAW3E,KAAK4E,cAAcT,EAAEC,OAAOpD,OAMlD6D,6BAESnE,YAAYE,iBAAiB,SAAS,oBAAS,UAC3CiD,eAAe7D,KAAKU,YAAYM,OAER,KAAzBhB,KAAKiC,qBAEA6B,kBAAkBC,UAAUC,IAAI,eAGhCF,kBAAkBC,UAAUE,OAAO,YAE7C,MAQPW,cAAcE,eACJ,IAAIC,8BAAuBD,4CAAmC9E,KAAKF,YAAY4D"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_group/comboboxsearch/group\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 {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n constructor() {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\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().catch(Notification.exception);\n }\n\n static init() {\n return new GroupSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.groupsearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: 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 groupFetch(this.courseID).then((r) => r.groups);\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((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[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((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\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 *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","triggerSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","registerInputEvents","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","async","setSearchTerms","clearSearchButton","add","filterrenderpipe","e","target","closest","clearSearch","stopPropagation","focus","changeHandler","window","location","selectOneLink","registerInputHandlers","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAKrCC,wGAFqB,CAAC,KAAM,OAAQ,uBAI3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,eAEnEQ,mBAAqBT,KAAKG,UAAUE,yBAAkBL,KAAKU,YAAYF,QAAQG,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAed,KAAKG,UAAUE,yBAAkBL,KAAKe,SAASP,QAAQG,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,QAAUnB,SAASC,yBAAkBL,KAAKU,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3B/B,KAAKU,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCAIjC,IAAIvC,YAQfU,0BACW,gBAQX8B,yBACW,uBAQXC,wBACW,kDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQxC,KAAKyC,oBACbC,WAAY1C,KAAKyC,oBAAoBE,OAAS,EAC9CC,WAAY5C,KAAK6C,qDAED7C,KAAKD,UAAUG,YAAaoC,KAAMC,SAEjD7B,YAAYY,gBAAgB,oDAO5BwB,wBAAwB9C,KAAK+C,oBAAoB/C,KAAKgD,oBACtDC,2BAECjD,KAAKkD,sBAENC,mBACAC,wDASQ,0BAAWpD,KAAKO,UAAU8C,MAAMC,GAAMA,EAAEd,6BASrCe,sBAEoB,KAAhCvD,KAAKwD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe9D,KAAK+D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAShE,KAAKwD,4BAOjEP,0BACSH,kBACD9C,KAAKyC,oBAAoB0B,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,mBASrClB,2BAES1C,YAAYE,iBAAiB,SAAS,oBAAS2D,eAC3CC,eAAexE,KAAKU,YAAYM,OAEN,KAA3BhB,KAAKU,YAAYM,WAEZyD,kBAAkB7C,UAAU8C,IAAI,eAGhCD,kBAAkB7C,UAAUC,OAAO,gBAGtC7B,KAAK2E,qBACZ,yBAQYC,GACXA,EAAEC,OAAOC,QAAQ9E,KAAKD,UAAUgF,eAChCH,EAAEI,uBAEGtE,YAAYM,MAAQ,QACpBwD,eAAexE,KAAKU,YAAYM,YAChCN,YAAYuE,aACZR,kBAAkB7C,UAAU8C,IAAI,gBAE/B1E,KAAK2E,oBASnBO,cAAcN,GACVO,OAAOC,SAAWpF,KAAKqF,cAAcT,EAAEC,OAAO7D,OAMlDsE,6BAES5E,YAAYE,iBAAiB,SAAS,oBAAS,UAC3C4D,eAAexE,KAAKU,YAAYM,OAER,KAAzBhB,KAAK6C,qBAEA4B,kBAAkB7C,UAAU8C,IAAI,eAGhCD,kBAAkB7C,UAAUC,OAAO,YAE7C,MAQPwD,cAAcE,eACJ,IAAIC,8BAAuBD,4CAAmCvF,KAAKF,YAAYuE"} \ No newline at end of file diff --git a/group/amd/src/comboboxsearch/group.js b/group/amd/src/comboboxsearch/group.js index 536db986ad8..c218f042e50 100644 --- a/group/amd/src/comboboxsearch/group.js +++ b/group/amd/src/comboboxsearch/group.js @@ -53,6 +53,24 @@ export default class GroupSearch extends search_combobox { 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().catch(Notification.exception); }