From d2881df06b81b805b42d82e8ee5a3738925f0604 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Sat, 4 Feb 2023 23:53:41 +1100 Subject: [PATCH] MDL-76246 core_grades: Group search widget to combobox --- grade/amd/build/searchwidget/group.min.js | 4 +- grade/amd/build/searchwidget/group.min.js.map | 2 +- grade/amd/src/searchwidget/group.js | 63 +++++++++++++++---- grade/renderer.php | 2 + grade/templates/group_selector.mustache | 22 ++++++- .../group/groupsearch_body.mustache | 11 +++- 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/grade/amd/build/searchwidget/group.min.js b/grade/amd/build/searchwidget/group.min.js index 9bac0576b75..e653f7edb7c 100644 --- a/grade/amd/build/searchwidget/group.min.js +++ b/grade/amd/build/searchwidget/group.min.js @@ -1,10 +1,10 @@ -define("core_grades/searchwidget/group",["exports","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","jquery","core_grades/searchwidget/selectors"],(function(_exports,_pending,Templates,Repository,WidgetBase,_jquery,Selectors){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_grades/searchwidget/group",["exports","core/local/aria/focuslock","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","jquery","core_grades/searchwidget/selectors"],(function(_exports,FocusLockManager,_pending,Templates,Repository,WidgetBase,_jquery,Selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * A widget to search groups within the gradebook. * * @module core_grades/searchwidget/group * @copyright 2022 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);let registered=!1;_exports.init=()=>{if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve(),registered=!0};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("group"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("group")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,actionBaseUrl=e.relatedTarget.dataset.actionBaseUrl;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.groupFetch(courseID,actionBaseUrl).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.groups,searchGroups())})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/group/groupsearch_body",[])),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("group")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchGroups=()=>()=>(groups,searchTerm)=>{if(""===searchTerm)return groups;searchTerm=searchTerm.toLowerCase();const searchResults=[];return groups.forEach((group=>{group.name.toLowerCase().includes(searchTerm)&&searchResults.push(group)})),searchResults}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,FocusLockManager=_interopRequireWildcard(FocusLockManager),_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);let initialised=!1;_exports.init=()=>{if(!initialised&&document.querySelector(Selectors.elements.getSearchWidgetSelector("group"))){const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()}initialised=!0};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("group")),menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("group")),inputElement=menuContainer.querySelector('input[name="group"]');(0,_jquery.default)(menuContainer).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,actionBaseUrl=e.relatedTarget.dataset.actionBaseUrl;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.groupFetch(courseID,actionBaseUrl).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&(await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.groups,searchGroups(),null,afterSelect),FocusLockManager.trapFocus(dropdownMenuContainer))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/group/groupsearch_body",[])),(0,_jquery.default)(menuContainer).on("hide.bs.dropdown",(()=>{FocusLockManager.untrapFocus()})),inputElement.addEventListener("change",(e=>{const toggle=menuContainer.querySelector(".dropdown-toggle"),courseId=toggle.dataset.courseid,actionUrl=toggle.dataset.actionBaseUrl?new URL(toggle.dataset.actionBaseUrl.replace(/&/g,"&")):new URL(location.href);actionUrl.searchParams.set("id",courseId),actionUrl.searchParams.set("group",e.target.value),location.href=actionUrl.href,e.stopPropagation()}))},searchGroups=()=>()=>(groups,searchTerm)=>{if(""===searchTerm)return groups;searchTerm=searchTerm.toLowerCase();const searchResults=[];return groups.forEach((group=>{group.name.toLowerCase().includes(searchTerm)&&searchResults.push(group)})),searchResults},afterSelect=selected=>{const menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("group")),inputElement=menuContainer.querySelector('input[name="group"]');(0,_jquery.default)(menuContainer).dropdown("hide"),inputElement.value!=selected&&(inputElement.value=selected,inputElement.dispatchEvent(new Event("change",{bubbles:!0})))}})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/grade/amd/build/searchwidget/group.min.js.map b/grade/amd/build/searchwidget/group.min.js.map index 5d5973c9fe1..96897c74364 100644 --- a/grade/amd/build/searchwidget/group.min.js.map +++ b/grade/amd/build/searchwidget/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/searchwidget/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 * A widget to search groups within the gradebook.\n *\n * @module core_grades/searchwidget/group\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport $ from 'jquery';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n/**\n * Our entry point into starting to build the group search widget.\n *\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n registered = true;\n};\n\n/**\n * Register event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('group'));\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(Selectors.elements.getSearchWidgetSelector('group')).on('show.bs.dropdown', async(e) => {\n const courseID = e.relatedTarget.dataset.courseid;\n const actionBaseUrl = e.relatedTarget.dataset.actionBaseUrl;\n // Display a loading icon in the dropdown menu container until the body promise is resolved.\n await WidgetBase.showLoader(dropdownMenuContainer);\n\n // If an error occurs while fetching the data, display the error within the dropdown menu.\n const data = await Repository.groupFetch(courseID, actionBaseUrl).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n await WidgetBase.init(\n dropdownMenuContainer,\n bodyPromise,\n data.groups,\n searchGroups(),\n );\n });\n\n // Resolvers for passed functions in the dropdown creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/group/groupsearch_body',\n []\n ));\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(Selectors.elements.getSearchWidgetSelector('group')).on('hide.bs.dropdown', () => {\n // Reset the state once the groups menu dropdown is closed.\n dropdownMenuContainer.innerHTML = '';\n });\n};\n\n/**\n * Define how we want to search and filter groups when the user decides to input a search value.\n *\n * @method registerListenerEvents\n * @returns {function(): function(*, *): (*)}\n */\nconst searchGroups = () => {\n return () => {\n return (groups, searchTerm) => {\n if (searchTerm === '') {\n return groups;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n groups.forEach((group) => {\n const groupName = group.name.toLowerCase();\n if (groupName.includes(searchTerm)) {\n searchResults.push(group);\n }\n });\n return searchResults;\n };\n };\n};\n"],"names":["registered","pendingPromise","Pending","registerListenerEvents","resolve","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","dropdownMenuContainer","document","querySelector","Selectors","elements","getSearchWidgetDropdownSelector","getSearchWidgetSelector","on","async","courseID","e","relatedTarget","dataset","courseid","actionBaseUrl","showLoader","data","Repository","groupFetch","catch","errorTemplateData","message","Templates","render","init","groups","searchGroups","innerHTML","searchTerm","toLowerCase","searchResults","forEach","group","name","includes","push"],"mappings":";;;;;;;gWAmCIA,YAAa,gBASG,QACZA,wBAGEC,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,UACfJ,YAAa,SAQXG,uBAAyB,SACvBE,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,6BAC9CC,sBAAwBC,SAASC,cAAcC,UAAUC,SAASC,gCAAgC,8BAGtGF,UAAUC,SAASE,wBAAwB,UAAUC,GAAG,oBAAoBC,MAAAA,UACpEC,SAAWC,EAAEC,cAAcC,QAAQC,SACnCC,cAAgBJ,EAAEC,cAAcC,QAAQE,oBAExChB,WAAWiB,WAAWf,6BAGtBgB,WAAaC,WAAWC,WAAWT,SAAUK,eAAeK,OAAMX,MAAAA,UAC9DY,kBAAoB,cACNV,EAAEW,SAEtBzB,0BACU0B,UAAUC,OAAO,iCAAkCH,uBAI7DJ,OAAS,UAGPlB,WAAW0B,KACbxB,sBACAH,YACAmB,KAAKS,OACLC,mBAKR9B,oBAAoB0B,UAAUC,OAC1B,kDACA,yBAIFpB,UAAUC,SAASE,wBAAwB,UAAUC,GAAG,oBAAoB,KAE1EP,sBAAsB2B,UAAY,OAUpCD,aAAe,IACV,IACI,CAACD,OAAQG,iBACO,KAAfA,kBACOH,OAEXG,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBL,OAAOM,SAASC,QACMA,MAAMC,KAAKJ,cACfK,SAASN,aACnBE,cAAcK,KAAKH,UAGpBF"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/searchwidget/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 * A widget to search groups within the gradebook.\n *\n * @module core_grades/searchwidget/group\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as FocusLockManager from 'core/local/aria/focuslock';\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport $ from 'jquery';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\n\n/**\n * Whether this module is already initialised.\n *\n * @type {boolean}\n */\nlet initialised = false;\n\n/**\n * Our entry point into starting to build the group search widget.\n *\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n if (!initialised && document.querySelector(Selectors.elements.getSearchWidgetSelector('group'))) {\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n }\n initialised = true;\n};\n\n/**\n * Register event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('group'));\n const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group'));\n const inputElement = menuContainer.querySelector('input[name=\"group\"]');\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(menuContainer).on('show.bs.dropdown', async(e) => {\n const courseID = e.relatedTarget.dataset.courseid;\n const actionBaseUrl = e.relatedTarget.dataset.actionBaseUrl;\n // Display a loading icon in the dropdown menu container until the body promise is resolved.\n await WidgetBase.showLoader(dropdownMenuContainer);\n\n // If an error occurs while fetching the data, display the error within the dropdown menu.\n const data = await Repository.groupFetch(courseID, actionBaseUrl).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n await WidgetBase.init(\n dropdownMenuContainer,\n bodyPromise,\n data.groups,\n searchGroups(),\n null,\n afterSelect\n );\n\n // Lock tab control. It has to be locked because the dropdown's role is dialog.\n FocusLockManager.trapFocus(dropdownMenuContainer);\n });\n\n // Resolvers for passed functions in the dropdown creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/group/groupsearch_body',\n []\n ));\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(menuContainer).on('hide.bs.dropdown', () => {\n FocusLockManager.untrapFocus();\n });\n\n inputElement.addEventListener('change', e => {\n const toggle = menuContainer.querySelector('.dropdown-toggle');\n const courseId = toggle.dataset.courseid;\n const actionUrl = toggle.dataset.actionBaseUrl ?\n new URL(toggle.dataset.actionBaseUrl.replace(/&/g, \"&\")) :\n new URL(location.href);\n actionUrl.searchParams.set('id', courseId);\n actionUrl.searchParams.set('group', e.target.value);\n\n location.href = actionUrl.href;\n\n e.stopPropagation();\n });\n};\n\n/**\n * Define how we want to search and filter groups when the user decides to input a search value.\n *\n * @method searchGroups\n * @returns {function(): function(*, *): (*)}\n */\nconst searchGroups = () => {\n return () => {\n return (groups, searchTerm) => {\n if (searchTerm === '') {\n return groups;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n groups.forEach((group) => {\n const groupName = group.name.toLowerCase();\n if (groupName.includes(searchTerm)) {\n searchResults.push(group);\n }\n });\n return searchResults;\n };\n };\n};\n\n/**\n * Define the action to be performed when an item is selected by the search widget.\n *\n * @param {String} selected The selected item's value.\n */\nconst afterSelect = (selected) => {\n const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group'));\n const inputElement = menuContainer.querySelector('input[name=\"group\"]');\n\n $(menuContainer).dropdown('hide'); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n if (inputElement.value != selected) {\n inputElement.value = selected;\n inputElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n};\n"],"names":["initialised","document","querySelector","Selectors","elements","getSearchWidgetSelector","pendingPromise","Pending","registerListenerEvents","resolve","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","dropdownMenuContainer","getSearchWidgetDropdownSelector","menuContainer","inputElement","on","async","courseID","e","relatedTarget","dataset","courseid","actionBaseUrl","showLoader","data","Repository","groupFetch","catch","errorTemplateData","message","Templates","render","init","groups","searchGroups","afterSelect","FocusLockManager","trapFocus","untrapFocus","addEventListener","toggle","courseId","actionUrl","URL","replace","location","href","searchParams","set","target","value","stopPropagation","searchTerm","toLowerCase","searchResults","forEach","group","name","includes","push","selected","dropdown","dispatchEvent","Event","bubbles"],"mappings":";;;;;;;2ZAoCIA,aAAc,gBASE,SACXA,aAAeC,SAASC,cAAcC,UAAUC,SAASC,wBAAwB,UAAW,OACvFC,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,UAEnBT,aAAc,SAQZQ,uBAAyB,SACvBE,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,6BAC9CC,sBAAwBb,SAASC,cAAcC,UAAUC,SAASW,gCAAgC,UAClGC,cAAgBf,SAASC,cAAcC,UAAUC,SAASC,wBAAwB,UAClFY,aAAeD,cAAcd,cAAc,2CAG/Cc,eAAeE,GAAG,oBAAoBC,MAAAA,UAC9BC,SAAWC,EAAEC,cAAcC,QAAQC,SACnCC,cAAgBJ,EAAEC,cAAcC,QAAQE,oBAExCb,WAAWc,WAAWZ,6BAGtBa,WAAaC,WAAWC,WAAWT,SAAUK,eAAeK,OAAMX,MAAAA,UAC9DY,kBAAoB,cACNV,EAAEW,SAEtBtB,0BACUuB,UAAUC,OAAO,iCAAkCH,uBAI7DJ,OAAS,WAGPf,WAAWuB,KACbrB,sBACAH,YACAgB,KAAKS,OACLC,eACA,KACAC,aAIJC,iBAAiBC,UAAU1B,2BAI/BJ,oBAAoBuB,UAAUC,OAC1B,kDACA,yBAIFlB,eAAeE,GAAG,oBAAoB,KACpCqB,iBAAiBE,iBAGrBxB,aAAayB,iBAAiB,UAAUrB,UAC9BsB,OAAS3B,cAAcd,cAAc,oBACrC0C,SAAWD,OAAOpB,QAAQC,SAC1BqB,UAAYF,OAAOpB,QAAQE,cAC7B,IAAIqB,IAAIH,OAAOpB,QAAQE,cAAcsB,QAAQ,SAAU,MACvD,IAAID,IAAIE,SAASC,MACrBJ,UAAUK,aAAaC,IAAI,KAAMP,UACjCC,UAAUK,aAAaC,IAAI,QAAS9B,EAAE+B,OAAOC,OAE7CL,SAASC,KAAOJ,UAAUI,KAE1B5B,EAAEiC,sBAUJjB,aAAe,IACV,IACI,CAACD,OAAQmB,iBACO,KAAfA,kBACOnB,OAEXmB,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBrB,OAAOsB,SAASC,QACMA,MAAMC,KAAKJ,cACfK,SAASN,aACnBE,cAAcK,KAAKH,UAGpBF,eAUbnB,YAAeyB,iBACX/C,cAAgBf,SAASC,cAAcC,UAAUC,SAASC,wBAAwB,UAClFY,aAAeD,cAAcd,cAAc,2CAE/Cc,eAAegD,SAAS,QAEtB/C,aAAaoC,OAASU,WACtB9C,aAAaoC,MAAQU,SACrB9C,aAAagD,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS"} \ No newline at end of file diff --git a/grade/amd/src/searchwidget/group.js b/grade/amd/src/searchwidget/group.js index 2882d6316c0..e5d214dc957 100644 --- a/grade/amd/src/searchwidget/group.js +++ b/grade/amd/src/searchwidget/group.js @@ -21,6 +21,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import * as FocusLockManager from 'core/local/aria/focuslock'; import Pending from 'core/pending'; import * as Templates from 'core/templates'; import * as Repository from 'core_grades/searchwidget/repository'; @@ -29,11 +30,11 @@ import $ from 'jquery'; import * as Selectors from 'core_grades/searchwidget/selectors'; /** - * Whether the event listener has already been registered for this module. + * Whether this module is already initialised. * * @type {boolean} */ -let registered = false; +let initialised = false; /** * Our entry point into starting to build the group search widget. @@ -43,13 +44,12 @@ let registered = false; * @method init */ export const init = () => { - if (registered) { - return; + if (!initialised && document.querySelector(Selectors.elements.getSearchWidgetSelector('group'))) { + const pendingPromise = new Pending(); + registerListenerEvents(); + pendingPromise.resolve(); } - const pendingPromise = new Pending(); - registerListenerEvents(); - pendingPromise.resolve(); - registered = true; + initialised = true; }; /** @@ -60,9 +60,11 @@ export const init = () => { const registerListenerEvents = () => { let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers(); const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('group')); + const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group')); + const inputElement = menuContainer.querySelector('input[name="group"]'); // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed). - $(Selectors.elements.getSearchWidgetSelector('group')).on('show.bs.dropdown', async(e) => { + $(menuContainer).on('show.bs.dropdown', async(e) => { const courseID = e.relatedTarget.dataset.courseid; const actionBaseUrl = e.relatedTarget.dataset.actionBaseUrl; // Display a loading icon in the dropdown menu container until the body promise is resolved. @@ -86,7 +88,12 @@ const registerListenerEvents = () => { bodyPromise, data.groups, searchGroups(), + null, + afterSelect ); + + // Lock tab control. It has to be locked because the dropdown's role is dialog. + FocusLockManager.trapFocus(dropdownMenuContainer); }); // Resolvers for passed functions in the dropdown creation. @@ -96,16 +103,29 @@ const registerListenerEvents = () => { )); // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed). - $(Selectors.elements.getSearchWidgetSelector('group')).on('hide.bs.dropdown', () => { - // Reset the state once the groups menu dropdown is closed. - dropdownMenuContainer.innerHTML = ''; + $(menuContainer).on('hide.bs.dropdown', () => { + FocusLockManager.untrapFocus(); + }); + + inputElement.addEventListener('change', e => { + const toggle = menuContainer.querySelector('.dropdown-toggle'); + const courseId = toggle.dataset.courseid; + const actionUrl = toggle.dataset.actionBaseUrl ? + new URL(toggle.dataset.actionBaseUrl.replace(/&/g, "&")) : + new URL(location.href); + actionUrl.searchParams.set('id', courseId); + actionUrl.searchParams.set('group', e.target.value); + + location.href = actionUrl.href; + + e.stopPropagation(); }); }; /** * Define how we want to search and filter groups when the user decides to input a search value. * - * @method registerListenerEvents + * @method searchGroups * @returns {function(): function(*, *): (*)} */ const searchGroups = () => { @@ -126,3 +146,20 @@ const searchGroups = () => { }; }; }; + +/** + * Define the action to be performed when an item is selected by the search widget. + * + * @param {String} selected The selected item's value. + */ +const afterSelect = (selected) => { + const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group')); + const inputElement = menuContainer.querySelector('input[name="group"]'); + + $(menuContainer).dropdown('hide'); // Otherwise the dropdown stays open when user choose an option using keyboard. + + if (inputElement.value != selected) { + inputElement.value = selected; + inputElement.dispatchEvent(new Event('change', {bubbles: true})); + } +}; diff --git a/grade/renderer.php b/grade/renderer.php index 10fe24c953a..be3e4e2d014 100644 --- a/grade/renderer.php +++ b/grade/renderer.php @@ -59,6 +59,7 @@ class core_grades_renderer extends plugin_renderer_base { get_string('selectgroupsseparate'); $data = [ + 'name' => 'group', 'label' => $label, 'courseid' => $course->id, 'groupactionbaseurl' => $groupactionbaseurl @@ -73,6 +74,7 @@ class core_grades_renderer extends plugin_renderer_base { } $activegroup = groups_get_course_group($course, true, $allowedgroups); + $data['group'] = $activegroup; if ($activegroup) { $group = groups_get_group($activegroup); diff --git a/grade/templates/group_selector.mustache b/grade/templates/group_selector.mustache index 987d1ef912e..94b4d0b5b60 100644 --- a/grade/templates/group_selector.mustache +++ b/grade/templates/group_selector.mustache @@ -21,6 +21,8 @@ Context variables required for this template: * label - The label text fot the group selector element. + * name - The name of the group selector element + * group - The value of the group selector element (id of the preselected group) * courseid - The course ID. * groupactionbaseurl - The base URL for the group action. * selectedgroup - The text of the selected group option. @@ -28,13 +30,26 @@ Example context (json): { "label": "Select separate groups", + "name": "group", + "group": "21", "courseid": "2", "groupactionbaseurl": "index.php?item=test", "selectedgroup": "Group 1" } }} diff --git a/grade/templates/searchwidget/group/groupsearch_body.mustache b/grade/templates/searchwidget/group/groupsearch_body.mustache index a1a8c96dfa2..a1b087eb67a 100644 --- a/grade/templates/searchwidget/group/groupsearch_body.mustache +++ b/grade/templates/searchwidget/group/groupsearch_body.mustache @@ -29,6 +29,13 @@ {{$placeholder}}{{#str}} searchgroups, core_grades {{/str}}{{/placeholder}} + {{$additionalattributes}} + role="combobox" + aria-expanded="true" + aria-controls="listbox-{{uniqid}}" + aria-autocomplete="list" + data-input-element="input-{{uniqid}}" + {{/additionalattributes}} {{/ core/search_input_auto }} - -
+ +