From ae6826953eda157c849e783cc83814dd7f2d1bad Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 13 Apr 2023 16:34:26 +0800 Subject: [PATCH] MDL-77902 gradereport_grader: Clear btn & user differentiation --- grade/report/grader/amd/build/search.min.js | 2 +- .../report/grader/amd/build/search.min.js.map | 2 +- .../amd/build/search/search_class.min.js | 2 +- .../amd/build/search/search_class.min.js.map | 2 +- grade/report/grader/amd/src/search.js | 24 ++++++-- .../grader/amd/src/search/search_class.js | 16 ++++-- .../grader/classes/output/action_bar.php | 2 + .../grader/lang/en/gradereport_grader.php | 4 +- grade/report/grader/lib.php | 3 +- grade/report/grader/styles.css | 3 +- .../templates/search/resultitem.mustache | 4 +- .../templates/search/resultset.mustache | 14 ++--- .../templates/search/searchinput.mustache | 6 +- .../tertiary_navigation_searching.feature | 57 ++++++++++++------- 14 files changed, 91 insertions(+), 50 deletions(-) diff --git a/grade/report/grader/amd/build/search.min.js b/grade/report/grader/amd/build/search.min.js index 4ffcb9153052e..1899d7b2de545 100644 --- a/grade/report/grader/amd/build/search.min.js +++ b/grade/report/grader/amd/build/search.min.js @@ -1,3 +1,3 @@ -define("gradereport_grader/search",["exports","gradereport_grader/search/search_class","gradereport_grader/search/repository","core/str","core/url","core/templates"],(function(_exports,_search_class,Repository,_str,_url,_templates){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_class=_interopRequireDefault(_search_class),Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository),_url=_interopRequireDefault(_url);const selectors_component=".user-search",selectors_courseid='[data-region="courseid"]',courseID=document.querySelector(selectors_component).querySelector(selectors_courseid).dataset.courseid,bannedFilterFields=["profileimageurlsmall","profileimageurl","id","link","matchingField","matchingFieldName"];class UserSearch extends _search_class.default{constructor(){var obj,key,value;super(),value=null,(key="profilestringmap")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}static init(){return new UserSearch}setComponentSelector(){return".user-search"}setDropdownSelector(){return".usersearchdropdown"}setTriggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/search/resultset",{users:this.getMatchedResults(),hasusers:this.getMatchedResults().length>0,total:this.getDatasetSize(),found:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){return Repository.userFetch(courseID).then((r=>r.users))}async filterDataset(filterableData){return filterableData.filter((user=>Object.keys(user).some((key=>""!==user[key]&&!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)){var _stringMap$get;const valueString=value.toString().toLowerCase();if(valueString.includes(this.getPreppedSearchTerm())){user.matchingFieldName=null!==(_stringMap$get=stringMap.get(key))&&void 0!==_stringMap$get?_stringMap$get:key,user.matchingField=valueString.replace(this.getPreppedSearchTerm(),''.concat(this.getSearchTerm(),"")),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink())}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){if(" "===e.key)break;window.location=this.selectAllResultsLink();break}if(document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch();break}e.preventDefault(),window.location=e.target.closest(".dropdown-item").href}}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.get_strings)(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("gradereport_grader/search",["exports","gradereport_grader/search/search_class","gradereport_grader/search/repository","core/str","core/url","core/templates"],(function(_exports,_search_class,Repository,_str,_url,_templates){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_class=_interopRequireDefault(_search_class),Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository),_url=_interopRequireDefault(_url);const selectors_component=".user-search",selectors_courseid='[data-region="courseid"]',selectors_resetPageButton='[data-action="resetpage"]',courseID=document.querySelector(selectors_component).querySelector(selectors_courseid).dataset.courseid,bannedFilterFields=["profileimageurlsmall","profileimageurl","id","link","matchingField","matchingFieldName"];class UserSearch extends _search_class.default{constructor(){var obj,key,value;super(),value=null,(key="profilestringmap")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}static init(){return new UserSearch}setComponentSelector(){return".user-search"}setDropdownSelector(){return".usersearchdropdown"}setTriggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/search/resultset",{users:this.getMatchedResults().slice(0,5),hasusers:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,showing:this.getMatchedResults().slice(0,5).length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){return Repository.userFetch(courseID).then((r=>r.users))}async filterDataset(filterableData){return filterableData.filter((user=>Object.keys(user).some((key=>""!==user[key]&&!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)){var _stringMap$get;const valueString=value.toString().toLowerCase();if(valueString.includes(this.getPreppedSearchTerm())){user.matchingFieldName=null!==(_stringMap$get=stringMap.get(key))&&void 0!==_stringMap$get?_stringMap$get:key,user.matchingField=valueString.replace(this.getPreppedSearchTerm(),''.concat(this.getSearchTerm(),"")),user.matchingField="".concat(user.matchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(selectors_resetPageButton)&&(window.location=e.target.closest(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){if(" "===e.key)break;window.location=this.selectAllResultsLink();break}if(document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(selectors_resetPageButton)){window.location=e.target.closest(selectors_resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}}}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.get_strings)(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=search.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/search.min.js.map b/grade/report/grader/amd/build/search.min.js.map index 22c3cd557f0f1..7abfd85812e91 100644 --- a/grade/report/grader/amd/build/search.min.js.map +++ b/grade/report/grader/amd/build/search.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search.min.js","sources":["../src/search.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 within the grader report.\n * Have to basically search twice on the dataset to avoid passing around massive csv params whilst allowing debouncing.\n *\n * @module gradereport_grader/search\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GradebookSearchClass from 'gradereport_grader/search/search_class';\nimport * as Repository from 'gradereport_grader/search/repository';\nimport {get_strings as getStrings} from 'core/str';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\nconst bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];\n\nexport default class UserSearch extends GradebookSearchClass {\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\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 setComponentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n setDropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n setTriggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/search/resultset', {\n users: this.getMatchedResults(),\n hasusers: this.getMatchedResults().length > 0,\n total: this.getDatasetSize(),\n found: 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 return Repository.userFetch(courseID).then((r) => r.users);\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] === \"\" || 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 const valueString = value.toString().toLowerCase();\n if (!valueString.includes(this.getPreppedSearchTerm())) {\n continue;\n }\n // Ensure we have a good string, otherwise fallback to the key.\n user.matchingFieldName = stringMap.get(key) ?? key;\n user.matchingField = valueString.replace(\n this.getPreppedSearchTerm(),\n `${this.getSearchTerm()}`\n );\n user.link = this.selectOneLink(user.id);\n break;\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);\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\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 === ' ') {\n break;\n } else {\n window.location = this.selectAllResultsLink();\n break;\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch();\n break;\n }\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm()\n }, false);\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 * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\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":["selectors","courseID","document","querySelector","dataset","courseid","bannedFilterFields","UserSearch","GradebookSearchClass","constructor","setComponentSelector","setDropdownSelector","setTriggerSelector","html","js","users","this","getMatchedResults","hasusers","length","total","getDatasetSize","found","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","fetchDataset","Repository","userFetch","then","r","filterableData","filter","user","Object","keys","some","key","includes","toString","toLowerCase","getPreppedSearchTerm","stringMap","getStringMap","setMatchedResults","map","value","entries","valueString","matchingFieldName","get","matchingField","replace","link","selectOneLink","id","clickHandler","e","target","currentViewAll","button","window","location","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","closest","href","Url","relativeUrl","searchvalue","userID","userid","profilestringmap","requiredStrings","stringArray","Map","index"],"mappings":"65CA8BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,SAC/DC,mBAAqB,CAAC,uBAAwB,kBAAmB,KAAM,OAAQ,gBAAiB,2BAEjFC,mBAAmBC,sBAKpCC,8CAFmB,qKAOR,IAAIF,WAQfG,6BACW,eAQXC,4BACW,sBAQXC,2BACW,iDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,MAAOC,KAAKC,oBACZC,SAAUF,KAAKC,oBAAoBE,OAAS,EAC5CC,MAAOJ,KAAKK,iBACZC,MAAON,KAAKC,oBAAoBE,OAChCI,WAAYP,KAAKQ,gBACjBC,UAAWT,KAAKU,4DAEAV,KAAKW,kBAAkBC,eAAgBf,KAAMC,IAQrEe,sBACWC,WAAWC,UAAU9B,UAAU+B,MAAMC,GAAMA,EAAElB,4BASpCmB,uBACTA,eAAeC,QAAQC,MAASC,OAAOC,KAAKF,MAAMG,MAAMC,KACzC,KAAdJ,KAAKI,OAAelC,mBAAmBmC,SAASD,MAG7CJ,KAAKI,KAAKE,WAAWC,cAAcF,SAASzB,KAAK4B,6DAUtDC,gBAAkB7B,KAAK8B,oBACxBC,kBACD/B,KAAKC,oBAAoB+B,KAAKZ,WACrB,MAAOI,IAAKS,SAAUZ,OAAOa,QAAQd,MAAO,0BACvCe,YAAcF,MAAMP,WAAWC,iBAChCQ,YAAYV,SAASzB,KAAK4B,yBAI/BR,KAAKgB,yCAAoBP,UAAUQ,IAAIb,8CAAQA,IAC/CJ,KAAKkB,cAAgBH,YAAYI,QAC7BvC,KAAK4B,gEAC6B5B,KAAKQ,4BAE3CY,KAAKoB,KAAOxC,KAAKyC,cAAcrB,KAAKsB,kBAGjCtB,SAUnBuB,aAAaC,SACHD,aAAaC,GACfA,EAAEC,SAAW7C,KAAKW,kBAAkBmC,gBAA+B,IAAbF,EAAEG,SACxDC,OAAOC,SAAWjD,KAAKU,wBAS/BwC,WAAWN,gBACDM,WAAWN,GAEbA,EAAEC,SAAW7C,KAAKW,kBAAkBmC,gBAA6B,UAAVF,EAAEpB,KAA6B,UAAVoB,EAAEpB,MAC9EwB,OAAOC,SAAWjD,KAAKU,wBAInBkC,EAAEpB,SACD,YACA,OACGtC,SAASiE,gBAAkBnD,KAAKW,kBAAkByC,YAAa,IACjD,MAAVR,EAAEpB,UAGFwB,OAAOC,SAAWjD,KAAKU,gCAI3BxB,SAASiE,gBAAkBnD,KAAKW,kBAAkB0C,kBAAmB,MAChEC,oBAGTV,EAAEW,iBACFP,OAAOC,SAAWL,EAAEC,OAAOW,QAAQ,kBAAkBC,MAUjE/C,8BACWgD,aAAIC,YAAY,iCAAkC,CACrDjB,GAAIzD,SACJ2E,YAAa5D,KAAKQ,kBACnB,GASPiC,cAAcoB,eACHH,aAAIC,YAAY,iCAAkC,CACrDjB,GAAIzD,SACJ2E,YAAa5D,KAAKQ,gBAClBsD,OAAQD,SACL,GASX/B,mBACS9B,KAAK+D,iBAAkB,OAClBC,gBAAkB,CACpB,WACA,YACA,WACA,QACA,OACA,UACA,aACA,cACA,WACA,SACA,eAECD,kBAAmB,oBAAWC,gBAAgBhC,KAAKR,OAAUA,IAAAA,SAC7DR,MAAMiD,aAAgB,IAAIC,IACvBF,gBAAgBhC,KAAI,CAACR,IAAK2C,QAAW,CAAC3C,IAAKyC,YAAYE,oBAG5DnE,KAAK+D"} \ No newline at end of file +{"version":3,"file":"search.min.js","sources":["../src/search.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 within the grader report.\n * Have to basically search twice on the dataset to avoid passing around massive csv params whilst allowing debouncing.\n *\n * @module gradereport_grader/search\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GradebookSearchClass from 'gradereport_grader/search/search_class';\nimport * as Repository from 'gradereport_grader/search/repository';\nimport {get_strings as getStrings} from 'core/str';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\nconst bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];\n\nexport default class UserSearch extends GradebookSearchClass {\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\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 setComponentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n setDropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n setTriggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/search/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasusers: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n showing: this.getMatchedResults().slice(0, 5).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 return Repository.userFetch(courseID).then((r) => r.users);\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] === \"\" || 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 const valueString = value.toString().toLowerCase();\n if (!valueString.includes(this.getPreppedSearchTerm())) {\n continue;\n }\n // Ensure we have a good string, otherwise fallback to the key.\n user.matchingFieldName = stringMap.get(key) ?? key;\n user.matchingField = valueString.replace(\n this.getPreppedSearchTerm(),\n `${this.getSearchTerm()}`\n );\n user.matchingField = `${user.matchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\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);\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(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 === ' ') {\n break;\n } else {\n window.location = this.selectAllResultsLink();\n break;\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(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 }\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm()\n }, false);\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 * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\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":["selectors","courseID","document","querySelector","dataset","courseid","bannedFilterFields","UserSearch","GradebookSearchClass","constructor","setComponentSelector","setDropdownSelector","setTriggerSelector","html","js","users","this","getMatchedResults","slice","hasusers","length","matches","showing","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","fetchDataset","Repository","userFetch","then","r","filterableData","filter","user","Object","keys","some","key","includes","toString","toLowerCase","getPreppedSearchTerm","stringMap","getStringMap","setMatchedResults","map","value","entries","valueString","matchingFieldName","get","matchingField","replace","email","link","selectOneLink","id","clickHandler","e","target","currentViewAll","button","window","location","closest","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","Url","relativeUrl","searchvalue","userID","userid","profilestringmap","requiredStrings","stringArray","Map","index"],"mappings":"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"} \ No newline at end of file diff --git a/grade/report/grader/amd/build/search/search_class.min.js b/grade/report/grader/amd/build/search/search_class.min.js index 90855441b3d09..1aabfffd312d1 100644 --- a/grade/report/grader/amd/build/search/search_class.min.js +++ b/grade/report/grader/amd/build/search/search_class.min.js @@ -5,6 +5,6 @@ define("gradereport_grader/search/search_class",["exports","jquery","core/custom * @module gradereport_grader/search/search_class * @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);const events=["keydown",(_custom_interaction_events=_interopRequireDefault(_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.setComponentSelector(),trigger:this.setTriggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.setDropdownSelector(),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,"$searchButton",(0,_jquery.default)(this.selectors.trigger)),_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()}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))}setComponentSelector(){throw new Error("setComponentSelector() must be implemented in ".concat(this.constructor.name))}setDropdownSelector(){throw new Error("setDropdownSelector() must be implemented in ".concat(this.constructor.name))}setTriggerSelector(){throw new Error("setTriggerSelector() 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}}closeSearch(){this.toggleDropdown(),this.clearSearchButton.classList.add("d-none"),this.setSearchTerms(""),this.searchInput.value=""}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),this.$searchButton.attr("aria-expanded",on),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.$searchButton.on("click",(()=>{this.toggleDropdown()})),this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()}))}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()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?(this.toggleDropdown(),this.clearSearchButton.classList.add("d-none")):(this.clearSearchButton.classList.remove("d-none"),await this.renderAndShow())}),300))}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))}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(),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();break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch()),e.target.closest(this.selectors.viewall)&&this.closeSearch()}}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery);const events=["keydown",(_custom_interaction_events=_interopRequireDefault(_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.setComponentSelector(),trigger:this.setTriggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.setDropdownSelector(),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,"$searchButton",(0,_jquery.default)(this.selectors.trigger)),_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()}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))}setComponentSelector(){throw new Error("setComponentSelector() must be implemented in ".concat(this.constructor.name))}setDropdownSelector(){throw new Error("setDropdownSelector() must be implemented in ".concat(this.constructor.name))}setTriggerSelector(){throw new Error("setTriggerSelector() 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}}closeSearch(){let clear=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.toggleDropdown(),this.clearSearchButton.classList.add("d-none"),clear&&(this.setSearchTerms(""),this.searchInput.value="")}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),this.$searchButton.attr("aria-expanded",on),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.$searchButton.on("click",(()=>{this.toggleDropdown()})),this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()}))}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()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?(this.toggleDropdown(),this.clearSearchButton.classList.add("d-none")):(this.clearSearchButton.classList.remove("d-none"),await this.renderAndShow())}),300))}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))}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();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()),e.target.closest(this.selectors.viewall)&&this.closeSearch()}}},_exports.default})); //# sourceMappingURL=search_class.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/search/search_class.min.js.map b/grade/report/grader/amd/build/search/search_class.min.js.map index 550c1f573653e..990b144b45cd7 100644 --- a/grade/report/grader/amd/build/search/search_class.min.js.map +++ b/grade/report/grader/amd/build/search/search_class.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search_class.min.js","sources":["../../src/search/search_class.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';\n\n/**\n * The class that manages the state of the search.\n *\n * @module gradereport_grader/search/search_class\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.setComponentSelector(),\n trigger: this.setTriggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.setDropdownSelector(),\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 $searchButton = $(this.selectors.trigger);\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\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 setComponentSelector() {\n throw new Error(`setComponentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setDropdownSelector() {\n throw new Error(`setDropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setTriggerSelector() {\n throw new Error(`setTriggerSelector() 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 };\n }\n\n /**\n * When called, close the dropdown and reset the input field attributes.\n */\n closeSearch() {\n this.toggleDropdown();\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 * 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 this.$searchButton.attr('aria-expanded', on);\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 // Prevent the click triggering the dropdown.\n this.$searchButton.on('click', () => {\n this.toggleDropdown();\n });\n\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\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\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 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 // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n await this.renderAndShow();\n }\n }, 300));\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 }\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();\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 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) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n // If the current focus is on the view all link, then close the widget then set focus on the next tertiary nav item.\n if (e.target.closest(this.selectors.viewall)) {\n this.closeSearch();\n }\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","activate","CustomEvents","keyboardActivate","constructor","component","this","setComponentSelector","trigger","setTriggerSelector","input","clearSearch","dropdown","setDropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","_this$searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","fetchDataset","Error","name","filterDataset","dataset","filterMatchDataset","renderDropdown","datasetSize","getDatasetSize","getMatchedResults","matchedResults","setMatchedResults","result","getSearchTerm","searchTerm","getPreppedSearchTerm","preppedSearchTerm","toLowerCase","getHTMLElements","updateNodes","currentViewAll","clearSearchButton","closeSearch","toggleDropdown","classList","add","on","$component","$searchButton","attr","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","e","target","closest","contains","define","forEach","event","keyHandler","async","renderAndShow","getDataset","keyUpDown","direction","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","button","window","location","href","key"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"search_class.min.js","sources":["../../src/search/search_class.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';\n\n/**\n * The class that manages the state of the search.\n *\n * @module gradereport_grader/search/search_class\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.setComponentSelector(),\n trigger: this.setTriggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.setDropdownSelector(),\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 $searchButton = $(this.selectors.trigger);\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\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 setComponentSelector() {\n throw new Error(`setComponentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setDropdownSelector() {\n throw new Error(`setDropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setTriggerSelector() {\n throw new Error(`setTriggerSelector() 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 };\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 // Hide the \"clear\" search button search bar.\n this.clearSearchButton.classList.add('d-none');\n if (clear) {\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 * 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 this.$searchButton.attr('aria-expanded', on);\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 // Prevent the click triggering the dropdown.\n this.$searchButton.on('click', () => {\n this.toggleDropdown();\n });\n\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\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\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 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 // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n await this.renderAndShow();\n }\n }, 300));\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 }\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 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 // If the current focus is on the view all link, then close the widget then set focus on the next tertiary nav item.\n if (e.target.closest(this.selectors.viewall)) {\n this.closeSearch();\n }\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","activate","CustomEvents","keyboardActivate","constructor","component","this","setComponentSelector","trigger","setTriggerSelector","input","clearSearch","dropdown","setDropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","_this$searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","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","on","$component","$searchButton","attr","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","e","target","closest","contains","define","forEach","event","keyHandler","async","renderAndShow","getDataset","keyUpDown","direction","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","button","window","location","href","key","shiftKey"],"mappings":"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"} \ No newline at end of file diff --git a/grade/report/grader/amd/src/search.js b/grade/report/grader/amd/src/search.js index 9cc7146cb0d10..e69450fbbe21e 100644 --- a/grade/report/grader/amd/src/search.js +++ b/grade/report/grader/amd/src/search.js @@ -31,6 +31,7 @@ import {renderForPromise, replaceNodeContents} from 'core/templates'; const selectors = { component: '.user-search', courseid: '[data-region="courseid"]', + resetPageButton: '[data-action="resetpage"]', }; const component = document.querySelector(selectors.component); const courseID = component.querySelector(selectors.courseid).dataset.courseid; @@ -81,10 +82,10 @@ export default class UserSearch extends GradebookSearchClass { */ async renderDropdown() { const {html, js} = await renderForPromise('gradereport_grader/search/resultset', { - users: this.getMatchedResults(), + users: this.getMatchedResults().slice(0, 5), hasusers: this.getMatchedResults().length > 0, - total: this.getDatasetSize(), - found: this.getMatchedResults().length, + matches: this.getMatchedResults().length, + showing: this.getMatchedResults().slice(0, 5).length, searchterm: this.getSearchTerm(), selectall: this.selectAllResultsLink(), }); @@ -135,6 +136,7 @@ export default class UserSearch extends GradebookSearchClass { this.getPreppedSearchTerm(), `${this.getSearchTerm()}` ); + user.matchingField = `${user.matchingField} (${user.email})`; user.link = this.selectOneLink(user.id); break; } @@ -153,6 +155,9 @@ export default class UserSearch extends GradebookSearchClass { if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) { window.location = this.selectAllResultsLink(); } + if (e.target.closest(selectors.resetPageButton)) { + window.location = e.target.closest(selectors.resetPageButton).href; + } } /** @@ -180,11 +185,18 @@ export default class UserSearch extends GradebookSearchClass { } } if (document.activeElement === this.getHTMLElements().clearSearchButton) { - this.closeSearch(); + this.closeSearch(true); + break; + } + if (e.target.closest(selectors.resetPageButton)) { + window.location = e.target.closest(selectors.resetPageButton).href; + break; + } + if (e.target.closest('.dropdown-item')) { + e.preventDefault(); + window.location = e.target.closest('.dropdown-item').href; break; } - e.preventDefault(); - window.location = e.target.closest('.dropdown-item').href; break; } } diff --git a/grade/report/grader/amd/src/search/search_class.js b/grade/report/grader/amd/src/search/search_class.js index f03fa15f78aa2..7070b7c4cf539 100644 --- a/grade/report/grader/amd/src/search/search_class.js +++ b/grade/report/grader/amd/src/search/search_class.js @@ -222,14 +222,18 @@ export default class { /** * When called, close the dropdown and reset the input field attributes. + * + * @param {Boolean} clear Conditionality clear the input box. */ - closeSearch() { + closeSearch(clear = false) { this.toggleDropdown(); // Hide the "clear" search button search bar. this.clearSearchButton.classList.add('d-none'); - // Clear the entered search query in the search bar and hide the search results container. - this.setSearchTerms(''); - this.searchInput.value = ""; + if (clear) { + // Clear the entered search query in the search bar and hide the search results container. + this.setSearchTerms(''); + this.searchInput.value = ""; + } } /** @@ -379,7 +383,7 @@ export default class { } // The "clear search" button is triggered. if (e.target.closest(this.selectors.clearSearch) && e.button === 0) { - this.closeSearch(); + this.closeSearch(true); this.searchInput.focus({preventScroll: true}); } // User may have accidentally clicked off the dropdown and wants to reopen it. @@ -418,7 +422,7 @@ export default class { case 'Tab': // If the current focus is on clear search, then check if viewall exists then around tab to it. if (e.target.closest(this.selectors.clearSearch)) { - if (this.currentViewAll) { + if (this.currentViewAll && !e.shiftKey) { e.preventDefault(); this.currentViewAll.focus({preventScroll: true}); } else { diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index b391fd53e945a..f6a367f301cbc 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -92,9 +92,11 @@ public function export_for_template(\renderer_base $output): array { $data['initialselector'] = $initialselector->export_for_template($output); $data['groupselector'] = $gradesrenderer->group_selector($course); + $resetlink = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]); $searchinput = $OUTPUT->render_from_template('gradereport_grader/search/searchinput', [ 'currentvalue' => $this->usersearch, 'courseid' => $courseid, + 'resetlink' => $resetlink->out(false), ]); $searchdropdown = new gradebook_dropdown( true, diff --git a/grade/report/grader/lang/en/gradereport_grader.php b/grade/report/grader/lang/en/gradereport_grader.php index a5c2ab8c78d50..55173e86dbb45 100644 --- a/grade/report/grader/lang/en/gradereport_grader.php +++ b/grade/report/grader/lang/en/gradereport_grader.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['clearsearch'] = 'Clear searched users'; $string['collapsedcolumns'] = 'Collapsed columns {$a}'; $string['eventgradereportviewed'] = 'Grader report viewed'; $string['grader:manage'] = 'Manage the grader report'; @@ -46,12 +47,11 @@ $string['privacy:request:preference:grade_report_grader_collapsed_categories'] = 'You have some gradebook categories collapsed in the "{$a->name}" course'; $string['reopencolumn'] = 'Reopen {$a} column'; $string['summarygrader'] = 'A table with the names of students in the first column, with assessable activities grouped by course and category across the top.'; -$string['showingxofy'] = 'Showing {$a->found} of {$a->total}'; $string['useractivitygrade'] = '{$a} grade'; $string['overriddengrade'] = 'Overridden grade'; $string['advancedgrading'] = 'View {$a} results'; $string['cellactions'] = 'Cell actions'; -$string['viewallresults'] = 'View all results for "{$a}"'; +$string['viewallresults'] = 'View all results ({$a})'; $string['viewresultsuser'] = 'View results for {$a}'; // Deprecated since Moodle 4.2. diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index 4dae0ae552ff9..3b135e60809bb 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -467,7 +467,8 @@ public function load_users(bool $allusers = false) { $this->groupwheresql ORDER BY $sort"; // We never work with unlimited result. Limit the number of records by MAX_STUDENTS_PER_PAGE if no other limit is specified. - $studentsperpage = ($this->get_students_per_page() && !$allusers) ? $this->get_students_per_page() : static::MAX_STUDENTS_PER_PAGE; + $studentsperpage = ($this->get_students_per_page() && !$allusers) ? + $this->get_students_per_page() : static::MAX_STUDENTS_PER_PAGE; $this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage); if (empty($this->users)) { diff --git a/grade/report/grader/styles.css b/grade/report/grader/styles.css index d8805ddeeb958..155f303a33be5 100644 --- a/grade/report/grader/styles.css +++ b/grade/report/grader/styles.css @@ -234,7 +234,8 @@ pointer-events: initial; z-index: 1; } -.path-grade-report-grader .usersearchwidget button { +.path-grade-report-grader .usersearchwidget button, +.path-grade-report-grader .usersearchwidget a { pointer-events: initial; } .path-grade-report-grader .usersearchdropdown { diff --git a/grade/report/grader/templates/search/resultitem.mustache b/grade/report/grader/templates/search/resultitem.mustache index eb13cdd1f1a45..4b663714af2e0 100644 --- a/grade/report/grader/templates/search/resultitem.mustache +++ b/grade/report/grader/templates/search/resultitem.mustache @@ -45,10 +45,10 @@ {{/profileimageurl}}
- + {{fullname}} -