diff --git a/user/amd/build/comboboxsearch/user.min.js b/user/amd/build/comboboxsearch/user.min.js
index 630c4c07f00..887542d2775 100644
--- a/user/amd/build/comboboxsearch/user.min.js
+++ b/user/amd/build/comboboxsearch/user.min.js
@@ -1,3 +1,3 @@
-define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery","core/notification"],(function(_exports,_search_combobox,_str,_templates,_jquery,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"bannedFilterFields",["profileimageurlsmall","profileimageurl","id","link","matchingField","matchingFieldName"]),_defineProperty(this,"profilestringmap",null),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()})),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}triggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){return filterableData.filter((user=>Object.keys(user).some((key=>""!==user[key]&&null!==user[key]&&!this.bannedFilterFields.includes(key)&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm();if(valueString.includes(preppedSearchTerm)&&!this.bannedFilterFields.includes(key)){var _stringMap$get;user.matchingFieldName=null!==(_stringMap$get=stringMap.get(key))&&void 0!==_stringMap$get?_stringMap$get:key;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e).catch(_notification.default.exception),e.target.closest(this.selectors.component)&&e.stopImmediatePropagation(),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(this.selectors.resetPageButton)&&(window.location=e.target.closest(this.selectors.resetPageButton).href)}keyHandler(e){switch(super.keyHandler(e),e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(document.activeElement===this.getHTMLElements().searchInput&&"Enter"===e.key&&null!==this.selectAllResultsLink()&&(window.location=this.selectAllResultsLink()),document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(this.selectors.resetPageButton)){window.location=e.target.closest(this.selectors.resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll&&!e.shiftKey?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch())}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.component.setAttribute("aria-expanded","true")):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.component.setAttribute("aria-expanded","false"))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default}));
+define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery","core/notification"],(function(_exports,_search_combobox,_str,_templates,_jquery,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()})),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}triggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e).catch(_notification.default.exception),e.target.closest(this.selectors.component)&&e.stopImmediatePropagation(),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(this.selectors.resetPageButton)&&(window.location=e.target.closest(this.selectors.resetPageButton).href)}keyHandler(e){switch(super.keyHandler(e),e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(document.activeElement===this.getHTMLElements().searchInput&&"Enter"===e.key&&null!==this.selectAllResultsLink()&&(window.location=this.selectAllResultsLink()),document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(this.selectors.resetPageButton)){window.location=e.target.closest(this.selectors.resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll&&!e.shiftKey?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch())}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.component.setAttribute("aria-expanded","true")):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.component.setAttribute("aria-expanded","false"))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default}));
//# sourceMappingURL=user.min.js.map
\ No newline at end of file
diff --git a/user/amd/build/comboboxsearch/user.min.js.map b/user/amd/build/comboboxsearch/user.min.js.map
index 3a1b8718c4a..87c9b6b9690 100644
--- a/user/amd/build/comboboxsearch/user.min.js.map
+++ b/user/amd/build/comboboxsearch/user.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to search for learners.\n *\n * @module core_user/comboboxsearch/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\nimport Notification from 'core/notification';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n if (valueString.includes(preppedSearchTerm) && !this.bannedFilterFields.includes(key)) {\n // Ensure we have a good string, otherwise fallback to the key.\n user.matchingFieldName = stringMap.get(key) ?? key;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\n }\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e).catch(Notification.exception);\n if (e.target.closest(this.selectors.component)) {\n // Forcibly prevent BS events so that we can control the open and close.\n // Really needed because by default input elements cant trigger a dropdown.\n e.stopImmediatePropagation();\n }\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n super.keyHandler(e);\n\n if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {\n window.location = this.selectAllResultsLink();\n }\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Enter':\n case ' ':\n if (document.activeElement === this.getHTMLElements().searchInput) {\n if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {\n window.location = this.selectAllResultsLink();\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n if (e.target.closest('.dropdown-item')) {\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll && !e.shiftKey) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.component.setAttribute('aria-expanded', 'true');\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n this.component.setAttribute('aria-expanded', 'false');\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","document","addEventListener","e","target","closest","this","selectors","component","searchDropdown","classList","contains","toggleDropdown","courseid","groupid","resetPageButton","querySelector","componentSelector","courseID","dataset","groupID","_document$querySelect","_document$querySelect2","dropdownSelector","triggerSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","fetchDataset","Error","name","filterableData","filter","user","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","getPreppedSearchTerm","stringMap","getStringMap","setMatchedResults","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","get","escapedMatchingField","replace","matchingField","email","link","selectOneLink","id","clickHandler","catch","Notification","exception","stopImmediatePropagation","currentViewAll","button","window","location","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","focus","preventScroll","clearSearch","shiftKey","add","show","setAttribute","remove","hide","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to search for learners.\n *\n * @module core_user/comboboxsearch/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\nimport Notification from 'core/notification';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n triggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\n }\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e).catch(Notification.exception);\n if (e.target.closest(this.selectors.component)) {\n // Forcibly prevent BS events so that we can control the open and close.\n // Really needed because by default input elements cant trigger a dropdown.\n e.stopImmediatePropagation();\n }\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n super.keyHandler(e);\n\n if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {\n window.location = this.selectAllResultsLink();\n }\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Enter':\n case ' ':\n if (document.activeElement === this.getHTMLElements().searchInput) {\n if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {\n window.location = this.selectAllResultsLink();\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(this.selectors.resetPageButton)) {\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n if (e.target.closest('.dropdown-item')) {\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll && !e.shiftKey) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.component.setAttribute('aria-expanded', 'true');\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n this.component.setAttribute('aria-expanded', 'false');\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'fullname',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","document","addEventListener","e","target","closest","this","selectors","component","searchDropdown","classList","contains","toggleDropdown","courseid","groupid","resetPageButton","querySelector","componentSelector","courseID","dataset","groupID","_document$querySelect","_document$querySelect2","dropdownSelector","triggerSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","fetchDataset","Error","name","filterableData","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","getPreppedSearchTerm","setMatchedResults","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","matchingField","email","link","selectOneLink","id","clickHandler","catch","Notification","exception","stopImmediatePropagation","currentViewAll","button","window","location","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","focus","preventScroll","clearSearch","shiftKey","add","show","setAttribute","remove","hide","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"}
\ No newline at end of file
diff --git a/user/amd/src/comboboxsearch/user.js b/user/amd/src/comboboxsearch/user.js
index 4e8aca0e217..ce8655de4c0 100644
--- a/user/amd/src/comboboxsearch/user.js
+++ b/user/amd/src/comboboxsearch/user.js
@@ -30,7 +30,6 @@ export default class UserSearch extends search_combobox {
courseID;
groupID;
- bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];
// A map of user profile field names that is human-readable.
profilestringmap = null;
@@ -118,8 +117,9 @@ export default class UserSearch extends search_combobox {
* @returns {Array} The users that match the given criteria.
*/
async filterDataset(filterableData) {
+ const stringMap = await this.getStringMap();
return filterableData.filter((user) => Object.keys(user).some((key) => {
- if (user[key] === "" || user[key] === null || this.bannedFilterFields.includes(key)) {
+ if (user[key] === "" || user[key] === null || !stringMap.get(key)) {
return false;
}
return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
@@ -145,9 +145,10 @@ export default class UserSearch extends search_combobox {
const preppedSearchTerm = this.getPreppedSearchTerm();
const searchTerm = this.getSearchTerm();
- if (valueString.includes(preppedSearchTerm) && !this.bannedFilterFields.includes(key)) {
- // Ensure we have a good string, otherwise fallback to the key.
- user.matchingFieldName = stringMap.get(key) ?? key;
+ // Ensure we match only on expected keys.
+ const matchingFieldName = stringMap.get(key);
+ if (matchingFieldName && valueString.includes(preppedSearchTerm)) {
+ user.matchingFieldName = matchingFieldName;
// Safely prepare our matching results.
const escapedValueString = valueString.replace(/