diff --git a/group/amd/build/comboboxsearch/group.min.js b/group/amd/build/comboboxsearch/group.min.js index ee1a6951c64ba..561ee3dd43461 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(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",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,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;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(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,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()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).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}))))}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)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),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(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]',togglegroupingbutton:'[data-action="togglegrouping"]',togglegroupingtarget:".toggle-grouping-target"};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;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(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){let groups=this.getMatchedResults(),groupings=[],groupWithoutGrouping=[];groups.forEach((group=>{0!==group.groupings.length?group.groupings.forEach((grouping=>{groupings[grouping.id]||(groupings[grouping.id]={id:grouping.id,name:grouping.name,groups:[{id:0,name:grouping.allparticipants,groupimageurl:grouping.groupingimageurl,groupings:[{id:grouping.id,name:grouping.name}]}]}),groupings[grouping.id].groups.push(group)})):groupWithoutGrouping.push(group)})),groupings=groupings.filter((grouping=>void 0!==grouping)),console.log(groupings);const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groupings:groupings,groupwithoutgrouping:groupWithoutGrouping,hasresults:groups.length>0,instance:this.instance,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()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).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,groupings:group.groupings}))))}async clickHandler(e){if(e.target.closest(this.selectors.togglegroupingbutton)){e.stopPropagation();let toggleButton=e.target.closest(this.selectors.togglegroupingbutton),toggleTarget=document.querySelector(toggleButton.dataset.target);return toggleTarget.classList.toggle("show"),toggleTarget.scrollIntoView(!1),document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target=>{target!==toggleTarget&&target.classList.remove("show")})),void toggleButton.setAttribute("aria-expanded","true"===toggleButton.getAttribute("aria-expanded")?"false":"true")}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)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),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 5a5c17602e515..61887857872bb 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 cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\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 // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\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 /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\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 * 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 instance: this.instance,\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 }\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, this.cmID).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 * 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(async() => {\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 // User has given something for us to filter against.\n await this.filterrenderpipe();\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} 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","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","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","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,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,QAAUrB,SAASC,yBAAkBL,KAAKY,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BjC,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAI1C,mEADI,MASnBW,0BACW,gBAQXgC,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQzC,KAAK0C,oBACbC,WAAY3C,KAAK0C,oBAAoBE,OAAS,EAC9CnC,SAAUT,KAAKS,SACfoC,WAAY7C,KAAK8C,qDAED9C,KAAKD,UAAUG,YAAaqC,KAAMC,SAEjD5B,YAAYY,gBAAgB,oDAO5BuB,wBAAwB/C,KAAKgD,oBAAoBhD,KAAKiD,oBACtDC,2BAEClD,KAAKmD,sBAENC,gDASQ,0BAAWpD,KAAKO,SAAUP,KAAKU,MAAM2C,MAAMC,GAAMA,EAAEb,6BAShDc,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,4BAOjEN,0BACSH,kBACD/C,KAAK0C,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQzE,KAAKD,UAAU2E,eAChCH,EAAEI,uBAEG/D,YAAYM,MAAQ,QACpB0D,eAAe5E,KAAKY,YAAYM,YAChCN,YAAYiE,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B/E,KAAKgF,oBASnBC,cAAcV,GACVW,OAAOC,SAAWnF,KAAKoF,cAAcb,EAAEC,OAAOtD,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAASwE,eAC3CV,eAAe5E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK8C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC/B,KAAKgF,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCvF,KAAKH,YAAYwE"} \ 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 cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n togglegroupingbutton: '[data-action=\"togglegrouping\"]',\n togglegroupingtarget: '.toggle-grouping-target',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\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 /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\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 * Build the content then replace the node.\n */\n async renderDropdown() {\n let groups = this.getMatchedResults();\n\n // Go through the groups and organise them by groupings.\n let groupings = [];\n\n // Groups without groupings.\n let groupWithoutGrouping = [];\n\n groups.forEach((group) => {\n // If the group has no groupings, add it to the group without grouping list.\n if (group.groupings.length === 0) {\n groupWithoutGrouping.push(group);\n return;\n }\n\n // Otherwise, add it to the groupings object.\n group.groupings.forEach((grouping) => {\n if (!groupings[grouping.id]) {\n groupings[grouping.id] = {\n id: grouping.id,\n name: grouping.name,\n groups: [\n // All participants option.\n {\n id: 0,\n name: grouping.allparticipants,\n groupimageurl: grouping.groupingimageurl,\n groupings: [\n {\n id: grouping.id,\n name: grouping.name,\n }\n ],\n }\n\n ],\n };\n }\n groupings[grouping.id].groups.push(group);\n });\n });\n\n // We used grouping id as the key, the array will have undefined values.\n // Hence, we replace the keys here.\n groupings = groupings.filter((grouping) => grouping !== undefined);\n\n console.log(groupings);\n\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groupings: groupings,\n groupwithoutgrouping: groupWithoutGrouping,\n hasresults: groups.length > 0,\n instance: this.instance,\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 }\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, this.cmID).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 groupings: group.groupings,\n };\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 async clickHandler(e) {\n // Toggle a grouping.\n if (e.target.closest(this.selectors.togglegroupingbutton)) {\n e.stopPropagation();\n\n // Toggle button.\n let toggleButton = e.target.closest(this.selectors.togglegroupingbutton);\n\n // Find the target which we want to hide or show.\n let toggleTarget = document.querySelector(toggleButton.dataset.target);\n\n // Toggle show class.\n toggleTarget.classList.toggle('show');\n\n // Scroll to the top of the grouping.\n toggleTarget.scrollIntoView(false);\n\n // Hide all other groupings.\n document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target) => {\n if (target !== toggleTarget) {\n target.classList.remove('show');\n }\n });\n\n // Toggle the aria-expanded attribute.\n toggleButton.setAttribute('aria-expanded', toggleButton.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');\n\n return;\n }\n\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(async() => {\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 // User has given something for us to filter against.\n await this.filterrenderpipe();\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} 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","cmid","selectors","this","courseid","placeholder","togglegroupingbutton","togglegroupingtarget","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","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","groups","getMatchedResults","groupings","groupWithoutGrouping","group","length","grouping","id","name","allparticipants","groupimageurl","groupingimageurl","push","filter","undefined","console","log","html","js","groupwithoutgrouping","hasresults","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","e","target","closest","stopPropagation","toggleButton","toggleTarget","toggle","scrollIntoView","setAttribute","clearSearch","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"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"} \ No newline at end of file diff --git a/group/amd/src/comboboxsearch/group.js b/group/amd/src/comboboxsearch/group.js index 7789ebbd79f04..2026a8dd12e56 100644 --- a/group/amd/src/comboboxsearch/group.js +++ b/group/amd/src/comboboxsearch/group.js @@ -42,6 +42,8 @@ export default class GroupSearch extends search_combobox { this.selectors = {...this.selectors, courseid: '[data-region="courseid"]', placeholder: '.groupsearchdropdown [data-region="searchplaceholder"]', + togglegroupingbutton: '[data-action="togglegrouping"]', + togglegroupingtarget: '.toggle-grouping-target', }; const component = document.querySelector(this.componentSelector()); this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; @@ -114,9 +116,55 @@ export default class GroupSearch extends search_combobox { * Build the content then replace the node. */ async renderDropdown() { + let groups = this.getMatchedResults(); + + // Go through the groups and organise them by groupings. + let groupings = []; + + // Groups without groupings. + let groupWithoutGrouping = []; + + groups.forEach((group) => { + // If the group has no groupings, add it to the group without grouping list. + if (group.groupings.length === 0) { + groupWithoutGrouping.push(group); + return; + } + + // Otherwise, add it to the groupings object. + group.groupings.forEach((grouping) => { + if (!groupings[grouping.id]) { + groupings[grouping.id] = { + id: grouping.id, + name: grouping.name, + groups: [ + // All participants option. + { + id: 0, + name: grouping.allparticipants, + groupimageurl: grouping.groupingimageurl, + groupings: [ + { + id: grouping.id, + name: grouping.name, + } + ], + } + ], + }; + } + groupings[grouping.id].groups.push(group); + }); + }); + + // We used grouping id as the key, the array will have undefined values. + // Hence, we replace the keys here. + groupings = groupings.filter((grouping) => grouping !== undefined); + const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', { - groups: this.getMatchedResults(), - hasresults: this.getMatchedResults().length > 0, + groupings: groupings, + groupwithoutgrouping: groupWithoutGrouping, + hasresults: groups.length > 0, instance: this.instance, searchterm: this.getSearchTerm(), }); @@ -175,6 +223,7 @@ export default class GroupSearch extends search_combobox { id: group.id, name: group.name, groupimageurl: group.groupimageurl, + groupings: group.groupings, }; }) ); @@ -186,6 +235,35 @@ export default class GroupSearch extends search_combobox { * @param {MouseEvent} e The triggering event that we are working with. */ async clickHandler(e) { + // Toggle a grouping. + if (e.target.closest(this.selectors.togglegroupingbutton)) { + e.stopPropagation(); + + // Toggle button. + let toggleButton = e.target.closest(this.selectors.togglegroupingbutton); + + // Find the target which we want to hide or show. + let toggleTarget = document.querySelector(toggleButton.dataset.target); + + // Toggle show class. + toggleTarget.classList.toggle('show'); + + // Scroll to the top of the grouping. + toggleTarget.scrollIntoView(false); + + // Hide all other groupings. + document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target) => { + if (target !== toggleTarget) { + target.classList.remove('show'); + } + }); + + // Toggle the aria-expanded attribute. + toggleButton.setAttribute('aria-expanded', toggleButton.getAttribute('aria-expanded') === 'true' ? 'false' : 'true'); + + return; + } + if (e.target.closest(this.selectors.clearSearch)) { e.stopPropagation(); // Clear the entered search query in the search bar. diff --git a/group/classes/external/get_groups_for_selector.php b/group/classes/external/get_groups_for_selector.php index cb2a9ee40c245..e6949e44dc5f4 100644 --- a/group/classes/external/get_groups_for_selector.php +++ b/group/classes/external/get_groups_for_selector.php @@ -139,10 +139,26 @@ public static function execute(int $courseid, ?int $cmid = null): array { $picture = $OUTPUT->image_url('g/g1'); } + // Get the groupings the group belongs to. + $groupings = groups_get_groupings_by_group($group->id); + + // Add all participant string and image url to groupings array. + $groupings = array_map(function($grouping) { + global $OUTPUT; + return (object) [ + 'id' => $grouping->id, + 'name' => $grouping->name, + 'allparticipants' => get_string('allparticipants'), + 'groupingimageurl' => $OUTPUT->image_url('g/g1')->out(false), + ]; + }, $groupings); + + return (object) [ 'id' => $group->id, 'name' => format_string($group->name, true, ['context' => $context]), 'groupimageurl' => $picture->out(false), + 'groupings' => $groupings, ]; }, $groupsmenu); } @@ -175,6 +191,12 @@ public static function group_description(): external_description { 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED), 'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED), 'groupimageurl' => new external_value(PARAM_URL, 'Group image URL', VALUE_OPTIONAL), + 'groupings' => new external_multiple_structure( new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Grouping ID', VALUE_REQUIRED), + 'name' => new external_value(PARAM_TEXT, 'Grouping name', VALUE_REQUIRED), + 'allparticipants' => new external_value(PARAM_TEXT, 'All participants string', VALUE_REQUIRED), + 'groupingimageurl' => new external_value(PARAM_URL, 'Grouping image URL', VALUE_REQUIRED), + ])), ]; return new external_single_structure($groupfields); } diff --git a/group/templates/comboboxsearch/resultset.mustache b/group/templates/comboboxsearch/resultset.mustache index fe077b9841d0b..2eeccf3b8b80f 100644 --- a/group/templates/comboboxsearch/resultset.mustache +++ b/group/templates/comboboxsearch/resultset.mustache @@ -24,16 +24,16 @@ Example context (json): { - "groups": [ + "groupwithoutgrouping": [ { "id": 2, "name": "Foo bar", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" }, { "id": 3, "name": "Bar Foo", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" } ], "instance": 25, @@ -44,8 +44,28 @@ {{core_group/comboboxsearch/resultitem}} - {{/groups}} + {{/groupwithoutgrouping}} + + {{#groupings}} + + + + +
+ {{#groups}} + {{>core_group/comboboxsearch/resultitem}} + {{/groups}} +
+ + {{/groupings}} {{/results}} {{/core/local/comboboxsearch/resultset}} diff --git a/lib/grouplib.php b/lib/grouplib.php index 730887178d3e3..28ed14720d254 100644 --- a/lib/grouplib.php +++ b/lib/grouplib.php @@ -1653,3 +1653,20 @@ function groups_get_activity_shared_group_members($cm, $userid = null) { } return groups_get_groups_members($groupsids); } + +/** + * Return groupings which a group belongs to. + * + * @param int $groupid The group id. + * @return array + */ +function groups_get_groupings_by_group(int $groupid): array { + global $DB; + + $sql = "SELECT g.id, g.name + FROM {groupings_groups} gg + JOIN {groupings} g ON gg.groupingid = g.id + WHERE gg.groupid = :groupid"; + + return $DB->get_records_sql($sql, ['groupid' => $groupid]); +}