mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 21:49:15 +01:00
MDL-77991 core: Move tertiary search dropdown component
This commit is contained in:
parent
a1d5d1b2f7
commit
a4b3b0d044
3
grade/amd/build/comboboxsearch/grade.min.js
vendored
Normal file
3
grade/amd/build/comboboxsearch/grade.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
define("core_grades/comboboxsearch/grade",["exports","core/comboboxsearch/search_combobox","core_grades/searchwidget/repository","core/templates","core/utils"],(function(_exports,_search_combobox,Repository,_templates,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=(obj=_search_combobox)&&obj.__esModule?obj:{default:obj},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);class GradeItemSearch extends _search_combobox.default{constructor(){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"courseID",void 0),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.gradesearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault()}static init(){return new GradeItemSearch}componentSelector(){return".grade-search"}dropdownSelector(){return".gradesearchdropdown"}triggerSelector(){return".gradesearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core/local/comboboxsearch/resultset",{results:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0})}))}async fetchDataset(){return await Repository.gradeitemFetch(this.courseID).then((r=>r.gradeitems))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((grade=>Object.keys(grade).some((key=>""!==grade[key]&&grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((grade=>({id:grade.id,name:grade.name,link:this.selectOneLink(grade.id)}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){switch(super.keyHandler(e),e.key){case"Tab":e.target.closest(this.selectors.input)&&(e.preventDefault(),this.clearSearchButton.focus({preventScroll:!0}));break;case"Escape":if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(gradeID){throw new Error("selectOneLink(".concat(gradeID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GradeItemSearch,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=grade.min.js.map
|
1
grade/amd/build/comboboxsearch/grade.min.js.map
Normal file
1
grade/amd/build/comboboxsearch/grade.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,10 +1,10 @@
|
||||
define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core_grades/searchwidget/selectors","core/notification"],(function(_exports,_utils,Templates,Selectors,_notification){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
|
||||
define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core_grades/searchwidget/selectors","core/notification","core/log"],(function(_exports,_utils,Templates,Selectors,_notification,_log){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
|
||||
/**
|
||||
* A widget to search users or grade items within the gradebook.
|
||||
*
|
||||
* @module core_grades/searchwidget/basewidget
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,afterSelect=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc,afterSelect)})).catch(_notification.default.exception)};const registerListenerEvents=function(widgetContentContainer,data,searchFunc){let afterSelect=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}));const inputElement=document.getElementById(searchInput.dataset.inputElement);inputElement&&afterSelect&&inputElement.addEventListener("change",(e=>{widgetContentContainer.querySelector(Selectors.elements.getSearchWidgetSelectOption(searchInput))&&afterSelect(e.target.value)})),widgetContentContainer.addEventListener("click",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption)if(inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}})),widgetContentContainer.addEventListener("keydown",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption&&(" "===e.key||"Enter"===e.key))if(e.preventDefault(),inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{container.innerHTML="";const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);if(await Templates.replaceNodeContents(searchResultsContainer,html,js),"listbox"!==searchResultsContainer.getAttribute("role")){const deprecatedOptions=searchResultsContainer.querySelectorAll('a.dropdown-item[role="menuitem"][href=""], .dropdown-item[role="option"]:not([href])');for(const option of deprecatedOptions)option.tabIndex=0,option.removeAttribute("href")}};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=_interopRequireDefault(_notification),_log=_interopRequireDefault(_log);_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,afterSelect=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;_log.default.debug("This pattern is no longer used, refer to core/search_combobox() instead."),bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc,afterSelect)})).catch(_notification.default.exception)};const registerListenerEvents=function(widgetContentContainer,data,searchFunc){let afterSelect=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}));const inputElement=document.getElementById(searchInput.dataset.inputElement);inputElement&&afterSelect&&inputElement.addEventListener("change",(e=>{widgetContentContainer.querySelector(Selectors.elements.getSearchWidgetSelectOption(searchInput))&&afterSelect(e.target.value)})),widgetContentContainer.addEventListener("click",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption)if(inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}})),widgetContentContainer.addEventListener("keydown",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption&&(" "===e.key||"Enter"===e.key))if(e.preventDefault(),inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{container.innerHTML="";const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);if(await Templates.replaceNodeContents(searchResultsContainer,html,js),"listbox"!==searchResultsContainer.getAttribute("role")){const deprecatedOptions=searchResultsContainer.querySelectorAll('a.dropdown-item[role="menuitem"][href=""], .dropdown-item[role="option"]:not([href])');for(const option of deprecatedOptions)option.tabIndex=0,option.removeAttribute("href")}};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
|
||||
|
||||
//# sourceMappingURL=basewidget.min.js.map
|
File diff suppressed because one or more lines are too long
10
grade/amd/build/searchwidget/group.min.js
vendored
10
grade/amd/build/searchwidget/group.min.js
vendored
@ -1,10 +0,0 @@
|
||||
define("core_grades/searchwidget/group",["exports","core/local/aria/focuslock","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","jquery","core_grades/searchwidget/selectors"],(function(_exports,FocusLockManager,_pending,Templates,Repository,WidgetBase,_jquery,Selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
|
||||
/**
|
||||
* A widget to search groups within the gradebook.
|
||||
*
|
||||
* @module core_grades/searchwidget/group
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,FocusLockManager=_interopRequireWildcard(FocusLockManager),_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);let initialised=!1;_exports.init=()=>{if(!initialised&&document.querySelector(Selectors.elements.getSearchWidgetSelector("group"))){const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()}initialised=!0};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("group")),menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("group")),inputElement=menuContainer.querySelector('input[name="group"]');(0,_jquery.default)(menuContainer).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.groupFetch(courseID).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&(await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.groups,searchGroups(),null,afterSelect),FocusLockManager.trapFocus(dropdownMenuContainer))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/group/groupsearch_body",[])),(0,_jquery.default)(menuContainer).on("hide.bs.dropdown",(()=>{FocusLockManager.untrapFocus()})),inputElement.addEventListener("change",(e=>{const toggle=menuContainer.querySelector(".dropdown-toggle"),courseId=toggle.dataset.courseid,actionUrl=toggle.dataset.actionBaseUrl?new URL(toggle.dataset.actionBaseUrl.replace(/&/g,"&")):new URL(location.href);actionUrl.searchParams.set("id",courseId),actionUrl.searchParams.set("group",e.target.value),actionUrl.searchParams.delete("page"),location.href=actionUrl.href,e.stopPropagation()}))},searchGroups=()=>()=>(groups,searchTerm)=>{if(""===searchTerm)return groups;searchTerm=searchTerm.toLowerCase();const searchResults=[];return groups.forEach((group=>{group.name.toLowerCase().includes(searchTerm)&&searchResults.push(group)})),searchResults},afterSelect=selected=>{const menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("group")),inputElement=menuContainer.querySelector('input[name="group"]');(0,_jquery.default)(menuContainer).dropdown("hide"),inputElement.value!=selected&&(inputElement.value=selected,inputElement.dispatchEvent(new Event("change",{bubbles:!0})))}}));
|
||||
|
||||
//# sourceMappingURL=group.min.js.map
|
File diff suppressed because one or more lines are too long
@ -5,6 +5,6 @@ define("core_grades/searchwidget/repository",["exports","core/ajax"],(function(_
|
||||
* @module core_grades/searchwidget/repository
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=_exports.groupFetch=_exports.gradeitemFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.userFetch=(courseid,groupId)=>{const request={methodname:"core_grades_get_enrolled_users_for_selector",args:{courseid:courseid,groupid:groupId}};return _ajax.default.call([request])[0]};_exports.groupFetch=courseid=>{const request={methodname:"core_grades_get_groups_for_selector",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.gradeitemFetch=courseid=>{const request={methodname:"gradereport_singleview_get_grade_items_for_search_widget",args:{courseid:courseid}};return _ajax.default.call([request])[0]}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=_exports.gradeitemFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.gradeitemFetch=courseid=>{const request={methodname:"gradereport_singleview_get_grade_items_for_search_widget",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.userFetch=(courseid,groupId)=>{const request={methodname:"core_grades_get_enrolled_users_for_selector",args:{courseid:courseid,groupid:groupId}};return _ajax.default.call([request])[0]}}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
@ -1 +1 @@
|
||||
{"version":3,"file":"repository.min.js","sources":["../../src/searchwidget/repository.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the search widget.\n *\n * @module core_grades/searchwidget/repository\n * @copyright 2022 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Given a course ID, we want to fetch the enrolled learners, so we may fetch their reports.\n *\n * @method userFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @param {int} groupId ID of the group to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const userFetch = (courseid, groupId) => {\n const request = {\n methodname: 'core_grades_get_enrolled_users_for_selector',\n args: {\n courseid: courseid,\n groupid: groupId,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a course ID, we want to fetch the groups, so we may fetch their users.\n *\n * @method groupFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const groupFetch = (courseid) => {\n const request = {\n methodname: 'core_grades_get_groups_for_selector',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.\n * Note: This will be worked upon in the single view issue.\n *\n * @method gradeitemFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const gradeitemFetch = (courseid) => {\n const request = {\n methodname: 'gradereport_singleview_get_grade_items_for_search_widget',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","groupId","request","methodname","args","groupid","ajax","call"],"mappings":";;;;;;;0MAiCyB,CAACA,SAAUC,iBAC1BC,QAAU,CACZC,WAAY,8CACZC,KAAM,CACFJ,SAAUA,SACVK,QAASJ,iBAGVK,cAAKC,KAAK,CAACL,UAAU,wBAULF,iBACjBE,QAAU,CACZC,WAAY,sCACZC,KAAM,CACFJ,SAAUA,kBAGXM,cAAKC,KAAK,CAACL,UAAU,4BAWDF,iBACrBE,QAAU,CACZC,WAAY,2DACZC,KAAM,CACFJ,SAAUA,kBAGXM,cAAKC,KAAK,CAACL,UAAU"}
|
||||
{"version":3,"file":"repository.min.js","sources":["../../src/searchwidget/repository.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the search widget.\n *\n * @module core_grades/searchwidget/repository\n * @copyright 2022 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.\n * Note: This will be worked upon in the single view issue.\n *\n * @method gradeitemFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const gradeitemFetch = (courseid) => {\n const request = {\n methodname: 'gradereport_singleview_get_grade_items_for_search_widget',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a course ID, we want to fetch the enrolled learners, so we may fetch their reports.\n *\n * @method userFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @param {int} groupId ID of the group to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const userFetch = (courseid, groupId) => {\n const request = {\n methodname: 'core_grades_get_enrolled_users_for_selector',\n args: {\n courseid: courseid,\n groupid: groupId,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call","groupId","groupid"],"mappings":";;;;;;;2LAiC+BA,iBACrBC,QAAU,CACZC,WAAY,2DACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU,uBAWP,CAACD,SAAUM,iBAC1BL,QAAU,CACZC,WAAY,8CACZC,KAAM,CACFH,SAAUA,SACVO,QAASD,iBAGVF,cAAKC,KAAK,CAACJ,UAAU"}
|
245
grade/amd/src/comboboxsearch/grade.js
Normal file
245
grade/amd/src/comboboxsearch/grade.js
Normal file
@ -0,0 +1,245 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Allow the user to search for grades within the grade area.
|
||||
*
|
||||
* @module core_grades/comboboxsearch/grade
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import search_combobox from 'core/comboboxsearch/search_combobox';
|
||||
import * as Repository from 'core_grades/searchwidget/repository';
|
||||
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
||||
import {debounce} from 'core/utils';
|
||||
|
||||
export default class GradeItemSearch extends search_combobox {
|
||||
|
||||
courseID;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Define our standard lookups.
|
||||
this.selectors = {
|
||||
...this.selectors,
|
||||
courseid: '[data-region="courseid"]',
|
||||
placeholder: '.gradesearchdropdown [data-region="searchplaceholder"]',
|
||||
};
|
||||
const component = document.querySelector(this.componentSelector());
|
||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||
|
||||
this.renderDefault();
|
||||
}
|
||||
|
||||
static init() {
|
||||
return new GradeItemSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* The overall div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
componentSelector() {
|
||||
return '.grade-search';
|
||||
}
|
||||
|
||||
/**
|
||||
* The dropdown div that contains the searching widget result space.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
dropdownSelector() {
|
||||
return '.gradesearchdropdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* The triggering div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
triggerSelector() {
|
||||
return '.gradesearchwidget';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node.
|
||||
*/
|
||||
async renderDropdown() {
|
||||
const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {
|
||||
results: this.getMatchedResults(),
|
||||
hasresults: this.getMatchedResults().length > 0,
|
||||
searchterm: this.getSearchTerm(),
|
||||
});
|
||||
replaceNodeContents(this.selectors.placeholder, html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node by default we want our form to exist.
|
||||
*/
|
||||
async renderDefault() {
|
||||
this.setMatchedResults(await this.filterDataset(await this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
|
||||
await this.renderDropdown();
|
||||
|
||||
this.updateNodes();
|
||||
this.registerInputEvents();
|
||||
|
||||
// Add a small BS listener so that we can set the focus correctly on open.
|
||||
this.$component.on('shown.bs.dropdown', () => {
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data we will be searching against in this component.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async fetchDataset() {
|
||||
return await Repository.gradeitemFetch(this.courseID).then((r) => r.gradeitems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictate to the search component how and what we want to match upon.
|
||||
*
|
||||
* @param {Array} filterableData
|
||||
* @returns {Array} The users that match the given criteria.
|
||||
*/
|
||||
async filterDataset(filterableData) {
|
||||
// Sometimes we just want to show everything.
|
||||
if (this.getPreppedSearchTerm() === '') {
|
||||
return filterableData;
|
||||
}
|
||||
return filterableData.filter((grade) => Object.keys(grade).some((key) => {
|
||||
if (grade[key] === "") {
|
||||
return false;
|
||||
}
|
||||
return grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
|
||||
*/
|
||||
filterMatchDataset() {
|
||||
this.setMatchedResults(
|
||||
this.getMatchedResults().map((grade) => {
|
||||
return {
|
||||
id: grade.id,
|
||||
name: grade.name,
|
||||
link: this.selectOneLink(grade.id),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any keyboard inputs.
|
||||
*/
|
||||
registerInputEvents() {
|
||||
// Register & handle the text input.
|
||||
this.searchInput.addEventListener('input', debounce(async() => {
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
// We can also require a set amount of input before search.
|
||||
if (this.searchInput.value === '') {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}
|
||||
// User has given something for us to filter against.
|
||||
await this.filterrenderpipe();
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user interacts with the component.
|
||||
*
|
||||
* @param {MouseEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
async clickHandler(e) {
|
||||
if (e.target.closest(this.selectors.dropdown)) {
|
||||
// Forcibly prevent BS events so that we can control the open and close.
|
||||
// Really needed because by default input elements cant trigger a dropdown.
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
this.clearSearchButton.addEventListener('click', async() => {
|
||||
this.searchInput.value = '';
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
await this.filterrenderpipe();
|
||||
});
|
||||
// Prevent normal key presses activating this.
|
||||
if (e.target.closest('.dropdown-item') && e.button === 0) {
|
||||
window.location = e.target.closest('.dropdown-item').href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user presses a key within the component.
|
||||
*
|
||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
keyHandler(e) {
|
||||
super.keyHandler(e);
|
||||
// Switch the key presses to handle keyboard nav.
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
if (e.target.closest(this.selectors.input)) {
|
||||
e.preventDefault();
|
||||
this.clearSearchButton.focus({preventScroll: true});
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
if (document.activeElement.getAttribute('role') === 'option') {
|
||||
e.stopPropagation();
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
} else if (e.target.closest(this.selectors.input)) {
|
||||
const trigger = this.component.querySelector(this.selectors.trigger);
|
||||
trigger.focus({preventScroll: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the input event listener for the text input area.
|
||||
*/
|
||||
registerInputHandlers() {
|
||||
// Register & handle the text input.
|
||||
this.searchInput.addEventListener('input', debounce(() => {
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
// We can also require a set amount of input before search.
|
||||
if (this.getSearchTerm() === '') {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up the view all link that is dedicated to a particular result.
|
||||
*
|
||||
* @param {Number} gradeID The ID of the grade item selected.
|
||||
*/
|
||||
selectOneLink(gradeID) {
|
||||
throw new Error(`selectOneLink(${gradeID}) must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ import {debounce} from 'core/utils';
|
||||
import * as Templates from 'core/templates';
|
||||
import * as Selectors from 'core_grades/searchwidget/selectors';
|
||||
import Notification from 'core/notification';
|
||||
import Log from 'core/log';
|
||||
|
||||
/**
|
||||
* Build the base searching widget.
|
||||
@ -44,6 +45,7 @@ export const init = async(
|
||||
unsearchableContent = null,
|
||||
afterSelect = null,
|
||||
) => {
|
||||
Log.debug('This pattern is no longer used, refer to core/search_combobox() instead.');
|
||||
bodyPromise.then(async(bodyContent) => {
|
||||
// Render the body content.
|
||||
widgetContentContainer.innerHTML = bodyContent;
|
||||
|
@ -1,165 +0,0 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A widget to search groups within the gradebook.
|
||||
*
|
||||
* @module core_grades/searchwidget/group
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import * as FocusLockManager from 'core/local/aria/focuslock';
|
||||
import Pending from 'core/pending';
|
||||
import * as Templates from 'core/templates';
|
||||
import * as Repository from 'core_grades/searchwidget/repository';
|
||||
import * as WidgetBase from 'core_grades/searchwidget/basewidget';
|
||||
import $ from 'jquery';
|
||||
import * as Selectors from 'core_grades/searchwidget/selectors';
|
||||
|
||||
/**
|
||||
* Whether this module is already initialised.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
let initialised = false;
|
||||
|
||||
/**
|
||||
* Our entry point into starting to build the group search widget.
|
||||
*
|
||||
* It'll eventually, based upon the listeners, open the search widget and allow filtering.
|
||||
*
|
||||
* @method init
|
||||
*/
|
||||
export const init = () => {
|
||||
if (!initialised && document.querySelector(Selectors.elements.getSearchWidgetSelector('group'))) {
|
||||
const pendingPromise = new Pending();
|
||||
registerListenerEvents();
|
||||
pendingPromise.resolve();
|
||||
}
|
||||
initialised = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register event listeners.
|
||||
*
|
||||
* @method registerListenerEvents
|
||||
*/
|
||||
const registerListenerEvents = () => {
|
||||
let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();
|
||||
const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('group'));
|
||||
const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group'));
|
||||
const inputElement = menuContainer.querySelector('input[name="group"]');
|
||||
|
||||
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
|
||||
$(menuContainer).on('show.bs.dropdown', async(e) => {
|
||||
const courseID = e.relatedTarget.dataset.courseid;
|
||||
// Display a loading icon in the dropdown menu container until the body promise is resolved.
|
||||
await WidgetBase.showLoader(dropdownMenuContainer);
|
||||
|
||||
// If an error occurs while fetching the data, display the error within the dropdown menu.
|
||||
const data = await Repository.groupFetch(courseID).catch(async(e) => {
|
||||
const errorTemplateData = {
|
||||
'errormessage': e.message
|
||||
};
|
||||
bodyPromiseResolver(
|
||||
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
|
||||
);
|
||||
});
|
||||
// Early return if there is no module data.
|
||||
if (data === []) {
|
||||
return;
|
||||
}
|
||||
await WidgetBase.init(
|
||||
dropdownMenuContainer,
|
||||
bodyPromise,
|
||||
data.groups,
|
||||
searchGroups(),
|
||||
null,
|
||||
afterSelect
|
||||
);
|
||||
|
||||
// Lock tab control. It has to be locked because the dropdown's role is dialog.
|
||||
FocusLockManager.trapFocus(dropdownMenuContainer);
|
||||
});
|
||||
|
||||
// Resolvers for passed functions in the dropdown creation.
|
||||
bodyPromiseResolver(Templates.render(
|
||||
'core_grades/searchwidget/group/groupsearch_body',
|
||||
[]
|
||||
));
|
||||
|
||||
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
|
||||
$(menuContainer).on('hide.bs.dropdown', () => {
|
||||
FocusLockManager.untrapFocus();
|
||||
});
|
||||
|
||||
inputElement.addEventListener('change', e => {
|
||||
const toggle = menuContainer.querySelector('.dropdown-toggle');
|
||||
const courseId = toggle.dataset.courseid;
|
||||
const actionUrl = toggle.dataset.actionBaseUrl ?
|
||||
new URL(toggle.dataset.actionBaseUrl.replace(/&/g, "&")) :
|
||||
new URL(location.href);
|
||||
actionUrl.searchParams.set('id', courseId);
|
||||
actionUrl.searchParams.set('group', e.target.value);
|
||||
actionUrl.searchParams.delete('page');
|
||||
|
||||
location.href = actionUrl.href;
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Define how we want to search and filter groups when the user decides to input a search value.
|
||||
*
|
||||
* @method searchGroups
|
||||
* @returns {function(): function(*, *): (*)}
|
||||
*/
|
||||
const searchGroups = () => {
|
||||
return () => {
|
||||
return (groups, searchTerm) => {
|
||||
if (searchTerm === '') {
|
||||
return groups;
|
||||
}
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
const searchResults = [];
|
||||
groups.forEach((group) => {
|
||||
const groupName = group.name.toLowerCase();
|
||||
if (groupName.includes(searchTerm)) {
|
||||
searchResults.push(group);
|
||||
}
|
||||
});
|
||||
return searchResults;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the action to be performed when an item is selected by the search widget.
|
||||
*
|
||||
* @param {String} selected The selected item's value.
|
||||
*/
|
||||
const afterSelect = (selected) => {
|
||||
const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('group'));
|
||||
const inputElement = menuContainer.querySelector('input[name="group"]');
|
||||
|
||||
$(menuContainer).dropdown('hide'); // Otherwise the dropdown stays open when user choose an option using keyboard.
|
||||
|
||||
if (inputElement.value != selected) {
|
||||
inputElement.value = selected;
|
||||
inputElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
};
|
@ -23,6 +23,24 @@
|
||||
|
||||
import ajax from 'core/ajax';
|
||||
|
||||
/**
|
||||
* Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.
|
||||
* Note: This will be worked upon in the single view issue.
|
||||
*
|
||||
* @method gradeitemFetch
|
||||
* @param {int} courseid ID of the course to fetch the users of.
|
||||
* @return {object} jQuery promise
|
||||
*/
|
||||
export const gradeitemFetch = (courseid) => {
|
||||
const request = {
|
||||
methodname: 'gradereport_singleview_get_grade_items_for_search_widget',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
},
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a course ID, we want to fetch the enrolled learners, so we may fetch their reports.
|
||||
*
|
||||
@ -41,38 +59,3 @@ export const userFetch = (courseid, groupId) => {
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a course ID, we want to fetch the groups, so we may fetch their users.
|
||||
*
|
||||
* @method groupFetch
|
||||
* @param {int} courseid ID of the course to fetch the users of.
|
||||
* @return {object} jQuery promise
|
||||
*/
|
||||
export const groupFetch = (courseid) => {
|
||||
const request = {
|
||||
methodname: 'core_grades_get_groups_for_selector',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
},
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.
|
||||
* Note: This will be worked upon in the single view issue.
|
||||
*
|
||||
* @method gradeitemFetch
|
||||
* @param {int} courseid ID of the course to fetch the users of.
|
||||
* @return {object} jQuery promise
|
||||
*/
|
||||
export const gradeitemFetch = (courseid) => {
|
||||
const request = {
|
||||
methodname: 'gradereport_singleview_get_grade_items_for_search_widget',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
},
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
||||
|
@ -16,19 +16,20 @@
|
||||
|
||||
namespace core_grades\external;
|
||||
|
||||
use core_user_external;
|
||||
use core_external\external_api;
|
||||
use core_external\external_description;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_value;
|
||||
use core_external\external_warnings;
|
||||
use core_external\restricted_context_exception;
|
||||
use core_user;
|
||||
use user_picture;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
require_once($CFG->dirroot.'/grade/lib.php');
|
||||
require_once($CFG->dirroot .'/user/externallib.php');
|
||||
|
||||
/**
|
||||
* Get the enrolled users within and map some fields to the returned array of user objects.
|
||||
@ -95,14 +96,13 @@ class get_enrolled_users_for_selector extends external_api {
|
||||
$users = [];
|
||||
|
||||
while ($userdata = $gui->next_user()) {
|
||||
$guiuser = $userdata->user;
|
||||
$user = new \stdClass();
|
||||
$user->fullname = fullname($guiuser);
|
||||
$user->id = $guiuser->id;
|
||||
$userpicture = new \user_picture($guiuser);
|
||||
$user = $userdata->user;
|
||||
$user->fullname = fullname($user);
|
||||
$userpicture = new user_picture($user);
|
||||
$userpicture->size = 1;
|
||||
$user->profileimage = $userpicture->get_url($PAGE)->out(false);
|
||||
$user->email = $guiuser->email;
|
||||
$user->profileimageurl = $userpicture->get_url($PAGE)->out(false);
|
||||
$userpicture->size = 0; // Size f2.
|
||||
$user->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
|
||||
|
||||
$users[] = $user;
|
||||
}
|
||||
@ -121,30 +121,8 @@ class get_enrolled_users_for_selector extends external_api {
|
||||
*/
|
||||
public static function execute_returns(): external_single_structure {
|
||||
return new external_single_structure([
|
||||
'users' => new external_multiple_structure(self::user_description()),
|
||||
'users' => new external_multiple_structure(core_user_external::user_description()),
|
||||
'warnings' => new external_warnings(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user return value description.
|
||||
*
|
||||
* @return external_description
|
||||
*/
|
||||
public static function user_description(): external_description {
|
||||
$userfields = [
|
||||
'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
|
||||
'profileimage' => new external_value(
|
||||
PARAM_URL,
|
||||
'The location of the users larger image',
|
||||
VALUE_OPTIONAL
|
||||
),
|
||||
'fullname' => new external_value(PARAM_TEXT, 'The full name of the user', VALUE_OPTIONAL),
|
||||
'email' => new external_value(
|
||||
core_user::get_property_type('email'),
|
||||
'An email address - allow email as root@localhost',
|
||||
VALUE_OPTIONAL)
|
||||
];
|
||||
return new external_single_structure($userfields);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
use core\output\comboboxsearch;
|
||||
use \core_grades\output\action_bar;
|
||||
use core_message\helper;
|
||||
use core_message\api;
|
||||
@ -55,6 +56,11 @@ class core_grades_renderer extends plugin_renderer_base {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sbody = $this->render_from_template('core_group/comboboxsearch/searchbody', [
|
||||
'courseid' => $course->id,
|
||||
'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS),
|
||||
]);
|
||||
|
||||
$label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') :
|
||||
get_string('selectgroupsseparate');
|
||||
|
||||
@ -83,8 +89,15 @@ class core_grades_renderer extends plugin_renderer_base {
|
||||
$data['selectedgroup'] = get_string('allparticipants');
|
||||
}
|
||||
|
||||
$this->page->requires->js_call_amd('core_grades/searchwidget/group', 'init');
|
||||
return $this->render_from_template('core_grades/group_selector', $data);
|
||||
$groupdropdown = new comboboxsearch(
|
||||
false,
|
||||
$this->render_from_template('core_group/comboboxsearch/group_selector', $data),
|
||||
$sbody,
|
||||
'group-search',
|
||||
'groupsearchwidget',
|
||||
'groupsearchdropdown overflow-auto w-100',
|
||||
);
|
||||
return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
grade/report/grader/amd/build/search.min.js
vendored
2
grade/report/grader/amd/build/search.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -21,7 +21,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import * as Repository from 'gradereport_grader/collapse/repository';
|
||||
import GradebookSearchClass from 'gradereport_grader/search/search_class';
|
||||
import GradebookSearchClass from 'core/tertiary_search_class';
|
||||
import {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';
|
||||
import {debounce} from 'core/utils';
|
||||
import $ from 'jquery';
|
||||
|
@ -21,7 +21,7 @@
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import GradebookSearchClass from 'gradereport_grader/search/search_class';
|
||||
import GradebookSearchClass from 'core/tertiary_search_class';
|
||||
import * as Repository from 'gradereport_grader/search/repository';
|
||||
import {get_strings as getStrings} from 'core/str';
|
||||
import Url from 'core/url';
|
||||
|
@ -16,9 +16,9 @@
|
||||
|
||||
namespace gradereport_grader\output;
|
||||
|
||||
use moodle_url;
|
||||
use core\output\tertiary_dropdown;
|
||||
use core_grades\output\general_action_bar;
|
||||
use core_grades\output\gradebook_dropdown;
|
||||
use moodle_url;
|
||||
|
||||
/**
|
||||
* Renderable class for the action bar elements in the grader report.
|
||||
@ -80,7 +80,7 @@ class action_bar extends \core_grades\output\action_bar {
|
||||
$this->context,
|
||||
'/grade/report/grader/index.php'
|
||||
);
|
||||
$initialselector = new gradebook_dropdown(
|
||||
$initialselector = new tertiary_dropdown(
|
||||
false,
|
||||
$initialscontent->buttoncontent,
|
||||
$initialscontent->dropdowncontent,
|
||||
@ -98,7 +98,7 @@ class action_bar extends \core_grades\output\action_bar {
|
||||
'courseid' => $courseid,
|
||||
'resetlink' => $resetlink->out(false),
|
||||
]);
|
||||
$searchdropdown = new gradebook_dropdown(
|
||||
$searchdropdown = new tertiary_dropdown(
|
||||
true,
|
||||
$searchinput,
|
||||
null,
|
||||
@ -110,7 +110,7 @@ class action_bar extends \core_grades\output\action_bar {
|
||||
);
|
||||
$data['searchdropdown'] = $searchdropdown->export_for_template($output);
|
||||
|
||||
$collapse = new gradebook_dropdown(
|
||||
$collapse = new tertiary_dropdown(
|
||||
true,
|
||||
get_string('collapsedcolumns', 'gradereport_grader', 0),
|
||||
null,
|
||||
|
@ -56,11 +56,13 @@
|
||||
{{#users}}
|
||||
{{>gradereport_grader/search/resultitem}}
|
||||
{{/users}}
|
||||
<li class="w-100 result-row p-1 border-top bottom-0 position-sticky" role="none" id="result-row-{{id}}">
|
||||
<a role="option" class="dropdown-item d-flex small p-3" id="select-all" href="{{{selectall}}}" tabindex="-1">
|
||||
{{#str}}viewallresults, gradereport_grader, {{matches}}{{/str}}
|
||||
</a>
|
||||
</li>
|
||||
{{#selectall}}
|
||||
<li class="w-100 result-row p-1 border-top bottom-0 position-sticky" role="none" id="result-row-{{id}}">
|
||||
<a role="option" class="dropdown-item d-flex small p-3" id="select-all" href="{{{selectall}}}" tabindex="-1">
|
||||
{{#str}}viewallresults, gradereport_grader, {{matches}}{{/str}}
|
||||
</a>
|
||||
</li>
|
||||
{{/selectall}}
|
||||
</ul>
|
||||
{{/hasusers}}
|
||||
{{^hasusers}}
|
||||
|
@ -2,12 +2,17 @@ This files describes API changes in /grade/report/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.3 ===
|
||||
|
||||
* The load_users() method in the gradereport_singleview\local\screen class has been deprecated. Please use
|
||||
get_gradable_users() instead.
|
||||
* The \gradereport_singleview\local\screen\select has been deprecated. This class generates the output for the initial
|
||||
view to select the single view item type (user or grade item) which is no longer actively used as we do not provide
|
||||
direct links to it.
|
||||
* The following reports now use the combobox search PHP & JS rather than defining their own:
|
||||
- gradereport_grader
|
||||
- gradereport_singleview
|
||||
- gradereport_user
|
||||
Each of these reports extend the base user, grade or group JS class in either core or core_grades so that they can define how their clickable links are generated.
|
||||
Any special handling of the search dropdowns should be done in the JS class "closest" to your plugin rather than in core_grades or the base search class itself.
|
||||
|
||||
=== 4.2 ===
|
||||
* 'Show calculations' setting has been removed from grader report (link is moved to grade action menu)
|
||||
|
@ -1,68 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/group_selector
|
||||
|
||||
The group selector trigger element.
|
||||
|
||||
Context variables required for this template:
|
||||
* label - The label text fot the group selector element.
|
||||
* name - The name of the group selector element
|
||||
* group - The value of the group selector element (id of the preselected group)
|
||||
* courseid - The course ID.
|
||||
* groupactionbaseurl - The base URL for the group action.
|
||||
* selectedgroup - The text of the selected group option.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"label": "Select separate groups",
|
||||
"name": "group",
|
||||
"group": "21",
|
||||
"courseid": "2",
|
||||
"groupactionbaseurl": "index.php?item=test",
|
||||
"selectedgroup": "Group 1"
|
||||
}
|
||||
}}
|
||||
<div class="search-widget d-flex dropdown" data-searchtype="group">
|
||||
<div
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
role="combobox"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="dialog-{{uniqid}}"
|
||||
data-toggle="dropdown"
|
||||
class="btn dropdown-toggle d-flex text-left align-items-center p-0"
|
||||
data-courseid="{{courseid}}"
|
||||
data-action-base-url="{{groupactionbaseurl}}"
|
||||
aria-label="{{#cleanstr}} selectagroup, core {{/cleanstr}}"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
>
|
||||
<div class="align-items-center d-flex">
|
||||
<div class="d-block pr-3">
|
||||
<span class="d-block small">
|
||||
{{label}}
|
||||
</span>
|
||||
<span class="p-0 font-weight-bold">
|
||||
{{selectedgroup}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu narrow" id="dialog-{{uniqid}}" role="dialog" aria-modal="true" aria-label="{{#cleanstr}} selectagroup, core {{/cleanstr}}">
|
||||
</div>
|
||||
<input type="hidden" name="{{name}}" value="{{group}}" id="input-{{uniqid}}" />
|
||||
</div>
|
@ -1,39 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/error
|
||||
|
||||
Chooser error template.
|
||||
|
||||
Variables required for this template:
|
||||
* errormessage - The error message
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"errormessage": "Error"
|
||||
}
|
||||
}}
|
||||
<div class="p-2 px-sm-5 py-sm-4">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fa fa-exclamation-circle fa-fw text-danger"></i>
|
||||
{{#str}} error, error {{/str}}
|
||||
</h5>
|
||||
<hr>
|
||||
<p class="text-break">{{{errormessage}}}</p>
|
||||
</div>
|
||||
</div>
|
@ -1,41 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/group/groupsearch_body
|
||||
|
||||
The body of the group widget.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchgroups, core_grades
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchgroups, core_grades
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="listbox-{{uniqid}}"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
<input type="hidden" name="search" id="input-{{uniqid}}"/>
|
||||
<div role="listbox" id="listbox-{{uniqid}}" class="searchresultscontainer" data-region="search-results-container-widget"></div>
|
@ -1,27 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/loading
|
||||
|
||||
This template renders the loading placeholder for the search widget.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div class="d-flex w-100 h-100 justify-content-center align-items-center">
|
||||
{{> core/loading }}
|
||||
</div>
|
@ -1,59 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/searchitem
|
||||
|
||||
Search result line items.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Quiz 1",
|
||||
"fullname": "Cameron Greeve"
|
||||
}
|
||||
}}
|
||||
<li
|
||||
role="option"
|
||||
class="dropdown-item d-flex px-2 py-1 {{^name}}align-items-center{{/name}}"
|
||||
{{#name}}id="item-{{id}}"{{/name}}
|
||||
{{^name}}id="user-{{id}}"{{/name}}
|
||||
data-value="{{id}}"
|
||||
>
|
||||
{{#name}}
|
||||
<span class="pull-left result-cell text-truncate mr-2">
|
||||
{{name}}
|
||||
</span>
|
||||
{{/name}}
|
||||
{{^name}}
|
||||
<div class="d-block pr-2 w-25">
|
||||
{{#profileimage}}
|
||||
<img class="userpicture w-75 mx-auto d-block" src="{{profileimage}}" alt="" />
|
||||
{{/profileimage}}
|
||||
{{^profileimage}}
|
||||
<span class="userinitials"></span>
|
||||
{{/profileimage}}
|
||||
</div>
|
||||
<div class="d-block pr-3 w-75">
|
||||
<span class="w-100 p-0 font-weight-bold">
|
||||
{{fullname}}
|
||||
</span>
|
||||
<span class="w-100 pull-left text-truncate small">
|
||||
{{email}}
|
||||
</span>
|
||||
</div>
|
||||
{{/name}}
|
||||
</li>
|
@ -1,45 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/searchresults
|
||||
|
||||
The wrapper in which our search results will be rendered
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"searchresults": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Quiz 1",
|
||||
"fullname": "Cameron Greeve",
|
||||
"sendmessage": "http://foo.bar/message/index.php?id=25",
|
||||
"addcontact": "http://foo.bar/message/index.php?user1=2&user2=14&addcontact=14&sesskey=XXXXX",
|
||||
"currentuser": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="searchresultitemscontainer-wrapper dropdown">
|
||||
<ul class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2 list-group rounded-0" role="none" data-region="search-result-items-container">
|
||||
{{#searchresults}}
|
||||
{{>core_grades/searchwidget/searchitem}}
|
||||
{{/searchresults}}
|
||||
{{^searchresults}}
|
||||
<p>{{#str}} resultsfound, core, 0 {{/str}}</p>
|
||||
{{/searchresults}}
|
||||
</ul>
|
||||
</div>
|
@ -1,43 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/searchwidget/user/usersearch_body
|
||||
|
||||
The body of the user widget.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"displayunsearchablecontent": true
|
||||
}
|
||||
}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchusers, core_grades
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchusers, core_grades
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="listbox-{{uniqid}}"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
<input type="hidden" name="search" id="input-{{uniqid}}"/>
|
||||
<div role="listbox" id="listbox-{{uniqid}}" class="searchresultscontainer" data-region="search-results-container-widget"></div>
|
||||
{{#displayunsearchablecontent}}
|
||||
<div class="unsearchablecontentcontainer" data-region="unsearchable-content-container-widget"></div>
|
||||
{{/displayunsearchablecontent}}
|
@ -1,93 +0,0 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/user_selector
|
||||
|
||||
The user selector trigger element.
|
||||
|
||||
Context variables required for this template:
|
||||
* name - The name of the user selector element
|
||||
* userid - The value of the user selector element (id of the preselected user)
|
||||
* courseid - The course ID.
|
||||
* groupid - The group ID.
|
||||
* selectedoption - (optional) Object containing information about the selected option.
|
||||
* image - The image corresponding to the selected option.
|
||||
* text - The text of the selected option.
|
||||
* additionaltext - (optional) Additional text displayed below the selected option (e.g. the user email)
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "userid",
|
||||
"userid": "21",
|
||||
"courseid": "2",
|
||||
"groupid": "2",
|
||||
"selectedoption": {
|
||||
"image": "<img src=\"http://example.com/pluginfile.php/14/user/icon/boost/f2\" class=\"userpicture\" width=\"40\" alt=\"\">",
|
||||
"text": "John Doe",
|
||||
"additionaltext": "johndoe@example.com"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div class="search-widget dropdown d-flex" data-searchtype="user">
|
||||
<div
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
role="combobox"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="dialog-{{uniqid}}"
|
||||
data-toggle="dropdown"
|
||||
class="btn dropdown-toggle d-flex text-left align-items-center p-0"
|
||||
data-courseid="{{courseid}}"
|
||||
data-groupid="{{groupid}}"
|
||||
aria-label="{{#cleanstr}} selectauser, core_grades {{/cleanstr}}"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
>
|
||||
<div class="align-items-center d-flex">
|
||||
{{#selectedoption}}
|
||||
<div class="selected-option-img d-block pr-2" aria-hidden="true">
|
||||
{{#image}}
|
||||
{{{image}}}
|
||||
{{/image}}
|
||||
{{^image}}
|
||||
<span class="userinitials"></span>
|
||||
{{/image}}
|
||||
</div>
|
||||
<div class="selected-option-info d-block pr-3 text-truncate">
|
||||
<span class="selected-option-text p-0 font-weight-bold">
|
||||
{{text}}
|
||||
</span>
|
||||
{{#additionaltext}}
|
||||
<span class="d-block small">
|
||||
{{additionaltext}}
|
||||
</span>
|
||||
{{/additionaltext}}
|
||||
</div>
|
||||
{{/selectedoption}}
|
||||
{{^selectedoption}}
|
||||
<div class="d-block pr-2">
|
||||
<span class="userinitials"></span>
|
||||
</div>
|
||||
<div class="user-info d-block pr-3">
|
||||
{{#str}} selectauser, core_grades {{/str}}
|
||||
</div>
|
||||
{{/selectedoption}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu wide" id="dialog-{{uniqid}}" role="dialog" aria-modal="true" aria-label="{{#cleanstr}} selectauser, core_grades {{/cleanstr}}">
|
||||
</div>
|
||||
<input type="hidden" name="{{name}}" value="{{userid}}" id="input-{{uniqid}}" />
|
||||
</div>
|
@ -378,6 +378,46 @@ class behat_grade extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We tend to use this series of steps a bit so define em once.
|
||||
*
|
||||
* @param string $haystack What are we searching within?
|
||||
* @param string $needle What are we looking for?
|
||||
* @param bool $fieldset Do we want to set the search field at the same time?
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
private function get_dropdown_selector(string $haystack, string $needle, bool $fieldset = true): string {
|
||||
$this->execute("behat_general::wait_until_the_page_is_ready");
|
||||
|
||||
// Set the default field to search and handle any special preamble.
|
||||
$string = get_string('searchusers', 'core');
|
||||
$selector = '.usersearchdropdown';
|
||||
if (strtolower($haystack) === 'group') {
|
||||
$string = get_string('searchgroups', 'core');
|
||||
$selector = '.groupsearchdropdown';
|
||||
$trigger = ".groupsearchwidget";
|
||||
$node = $this->find("css_element", $selector);
|
||||
if (!$node->isVisible()) {
|
||||
$this->execute("behat_general::i_click_on", [$trigger, "css_element"]);
|
||||
}
|
||||
} else if (strtolower($haystack) === 'grade') {
|
||||
$string = get_string('searchitems', 'core');
|
||||
$selector = '.gradesearchdropdown';
|
||||
$trigger = ".gradesearchwidget";
|
||||
$node = $this->find("css_element", $selector);
|
||||
if (!$node->isVisible()) {
|
||||
$this->execute("behat_general::i_click_on", [$trigger, "css_element"]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($fieldset) {
|
||||
$this->execute("behat_forms::set_field_value", [$string, $needle]);
|
||||
$this->execute("behat_general::wait_until_exists", [$needle, "list_item"]);
|
||||
}
|
||||
return $selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm if a value is within the search widget within the gradebook.
|
||||
*
|
||||
@ -391,17 +431,8 @@ class behat_grade extends behat_base {
|
||||
* @param string $haystack The type of the search widget.
|
||||
*/
|
||||
public function i_confirm_in_search_within_the_gradebook_widget_exists($needle, $haystack) {
|
||||
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
|
||||
|
||||
// Make sure that the dropdown menu is visible.
|
||||
$node = $this->find("css_element", "{$triggercssselector} .dropdown-menu");
|
||||
if (!$node->isVisible()) {
|
||||
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
|
||||
}
|
||||
|
||||
$this->execute("behat_general::wait_until_the_page_is_ready");
|
||||
$this->execute("behat_general::assert_element_contains_text",
|
||||
[$needle, "{$triggercssselector} .dropdown-menu", "css_element"]);
|
||||
[$needle, $this->get_dropdown_selector($haystack, $needle, false), "css_element"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -417,17 +448,8 @@ class behat_grade extends behat_base {
|
||||
* @param string $haystack The type of the search widget.
|
||||
*/
|
||||
public function i_confirm_in_search_within_the_gradebook_widget_does_not_exist($needle, $haystack) {
|
||||
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
|
||||
|
||||
// Make sure that the dropdown menu is visible.
|
||||
$node = $this->find("css_element", "{$triggercssselector} .dropdown-menu");
|
||||
if (!$node->isVisible()) {
|
||||
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
|
||||
}
|
||||
|
||||
$this->execute("behat_general::wait_until_the_page_is_ready");
|
||||
$this->execute("behat_general::assert_element_not_contains_text",
|
||||
[$needle, "{$triggercssselector} .dropdown-menu", "css_element"]);
|
||||
[$needle, $this->get_dropdown_selector($haystack, $needle, false), "css_element"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -443,17 +465,11 @@ class behat_grade extends behat_base {
|
||||
* @param string $haystack The type of the search widget.
|
||||
*/
|
||||
public function i_click_on_in_search_widget(string $needle, string $haystack) {
|
||||
$this->execute("behat_general::wait_until_the_page_is_ready");
|
||||
|
||||
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
|
||||
|
||||
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
|
||||
$this->execute("behat_general::wait_until_the_page_is_ready");
|
||||
$selector = $this->get_dropdown_selector($haystack, $needle);
|
||||
$this->execute('behat_general::i_click_on_in_the', [
|
||||
"//li[@role='option'][contains(., '" . $needle . "')] | //a[contains(., '" . $needle . "')]",
|
||||
"xpath_element",
|
||||
"{$triggercssselector} .dropdown-menu",
|
||||
"css_element"
|
||||
$needle, "list_item",
|
||||
$selector, "css_element"
|
||||
]);
|
||||
$this->execute("behat_general::i_wait_to_be_redirected");
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,9 @@ Feature: We can enter in grades and view reports from the gradebook
|
||||
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
|
||||
When I navigate to "View > User report" in the course gradebook
|
||||
And the "Gradebook navigation menu" select menu should contain "Grader report"
|
||||
And I click on "All users (1)" in the "user" search widget
|
||||
And I set the field "Search users" to "Student"
|
||||
And "View all results (1)" "list_item" should exist
|
||||
And I click on "View all results (1)" "option_role"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I follow "Grades" in the user menu
|
||||
|
@ -5,7 +5,18 @@ Information provided here is intended especially for developers.
|
||||
* The $showtitle parameter in the print_grade_page_head function located inside grade/lib.php has been deprecated and is not used anymore.
|
||||
* The deprecated `core_grades_create_gradecategory` external method has been removed, in addition to the accompanying
|
||||
`core_grades_external::create_gradecategory` methods
|
||||
* The grade_edit_tree_column_select class has been deprecated.
|
||||
* User, Group & Grade item JS classes have been added to easily enable the creation of combobox search components.
|
||||
Refer to the dev docs on details of how to implement this feature if desired.
|
||||
* basewidget.js has been deprecated and its uses replaced with the new JS classes mentioned above.
|
||||
* With the basewidget being deprecated all usages of it in core have now been migrated and the files that used it removed.
|
||||
The basewidget itself will go through the proper deprecation process and be removed in a future version.
|
||||
* The following webservices have had some optional parameters changed:
|
||||
- grade/classes/external/get_enrolled_users_for_selector
|
||||
- profileimage -> profileimageurl & profileimageurlsmall
|
||||
- return changed to use the core_user_external::user_description() method instead of a custom baked one.
|
||||
- grade/classes/external/get_groups_for_selector
|
||||
- Has been relocated to core_group_external::get_groups_for_selector()
|
||||
- groupimageurl is now available as an optional parameter to give the user an image either set or generated for the group.
|
||||
|
||||
=== 4.1 ===
|
||||
* The $importactiveurl parameter in the constructor of the core_grades\output\import_action_bar class has been deprecated and is not used anymore.
|
||||
|
3
group/amd/build/comboboxsearch/group.min.js
vendored
Normal file
3
group/amd/build/comboboxsearch/group.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0})}))}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,link:this.selectOneLink(group.id),groupimageurl:group.groupimageurl}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){switch(super.keyHandler(e),e.key){case"Tab":e.target.closest(this.selectors.input)&&(e.preventDefault(),this.clearSearchButton.focus({preventScroll:!0}));break;case"Escape":if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=group.min.js.map
|
1
group/amd/build/comboboxsearch/group.min.js.map
Normal file
1
group/amd/build/comboboxsearch/group.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10
group/amd/build/comboboxsearch/repository.min.js
vendored
Normal file
10
group/amd/build/comboboxsearch/repository.min.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
define("core_group/comboboxsearch/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
|
||||
/**
|
||||
* A repo for the comboboxsearch group type.
|
||||
*
|
||||
* @module core_group/comboboxsearch/repository
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.groupFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.groupFetch=courseid=>{const request={methodname:"core_group_get_groups_for_selector",args:{courseid:courseid}};return _ajax.default.call([request])[0]}}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
1
group/amd/build/comboboxsearch/repository.min.js.map
Normal file
1
group/amd/build/comboboxsearch/repository.min.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"repository.min.js","sources":["../../src/comboboxsearch/repository.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the comboboxsearch group type.\n *\n * @module core_group/comboboxsearch/repository\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\n\n/**\n * Given a course ID, we want to fetch the groups, so we may fetch their users.\n *\n * @method groupFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const groupFetch = (courseid) => {\n const request = {\n methodname: 'core_group_get_groups_for_selector',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAgC2BA,iBACjBC,QAAU,CACZC,WAAY,qCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"}
|
245
group/amd/src/comboboxsearch/group.js
Normal file
245
group/amd/src/comboboxsearch/group.js
Normal file
@ -0,0 +1,245 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Allow the user to search for groups.
|
||||
*
|
||||
* @module core_group/comboboxsearch/group
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import search_combobox from 'core/comboboxsearch/search_combobox';
|
||||
import {groupFetch} from 'core_group/comboboxsearch/repository';
|
||||
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
||||
import {debounce} from 'core/utils';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
export default class GroupSearch extends search_combobox {
|
||||
|
||||
courseID;
|
||||
bannedFilterFields = ['id', 'link', 'groupimageurl'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selectors = {...this.selectors,
|
||||
courseid: '[data-region="courseid"]',
|
||||
placeholder: '.groupsearchdropdown [data-region="searchplaceholder"]',
|
||||
};
|
||||
const component = document.querySelector(this.componentSelector());
|
||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||
this.renderDefault().catch(Notification.exception);
|
||||
}
|
||||
|
||||
static init() {
|
||||
return new GroupSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* The overall div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
componentSelector() {
|
||||
return '.group-search';
|
||||
}
|
||||
|
||||
/**
|
||||
* The dropdown div that contains the searching widget result space.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
dropdownSelector() {
|
||||
return '.groupsearchdropdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* The triggering div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
triggerSelector() {
|
||||
return '.groupsearchwidget';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node.
|
||||
*/
|
||||
async renderDropdown() {
|
||||
const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {
|
||||
groups: this.getMatchedResults(),
|
||||
hasresults: this.getMatchedResults().length > 0,
|
||||
searchterm: this.getSearchTerm(),
|
||||
});
|
||||
replaceNodeContents(this.selectors.placeholder, html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node by default we want our form to exist.
|
||||
*/
|
||||
async renderDefault() {
|
||||
this.setMatchedResults(await this.filterDataset(await this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
|
||||
await this.renderDropdown();
|
||||
|
||||
this.updateNodes();
|
||||
this.registerInputEvents();
|
||||
|
||||
// Add a small BS listener so that we can set the focus correctly on open.
|
||||
this.$component.on('shown.bs.dropdown', () => {
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data we will be searching against in this component.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async fetchDataset() {
|
||||
return await groupFetch(this.courseID).then((r) => r.groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictate to the search component how and what we want to match upon.
|
||||
*
|
||||
* @param {Array} filterableData
|
||||
* @returns {Array} The users that match the given criteria.
|
||||
*/
|
||||
async filterDataset(filterableData) {
|
||||
// Sometimes we just want to show everything.
|
||||
if (this.getPreppedSearchTerm() === '') {
|
||||
return filterableData;
|
||||
}
|
||||
return filterableData.filter((group) => Object.keys(group).some((key) => {
|
||||
if (group[key] === "" || this.bannedFilterFields.includes(key)) {
|
||||
return false;
|
||||
}
|
||||
return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
|
||||
*/
|
||||
filterMatchDataset() {
|
||||
this.setMatchedResults(
|
||||
this.getMatchedResults().map((group) => {
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
link: this.selectOneLink(group.id),
|
||||
groupimageurl: group.groupimageurl,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any keyboard inputs.
|
||||
*/
|
||||
registerInputEvents() {
|
||||
// Register & handle the text input.
|
||||
this.searchInput.addEventListener('input', debounce(async() => {
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
// We can also require a set amount of input before search.
|
||||
if (this.searchInput.value === '') {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}
|
||||
// User has given something for us to filter against.
|
||||
await this.filterrenderpipe();
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user interacts with the component.
|
||||
*
|
||||
* @param {MouseEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
async clickHandler(e) {
|
||||
if (e.target.closest(this.selectors.dropdown)) {
|
||||
// Forcibly prevent BS events so that we can control the open and close.
|
||||
// Really needed because by default input elements cant trigger a dropdown.
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
this.clearSearchButton.addEventListener('click', async() => {
|
||||
this.searchInput.value = '';
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
await this.filterrenderpipe();
|
||||
});
|
||||
// Prevent normal key presses activating this.
|
||||
if (e.target.closest('.dropdown-item') && e.button === 0) {
|
||||
window.location = e.target.closest('.dropdown-item').href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user presses a key within the component.
|
||||
*
|
||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
keyHandler(e) {
|
||||
super.keyHandler(e);
|
||||
// Switch the key presses to handle keyboard nav.
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
if (e.target.closest(this.selectors.input)) {
|
||||
e.preventDefault();
|
||||
this.clearSearchButton.focus({preventScroll: true});
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
if (document.activeElement.getAttribute('role') === 'option') {
|
||||
e.stopPropagation();
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
} else if (e.target.closest(this.selectors.input)) {
|
||||
const trigger = this.component.querySelector(this.selectors.trigger);
|
||||
trigger.focus({preventScroll: true});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the input event listener for the text input area.
|
||||
*/
|
||||
registerInputHandlers() {
|
||||
// Register & handle the text input.
|
||||
this.searchInput.addEventListener('input', debounce(() => {
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
// We can also require a set amount of input before search.
|
||||
if (this.getSearchTerm() === '') {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up the view all link that is dedicated to a particular result.
|
||||
*
|
||||
* @param {Number} groupID The ID of the group selected.
|
||||
*/
|
||||
selectOneLink(groupID) {
|
||||
throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
}
|
41
group/amd/src/comboboxsearch/repository.js
Normal file
41
group/amd/src/comboboxsearch/repository.js
Normal file
@ -0,0 +1,41 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A repo for the comboboxsearch group type.
|
||||
*
|
||||
* @module core_group/comboboxsearch/repository
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import ajax from "core/ajax";
|
||||
|
||||
/**
|
||||
* Given a course ID, we want to fetch the groups, so we may fetch their users.
|
||||
*
|
||||
* @method groupFetch
|
||||
* @param {int} courseid ID of the course to fetch the users of.
|
||||
* @return {object} jQuery promise
|
||||
*/
|
||||
export const groupFetch = (courseid) => {
|
||||
const request = {
|
||||
methodname: 'core_group_get_groups_for_selector',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
},
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_grades\external;
|
||||
namespace core_group\external;
|
||||
|
||||
use context_course;
|
||||
use core_external\external_api;
|
||||
@ -24,11 +24,16 @@ use core_external\external_multiple_structure;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_value;
|
||||
use core_external\external_warnings;
|
||||
use core_grades\external\coding_exception;
|
||||
use core_grades\external\invalid_parameter_exception;
|
||||
use core_grades\external\moodle_exception;
|
||||
use core_grades\external\restricted_context_exception;
|
||||
use moodle_url;
|
||||
|
||||
/**
|
||||
* External group report API implementation
|
||||
* External group name and image API implementation
|
||||
*
|
||||
* @package core_grades
|
||||
* @package core_group
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @category external
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@ -59,7 +64,7 @@ class get_groups_for_selector extends external_api {
|
||||
* @throws restricted_context_exception
|
||||
*/
|
||||
protected static function execute(int $courseid): array {
|
||||
global $DB, $USER;
|
||||
global $DB, $USER, $OUTPUT;
|
||||
|
||||
$params = self::validate_parameters(
|
||||
self::execute_parameters(),
|
||||
@ -99,10 +104,18 @@ class get_groups_for_selector extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
$mappedgroups = array_map(function($group) use ($context) {
|
||||
$mappedgroups = array_map(function($group) use ($context, $OUTPUT) {
|
||||
if ($group->id) { // Particular group. Get the group picture if it exists, otherwise return a generic image.
|
||||
$picture = get_group_picture_url($group, $group->courseid, true) ??
|
||||
moodle_url::make_pluginfile_url($context->id, 'group', 'generated', $group->id, '/', 'group.svg');
|
||||
} else { // All participants.
|
||||
$picture = $OUTPUT->image_url('g/g1');
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'id' => $group->id,
|
||||
'name' => format_string($group->name, true, ['context' => $context])
|
||||
'name' => format_string($group->name, true, ['context' => $context]),
|
||||
'groupimageurl' => $picture->out(false),
|
||||
];
|
||||
}, $groupsmenu);
|
||||
}
|
||||
@ -133,7 +146,8 @@ class get_groups_for_selector extends external_api {
|
||||
public static function group_description(): external_description {
|
||||
$groupfields = [
|
||||
'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED),
|
||||
'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED)
|
||||
'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED),
|
||||
'groupimageurl' => new external_value(PARAM_URL, 'Group image URL', VALUE_OPTIONAL),
|
||||
];
|
||||
return new external_single_structure($groupfields);
|
||||
}
|
44
group/templates/comboboxsearch/group_selector.mustache
Normal file
44
group/templates/comboboxsearch/group_selector.mustache
Normal file
@ -0,0 +1,44 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_group/comboboxsearch/group_selector
|
||||
|
||||
The group selector trigger element.
|
||||
|
||||
Context variables required for this template:
|
||||
* label - The label text fot the group selector element.
|
||||
* group - The value of the group selector element (id of the preselected group)
|
||||
* selectedgroup - The text of the selected group option.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"label": "Select separate groups",
|
||||
"group": "21",
|
||||
"selectedgroup": "Group 1"
|
||||
}
|
||||
}}
|
||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}" aria-hidden="true"></span>
|
||||
<div class="align-items-center d-flex">
|
||||
<div class="d-block pr-3 text-truncate">
|
||||
<span class="d-block small">
|
||||
{{label}}
|
||||
</span>
|
||||
<span class="p-0 font-weight-bold">
|
||||
{{selectedgroup}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
44
group/templates/comboboxsearch/resultitem.mustache
Normal file
44
group/templates/comboboxsearch/resultitem.mustache
Normal file
@ -0,0 +1,44 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_group/comboboxsearch/resultitem
|
||||
|
||||
Template for the individual result item.
|
||||
|
||||
Context variables required for this template:
|
||||
* id - Group system ID.
|
||||
* name - Groups' name.
|
||||
* link - The link used to redirect upon self to show only this specific group.
|
||||
* groupimageurl - The link of the groups picture.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Foo bar",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2",
|
||||
"groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
}
|
||||
}}
|
||||
{{<core/local/comboboxsearch/resultitem }}
|
||||
{{$content}}
|
||||
<div class="pr-2 pl-1 w-25">
|
||||
<img class="rounded-circle mx-auto img-fluid" src="{{groupimageurl}}" alt=""/>
|
||||
</div>
|
||||
<div class="pr-3 w-75">
|
||||
<span class="d-block w-100 p-0 text-truncate">
|
||||
{{name}}
|
||||
</span>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/core/local/comboboxsearch/resultitem}}
|
49
group/templates/comboboxsearch/resultset.mustache
Normal file
49
group/templates/comboboxsearch/resultset.mustache
Normal file
@ -0,0 +1,49 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_group/comboboxsearch/resultset
|
||||
|
||||
Wrapping template for returned result items.
|
||||
|
||||
Context variables required for this template:
|
||||
* groups - Our returned groups to render.
|
||||
* searchterm - The entered text to find these results.
|
||||
* hasgroups - Allow the handling where no users exist for the returned search term.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Foo bar",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Bar Foo",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
||||
}
|
||||
],
|
||||
"searchterm": "Foo",
|
||||
"hasresults": true
|
||||
}
|
||||
}}
|
||||
{{<core/local/comboboxsearch/resultset}}
|
||||
{{$listid}}groups{{/listid}}
|
||||
{{$results}}
|
||||
{{#groups}}
|
||||
{{>core_group/comboboxsearch/resultitem}}
|
||||
{{/groups}}
|
||||
{{/results}}
|
||||
{{/core/local/comboboxsearch/resultset}}
|
70
group/templates/comboboxsearch/searchbody.mustache
Normal file
70
group/templates/comboboxsearch/searchbody.mustache
Normal file
@ -0,0 +1,70 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_group/comboboxsearch/searchbody
|
||||
|
||||
Wrapping template for search input.
|
||||
|
||||
Context variables required for this template:
|
||||
* courseid - The id of the course to search within.
|
||||
* groupid - The id of the group to search within.
|
||||
* currentvalue - The prefill value for the search input if provided
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"courseid": 2,
|
||||
"groupid": 25,
|
||||
"currentvalue": "bar"
|
||||
}
|
||||
}}
|
||||
<div class="flex-column h-100 w-100">
|
||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
||||
<span class="d-none" data-region="groupid" data-groupid="{{groupid}}" aria-hidden="true"></span>
|
||||
|
||||
{{#currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchgroups, core
|
||||
{{/str}}{{/label}}
|
||||
{{$value}}{{{currentvalue}}}{{/value}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="groups-result-listbox"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
{{^currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchgroups, core
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchgroups, core
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="groups-result-listbox"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
<input type="hidden" name="search" id="input-{{uniqid}}"/>
|
||||
|
||||
<div data-region="searchplaceholder"></div>
|
||||
</div>
|
@ -577,7 +577,6 @@ $string['nooutcome'] = 'No outcome';
|
||||
$string['nooutcomes'] = 'Outcome items must be linked to a course outcome, but there are no outcomes for this course. Would you like to add one?';
|
||||
$string['nopermissiontoresetweights'] = 'No permission to reset the weights';
|
||||
$string['nopublish'] = 'Do not publish';
|
||||
$string['noresultsfor'] = 'No results for "{$a}"';
|
||||
$string['noreports'] = 'You are not enrolled in, nor teaching any courses on this site.';
|
||||
$string['norolesdefined'] = 'No roles defined in Administration > Grades > General settings > Graded roles';
|
||||
$string['noscales'] = 'Outcomes must be linked to a course scale or global scale, but there are none. Would you like to add one?';
|
||||
@ -734,8 +733,6 @@ $string['savepreferences'] = 'Save preferences';
|
||||
$string['scaleconfirmdelete'] = 'Are you sure you wish to delete the scale "{$a}"?';
|
||||
$string['scaledpct'] = 'Scaled %';
|
||||
$string['searchcollapsedcolumns'] = 'Search collapsed columns';
|
||||
$string['searchgroups'] = 'Search groups';
|
||||
$string['searchusers'] = 'Search users';
|
||||
$string['seeallcoursegrades'] = 'See all course grades';
|
||||
$string['select'] = 'Select {$a}';
|
||||
$string['selectalloroneuser'] = 'Select all or one user';
|
||||
@ -857,7 +854,6 @@ $string['userfields_show_help'] = 'Show additional user fields like email addres
|
||||
$string['usergrade'] = 'User {$a->fullname} ({$a->useridnumber}) on item {$a->gradeidnumber}';
|
||||
$string['userid'] = 'User ID';
|
||||
$string['useridnumberwarning'] = 'Users without an ID number are excluded from the export as they cannot be imported';
|
||||
$string['usermatchedon'] = 'This user matches the search via the following field';
|
||||
$string['usermappingerror'] = 'User mapping error: Could not find user with {$a->field} of "{$a->value}".';
|
||||
$string['usermappingerrormultipleusersfound'] = 'User mapping error: Multiple users found with {$a->field} of "{$a->value}". Please use a more unique mapping field.';
|
||||
$string['usermappingerrorusernotfound'] = 'User mapping error. Could not find user.';
|
||||
@ -893,6 +889,7 @@ $string['yes'] = 'Yes';
|
||||
$string['yourgrade'] = 'Your grade';
|
||||
|
||||
$string['aria-toggledropdown'] = 'Toggle the following dropdown';
|
||||
$string['aria:dropdowngrades'] = 'Grade items found';
|
||||
|
||||
// Deprecated since Moodle 4.0.
|
||||
$string['navmethod'] = 'Navigation method';
|
||||
|
@ -1546,6 +1546,7 @@ $string['none'] = 'None';
|
||||
$string['noneditingteacher'] = 'Non-editing teacher';
|
||||
$string['noneditingteacherdescription'] = 'Non-editing teachers can teach in courses and grade students, but may not alter activities.';
|
||||
$string['nonstandard'] = 'Non-standard';
|
||||
$string['noresultsfor'] = 'No results for "{$a}"';
|
||||
$string['nopendingcourses'] = 'There are no courses pending approval';
|
||||
$string['nopotentialadmins'] = 'No potential admins';
|
||||
$string['nopotentialcreators'] = 'No potential course creators';
|
||||
@ -1934,8 +1935,11 @@ $string['searchbyemail'] = 'Search by email address';
|
||||
$string['searchbyusername'] = 'Search by username';
|
||||
$string['searchcoursecategories'] = 'Search categories';
|
||||
$string['searchcourses'] = 'Search courses';
|
||||
$string['searchgroups'] = 'Search groups';
|
||||
$string['searchitems'] = 'Search items';
|
||||
$string['searchoptions'] = 'Search options';
|
||||
$string['searchresults'] = 'Search results';
|
||||
$string['searchusers'] = 'Search users';
|
||||
$string['sec'] = 'sec';
|
||||
$string['secondsleft'] = '{$a} secs';
|
||||
$string['seconds'] = 'seconds';
|
||||
@ -2321,6 +2325,7 @@ $string['userdescription_help'] = 'This box enables you to enter some text about
|
||||
$string['userdetails'] = 'User details';
|
||||
$string['userfiles'] = 'User files';
|
||||
$string['userlist'] = 'User list';
|
||||
$string['usermatchedon'] = 'This user matches the search via the following field';
|
||||
$string['usermenu'] = 'User menu';
|
||||
$string['usermenugoback'] = 'Go back to user menu';
|
||||
$string['username'] = 'Username';
|
||||
@ -2351,9 +2356,11 @@ $string['view'] = 'View';
|
||||
$string['viewing'] = 'Viewing:';
|
||||
$string['viewallcourses'] = 'View all courses';
|
||||
$string['viewallcoursescategories'] = 'View all courses and categories';
|
||||
$string['viewallresults'] = 'View all results ({$a})';
|
||||
$string['viewmore'] = 'View more';
|
||||
$string['viewallsubcategories'] = 'View all subcategories';
|
||||
$string['viewfileinpopup'] = 'View file in a popup window';
|
||||
$string['viewresults'] = 'View results for {$a}';
|
||||
$string['viewprofile'] = 'View profile';
|
||||
$string['views'] = 'Views';
|
||||
$string['viewsolution'] = 'view solution';
|
||||
|
10
lib/amd/build/comboboxsearch/search_combobox.min.js
vendored
Normal file
10
lib/amd/build/comboboxsearch/search_combobox.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/amd/build/comboboxsearch/search_combobox.min.js.map
Normal file
1
lib/amd/build/comboboxsearch/search_combobox.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -16,11 +16,12 @@
|
||||
import $ from 'jquery';
|
||||
import CustomEvents from "core/custom_interaction_events";
|
||||
import {debounce} from 'core/utils';
|
||||
import Pending from 'core/pending';
|
||||
|
||||
/**
|
||||
* The class that manages the state of the search.
|
||||
* The class that manages the state of the search within a combobox.
|
||||
*
|
||||
* @module gradereport_grader/search/search_class
|
||||
* @module core/comboboxsearch/search_combobox
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@ -36,11 +37,11 @@ const DOWN = 1;
|
||||
export default class {
|
||||
// Define our standard lookups.
|
||||
selectors = {
|
||||
component: this.setComponentSelector(),
|
||||
trigger: this.setTriggerSelector(),
|
||||
component: this.componentSelector(),
|
||||
trigger: this.triggerSelector(),
|
||||
input: '[data-action="search"]',
|
||||
clearSearch: '[data-action="clearsearch"]',
|
||||
dropdown: this.setDropdownSelector(),
|
||||
dropdown: this.dropdownSelector(),
|
||||
resultitems: '[role="option"]',
|
||||
viewall: '#select-all',
|
||||
};
|
||||
@ -71,7 +72,6 @@ export default class {
|
||||
component = document.querySelector(this.selectors.component);
|
||||
searchInput = this.component.querySelector(this.selectors.input);
|
||||
searchDropdown = this.component.querySelector(this.selectors.dropdown);
|
||||
$searchButton = $(this.selectors.trigger);
|
||||
clearSearchButton = this.component.querySelector(this.selectors.clearSearch);
|
||||
$component = $(this.component);
|
||||
|
||||
@ -119,22 +119,22 @@ export default class {
|
||||
/**
|
||||
* Stub out a required function.
|
||||
*/
|
||||
setComponentSelector() {
|
||||
throw new Error(`setComponentSelector() must be implemented in ${this.constructor.name}`);
|
||||
componentSelector() {
|
||||
throw new Error(`componentSelector() must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub out a required function.
|
||||
*/
|
||||
setDropdownSelector() {
|
||||
throw new Error(`setDropdownSelector() must be implemented in ${this.constructor.name}`);
|
||||
dropdownSelector() {
|
||||
throw new Error(`dropdownSelector() must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub out a required function.
|
||||
*/
|
||||
setTriggerSelector() {
|
||||
throw new Error(`setTriggerSelector() must be implemented in ${this.constructor.name}`);
|
||||
triggerSelector() {
|
||||
throw new Error(`triggerSelector() must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -216,7 +216,8 @@ export default class {
|
||||
searchDropdown: this.searchDropdown,
|
||||
currentViewAll: this.currentViewAll,
|
||||
searchInput: this.searchInput,
|
||||
clearSearchButton: this.clearSearchButton
|
||||
clearSearchButton: this.clearSearchButton,
|
||||
trigger: this.component.querySelector(this.selectors.trigger),
|
||||
};
|
||||
}
|
||||
|
||||
@ -243,7 +244,6 @@ export default class {
|
||||
*/
|
||||
toggleDropdown(on = false) {
|
||||
this.$component.dropdown('toggle');
|
||||
this.$searchButton.attr('aria-expanded', on);
|
||||
if (on) {
|
||||
this.searchDropdown.classList.add('show');
|
||||
$(this.searchDropdown).show();
|
||||
@ -269,21 +269,8 @@ export default class {
|
||||
* Register clickable event listeners.
|
||||
*/
|
||||
registerClickHandlers() {
|
||||
// Prevent the click triggering the dropdown.
|
||||
this.$searchButton.on('click', () => {
|
||||
this.toggleDropdown();
|
||||
});
|
||||
|
||||
// Register click events within the component.
|
||||
this.component.addEventListener('click', this.clickHandler.bind(this));
|
||||
|
||||
// Register a small click event onto the document since we need to check if they are clicking off the component.
|
||||
document.addEventListener('click', (e) => {
|
||||
// Since we are handling dropdowns manually, ensure we can close it when clicking off.
|
||||
if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,13 +298,30 @@ export default class {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
await this.renderAndShow();
|
||||
const pendingPromise = new Pending();
|
||||
await this.renderAndShow().then(() => {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}).then(() => {
|
||||
pendingPromise.resolve();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any changeable nodes, filter and then render the result.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async filterrenderpipe() {
|
||||
this.updateNodes();
|
||||
this.setMatchedResults(await this.filterDataset(await this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
await this.renderDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* A combo method to take the matching fields and render out the results.
|
||||
*
|
@ -91,6 +91,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||
'block' => 'block',
|
||||
'button' => 'button',
|
||||
'checkbox' => 'checkbox',
|
||||
'combobox' => 'combobox',
|
||||
'css_element' => 'css_element',
|
||||
'dialogue' => 'dialogue',
|
||||
'field' => 'field',
|
||||
@ -159,6 +160,9 @@ XPATH
|
||||
.//*[@data-block][contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
|
||||
descendant::*[self::h2|self::h3|self::h4|self::h5][normalize-space(.) = %locator%] or
|
||||
@aria-label = %locator%]
|
||||
XPATH
|
||||
, 'combobox' => <<<XPATH
|
||||
.//*[@role='combobox'][%titleMatch% or %ariaLabelMatch% or text()[contains(., %locator%)]]
|
||||
XPATH
|
||||
, 'dialogue' => <<<XPATH
|
||||
.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_grades\output;
|
||||
namespace core\output;
|
||||
|
||||
use moodle_exception;
|
||||
use renderable;
|
||||
@ -22,18 +22,13 @@ use renderer_base;
|
||||
use templatable;
|
||||
|
||||
/**
|
||||
* Renderable class for the dropdown in the gradebook pages.
|
||||
* Renderable class for the comboboxsearch.
|
||||
*
|
||||
* We have opted to have this as a class as opposed to a renderable for prosperity
|
||||
* in the case that custom handling is required by the calling code.
|
||||
*
|
||||
* This could become a abstract class if other components require similar functionality and wish to extend the base here.
|
||||
*
|
||||
* @package core_grades
|
||||
* @package core_output
|
||||
* @copyright 2022 Mathew May <Mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class gradebook_dropdown implements renderable, templatable {
|
||||
class comboboxsearch implements renderable, templatable {
|
||||
|
||||
/** @var bool $renderlater Should the dropdown render straightaway? */
|
||||
protected $renderlater;
|
||||
@ -86,7 +81,7 @@ class gradebook_dropdown implements renderable, templatable {
|
||||
if (!$renderlater && empty($dropdowncontent)) {
|
||||
throw new moodle_exception(
|
||||
'incorrectdropdownvars',
|
||||
'core_grades',
|
||||
'core',
|
||||
'', null,
|
||||
'Dropdown content must be set to render later.'
|
||||
);
|
||||
@ -119,6 +114,7 @@ class gradebook_dropdown implements renderable, templatable {
|
||||
'dropdownclasses' => $this->dropdownclasses,
|
||||
'buttonheader' => $this->buttonheader,
|
||||
'usebutton' => $this->usesbutton,
|
||||
'instance' => rand(), // Template uniqid is per render out so sometimes these conflict.
|
||||
];
|
||||
}
|
||||
|
||||
@ -128,6 +124,6 @@ class gradebook_dropdown implements renderable, templatable {
|
||||
* @return string
|
||||
*/
|
||||
public function get_template(): string {
|
||||
return 'core_grades/tertiary_navigation_dropdown';
|
||||
return 'core/comboboxsearch';
|
||||
}
|
||||
}
|
@ -978,17 +978,19 @@ $functions = array(
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
),
|
||||
'core_grades_get_groups_for_search_widget' => [
|
||||
'classname' => 'core_grades\external\get_groups_for_selector',
|
||||
'classname' => 'core_group\external\get_groups_for_selector',
|
||||
'description' => '** DEPRECATED ** Please do not call this function any more. ' .
|
||||
'Use core_grades_get_groups_for_selector instead. ' .
|
||||
'Use core_group_get_groups_for_selector instead. ' .
|
||||
'Get the group/(s) for a course',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_grades_get_groups_for_selector' => [
|
||||
'classname' => 'core_grades\external\get_groups_for_selector',
|
||||
'description' => 'Get the group/(s) for a course',
|
||||
'classname' => 'core_group\external\get_groups_for_selector',
|
||||
'description' => '** DEPRECATED ** Please do not call this function any more. ' .
|
||||
'Use core_group_get_groups_for_selector instead. ' .
|
||||
'Get the group/(s) for a course',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
@ -1143,6 +1145,13 @@ $functions = array(
|
||||
'type' => 'read',
|
||||
'capabilities' => 'moodle/course:managegroups'
|
||||
),
|
||||
'core_group_get_groups_for_selector' => [
|
||||
'classname' => 'core_group\external\get_groups_for_selector',
|
||||
'description' => 'Get the group/(s) for a course',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_group_unassign_grouping' => array(
|
||||
'classname' => 'core_group_external',
|
||||
'methodname' => 'unassign_grouping',
|
||||
|
@ -12,37 +12,71 @@
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_grades/tertiary_navigation_dropdown
|
||||
@template core/comboboxsearch
|
||||
|
||||
Tertiary navigation dropdown selector.
|
||||
Combobox search selector dropdown.
|
||||
|
||||
Context variables required for this template:
|
||||
* rtl - Is this dropdown being used in a RTL case? if so, we need to ensure it drops down in the right place.
|
||||
* renderlater - This determines if we show a placeholder whilst fetching content to replace within the placeholder region
|
||||
* buttoncontent - The string to be shown to users to trigger the dropdown
|
||||
* usebutton - If we want to use a button to trigger the dropdown, or just the dropdown itself
|
||||
* dropdowncontent - If rendering now, The content within the dropdown
|
||||
* parentclasses - Our class for the DOM Node that the default bootstrap dropdown events are tagged onto
|
||||
* buttonclasses - If you want special handling add classes here
|
||||
* dropdownclasses - If you want special handling or sizing etc add classes here
|
||||
* instance - The instance ID of the tertiary navigation dropdown
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"rtl": false,
|
||||
"renderlater": false,
|
||||
"usebutton": true,
|
||||
"buttoncontent": "Example dropdown button",
|
||||
"dropdowncontent": "Some body content to render right now",
|
||||
"parentclasses": "my-dropdown",
|
||||
"buttonclasses": "my-button",
|
||||
"dropdownclasses": "my-cool-dropdown"
|
||||
"dropdownclasses": "my-cool-dropdown",
|
||||
"instance": 25
|
||||
}
|
||||
}}
|
||||
{{#buttonheader}}
|
||||
<small>{{.}}</small>
|
||||
{{/buttonheader}}
|
||||
<div class="{{#parentclasses}}{{.}}{{/parentclasses}}">
|
||||
<div class="{{#buttonclasses}}{{.}}{{/buttonclasses}} btn dropdown-toggle keep-open d-flex text-left align-items-center p-0 font-weight-bold" data-toggle="dropdown" aria-haspopup="true" {{#usebutton}}role="button" aria-expanded="false" tabindex="0"{{/usebutton}} {{^usebutton}}tabindex="-1"{{/usebutton}} aria-label="{{#str}} aria-toggledropdown, core_grades {{/str}}" >
|
||||
{{{buttoncontent}}}
|
||||
</div>
|
||||
<div class="{{#dropdownclasses}}{{.}}{{/dropdownclasses}} dropdown-menu {{#rtl}}dropdown-menu-right{{/rtl}}">
|
||||
<div class="{{#parentclasses}}{{.}}{{/parentclasses}}"
|
||||
{{^usebutton}}
|
||||
tabindex="-1"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
role="combobox"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="dialog-{{instance}}-{{uniqid}}"
|
||||
aria-label="{{#cleanstr}} aria-toggledropdown, core_grades {{/cleanstr}}"
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/usebutton}}>
|
||||
|
||||
{{#usebutton}}
|
||||
<div tabindex="0"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
role="combobox"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="dialog-{{instance}}-{{uniqid}}"
|
||||
class="{{#buttonclasses}}{{.}}{{/buttonclasses}} btn dropdown-toggle keep-open d-flex text-left align-items-center p-0 font-weight-bold"
|
||||
aria-label="{{#cleanstr}} aria-toggledropdown, core_grades {{/cleanstr}}"
|
||||
data-input-element="input-{{uniqid}}">
|
||||
{{{buttoncontent}}}
|
||||
</div>
|
||||
{{/usebutton}}
|
||||
|
||||
{{^usebutton}}{{{buttoncontent}}}{{/usebutton}}
|
||||
|
||||
<div class="{{#dropdownclasses}}{{.}}{{/dropdownclasses}} dropdown-menu {{#rtl}}dropdown-menu-right{{/rtl}}"
|
||||
id="dialog-{{instance}}-{{uniqid}}"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="{{#cleanstr}} selectagroup, core {{/cleanstr}}"
|
||||
>
|
||||
<div class="w-100 p-3" data-region="placeholder">
|
||||
{{#renderlater}}
|
||||
<div class="d-flex flex-column align-items-stretch justify-content-between" style="height: 150px; width: 300px;">
|
39
lib/templates/local/comboboxsearch/resultitem.mustache
Normal file
39
lib/templates/local/comboboxsearch/resultitem.mustache
Normal file
@ -0,0 +1,39 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/comboboxsearch/resultitem
|
||||
|
||||
Template for the individual result item.
|
||||
|
||||
Context variables required for this template:
|
||||
* id - Result item system ID.
|
||||
* name - Result item human readable name.
|
||||
* link - The link used to redirect upon self to show only this specific result item.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Foo bar",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
}
|
||||
}}
|
||||
<li class="w-100 result-row" role="none" id="result-row-{{id}}">
|
||||
<a role="option" id="result-{{id}}" href="{{{link}}}" class="dropdown-item d-flex px-0 py-1 align-items-center" tabindex="-1" aria-label="{{$arialabel}}{{#str}}viewresults, core, {{name}}{{/str}}{{/arialabel}}">
|
||||
{{$content}}
|
||||
<span class="d-block w-100 px-2 text-truncate">
|
||||
{{name}}
|
||||
</span>
|
||||
{{/content}}
|
||||
</a>
|
||||
</li>
|
57
lib/templates/local/comboboxsearch/resultset.mustache
Normal file
57
lib/templates/local/comboboxsearch/resultset.mustache
Normal file
@ -0,0 +1,57 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/comboboxsearch/resultset
|
||||
|
||||
Wrapping template for returned result items.
|
||||
|
||||
Context variables required for this template:
|
||||
* results - Our returned results to render.
|
||||
* searchterm - The entered text to find these results.
|
||||
* hasresult - Allow the handling where no results exist for the returned search term.
|
||||
* noresults - Our fall through case if nothing matches.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Foo bar",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Bar Foo",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
||||
}
|
||||
],
|
||||
"searchterm": "Foo",
|
||||
"hasresults": true
|
||||
}
|
||||
}}
|
||||
<div class="d-flex flex-column mh-100 h-100">
|
||||
{{#hasresults}}
|
||||
<ul id="{{$listid}}list{{/listid}}-result-listbox" class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2 list-group h-100 mx-0 {{$listclasses}}{{/listclasses}}" role="listbox" data-region="search-result-items-container" tabindex="-1" aria-label="{{#cleanstr}} aria:dropdowngrades, core_grades {{/cleanstr}}">
|
||||
{{$results}}
|
||||
{{#results}}
|
||||
{{>core/local/comboboxsearch/resultitem}}
|
||||
{{/results}}
|
||||
{{/results}}
|
||||
{{$selectall}}{{/selectall}}
|
||||
</ul>
|
||||
{{/hasresults}}
|
||||
{{^hasresults}}
|
||||
<span class="small d-block px-4 my-4">{{#str}} noresultsfor, core, {{searchterm}}{{/str}}</span>
|
||||
{{/hasresults}}
|
||||
</div>
|
66
lib/templates/local/comboboxsearch/searchbody.mustache
Normal file
66
lib/templates/local/comboboxsearch/searchbody.mustache
Normal file
@ -0,0 +1,66 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/comboboxsearch/searchbody
|
||||
|
||||
Wrapping template for search input.
|
||||
|
||||
Context variables required for this template:
|
||||
* courseid - The id of the course to search within the report of.
|
||||
* currentvalue - The string the user searched for previously.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"courseid": 2,
|
||||
"currentvalue": "Abed"
|
||||
}
|
||||
}}
|
||||
<div class="flex-column h-100 w-100">
|
||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
||||
|
||||
{{#currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchitems, core
|
||||
{{/str}}{{/label}}
|
||||
{{$value}}{{{currentvalue}}}{{/value}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="list-result-listbox"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="result-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
{{^currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchitems, core
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchitems, core
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="list-result-listbox"
|
||||
aria-autocomplete="list"
|
||||
data-input-element="result-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
<input type="hidden" name="search" id="result-input-{{uniqid}}"/>
|
||||
<div data-region="searchplaceholder"></div>
|
||||
</div>
|
@ -127,7 +127,7 @@ Feature: Initials bar
|
||||
And I am on the "assign1" "Activity" page logged in as "teacher"
|
||||
When I follow "View all submissions"
|
||||
And I select "View gradebook" from the "jump" singleselect
|
||||
And I press "Filter by name"
|
||||
And I click on "Filter by name" "combobox"
|
||||
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
|
||||
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
|
||||
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
|
||||
@ -144,7 +144,7 @@ Feature: Initials bar
|
||||
And I should see "Astudent Astudent"
|
||||
And I should see "Bstudent Astudent"
|
||||
And I should not see "Cstudent Cstudent"
|
||||
And I press "Last (A)"
|
||||
And I click on "Last (A)" "combobox"
|
||||
And I select "B" in the "First name" "core_grades > initials bar"
|
||||
And I press "Apply"
|
||||
And I wait until the page is ready
|
||||
@ -165,7 +165,7 @@ Feature: Initials bar
|
||||
And I should not see "Astudent Astudent"
|
||||
And I should see "Bstudent Astudent"
|
||||
And I should not see "Cstudent Cstudent"
|
||||
And I press "First (B) Last (A)"
|
||||
And I click on "First (B) Last (A)" "combobox"
|
||||
And I select "All" in the "First name" "core_grades > initials bar"
|
||||
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
|
||||
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
|
||||
@ -176,7 +176,7 @@ Feature: Initials bar
|
||||
And I should see "Astudent Astudent"
|
||||
And I should see "Bstudent Astudent"
|
||||
And I should not see "Cstudent Cstudent"
|
||||
And I press "Last (A)"
|
||||
And I click on "Last (A)" "combobox"
|
||||
And I select "All" in the "Last name" "core_grades > initials bar"
|
||||
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
|
||||
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
|
||||
|
@ -54,6 +54,8 @@ information provided here is intended especially for developers.
|
||||
* New method moodleform::filter_shown_headers() is created to show some expanded headers only and hide the rest.
|
||||
* count_words() and count_letters() have a new optional parameter called $format to format the text before doing the counting.
|
||||
* New core_renderer::sr_text method to generate screen reader only inline texts without using html_writter.
|
||||
* The external function core_grades_get_groups_for_selector is now relocated.
|
||||
Please use it at core_group_get_groups_for_selector instead.
|
||||
|
||||
=== 4.2 ===
|
||||
|
||||
|
@ -3189,3 +3189,19 @@ blockquote {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Combobox search dropdowns */
|
||||
.usersearchdropdown,
|
||||
.gradesearchdropdown,
|
||||
.groupsearchdropdown {
|
||||
max-width: 350px;
|
||||
.searchresultitemscontainer {
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
/* stylelint-disable declaration-no-important */
|
||||
img {
|
||||
height: 48px !important;
|
||||
width: 48px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -632,7 +632,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.search-widget {
|
||||
.dropdown-menu {
|
||||
padding: 0.8rem 1.2rem;
|
||||
|
@ -25984,6 +25984,26 @@ blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Combobox search dropdowns */
|
||||
.usersearchdropdown,
|
||||
.gradesearchdropdown,
|
||||
.groupsearchdropdown {
|
||||
max-width: 350px;
|
||||
}
|
||||
.usersearchdropdown .searchresultitemscontainer,
|
||||
.gradesearchdropdown .searchresultitemscontainer,
|
||||
.groupsearchdropdown .searchresultitemscontainer {
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
/* stylelint-disable declaration-no-important */
|
||||
}
|
||||
.usersearchdropdown .searchresultitemscontainer img,
|
||||
.gradesearchdropdown .searchresultitemscontainer img,
|
||||
.groupsearchdropdown .searchresultitemscontainer img {
|
||||
height: 48px !important;
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
@ -25984,6 +25984,26 @@ blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Combobox search dropdowns */
|
||||
.usersearchdropdown,
|
||||
.gradesearchdropdown,
|
||||
.groupsearchdropdown {
|
||||
max-width: 350px;
|
||||
}
|
||||
.usersearchdropdown .searchresultitemscontainer,
|
||||
.gradesearchdropdown .searchresultitemscontainer,
|
||||
.groupsearchdropdown .searchresultitemscontainer {
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
/* stylelint-disable declaration-no-important */
|
||||
}
|
||||
.usersearchdropdown .searchresultitemscontainer img,
|
||||
.gradesearchdropdown .searchresultitemscontainer img,
|
||||
.groupsearchdropdown .searchresultitemscontainer img {
|
||||
height: 48px !important;
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
3
user/amd/build/comboboxsearch/user.min.js
vendored
Normal file
3
user/amd/build/comboboxsearch/user.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
user/amd/build/comboboxsearch/user.min.js.map
Normal file
1
user/amd/build/comboboxsearch/user.min.js.map
Normal file
File diff suppressed because one or more lines are too long
295
user/amd/src/comboboxsearch/user.js
Normal file
295
user/amd/src/comboboxsearch/user.js
Normal file
@ -0,0 +1,295 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Allow the user to search for learners.
|
||||
*
|
||||
* @module core_user/comboboxsearch/user
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import search_combobox from 'core/comboboxsearch/search_combobox';
|
||||
import {get_strings as getStrings} from 'core/str';
|
||||
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
||||
import $ from 'jquery';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
export default class UserSearch extends search_combobox {
|
||||
|
||||
courseID;
|
||||
groupID;
|
||||
bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];
|
||||
|
||||
// A map of user profile field names that is human-readable.
|
||||
profilestringmap = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Register a small click event onto the document since we need to check if they are clicking off the component.
|
||||
document.addEventListener('click', (e) => {
|
||||
// Since we are handling dropdowns manually, ensure we can close it when clicking off.
|
||||
if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Define our standard lookups.
|
||||
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 = document.querySelector(this.selectors.groupid)?.dataset?.groupid;
|
||||
}
|
||||
|
||||
static init() {
|
||||
return new UserSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* The overall div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
componentSelector() {
|
||||
return '.user-search';
|
||||
}
|
||||
|
||||
/**
|
||||
* The dropdown div that contains the searching widget result space.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
dropdownSelector() {
|
||||
return '.usersearchdropdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* The triggering div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
triggerSelector() {
|
||||
return '.usersearchwidget';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node.
|
||||
*/
|
||||
async renderDropdown() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data we will be searching against in this component.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
fetchDataset() {
|
||||
throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictate to the search component how and what we want to match upon.
|
||||
*
|
||||
* @param {Array} filterableData
|
||||
* @returns {Array} The users that match the given criteria.
|
||||
*/
|
||||
async filterDataset(filterableData) {
|
||||
return filterableData.filter((user) => Object.keys(user).some((key) => {
|
||||
if (user[key] === "" || user[key] === null || this.bannedFilterFields.includes(key)) {
|
||||
return false;
|
||||
}
|
||||
return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
|
||||
*
|
||||
* @returns {Array} The results with the matched fields inserted.
|
||||
*/
|
||||
async filterMatchDataset() {
|
||||
const stringMap = await this.getStringMap();
|
||||
this.setMatchedResults(
|
||||
this.getMatchedResults().map((user) => {
|
||||
for (const [key, value] of Object.entries(user)) {
|
||||
// Sometimes users have null values in their profile fields.
|
||||
if (value === null) {
|
||||
continue;
|
||||
}
|
||||
const valueString = value.toString().toLowerCase();
|
||||
if (valueString.includes(this.getPreppedSearchTerm()) && !this.bannedFilterFields.includes(key)) {
|
||||
// Ensure we have a good string, otherwise fallback to the key.
|
||||
user.matchingFieldName = stringMap.get(key) ?? key;
|
||||
user.matchingField = valueString.replace(
|
||||
this.getPreppedSearchTerm(),
|
||||
`<span class="font-weight-bold">${this.getSearchTerm()}</span>`
|
||||
);
|
||||
user.matchingField = `${user.matchingField} (${user.email})`;
|
||||
user.link = this.selectOneLink(user.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return user;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user interacts with the component.
|
||||
*
|
||||
* @param {MouseEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
clickHandler(e) {
|
||||
super.clickHandler(e).catch(Notification.exception);
|
||||
if (e.target.closest(this.selectors.component)) {
|
||||
// Forcibly prevent BS events so that we can control the open and close.
|
||||
// Really needed because by default input elements cant trigger a dropdown.
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
if (e.target.closest(this.selectors.resetPageButton)) {
|
||||
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user presses a key within the component.
|
||||
*
|
||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
keyHandler(e) {
|
||||
super.keyHandler(e);
|
||||
|
||||
if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
|
||||
// Switch the key presses to handle keyboard nav.
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (document.activeElement === this.getHTMLElements().searchInput) {
|
||||
if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
}
|
||||
if (document.activeElement === this.getHTMLElements().clearSearchButton) {
|
||||
this.closeSearch(true);
|
||||
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: true});
|
||||
break;
|
||||
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 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.currentViewAll.focus({preventScroll: true});
|
||||
} else {
|
||||
this.closeSearch();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When called, hide or show the users dropdown.
|
||||
*
|
||||
* @param {Boolean} on Flag to toggle hiding or showing values.
|
||||
*/
|
||||
toggleDropdown(on = false) {
|
||||
if (on) {
|
||||
this.searchDropdown.classList.add('show');
|
||||
$(this.searchDropdown).show();
|
||||
this.component.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
this.searchDropdown.classList.remove('show');
|
||||
$(this.searchDropdown).hide();
|
||||
this.component.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up the view all link.
|
||||
*/
|
||||
selectAllResultsLink() {
|
||||
throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up the view all link that is dedicated to a particular result.
|
||||
*
|
||||
* @param {Number} userID The ID of the user selected.
|
||||
*/
|
||||
selectOneLink(userID) {
|
||||
throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the set of profile fields we can possibly search, fetch their strings,
|
||||
* so we can report to screen readers the field that matched.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
getStringMap() {
|
||||
if (!this.profilestringmap) {
|
||||
const requiredStrings = [
|
||||
'username',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
'city',
|
||||
'country',
|
||||
'department',
|
||||
'institution',
|
||||
'idnumber',
|
||||
'phone1',
|
||||
'phone2',
|
||||
];
|
||||
this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))
|
||||
.then((stringArray) => new Map(
|
||||
requiredStrings.map((key, index) => ([key, stringArray[index]]))
|
||||
));
|
||||
}
|
||||
return this.profilestringmap;
|
||||
}
|
||||
}
|
58
user/templates/comboboxsearch/resultitem.mustache
Normal file
58
user/templates/comboboxsearch/resultitem.mustache
Normal file
@ -0,0 +1,58 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_user/comboboxsearch/resultitem
|
||||
|
||||
Template for the individual result item.
|
||||
|
||||
Context variables required for this template:
|
||||
* id - User system ID.
|
||||
* fullname - Users' full name.
|
||||
* profileimageurl - Link for the users' large profile image.
|
||||
* matchingField - The field in the user object that matched the search criteria.
|
||||
* matchingFieldName - The name of the field that was matched upon for A11y purposes.
|
||||
* link - The link used to redirect upon self to show only this specific user.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": 2,
|
||||
"fullname": "Foo bar",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
}
|
||||
}}
|
||||
{{<core/local/comboboxsearch/resultitem }}
|
||||
{{$arialabel}}{{#str}}viewresults, core, {{fullname}}{{/str}}{{/arialabel}}
|
||||
{{$content}}
|
||||
<span class="d-block px-2 w-25">
|
||||
{{#profileimageurl}}
|
||||
<img class="userpicture w-100 mx-auto d-block" src="{{profileimageurl}}" alt=""/>
|
||||
{{/profileimageurl}}
|
||||
{{^profileimageurl}}
|
||||
<span class="userinitials"></span>
|
||||
{{/profileimageurl}}
|
||||
</span>
|
||||
<span class="d-block pr-3 w-75">
|
||||
<span class="d-block w-100 p-0 text-truncate font-weight-bold">
|
||||
{{fullname}}
|
||||
</span>
|
||||
<span class="d-block w-100 pull-left text-truncate small" aria-hidden="true">
|
||||
{{{matchingField}}}
|
||||
</span>
|
||||
<span class="sr-only" aria-label="{{#str}}usermatchedon, core{{/str}}">{{matchingFieldName}}</span>
|
||||
</span>
|
||||
{{/content}}
|
||||
{{/core/local/comboboxsearch/resultitem}}
|
70
user/templates/comboboxsearch/resultset.mustache
Normal file
70
user/templates/comboboxsearch/resultset.mustache
Normal file
@ -0,0 +1,70 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_user/comboboxsearch/resultset
|
||||
|
||||
Wrapping template for returned result items.
|
||||
|
||||
Context variables required for this template:
|
||||
* users - Our returned users to render.
|
||||
* found - Count of the found users.
|
||||
* total - Total count of users within this report.
|
||||
* selectall - The created link that allows users to select all of the results.
|
||||
* searchterm - The entered text to find these results.
|
||||
* hasusers - Allow the handling where no users exist for the returned search term.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"id": 2,
|
||||
"fullname": "Foo bar",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"fullname": "Bar Foo",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/80/user/icon/boost/f1?rev=7631",
|
||||
"matchingField": "Bar <span class=\"font-weight-bold\">Foo</span>",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
||||
}
|
||||
],
|
||||
"matches": 20,
|
||||
"selectall": "https://foo.bar/grade/report/grader/index.php?id=2&searchvalue=abe",
|
||||
"searchterm": "Foo",
|
||||
"hasresults": true
|
||||
}
|
||||
}}
|
||||
{{<core/local/comboboxsearch/resultset}}
|
||||
{{$listid}}user{{/listid}}
|
||||
{{$results}}
|
||||
{{#users}}
|
||||
{{>core_user/comboboxsearch/resultitem}}
|
||||
{{/users}}
|
||||
{{/results}}
|
||||
{{$selectall}}
|
||||
{{#selectall}}
|
||||
<li class="w-100 result-row p-1 border-top bottom-0 position-sticky" role="none" id="result-row-{{id}}">
|
||||
<a role="option" class="dropdown-item d-flex small p-3" id="select-all" href="{{{selectall}}}" tabindex="-1">
|
||||
{{#str}}viewallresults, core, {{matches}}{{/str}}
|
||||
</a>
|
||||
</li>
|
||||
{{/selectall}}
|
||||
{{/selectall}}
|
||||
{{$listclasses}}mh-100{{/listclasses}}
|
||||
{{/core/local/comboboxsearch/resultset}}
|
67
user/templates/comboboxsearch/user_selector.mustache
Normal file
67
user/templates/comboboxsearch/user_selector.mustache
Normal file
@ -0,0 +1,67 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_user/comboboxsearch/user_selector
|
||||
|
||||
The user selector trigger element.
|
||||
|
||||
Context variables required for this template:
|
||||
* currentvalue - If the user has already searched, set the value to that.
|
||||
* courseid - The course ID.
|
||||
* group - The group ID.
|
||||
* resetlink - The link to reset the page.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"currentvalue": "bar",
|
||||
"courseid": 2,
|
||||
"group": 25,
|
||||
"resetlink": "grade/report/grader/index.php?id=2"
|
||||
}
|
||||
}}
|
||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}" aria-hidden="true"></span>
|
||||
{{#currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/label}}
|
||||
{{$value}}{{{currentvalue}}}{{/value}}
|
||||
{{$additionalattributes}}
|
||||
aria-autocomplete="list"
|
||||
data-input-element="user-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
<a class="ml-2 btn btn-link border-0 align-self-center" href="{{resetlink}}" data-action="resetpage" role="link" aria-label="{{#str}}clearsearch, core{{/str}}">
|
||||
{{#str}}clear{{/str}}
|
||||
</a>
|
||||
{{/currentvalue}}
|
||||
{{^currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
aria-autocomplete="list"
|
||||
data-input-element="user-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
<input type="hidden" name="search" id="user-input-{{uniqid}}"/>
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2023072800.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2023072800.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.3dev (Build: 20230728)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user