From d18f7bf5cd7ea00d6b9957b7c2e7c1d214f8e65b Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Tue, 31 Oct 2023 17:26:01 +1100 Subject: [PATCH] MDL-78885 core: combobox should have a valid aria-controls - The aria-controls attribute cannot refer to an element that doesn't exist - Also update filterDataset so it returns [] when searchTerm is empty --- .../comboboxsearch/search_combobox.min.js | 2 +- .../comboboxsearch/search_combobox.min.js.map | 2 +- lib/amd/src/comboboxsearch/search_combobox.js | 17 +++++------- .../local/comboboxsearch/resultset.mustache | 10 +++---- user/amd/build/comboboxsearch/user.min.js | 2 +- user/amd/build/comboboxsearch/user.min.js.map | 2 +- user/amd/src/comboboxsearch/user.js | 27 +++++++++---------- 7 files changed, 27 insertions(+), 35 deletions(-) diff --git a/lib/amd/build/comboboxsearch/search_combobox.min.js b/lib/amd/build/comboboxsearch/search_combobox.min.js index e440493edcc..5fd48718734 100644 --- a/lib/amd/build/comboboxsearch/search_combobox.min.js +++ b/lib/amd/build/comboboxsearch/search_combobox.min.js @@ -5,6 +5,6 @@ define("core/comboboxsearch/search_combobox",["exports","jquery","core/custom_in * @module core/comboboxsearch/search_combobox * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_pending=_interopRequireDefault(_pending),FocusLockManager=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FocusLockManager);const events=["keydown",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];return _exports.default=class{constructor(){var _this$searchInput$val,_this$searchInput;_defineProperty(this,"selectors",{component:this.componentSelector(),trigger:this.triggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.dropdownSelector(),resultitems:'[role="option"]',viewall:"#select-all"}),_defineProperty(this,"matchedResults",[]),_defineProperty(this,"searchTerm",""),_defineProperty(this,"preppedSearchTerm",null),_defineProperty(this,"resultNodes",[]),_defineProperty(this,"currentNode",null),_defineProperty(this,"currentViewAll",null),_defineProperty(this,"dataset",null),_defineProperty(this,"datasetSize",0),_defineProperty(this,"component",document.querySelector(this.selectors.component)),_defineProperty(this,"searchInput",this.component.querySelector(this.selectors.input)),_defineProperty(this,"searchDropdown",this.component.querySelector(this.selectors.dropdown)),_defineProperty(this,"clearSearchButton",this.component.querySelector(this.selectors.clearSearch)),_defineProperty(this,"$component",(0,_jquery.default)(this.component)),_defineProperty(this,"selectNode",(node=>{node.focus({preventScroll:!0}),this.searchDropdown.scrollTop=node.offsetTop-node.clientHeight/2})),_defineProperty(this,"moveToFirstNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[0])})),_defineProperty(this,"moveToLastNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[this.resultNodes.length-1])})),_defineProperty(this,"moveToNode",(index=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[index])})),this.setSearchTerms(null!==(_this$searchInput$val=null===(_this$searchInput=this.searchInput)||void 0===_this$searchInput?void 0:_this$searchInput.value)&&void 0!==_this$searchInput$val?_this$searchInput$val:""),this.registerClickHandlers(),this.registerKeyHandlers(),null!==this.searchInput&&this.registerInputHandlers(),this.$component.on("shown.bs.dropdown",(()=>{FocusLockManager.trapFocus(this.component)})),this.$component.on("hidden.bs.dropdown",(()=>{FocusLockManager.untrapFocus()}))}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}filterDataset(dataset){throw new Error("filterDataset(".concat(dataset,") must be implemented in ").concat(this.constructor.name))}filterMatchDataset(){throw new Error("filterMatchDataset() must be implemented in ".concat(this.constructor.name))}renderDropdown(){throw new Error("renderDropdown() must be implemented in ".concat(this.constructor.name))}componentSelector(){throw new Error("componentSelector() must be implemented in ".concat(this.constructor.name))}dropdownSelector(){throw new Error("dropdownSelector() must be implemented in ".concat(this.constructor.name))}triggerSelector(){throw new Error("triggerSelector() must be implemented in ".concat(this.constructor.name))}async getDataset(){return this.dataset||(this.dataset=await this.fetchDataset()),this.datasetSize=this.dataset.length,this.dataset}getDatasetSize(){return this.datasetSize}getMatchedResults(){return this.matchedResults}setMatchedResults(result){this.matchedResults=result}getSearchTerm(){return this.searchTerm}getPreppedSearchTerm(){return this.preppedSearchTerm}setSearchTerms(result){this.searchTerm=result,this.preppedSearchTerm=result.toLowerCase()}getHTMLElements(){return this.updateNodes(),{searchDropdown:this.searchDropdown,currentViewAll:this.currentViewAll,searchInput:this.searchInput,clearSearchButton:this.clearSearchButton,trigger:this.component.querySelector(this.selectors.trigger)}}closeSearch(){let clear=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.toggleDropdown(),clear&&(this.clearSearchButton.classList.add("d-none"),this.setSearchTerms(""),this.searchInput.value="")}searchResultsVisible(){const{searchDropdown:searchDropdown}=this.getHTMLElements();return null!==searchDropdown.offsetParent}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),on?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show()):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide())}updateNodes(){this.resultNodes=[...this.component.querySelectorAll(this.selectors.resultitems)],this.currentNode=this.resultNodes.find((r=>r.id===document.activeElement.id)),this.currentViewAll=this.component.querySelector(this.selectors.viewall),this.clearSearchButton=this.component.querySelector(this.selectors.clearSearch),this.searchInput=this.component.querySelector(this.selectors.input),this.searchDropdown=this.component.querySelector(this.selectors.dropdown)}registerClickHandlers(){this.component.addEventListener("click",this.clickHandler.bind(this))}registerKeyHandlers(){_custom_interaction_events.default.define(document,events),events.forEach((event=>{this.component.addEventListener(event,this.keyHandler.bind(this))}))}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{if(this.getSearchTerm()===this.searchInput.value&&this.searchResultsVisible())window.console.warn("Search term matches input value - skipping");else if(this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm())this.toggleDropdown(),this.clearSearchButton.classList.add("d-none");else{const pendingPromise=new _pending.default;await this.renderAndShow().then((()=>{this.clearSearchButton.classList.remove("d-none")})).then((()=>(pendingPromise.resolve(),!0)))}}),300,{pending:!0}))}async filterrenderpipe(){this.updateNodes(),this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}async renderAndShow(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),await this.filterMatchDataset(),await this.renderDropdown(),this.toggleDropdown(!0)}keyUpDown(direction,e){e.preventDefault(),e.stopPropagation(),document.activeElement===this.searchInput&&this.resultNodes.length>0&&(-1===direction?this.moveToLastNode():this.moveToFirstNode());const index=this.resultNodes.indexOf(this.currentNode);this.currentNode?-1===direction?0===index?this.moveToLastNode():this.moveToNode(index-1):index+1>=this.resultNodes.length?this.moveToFirstNode():this.moveToNode(index+1):-1===direction?this.moveToLastNode():this.moveToFirstNode()}async clickHandler(e){this.updateNodes(),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href),e.target.closest(this.selectors.clearSearch)&&0===e.button&&(this.closeSearch(!0),this.searchInput.focus({preventScroll:!0})),e.target.closest(this.selectors.input)&&""!==this.getSearchTerm()&&0===e.button&&await this.renderAndShow()}keyHandler(e){switch(this.updateNodes(),e.key){case"ArrowUp":this.keyUpDown(-1,e);break;case"ArrowDown":this.keyUpDown(1,e);break;case"Home":e.preventDefault(),this.moveToFirstNode();break;case"End":e.preventDefault(),this.moveToLastNode()}}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_pending=_interopRequireDefault(_pending),FocusLockManager=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FocusLockManager);const events=["keydown",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];return _exports.default=class{constructor(){var _this$searchInput$val,_this$searchInput;_defineProperty(this,"selectors",{component:this.componentSelector(),trigger:this.triggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.dropdownSelector(),resultitems:'[role="option"]',viewall:"#select-all"}),_defineProperty(this,"matchedResults",[]),_defineProperty(this,"searchTerm",""),_defineProperty(this,"preppedSearchTerm",null),_defineProperty(this,"resultNodes",[]),_defineProperty(this,"currentNode",null),_defineProperty(this,"currentViewAll",null),_defineProperty(this,"dataset",null),_defineProperty(this,"datasetSize",0),_defineProperty(this,"component",document.querySelector(this.selectors.component)),_defineProperty(this,"searchInput",this.component.querySelector(this.selectors.input)),_defineProperty(this,"searchDropdown",this.component.querySelector(this.selectors.dropdown)),_defineProperty(this,"clearSearchButton",this.component.querySelector(this.selectors.clearSearch)),_defineProperty(this,"$component",(0,_jquery.default)(this.component)),_defineProperty(this,"selectNode",(node=>{node.focus({preventScroll:!0}),this.searchDropdown.scrollTop=node.offsetTop-node.clientHeight/2})),_defineProperty(this,"moveToFirstNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[0])})),_defineProperty(this,"moveToLastNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[this.resultNodes.length-1])})),_defineProperty(this,"moveToNode",(index=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[index])})),this.setSearchTerms(null!==(_this$searchInput$val=null===(_this$searchInput=this.searchInput)||void 0===_this$searchInput?void 0:_this$searchInput.value)&&void 0!==_this$searchInput$val?_this$searchInput$val:""),this.registerClickHandlers(),this.registerKeyHandlers(),null!==this.searchInput&&this.registerInputHandlers(),this.$component.on("shown.bs.dropdown",(()=>{FocusLockManager.trapFocus(this.component)})),this.$component.on("hidden.bs.dropdown",(()=>{FocusLockManager.untrapFocus()}))}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}filterDataset(dataset){throw new Error("filterDataset(".concat(dataset,") must be implemented in ").concat(this.constructor.name))}filterMatchDataset(){throw new Error("filterMatchDataset() must be implemented in ".concat(this.constructor.name))}renderDropdown(){throw new Error("renderDropdown() must be implemented in ".concat(this.constructor.name))}componentSelector(){throw new Error("componentSelector() must be implemented in ".concat(this.constructor.name))}dropdownSelector(){throw new Error("dropdownSelector() must be implemented in ".concat(this.constructor.name))}triggerSelector(){throw new Error("triggerSelector() must be implemented in ".concat(this.constructor.name))}async getDataset(){return this.dataset||(this.dataset=await this.fetchDataset()),this.datasetSize=this.dataset.length,this.dataset}getDatasetSize(){return this.datasetSize}getMatchedResults(){return this.matchedResults}setMatchedResults(result){this.matchedResults=result}getSearchTerm(){return this.searchTerm}getPreppedSearchTerm(){return this.preppedSearchTerm}setSearchTerms(result){this.searchTerm=result,this.preppedSearchTerm=result.toLowerCase()}getHTMLElements(){return this.updateNodes(),{searchDropdown:this.searchDropdown,currentViewAll:this.currentViewAll,searchInput:this.searchInput,clearSearchButton:this.clearSearchButton,trigger:this.component.querySelector(this.selectors.trigger)}}closeSearch(){let clear=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.toggleDropdown(),clear&&(this.clearSearchButton.classList.add("d-none"),this.setSearchTerms(""),this.searchInput.value="")}searchResultsVisible(){const{searchDropdown:searchDropdown}=this.getHTMLElements();return null!==searchDropdown.offsetParent}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),on?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show()):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide())}updateNodes(){this.resultNodes=[...this.component.querySelectorAll(this.selectors.resultitems)],this.currentNode=this.resultNodes.find((r=>r.id===document.activeElement.id)),this.currentViewAll=this.component.querySelector(this.selectors.viewall),this.clearSearchButton=this.component.querySelector(this.selectors.clearSearch),this.searchInput=this.component.querySelector(this.selectors.input),this.searchDropdown=this.component.querySelector(this.selectors.dropdown)}registerClickHandlers(){this.component.addEventListener("click",this.clickHandler.bind(this))}registerKeyHandlers(){_custom_interaction_events.default.define(document,events),events.forEach((event=>{this.component.addEventListener(event,this.keyHandler.bind(this))}))}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{if(this.getSearchTerm()===this.searchInput.value&&this.searchResultsVisible())return void window.console.warn("Search term matches input value - skipping");this.setSearchTerms(this.searchInput.value);const pendingPromise=new _pending.default;""===this.getSearchTerm()?(this.toggleDropdown(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe()):(this.clearSearchButton.classList.remove("d-none"),await this.renderAndShow()),pendingPromise.resolve()}),300,{pending:!0}))}async filterrenderpipe(){this.updateNodes(),this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}async renderAndShow(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),await this.filterMatchDataset(),await this.renderDropdown(),this.toggleDropdown(!0)}keyUpDown(direction,e){e.preventDefault(),e.stopPropagation(),document.activeElement===this.searchInput&&this.resultNodes.length>0&&(-1===direction?this.moveToLastNode():this.moveToFirstNode());const index=this.resultNodes.indexOf(this.currentNode);this.currentNode?-1===direction?0===index?this.moveToLastNode():this.moveToNode(index-1):index+1>=this.resultNodes.length?this.moveToFirstNode():this.moveToNode(index+1):-1===direction?this.moveToLastNode():this.moveToFirstNode()}async clickHandler(e){this.updateNodes(),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href),e.target.closest(this.selectors.clearSearch)&&0===e.button&&(this.closeSearch(!0),this.searchInput.focus({preventScroll:!0})),e.target.closest(this.selectors.input)&&""!==this.getSearchTerm()&&0===e.button&&await this.renderAndShow()}keyHandler(e){switch(this.updateNodes(),e.key){case"ArrowUp":this.keyUpDown(-1,e);break;case"ArrowDown":this.keyUpDown(1,e);break;case"Home":e.preventDefault(),this.moveToFirstNode();break;case"End":e.preventDefault(),this.moveToLastNode()}}},_exports.default})); //# sourceMappingURL=search_combobox.min.js.map \ No newline at end of file diff --git a/lib/amd/build/comboboxsearch/search_combobox.min.js.map b/lib/amd/build/comboboxsearch/search_combobox.min.js.map index 50f46056f16..e2b7bca61c9 100644 --- a/lib/amd/build/comboboxsearch/search_combobox.min.js.map +++ b/lib/amd/build/comboboxsearch/search_combobox.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search_combobox.min.js","sources":["../../src/comboboxsearch/search_combobox.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\nimport $ from 'jquery';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport {debounce} from 'core/utils';\nimport Pending from 'core/pending';\nimport * as FocusLockManager from 'core/local/aria/focuslock';\n\n/**\n * The class that manages the state of the search within a combobox.\n *\n * @module core/comboboxsearch/search_combobox\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Reused variables for the class.\nconst events = [\n 'keydown',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n];\nconst UP = -1;\nconst DOWN = 1;\n\nexport default class {\n // Define our standard lookups.\n selectors = {\n component: this.componentSelector(),\n trigger: this.triggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.dropdownSelector(),\n resultitems: '[role=\"option\"]',\n viewall: '#select-all',\n };\n\n // The results from the called filter function.\n matchedResults = [];\n\n // What did the user search for?\n searchTerm = '';\n\n // What the user searched for as a lowercase.\n preppedSearchTerm = null;\n\n // The DOM nodes after the dropdown render.\n resultNodes = [];\n\n // Where does the user currently have focus?\n currentNode = null;\n\n // The current node for the view all link.\n currentViewAll = null;\n\n dataset = null;\n\n datasetSize = 0;\n\n // DOM nodes that persist.\n component = document.querySelector(this.selectors.component);\n searchInput = this.component.querySelector(this.selectors.input);\n searchDropdown = this.component.querySelector(this.selectors.dropdown);\n clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n $component = $(this.component);\n\n constructor() {\n // If we have a search input, try to get the value otherwise fallback.\n this.setSearchTerms(this.searchInput?.value ?? '');\n // Begin handling the base search component.\n this.registerClickHandlers();\n this.registerKeyHandlers();\n // Conditionally set up the input handler since we don't know exactly how we were called.\n if (this.searchInput !== null) {\n this.registerInputHandlers();\n }\n\n // Handle the dropdown opening and closing trap focus.\n this.$component.on('shown.bs.dropdown', () => {\n FocusLockManager.trapFocus(this.component);\n });\n this.$component.on('hidden.bs.dropdown', () => {\n FocusLockManager.untrapFocus();\n });\n }\n\n /**\n * Stub out a required function.\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n * @param {Array} dataset\n */\n filterDataset(dataset) {\n throw new Error(`filterDataset(${dataset}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n filterMatchDataset() {\n throw new Error(`filterMatchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n renderDropdown() {\n throw new Error(`renderDropdown() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n componentSelector() {\n throw new Error(`componentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n dropdownSelector() {\n throw new Error(`dropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n triggerSelector() {\n throw new Error(`triggerSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Promise}\n */\n async getDataset() {\n if (!this.dataset) {\n this.dataset = await this.fetchDataset();\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Return the size of the dataset.\n *\n * @returns {number}\n */\n getDatasetSize() {\n return this.datasetSize;\n }\n\n /**\n * Return the results of the filter upon the dataset.\n *\n * @returns {Array}\n */\n getMatchedResults() {\n return this.matchedResults;\n }\n\n /**\n * Given a filter has been run across the dataset, store the matched results.\n *\n * @param {Array} result\n */\n setMatchedResults(result) {\n this.matchedResults = result;\n }\n\n /**\n * Get the value that the user entered.\n *\n * @returns {string}\n */\n getSearchTerm() {\n return this.searchTerm;\n }\n\n /**\n * Get the transformed search value.\n *\n * @returns {string}\n */\n getPreppedSearchTerm() {\n return this.preppedSearchTerm;\n }\n\n /**\n * When a user searches for something, set our variable to manage it.\n *\n * @param {string} result\n */\n setSearchTerms(result) {\n this.searchTerm = result;\n this.preppedSearchTerm = result.toLowerCase();\n }\n\n /**\n * Return an object containing a handfull of dom nodes that we sometimes need the value of.\n *\n * @returns {object}\n */\n getHTMLElements() {\n this.updateNodes();\n return {\n searchDropdown: this.searchDropdown,\n currentViewAll: this.currentViewAll,\n searchInput: this.searchInput,\n clearSearchButton: this.clearSearchButton,\n trigger: this.component.querySelector(this.selectors.trigger),\n };\n }\n\n /**\n * When called, close the dropdown and reset the input field attributes.\n *\n * @param {Boolean} clear Conditionality clear the input box.\n */\n closeSearch(clear = false) {\n this.toggleDropdown();\n if (clear) {\n // Hide the \"clear\" search button search bar.\n this.clearSearchButton.classList.add('d-none');\n // Clear the entered search query in the search bar and hide the search results container.\n this.setSearchTerms('');\n this.searchInput.value = \"\";\n }\n }\n\n /**\n * Check whether search results are currently visible.\n *\n * @returns {Boolean}\n */\n searchResultsVisible() {\n const {searchDropdown} = this.getHTMLElements();\n // If a Node is not visible, then the offsetParent is null.\n return searchDropdown.offsetParent !== null;\n }\n\n /**\n * When called, update the dropdown fields.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n this.$component.dropdown('toggle');\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n }\n }\n\n /**\n * These class members change when a new result set is rendered. So update for fresh data.\n */\n updateNodes() {\n this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)];\n this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id);\n this.currentViewAll = this.component.querySelector(this.selectors.viewall);\n this.clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n this.searchInput = this.component.querySelector(this.selectors.input);\n this.searchDropdown = this.component.querySelector(this.selectors.dropdown);\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n }\n\n /**\n * Register key event listeners.\n */\n registerKeyHandlers() {\n CustomEvents.define(document, events);\n\n // Register click events.\n events.forEach((event) => {\n this.component.addEventListener(event, this.keyHandler.bind(this));\n });\n }\n\n /**\n * Register 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 if (this.getSearchTerm() === this.searchInput.value && this.searchResultsVisible()) {\n window.console.warn(`Search term matches input value - skipping`);\n // The debounce canhappen multiple times quickly. GRrargh\n return;\n }\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n this.toggleDropdown();\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n const pendingPromise = new Pending();\n await this.renderAndShow().then(() => {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n return;\n }).then(() => {\n pendingPromise.resolve();\n return true;\n });\n }\n }, 300, {pending: true}));\n }\n\n /**\n * Update any changeable nodes, filter and then render the result.\n *\n * @returns {Promise}\n */\n async filterrenderpipe() {\n this.updateNodes();\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n await this.renderDropdown();\n }\n\n /**\n * A combo method to take the matching fields and render out the results.\n *\n * @returns {Promise}\n */\n async renderAndShow() {\n // User has given something for us to filter against.\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n await this.filterMatchDataset();\n // Replace the dropdown node contents and show the results.\n await this.renderDropdown();\n // Set the dropdown to open.\n this.toggleDropdown(true);\n }\n\n /**\n * Set the current focus either on the preceding or next result item.\n *\n * @param {Number} direction Is the user moving up or down the resultset?\n * @param {KeyboardEvent} e The JS event from the event handler.\n */\n keyUpDown(direction, e) {\n e.preventDefault();\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n // Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.\n if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\n }\n }\n const index = this.resultNodes.indexOf(this.currentNode);\n if (this.currentNode) {\n if (direction === UP) {\n if (index === 0) {\n this.moveToLastNode();\n } else {\n this.moveToNode(index - 1);\n }\n } else {\n if (index + 1 >= this.resultNodes.length) {\n this.moveToFirstNode();\n } else {\n this.moveToNode(index + 1);\n }\n }\n } else {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\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 this.updateNodes();\n\n // Prevent normal key presses activating this.\n if (e.target.closest('.dropdown-item') && e.button === 0) {\n window.location = e.target.closest('.dropdown-item').href;\n }\n // The \"clear search\" button is triggered.\n if (e.target.closest(this.selectors.clearSearch) && e.button === 0) {\n this.closeSearch(true);\n this.searchInput.focus({preventScroll: true});\n }\n // User may have accidentally clicked off the dropdown and wants to reopen it.\n if (e.target.closest(this.selectors.input) && this.getSearchTerm() !== '' && e.button === 0) {\n await this.renderAndShow();\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 this.updateNodes();\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n this.keyUpDown(UP, e);\n break;\n case 'ArrowDown':\n this.keyUpDown(DOWN, e);\n break;\n case 'Home':\n e.preventDefault();\n this.moveToFirstNode();\n break;\n case 'End':\n e.preventDefault();\n this.moveToLastNode();\n break;\n }\n }\n\n /**\n * Set focus on a given node after parsed through the calling functions.\n *\n * @param {HTMLElement} node The node to set focus upon.\n */\n selectNode = (node) => {\n node.focus({preventScroll: true});\n this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);\n };\n\n /**\n * Set the focus on the first node within the array.\n */\n moveToFirstNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[0]);\n }\n };\n\n /**\n * Set the focus to the final node within the array.\n */\n moveToLastNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[this.resultNodes.length - 1]);\n }\n };\n\n /**\n * Set focus on any given specified node within the node array.\n *\n * @param {Number} index Which item within the array to set focus upon.\n */\n moveToNode = (index) => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[index]);\n }\n };\n}\n"],"names":["events","CustomEvents","activate","keyboardActivate","constructor","component","this","componentSelector","trigger","triggerSelector","input","clearSearch","dropdown","dropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","_this$searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","$component","on","FocusLockManager","trapFocus","untrapFocus","fetchDataset","Error","name","filterDataset","dataset","filterMatchDataset","renderDropdown","datasetSize","getDatasetSize","getMatchedResults","matchedResults","setMatchedResults","result","getSearchTerm","searchTerm","getPreppedSearchTerm","preppedSearchTerm","toLowerCase","getHTMLElements","updateNodes","currentViewAll","clearSearchButton","closeSearch","clear","toggleDropdown","classList","add","searchResultsVisible","offsetParent","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","define","forEach","event","keyHandler","async","window","console","warn","pendingPromise","Pending","renderAndShow","then","resolve","pending","getDataset","keyUpDown","direction","e","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","target","closest","button","location","href","key"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"search_combobox.min.js","sources":["../../src/comboboxsearch/search_combobox.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\nimport $ from 'jquery';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport {debounce} from 'core/utils';\nimport Pending from 'core/pending';\nimport * as FocusLockManager from 'core/local/aria/focuslock';\n\n/**\n * The class that manages the state of the search within a combobox.\n *\n * @module core/comboboxsearch/search_combobox\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Reused variables for the class.\nconst events = [\n 'keydown',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n];\nconst UP = -1;\nconst DOWN = 1;\n\nexport default class {\n // Define our standard lookups.\n selectors = {\n component: this.componentSelector(),\n trigger: this.triggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.dropdownSelector(),\n resultitems: '[role=\"option\"]',\n viewall: '#select-all',\n };\n\n // The results from the called filter function.\n matchedResults = [];\n\n // What did the user search for?\n searchTerm = '';\n\n // What the user searched for as a lowercase.\n preppedSearchTerm = null;\n\n // The DOM nodes after the dropdown render.\n resultNodes = [];\n\n // Where does the user currently have focus?\n currentNode = null;\n\n // The current node for the view all link.\n currentViewAll = null;\n\n dataset = null;\n\n datasetSize = 0;\n\n // DOM nodes that persist.\n component = document.querySelector(this.selectors.component);\n searchInput = this.component.querySelector(this.selectors.input);\n searchDropdown = this.component.querySelector(this.selectors.dropdown);\n clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n $component = $(this.component);\n\n constructor() {\n // If we have a search input, try to get the value otherwise fallback.\n this.setSearchTerms(this.searchInput?.value ?? '');\n // Begin handling the base search component.\n this.registerClickHandlers();\n this.registerKeyHandlers();\n // Conditionally set up the input handler since we don't know exactly how we were called.\n if (this.searchInput !== null) {\n this.registerInputHandlers();\n }\n\n // Handle the dropdown opening and closing trap focus.\n this.$component.on('shown.bs.dropdown', () => {\n FocusLockManager.trapFocus(this.component);\n });\n this.$component.on('hidden.bs.dropdown', () => {\n FocusLockManager.untrapFocus();\n });\n }\n\n /**\n * Stub out a required function.\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n * @param {Array} dataset\n */\n filterDataset(dataset) {\n throw new Error(`filterDataset(${dataset}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n filterMatchDataset() {\n throw new Error(`filterMatchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n renderDropdown() {\n throw new Error(`renderDropdown() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n componentSelector() {\n throw new Error(`componentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n dropdownSelector() {\n throw new Error(`dropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n triggerSelector() {\n throw new Error(`triggerSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Promise}\n */\n async getDataset() {\n if (!this.dataset) {\n this.dataset = await this.fetchDataset();\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Return the size of the dataset.\n *\n * @returns {number}\n */\n getDatasetSize() {\n return this.datasetSize;\n }\n\n /**\n * Return the results of the filter upon the dataset.\n *\n * @returns {Array}\n */\n getMatchedResults() {\n return this.matchedResults;\n }\n\n /**\n * Given a filter has been run across the dataset, store the matched results.\n *\n * @param {Array} result\n */\n setMatchedResults(result) {\n this.matchedResults = result;\n }\n\n /**\n * Get the value that the user entered.\n *\n * @returns {string}\n */\n getSearchTerm() {\n return this.searchTerm;\n }\n\n /**\n * Get the transformed search value.\n *\n * @returns {string}\n */\n getPreppedSearchTerm() {\n return this.preppedSearchTerm;\n }\n\n /**\n * When a user searches for something, set our variable to manage it.\n *\n * @param {string} result\n */\n setSearchTerms(result) {\n this.searchTerm = result;\n this.preppedSearchTerm = result.toLowerCase();\n }\n\n /**\n * Return an object containing a handfull of dom nodes that we sometimes need the value of.\n *\n * @returns {object}\n */\n getHTMLElements() {\n this.updateNodes();\n return {\n searchDropdown: this.searchDropdown,\n currentViewAll: this.currentViewAll,\n searchInput: this.searchInput,\n clearSearchButton: this.clearSearchButton,\n trigger: this.component.querySelector(this.selectors.trigger),\n };\n }\n\n /**\n * When called, close the dropdown and reset the input field attributes.\n *\n * @param {Boolean} clear Conditionality clear the input box.\n */\n closeSearch(clear = false) {\n this.toggleDropdown();\n if (clear) {\n // Hide the \"clear\" search button search bar.\n this.clearSearchButton.classList.add('d-none');\n // Clear the entered search query in the search bar and hide the search results container.\n this.setSearchTerms('');\n this.searchInput.value = \"\";\n }\n }\n\n /**\n * Check whether search results are currently visible.\n *\n * @returns {Boolean}\n */\n searchResultsVisible() {\n const {searchDropdown} = this.getHTMLElements();\n // If a Node is not visible, then the offsetParent is null.\n return searchDropdown.offsetParent !== null;\n }\n\n /**\n * When called, update the dropdown fields.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n this.$component.dropdown('toggle');\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n }\n }\n\n /**\n * These class members change when a new result set is rendered. So update for fresh data.\n */\n updateNodes() {\n this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)];\n this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id);\n this.currentViewAll = this.component.querySelector(this.selectors.viewall);\n this.clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n this.searchInput = this.component.querySelector(this.selectors.input);\n this.searchDropdown = this.component.querySelector(this.selectors.dropdown);\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n }\n\n /**\n * Register key event listeners.\n */\n registerKeyHandlers() {\n CustomEvents.define(document, events);\n\n // Register click events.\n events.forEach((event) => {\n this.component.addEventListener(event, this.keyHandler.bind(this));\n });\n }\n\n /**\n * Register 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 if (this.getSearchTerm() === this.searchInput.value && this.searchResultsVisible()) {\n window.console.warn(`Search term matches input value - skipping`);\n // The debounce canhappen multiple times quickly. GRrargh\n return;\n }\n this.setSearchTerms(this.searchInput.value);\n\n const pendingPromise = new Pending();\n if (this.getSearchTerm() === '') {\n this.toggleDropdown();\n this.clearSearchButton.classList.add('d-none');\n await this.filterrenderpipe();\n } else {\n this.clearSearchButton.classList.remove('d-none');\n await this.renderAndShow();\n }\n pendingPromise.resolve();\n }, 300, {pending: true}));\n }\n\n /**\n * Update any changeable nodes, filter and then render the result.\n *\n * @returns {Promise}\n */\n async filterrenderpipe() {\n this.updateNodes();\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n await this.renderDropdown();\n }\n\n /**\n * A combo method to take the matching fields and render out the results.\n *\n * @returns {Promise}\n */\n async renderAndShow() {\n // User has given something for us to filter against.\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n await this.filterMatchDataset();\n // Replace the dropdown node contents and show the results.\n await this.renderDropdown();\n // Set the dropdown to open.\n this.toggleDropdown(true);\n }\n\n /**\n * Set the current focus either on the preceding or next result item.\n *\n * @param {Number} direction Is the user moving up or down the resultset?\n * @param {KeyboardEvent} e The JS event from the event handler.\n */\n keyUpDown(direction, e) {\n e.preventDefault();\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n // Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.\n if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\n }\n }\n const index = this.resultNodes.indexOf(this.currentNode);\n if (this.currentNode) {\n if (direction === UP) {\n if (index === 0) {\n this.moveToLastNode();\n } else {\n this.moveToNode(index - 1);\n }\n } else {\n if (index + 1 >= this.resultNodes.length) {\n this.moveToFirstNode();\n } else {\n this.moveToNode(index + 1);\n }\n }\n } else {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\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 this.updateNodes();\n\n // Prevent normal key presses activating this.\n if (e.target.closest('.dropdown-item') && e.button === 0) {\n window.location = e.target.closest('.dropdown-item').href;\n }\n // The \"clear search\" button is triggered.\n if (e.target.closest(this.selectors.clearSearch) && e.button === 0) {\n this.closeSearch(true);\n this.searchInput.focus({preventScroll: true});\n }\n // User may have accidentally clicked off the dropdown and wants to reopen it.\n if (e.target.closest(this.selectors.input) && this.getSearchTerm() !== '' && e.button === 0) {\n await this.renderAndShow();\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 this.updateNodes();\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n this.keyUpDown(UP, e);\n break;\n case 'ArrowDown':\n this.keyUpDown(DOWN, e);\n break;\n case 'Home':\n e.preventDefault();\n this.moveToFirstNode();\n break;\n case 'End':\n e.preventDefault();\n this.moveToLastNode();\n break;\n }\n }\n\n /**\n * Set focus on a given node after parsed through the calling functions.\n *\n * @param {HTMLElement} node The node to set focus upon.\n */\n selectNode = (node) => {\n node.focus({preventScroll: true});\n this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);\n };\n\n /**\n * Set the focus on the first node within the array.\n */\n moveToFirstNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[0]);\n }\n };\n\n /**\n * Set the focus to the final node within the array.\n */\n moveToLastNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[this.resultNodes.length - 1]);\n }\n };\n\n /**\n * Set focus on any given specified node within the node array.\n *\n * @param {Number} index Which item within the array to set focus upon.\n */\n moveToNode = (index) => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[index]);\n }\n };\n}\n"],"names":["events","CustomEvents","activate","keyboardActivate","constructor","component","this","componentSelector","trigger","triggerSelector","input","clearSearch","dropdown","dropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","_this$searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","$component","on","FocusLockManager","trapFocus","untrapFocus","fetchDataset","Error","name","filterDataset","dataset","filterMatchDataset","renderDropdown","datasetSize","getDatasetSize","getMatchedResults","matchedResults","setMatchedResults","result","getSearchTerm","searchTerm","getPreppedSearchTerm","preppedSearchTerm","toLowerCase","getHTMLElements","updateNodes","currentViewAll","clearSearchButton","closeSearch","clear","toggleDropdown","classList","add","searchResultsVisible","offsetParent","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","define","forEach","event","keyHandler","async","window","console","warn","pendingPromise","Pending","filterrenderpipe","renderAndShow","resolve","pending","getDataset","keyUpDown","direction","e","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","target","closest","button","location","href","key"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/comboboxsearch/search_combobox.js b/lib/amd/src/comboboxsearch/search_combobox.js index f7e26533a65..ea9d789f051 100644 --- a/lib/amd/src/comboboxsearch/search_combobox.js +++ b/lib/amd/src/comboboxsearch/search_combobox.js @@ -317,22 +317,17 @@ export default class { return; } this.setSearchTerms(this.searchInput.value); - // We can also require a set amount of input before search. + + const pendingPromise = new Pending(); if (this.getSearchTerm() === '') { this.toggleDropdown(); - // Hide the "clear" search button in the search bar. this.clearSearchButton.classList.add('d-none'); + await this.filterrenderpipe(); } else { - const pendingPromise = new Pending(); - await this.renderAndShow().then(() => { - // Display the "clear" search button in the search bar. - this.clearSearchButton.classList.remove('d-none'); - return; - }).then(() => { - pendingPromise.resolve(); - return true; - }); + this.clearSearchButton.classList.remove('d-none'); + await this.renderAndShow(); } + pendingPromise.resolve(); }, 300, {pending: true})); } diff --git a/lib/templates/local/comboboxsearch/resultset.mustache b/lib/templates/local/comboboxsearch/resultset.mustache index ad8dc0fc3e9..27133d9d8f8 100644 --- a/lib/templates/local/comboboxsearch/resultset.mustache +++ b/lib/templates/local/comboboxsearch/resultset.mustache @@ -41,17 +41,17 @@ } }}
- {{#hasresults}} -
    +
      + {{#hasresults}} {{$results}} {{#results}} {{>core/local/comboboxsearch/resultitem}} {{/results}} {{/results}} {{$selectall}}{{/selectall}} -
    - {{/hasresults}} + {{/hasresults}} +
{{^hasresults}} - {{#str}} noresultsfor, core, {{searchterm}}{{/str}} + {{#str}} noresultsfor, core, {{searchterm}}{{/str}} {{/hasresults}}
diff --git a/user/amd/build/comboboxsearch/user.min.js b/user/amd/build/comboboxsearch/user.min.js index 770cfc46df5..9eed9564a30 100644 --- a/user/amd/build/comboboxsearch/user.min.js +++ b/user/amd/build/comboboxsearch/user.min.js @@ -1,3 +1,3 @@ -define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery","core/notification"],(function(_exports,_search_combobox,_str,_templates,_jquery,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()})),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid,this.renderDefault()}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)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset();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(e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(e.stopPropagation(),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&&this.closeSearch()}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","true")):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","false"))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default})); +define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery","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,this.renderDefault()}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)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){if(this.getPreppedSearchTerm()){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}return[]}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")"),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(e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(e.stopPropagation(),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&&this.closeSearch()}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","true")):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.getHTMLElements().searchInput.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 b9f090d1b3a..7f76dd3b476 100644 --- a/user/amd/build/comboboxsearch/user.min.js.map +++ b/user/amd/build/comboboxsearch/user.min.js.map @@ -1 +1 @@ -{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to search for learners.\n *\n * @module core_user/comboboxsearch/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\nimport Notification from 'core/notification';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n\n // We need to render some content by default for ARIA purposes.\n this.renderDefault();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * 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 * 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 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 // We don't call the super here because we want to let aria.js handle the key presses mostly.\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 e.stopPropagation();\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 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.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n this.getHTMLElements().searchInput.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","renderDefault","dropdownSelector","triggerSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","setMatchedResults","filterDataset","getDataset","filterMatchDataset","fetchDataset","Error","name","filterableData","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","getPreppedSearchTerm","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","stopPropagation","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","focus","preventScroll","clearSearch","shiftKey","add","show","setAttribute","remove","hide","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"+rBA4BqBA,mBAAmBC,yBAQpCC,8LAFmB,MAKfC,SAASC,iBAAiB,SAAUC,KAE3BA,EAAEC,OAAOC,QAAQC,KAAKC,UAAUC,YAAcF,KAAKG,eAAeC,UAAUC,SAAS,cACjFC,yBAKRL,UAAY,IAAID,KAAKC,UACtBM,SAAU,2BACVC,QAAS,0BACTC,gBAAiB,mCAGfP,UAAYP,SAASe,cAAcV,KAAKW,0BACzCC,SAAWV,UAAUQ,cAAcV,KAAKC,UAAUM,UAAUM,QAAQN,cACpEO,sCAAUnB,SAASe,cAAcV,KAAKC,UAAUO,0EAAtCO,sBAAgDF,iDAAhDG,uBAAyDR,aAGnES,qCAIE,IAAIzB,WAQfmB,0BACW,eAQXO,yBACW,sBAQXC,wBACW,iDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOtB,KAAKuB,oBAAoBC,MAAM,EAAG,GACzCC,WAAYzB,KAAKuB,oBAAoBG,OAAS,EAC9CC,QAAS3B,KAAKuB,oBAAoBG,OAClCE,WAAY5B,KAAK6B,gBACjBC,UAAW9B,KAAK+B,4DAEA/B,KAAKgC,kBAAkB7B,eAAgBiB,KAAMC,+BAO5DY,wBAAwBjC,KAAKkC,oBAAoBlC,KAAKmC,oBACtDC,2BAEChB,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOtB,KAAKuB,oBAAoBC,MAAM,EAAG,GACzCC,WAAYzB,KAAKuB,oBAAoBG,OAAS,EAC9CC,QAAS3B,KAAKuB,oBAAoBG,OAClCE,WAAY5B,KAAK6B,gBACjBC,UAAW9B,KAAK+B,4DAEA/B,KAAKgC,kBAAkB7B,eAAgBiB,KAAMC,IAQrEgB,qBACU,IAAIC,sDAA+CtC,KAAKN,YAAY6C,2BAS1DC,sBACVC,gBAAkBzC,KAAK0C,sBACtBF,eAAeG,QAAQC,MAASC,OAAOC,KAAKF,MAAMG,MAAMC,OACzC,KAAdJ,KAAKI,MAA6B,OAAdJ,KAAKI,OAAkBP,UAAUQ,IAAID,OAGtDJ,KAAKI,KAAKE,WAAWC,cAAcC,SAASpD,KAAKqD,6DAUtDZ,gBAAkBzC,KAAK0C,oBACxBT,kBACDjC,KAAKuB,oBAAoB+B,KAAKV,WACrB,MAAOI,IAAKO,SAAUV,OAAOW,QAAQZ,MAAO,IAE/B,OAAVW,qBAIEE,YAAcF,MAAML,WAAWC,cAC/BO,kBAAoB1D,KAAKqD,uBACzBM,WAAa3D,KAAK6B,gBAGlB+B,kBAAoBnB,UAAUQ,IAAID,QACpCY,mBAAqBH,YAAYL,SAASM,mBAAoB,CAC9Dd,KAAKgB,kBAAoBA,wBAInBC,qBADqBJ,YAAYK,QAAQ,KAAM,QACLA,QAC5CJ,kBAAkBI,QAAQ,KAAM,iDACEH,WAAWG,QAAQ,KAAM,oBAG/DlB,KAAKmB,wBAAmBF,kCAAyBjB,KAAKoB,WACtDpB,KAAKqB,KAAOjE,KAAKkE,cAActB,KAAKuB,kBAIrCvB,SAUnBwB,aAAavE,SACHuE,aAAavE,GAAGwE,MAAMC,sBAAaC,WACrC1E,EAAEC,OAAOC,QAAQC,KAAKC,UAAUC,YAGhCL,EAAE2E,2BAEF3E,EAAEC,SAAWE,KAAKgC,kBAAkByC,gBAA+B,IAAb5E,EAAE6E,SACxDC,OAAOC,SAAW5E,KAAK+B,wBAEvBlC,EAAEC,OAAOC,QAAQC,KAAKC,UAAUQ,mBAChCkE,OAAOC,SAAW/E,EAAEC,OAAOC,QAAQC,KAAKC,UAAUQ,iBAAiBoE,MAS3EC,WAAWjF,UAGHA,EAAEC,SAAWE,KAAKgC,kBAAkByC,gBAA6B,UAAV5E,EAAEmD,KAA6B,UAAVnD,EAAEmD,MAC9E2B,OAAOC,SAAW5E,KAAK+B,wBAInBlC,EAAEmD,SACD,YACA,OACDnD,EAAEkF,kBACEpF,SAASqF,gBAAkBhF,KAAKgC,kBAAkBiD,aACpC,UAAVpF,EAAEmD,KAAmD,OAAhChD,KAAK+B,yBAC1B4C,OAAOC,SAAW5E,KAAK+B,wBAG3BpC,SAASqF,gBAAkBhF,KAAKgC,kBAAkBkD,kBAAmB,MAChEC,aAAY,YAGjBtF,EAAEC,OAAOC,QAAQC,KAAKC,UAAUQ,iBAAkB,CAClDkE,OAAOC,SAAW/E,EAAEC,OAAOC,QAAQC,KAAKC,UAAUQ,iBAAiBoE,cAGnEhF,EAAEC,OAAOC,QAAQ,kBAAmB,CACpCF,EAAEuF,iBACFT,OAAOC,SAAW/E,EAAEC,OAAOC,QAAQ,kBAAkB8E,qBAIxD,cACIvE,sBACA2E,YAAYI,MAAM,CAACC,eAAe,cAEtC,MAEGzF,EAAEC,OAAOC,QAAQC,KAAKC,UAAUsF,cAC5BvF,KAAKyE,iBAAmB5E,EAAE2F,eACrBL,eAYzB7E,+EAEaH,eAAeC,UAAUqF,IAAI,4BAChCzF,KAAKG,gBAAgBuF,YAClB1D,kBAAkBiD,YAAYU,aAAa,gBAAiB,eAE5DxF,eAAeC,UAAUwF,OAAO,4BACnC5F,KAAKG,gBAAgB0F,YAClB7D,kBAAkBiD,YAAYU,aAAa,gBAAiB,UAOzE5D,6BACU,IAAIO,8DAAuDtC,KAAKN,YAAY6C,OAQtF2B,cAAc4B,cACJ,IAAIxD,8BAAuBwD,2CAAkC9F,KAAKN,YAAY6C,OASxFG,mBACS1C,KAAK+F,iBAAkB,OAClBC,gBAAkB,CACpB,WACA,WACA,YACA,WACA,QACA,OACA,UACA,aACA,cACA,WACA,SACA,eAECD,kBAAmB,mBAAWC,gBAAgB1C,KAAKN,OAAUA,IAAAA,SAC7DiD,MAAMC,aAAgB,IAAIC,IACvBH,gBAAgB1C,KAAI,CAACN,IAAKoD,QAAW,CAACpD,IAAKkD,YAAYE,oBAG5DpG,KAAK+F"} \ 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 // We need to render some content by default for ARIA purposes.\n this.renderDefault();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * 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 * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n if (this.getPreppedSearchTerm()) {\n const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n } else {\n return [];\n }\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n 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 // We don't call the super here because we want to let aria.js handle the key presses mostly.\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 e.stopPropagation();\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 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.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n this.getHTMLElements().searchInput.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","renderDefault","dropdownSelector","triggerSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","fetchDataset","Error","name","filterableData","getPreppedSearchTerm","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","matchingField","email","link","selectOneLink","id","clickHandler","catch","Notification","exception","stopImmediatePropagation","currentViewAll","button","window","location","href","keyHandler","stopPropagation","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 e639932b84a..343fad197e2 100644 --- a/user/amd/src/comboboxsearch/user.js +++ b/user/amd/src/comboboxsearch/user.js @@ -111,14 +111,7 @@ export default class UserSearch extends search_combobox { this.setMatchedResults(await this.filterDataset(await this.getDataset())); this.filterMatchDataset(); - const {html, js} = await 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(), - }); - replaceNodeContents(this.getHTMLElements().searchDropdown, html, js); + await this.renderDropdown(); } /** @@ -137,13 +130,17 @@ export default class UserSearch extends search_combobox { * @returns {Array} The users that match the given criteria. */ async filterDataset(filterableData) { - const stringMap = await this.getStringMap(); - return filterableData.filter((user) => Object.keys(user).some((key) => { - if (user[key] === "" || user[key] === null || !stringMap.get(key)) { - return false; - } - return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm()); - })); + if (this.getPreppedSearchTerm()) { + const stringMap = await this.getStringMap(); + return filterableData.filter((user) => Object.keys(user).some((key) => { + if (user[key] === "" || user[key] === null || !stringMap.get(key)) { + return false; + } + return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm()); + })); + } else { + return []; + } } /**