diff --git a/course/amd/build/actionbar/initials.min.js b/course/amd/build/actionbar/initials.min.js new file mode 100644 index 00000000000..b712e4d2a31 --- /dev/null +++ b/course/amd/build/actionbar/initials.min.js @@ -0,0 +1,10 @@ +define("core_course/actionbar/initials",["exports","core/pending","core/url","core/custom_interaction_events","jquery"],(function(_exports,_pending,Url,_custom_interaction_events,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * A small dropdown to filter users. + * + * @module core_course/actionbar/initials + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Url=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}(Url),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);let registered=!1;const selectors_pageListItem="page-item",selectors_pageClickableItem=".page-link",selectors_activeItem="active",selectors_formDropdown=".initialsdropdownform",selectors_parentDomNode=".initials-selector",selectors_firstInitial="firstinitial",selectors_lastInitial="lastinitial",selectors_initialBars=".initialbar",selectors_targetButton="initialswidget",selectors_formItems={type:"submit",save:"save",cancel:"cancel"};_exports.init=function(callingLink){let firstInitialParam=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"sifirst",lastInitialParam=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"silast",additionalParams=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(callingLink,firstInitialParam,lastInitialParam,additionalParams),(0,_jquery.default)(selectors_parentDomNode).on("shown.bs.dropdown",(()=>{document.querySelector(selectors_pageClickableItem).focus({preventScroll:!0})})),pendingPromise.resolve(),registered=!0};const registerListenerEvents=function(callingLink){let firstInitialParam=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"sifirst",lastInitialParam=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"silast",additionalParams=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(e=>{let{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}=onClickVariables(),itemToReset="";if(e.target.closest(selectors_formDropdown)&&e.preventDefault(),e.target.closest("".concat(selectors_formDropdown," .").concat(selectors_pageListItem))){if(e.target.classList.contains(selectors_pageListItem))return;e.target.closest(selectors_initialBars).classList.contains(selectors_firstInitial)?(sifirst=e.target,itemToReset=firstActive):(silast=e.target,itemToReset=lastActive),swapActiveItems(itemToReset,e)}if(e.target.closest("".concat(selectors_formDropdown))&&e.target.type===selectors_formItems.type){if(e.target.dataset.action===selectors_formItems.save){const params={id:e.target.closest(selectors_formDropdown).dataset.courseid,[firstInitialParam]:sifirst.parentElement.classList.contains("initialbarall")?"":sifirst.value,[lastInitialParam]:silast.parentElement.classList.contains("initialbarall")?"":silast.value};for(const[key,value]of Object.entries(additionalParams))params[key]=value;window.location=Url.relativeUrl(callingLink,params)}e.target.dataset.action===selectors_formItems.cancel&&(0,_jquery.default)(".".concat(selectors_targetButton)).dropdown("toggle")}}))}))},onClickVariables=()=>{const firstItems=[...document.querySelectorAll(".".concat(selectors_firstInitial," li"))],lastItems=[...document.querySelectorAll(".".concat(selectors_lastInitial," li"))],firstActive=firstItems.filter((item=>item.classList.contains(selectors_activeItem)))[0],lastActive=lastItems.filter((item=>item.classList.contains(selectors_activeItem)))[0];let sifirst=firstActive.querySelector(selectors_pageClickableItem),silast=lastActive.querySelector(selectors_pageClickableItem);return{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}},swapActiveItems=(itemToReset,e)=>{itemToReset.classList.remove(selectors_activeItem),itemToReset.querySelector(selectors_pageClickableItem).ariaCurrent=!1;e.target.parentElement.classList.add(selectors_activeItem),e.target.ariaCurrent=!0}})); + +//# sourceMappingURL=initials.min.js.map \ No newline at end of file diff --git a/course/amd/build/actionbar/initials.min.js.map b/course/amd/build/actionbar/initials.min.js.map new file mode 100644 index 00000000000..9cc36c1bfd9 --- /dev/null +++ b/course/amd/build/actionbar/initials.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"initials.min.js","sources":["../../src/actionbar/initials.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A small dropdown to filter users.\n *\n * @module core_course/actionbar/initials\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Url from 'core/url';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport $ from 'jquery';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n pageListItem: 'page-item',\n pageClickableItem: '.page-link',\n activeItem: 'active',\n formDropdown: '.initialsdropdownform',\n parentDomNode: '.initials-selector',\n firstInitial: 'firstinitial',\n lastInitial: 'lastinitial',\n initialBars: '.initialbar', // Both first and last name use this class.\n targetButton: 'initialswidget',\n formItems: {\n type: 'submit',\n save: 'save',\n cancel: 'cancel'\n }\n};\n\n/**\n * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {String} firstInitialParam The URL parameter to set for the first name initial.\n * @param {String} lastInitialParam The URL parameter to set for the last name initial.\n * @param {Array} additionalParams Any additional parameters to set for the URL.\n */\nexport const init = (callingLink, firstInitialParam = 'sifirst',\n lastInitialParam = 'silast', additionalParams = []) => {\n if (registered) {\n return;\n }\n const pendingPromise = new Pending();\n registerListenerEvents(callingLink, firstInitialParam, lastInitialParam, additionalParams);\n // BS events always bubble so, we need to listen for the event higher up the chain.\n $(selectors.parentDomNode).on('shown.bs.dropdown', () => {\n document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});\n });\n pendingPromise.resolve();\n registered = true;\n};\n\n/**\n * Register event listeners.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {String} firstInitialParam The URL parameter to set for the first name initial.\n * @param {String} lastInitialParam The URL parameter to set for the last name initial.\n * @param {Array} additionalParams Any additional parameters to set for the URL.\n */\nconst registerListenerEvents = (callingLink, firstInitialParam = 'sifirst',\n lastInitialParam = 'silast', additionalParams = []) => {\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n // Register events.\n events.forEach((event) => {\n document.addEventListener(event, (e) => {\n // Always fetch the latest information when we click as state is a fickle thing.\n let {firstActive, lastActive, sifirst, silast} = onClickVariables();\n let itemToReset = '';\n\n // Prevent the usual form behaviour.\n if (e.target.closest(selectors.formDropdown)) {\n e.preventDefault();\n }\n\n // Handle the state of active initials before form submission.\n if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {\n // Ensure the li items don't cause weird clicking emptying out the form.\n if (e.target.classList.contains(selectors.pageListItem)) {\n return;\n }\n\n const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.\n\n // We want to find the current active item in the menu area the user selected.\n // We also want to fetch the raw item out of the array for instant manipulation.\n if (initialsBar.classList.contains(selectors.firstInitial)) {\n sifirst = e.target;\n itemToReset = firstActive;\n } else {\n silast = e.target;\n itemToReset = lastActive;\n }\n swapActiveItems(itemToReset, e);\n }\n\n // Handle form submissions.\n if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) {\n if (e.target.dataset.action === selectors.formItems.save) {\n // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.\n // Then we will redirect the user back onto the page with new filters applied.\n const params = {\n 'id': e.target.closest(selectors.formDropdown).dataset.courseid,\n [firstInitialParam]: sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,\n [lastInitialParam]: silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,\n };\n\n // If additional parameters are passed, add them here (overriding any already set above).\n for (const [key, value] of Object.entries(additionalParams)) {\n params[key] = value;\n }\n window.location = Url.relativeUrl(callingLink, params);\n }\n if (e.target.dataset.action === selectors.formItems.cancel) {\n $(`.${selectors.targetButton}`).dropdown('toggle');\n }\n }\n });\n });\n};\n\n/**\n * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.\n *\n * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}\n */\nconst onClickVariables = () => {\n // Ensure we have an up-to-date initials bar.\n const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];\n const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];\n const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n // Ensure we retain both of the selections from a previous instance.\n let sifirst = firstActive.querySelector(selectors.pageClickableItem);\n let silast = lastActive.querySelector(selectors.pageClickableItem);\n return {firstActive, lastActive, sifirst, silast};\n};\n\n/**\n * Given we are provided the old li and current click event, swap around the active properties.\n *\n * @param {HTMLElement} itemToReset\n * @param {Event} e\n */\nconst swapActiveItems = (itemToReset, e) => {\n itemToReset.classList.remove(selectors.activeItem);\n itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;\n\n // Set the select item as the current item.\n const itemToSetActive = e.target.parentElement;\n itemToSetActive.classList.add(selectors.activeItem);\n e.target.ariaCurrent = true;\n};\n"],"names":["registered","selectors","type","save","cancel","callingLink","firstInitialParam","lastInitialParam","additionalParams","pendingPromise","Pending","registerListenerEvents","on","document","querySelector","focus","preventScroll","resolve","events","CustomEvents","activate","keyboardActivate","define","forEach","event","addEventListener","e","firstActive","lastActive","sifirst","silast","onClickVariables","itemToReset","target","closest","preventDefault","classList","contains","swapActiveItems","dataset","action","params","courseid","parentElement","value","key","Object","entries","window","location","Url","relativeUrl","dropdown","firstItems","querySelectorAll","lastItems","filter","item","remove","ariaCurrent","add"],"mappings":";;;;;;;44BAiCIA,YAAa,QAGXC,uBACY,YADZA,4BAEiB,aAFjBA,qBAGU,SAHVA,uBAIY,wBAJZA,wBAKa,qBALbA,uBAMY,eANZA,sBAOW,cAPXA,sBAQW,cARXA,uBASY,iBATZA,oBAUS,CACPC,KAAM,SACNC,KAAM,OACNC,OAAQ,wBAYI,SAACC,iBAAaC,yEAAoB,UAC9CC,wEAAmB,SAAUC,wEAAmB,MAChDR,wBAGES,eAAiB,IAAIC,iBAC3BC,uBAAuBN,YAAaC,kBAAmBC,iBAAkBC,sCAEvEP,yBAAyBW,GAAG,qBAAqB,KAC/CC,SAASC,cAAcb,6BAA6Bc,MAAM,CAACC,eAAe,OAE9EP,eAAeQ,UACfjB,YAAa,SAWXW,uBAAyB,SAACN,iBAAaC,yEAAoB,UACzDC,wEAAmB,SAAUC,wEAAmB,SAC9CU,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOT,SAAUK,QAG9BA,OAAOK,SAASC,QACZX,SAASY,iBAAiBD,OAAQE,QAE1BC,YAACA,YAADC,WAAcA,WAAdC,QAA0BA,QAA1BC,OAAmCA,QAAUC,mBAC7CC,YAAc,MAGdN,EAAEO,OAAOC,QAAQjC,yBACjByB,EAAES,iBAIFT,EAAEO,OAAOC,kBAAWjC,oCAA2BA,yBAA2B,IAEtEyB,EAAEO,OAAOG,UAAUC,SAASpC,+BAIZyB,EAAEO,OAAOC,QAAQjC,uBAIrBmC,UAAUC,SAASpC,yBAC/B4B,QAAUH,EAAEO,OACZD,YAAcL,cAEdG,OAASJ,EAAEO,OACXD,YAAcJ,YAElBU,gBAAgBN,YAAaN,MAI7BA,EAAEO,OAAOC,kBAAWjC,0BAA6ByB,EAAEO,OAAO/B,OAASD,oBAAoBC,KAAM,IACzFwB,EAAEO,OAAOM,QAAQC,SAAWvC,oBAAoBE,KAAM,OAGhDsC,OAAS,IACLf,EAAEO,OAAOC,QAAQjC,wBAAwBsC,QAAQG,UACtDpC,mBAAoBuB,QAAQc,cAAcP,UAAUC,SAAS,iBAAmB,GAAKR,QAAQe,OAC7FrC,kBAAmBuB,OAAOa,cAAcP,UAAUC,SAAS,iBAAmB,GAAKP,OAAOc,WAI1F,MAAOC,IAAKD,SAAUE,OAAOC,QAAQvC,kBACtCiC,OAAOI,KAAOD,MAElBI,OAAOC,SAAWC,IAAIC,YAAY9C,YAAaoC,QAE/Cf,EAAEO,OAAOM,QAAQC,SAAWvC,oBAAoBG,uCAC1CH,yBAA0BmD,SAAS,kBAYvDrB,iBAAmB,WAEfsB,WAAa,IAAIxC,SAASyC,4BAAqBrD,gCAC/CsD,UAAY,IAAI1C,SAASyC,4BAAqBrD,+BAC9C0B,YAAc0B,WAAWG,QAAQC,MAASA,KAAKrB,UAAUC,SAASpC,wBAAuB,GACzF2B,WAAa2B,UAAUC,QAAQC,MAASA,KAAKrB,UAAUC,SAASpC,wBAAuB,OAEzF4B,QAAUF,YAAYb,cAAcb,6BACpC6B,OAASF,WAAWd,cAAcb,mCAC/B,CAAC0B,YAAAA,YAAaC,WAAAA,WAAYC,QAAAA,QAASC,OAAAA,SASxCQ,gBAAkB,CAACN,YAAaN,KAClCM,YAAYI,UAAUsB,OAAOzD,sBAC7B+B,YAAYlB,cAAcb,6BAA6B0D,aAAc,EAG7CjC,EAAEO,OAAOU,cACjBP,UAAUwB,IAAI3D,sBAC9ByB,EAAEO,OAAO0B,aAAc"} \ No newline at end of file diff --git a/course/amd/src/actionbar/initials.js b/course/amd/src/actionbar/initials.js new file mode 100644 index 00000000000..e48bf533a90 --- /dev/null +++ b/course/amd/src/actionbar/initials.js @@ -0,0 +1,183 @@ +// 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 . + +/** + * A small dropdown to filter users. + * + * @module core_course/actionbar/initials + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Pending from 'core/pending'; +import * as Url from 'core/url'; +import CustomEvents from "core/custom_interaction_events"; +import $ from 'jquery'; + +/** + * Whether the event listener has already been registered for this module. + * + * @type {boolean} + */ +let registered = false; + +// Contain our selectors within this file until they could be of use elsewhere. +const selectors = { + pageListItem: 'page-item', + pageClickableItem: '.page-link', + activeItem: 'active', + formDropdown: '.initialsdropdownform', + parentDomNode: '.initials-selector', + firstInitial: 'firstinitial', + lastInitial: 'lastinitial', + initialBars: '.initialbar', // Both first and last name use this class. + targetButton: 'initialswidget', + formItems: { + type: 'submit', + save: 'save', + cancel: 'cancel' + } +}; + +/** + * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form. + * + * @param {String} callingLink The link to redirect upon form submission. + * @param {String} firstInitialParam The URL parameter to set for the first name initial. + * @param {String} lastInitialParam The URL parameter to set for the last name initial. + * @param {Array} additionalParams Any additional parameters to set for the URL. + */ +export const init = (callingLink, firstInitialParam = 'sifirst', + lastInitialParam = 'silast', additionalParams = []) => { + if (registered) { + return; + } + const pendingPromise = new Pending(); + registerListenerEvents(callingLink, firstInitialParam, lastInitialParam, additionalParams); + // BS events always bubble so, we need to listen for the event higher up the chain. + $(selectors.parentDomNode).on('shown.bs.dropdown', () => { + document.querySelector(selectors.pageClickableItem).focus({preventScroll: true}); + }); + pendingPromise.resolve(); + registered = true; +}; + +/** + * Register event listeners. + * + * @param {String} callingLink The link to redirect upon form submission. + * @param {String} firstInitialParam The URL parameter to set for the first name initial. + * @param {String} lastInitialParam The URL parameter to set for the last name initial. + * @param {Array} additionalParams Any additional parameters to set for the URL. + */ +const registerListenerEvents = (callingLink, firstInitialParam = 'sifirst', + lastInitialParam = 'silast', additionalParams = []) => { + const events = [ + 'click', + CustomEvents.events.activate, + CustomEvents.events.keyboardActivate + ]; + CustomEvents.define(document, events); + + // Register events. + events.forEach((event) => { + document.addEventListener(event, (e) => { + // Always fetch the latest information when we click as state is a fickle thing. + let {firstActive, lastActive, sifirst, silast} = onClickVariables(); + let itemToReset = ''; + + // Prevent the usual form behaviour. + if (e.target.closest(selectors.formDropdown)) { + e.preventDefault(); + } + + // Handle the state of active initials before form submission. + if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) { + // Ensure the li items don't cause weird clicking emptying out the form. + if (e.target.classList.contains(selectors.pageListItem)) { + return; + } + + const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in. + + // We want to find the current active item in the menu area the user selected. + // We also want to fetch the raw item out of the array for instant manipulation. + if (initialsBar.classList.contains(selectors.firstInitial)) { + sifirst = e.target; + itemToReset = firstActive; + } else { + silast = e.target; + itemToReset = lastActive; + } + swapActiveItems(itemToReset, e); + } + + // Handle form submissions. + if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) { + if (e.target.dataset.action === selectors.formItems.save) { + // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar. + // Then we will redirect the user back onto the page with new filters applied. + const params = { + 'id': e.target.closest(selectors.formDropdown).dataset.courseid, + [firstInitialParam]: sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value, + [lastInitialParam]: silast.parentElement.classList.contains('initialbarall') ? '' : silast.value, + }; + + // If additional parameters are passed, add them here (overriding any already set above). + for (const [key, value] of Object.entries(additionalParams)) { + params[key] = value; + } + window.location = Url.relativeUrl(callingLink, params); + } + if (e.target.dataset.action === selectors.formItems.cancel) { + $(`.${selectors.targetButton}`).dropdown('toggle'); + } + } + }); + }); +}; + +/** + * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes. + * + * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}} + */ +const onClickVariables = () => { + // Ensure we have an up-to-date initials bar. + const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)]; + const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)]; + const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0]; + const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0]; + // Ensure we retain both of the selections from a previous instance. + let sifirst = firstActive.querySelector(selectors.pageClickableItem); + let silast = lastActive.querySelector(selectors.pageClickableItem); + return {firstActive, lastActive, sifirst, silast}; +}; + +/** + * Given we are provided the old li and current click event, swap around the active properties. + * + * @param {HTMLElement} itemToReset + * @param {Event} e + */ +const swapActiveItems = (itemToReset, e) => { + itemToReset.classList.remove(selectors.activeItem); + itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false; + + // Set the select item as the current item. + const itemToSetActive = e.target.parentElement; + itemToSetActive.classList.add(selectors.activeItem); + e.target.ariaCurrent = true; +}; diff --git a/course/classes/output/actionbar/group_selector.php b/course/classes/output/actionbar/group_selector.php index 26793a9d6da..df0b35697ce 100644 --- a/course/classes/output/actionbar/group_selector.php +++ b/course/classes/output/actionbar/group_selector.php @@ -28,28 +28,26 @@ use stdClass; */ class group_selector extends comboboxsearch { - /** - * @var stdClass The context object. - */ - private stdClass $context; + /** @var int|bool the active group, false if groups not used. */ + private int|bool $activegroup; /** * The class constructor. * - * @param null|stdClass $course This parameter has been deprecated since Moodle 4.5 and should not be used anymore. * @param stdClass $context The context object. */ - public function __construct(?stdClass $course, stdClass $context) { - if ($course !== null) { - debugging( - 'The course argument has been deprecated. Please remove it from your group_selector class instances.', - DEBUG_DEVELOPER, - ); - } - $this->context = $context; + public function __construct(private stdClass $context) { + $this->activegroup = $this->get_active_group(); + $this->label = $this->get_label(); + + // The second and third arguments (buttoncontent and dropdowncontent) need to be rendered here, since the comboboxsearch + // template expects HTML in its respective context properties. Ideally, children of comboboxsearch would leverage Mustache's + // blocks pragma, meaning a child template could extend the comboboxsearch, allowing rendering of the child component, + // instead of needing to inject the child's content HTML as part of rendering the comboboxsearch parent, as is the case + // here. Achieving this, however, requires a refactor of comboboxsearch. For now, this must be pre-rendered and injected. parent::__construct(false, $this->get_button_content(), $this->get_dropdown_content(), 'group-search', - 'groupsearchwidget', 'groupsearchdropdown overflow-auto', null, true, $this->get_label(), 'group', - $this->get_active_group()); + 'groupsearchwidget', 'groupsearchdropdown overflow-auto', null, true, $this->label, 'group', + $this->activegroup); } /** @@ -58,23 +56,10 @@ class group_selector extends comboboxsearch { * @return string HTML fragment */ private function get_button_content(): string { - global $OUTPUT; + global $PAGE; + $groupsselectorbutton = new group_selector_button($this->context, $this->activegroup, $this->label); - $activegroup = $this->get_active_group(); - $buttondata = [ - 'label' => $this->get_label(), - 'group' => $activegroup, - ]; - - if ($activegroup) { - $group = groups_get_group($activegroup); - $buttondata['selectedgroup'] = format_string($group->name, true, - ['context' => $this->context->get_course_context()]); - } else if ($activegroup === 0) { - $buttondata['selectedgroup'] = get_string('allparticipants'); - } - - return $OUTPUT->render_from_template('core_group/comboboxsearch/group_selector', $buttondata); + return $PAGE->get_renderer('core', 'course')->render($groupsselectorbutton); } /** @@ -83,13 +68,10 @@ class group_selector extends comboboxsearch { * @return string HTML fragment */ private function get_dropdown_content(): string { - global $OUTPUT; + global $PAGE; + $groupsdropdownform = new group_selector_dropdown_form($this->context); - return $OUTPUT->render_from_template('core_group/comboboxsearch/searchbody', [ - 'courseid' => $this->context->get_course_context()->instanceid, - 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), - 'instance' => rand(), - ]); + return $PAGE->get_renderer('core', 'course')->render($groupsdropdownform); } /** diff --git a/course/classes/output/actionbar/group_selector_button.php b/course/classes/output/actionbar/group_selector_button.php new file mode 100644 index 00000000000..8da3f99eee9 --- /dev/null +++ b/course/classes/output/actionbar/group_selector_button.php @@ -0,0 +1,69 @@ +. + +namespace core_course\output\actionbar; + +use context; +use core\output\named_templatable; +use core\output\renderable; +use core\output\renderer_base; + +/** + * Renderable class for the group selection button state. + * + * This form is the button state for the group_selector renderable, which itself is an extension of the comboboxsearch component. + * {@see group_selector}. + * + * @package core_course + * @copyright 2024 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_selector_button implements renderable, named_templatable { + + /** + * The class constructor. + * + * @param context $context The context instance. + * @param int|bool $activegroup The active group, or false if groups not used. + * @param string $label the label string. + */ + public function __construct( + protected context $context, + protected int|bool $activegroup, + protected string $label + ) { + } + + public function export_for_template(renderer_base $output) { + $context = [ + 'label' => $this->label, + 'group' => $this->activegroup, + ]; + + if ($this->activegroup) { + $group = groups_get_group($this->activegroup); + $context['selectedgroup'] = format_string($group->name, true, ['context' => $this->context->get_course_context()]); + } else if ($this->activegroup === 0) { + $context['selectedgroup'] = get_string('allparticipants'); + } + + return $context; + } + + public function get_template_name(renderer_base $renderer): string { + return 'core_group/comboboxsearch/group_selector'; + } +} diff --git a/course/classes/output/actionbar/group_selector_dropdown_form.php b/course/classes/output/actionbar/group_selector_dropdown_form.php new file mode 100644 index 00000000000..8a98f6a6608 --- /dev/null +++ b/course/classes/output/actionbar/group_selector_dropdown_form.php @@ -0,0 +1,56 @@ +. + +namespace core_course\output\actionbar; + +use core\output\named_templatable; +use core\output\renderable; +use core\output\renderer_base; + +/** + * Renderable class for the group selection dropdown form. + * + * This form is the content for the group_selector renderable, which itself is an extension of the comboboxsearch component. + * {@see group_selector}. + * + * @package core_course + * @copyright 2024 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_selector_dropdown_form implements renderable, named_templatable { + + /** + * The class constructor. + * + * @param \context $context The context instance. + */ + public function __construct( + protected \context $context + ) { + } + + public function export_for_template(renderer_base $output) { + return [ + 'courseid' => $this->context->get_course_context()->instanceid, + 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), + 'instance' => rand(), + ]; + } + + public function get_template_name(renderer_base $renderer): string { + return 'core_group/comboboxsearch/searchbody'; + } +} diff --git a/course/classes/output/actionbar/initials_dropdown_form.php b/course/classes/output/actionbar/initials_dropdown_form.php new file mode 100644 index 00000000000..967e7f2fe85 --- /dev/null +++ b/course/classes/output/actionbar/initials_dropdown_form.php @@ -0,0 +1,70 @@ +. + +namespace core_course\output\actionbar; + +use core\output\renderable; +use core\output\renderer_base; +use stdClass; +use templatable; + +/** + * Renderable class for the initial_dropdown_form. + * + * This form is the content for the initials_selector renderable, which itself is an extension of the comboboxsearch component. + * {@see initials_selector}. + * + * @package core_course + * @copyright 2024 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class initials_dropdown_form implements renderable, templatable { + + /** + * The class constructor. + * + * @param stdClass $course The course object. + * @param string $targeturl The target URL to send the form to. + * @param string $firstinitial The selected first initial. + * @param string $lastinitial The selected last initial. + * @param string $firstinitialparam The parameter name for the first initial. + * @param string $lastinitialparam The parameter name for the last initial. + * @param array $additionalparams Any additional parameters required for the form submission URL. + */ + public function __construct( + protected stdClass $course, + protected string $targeturl, + protected string $firstinitial = '', + protected string $lastinitial = '', + protected string $firstinitialparam = 'sifirst', + protected string $lastinitialparam = 'silast', + protected array $additionalparams = [] + ) { + } + + public function export_for_template(renderer_base $output) { + global $PAGE; + + $PAGE->requires->js_call_amd('core_course/actionbar/initials', 'init', + [$this->targeturl, $this->firstinitialparam, $this->lastinitialparam, $this->additionalparams]); + $renderer = $PAGE->get_renderer('core_user'); + + return (object) [ + 'courseid' => $this->course->id, + 'initialsbars' => $renderer->partial_user_search($this->targeturl, $this->firstinitial, $this->lastinitial, true), + ]; + } +} diff --git a/course/classes/output/actionbar/initials_selector.php b/course/classes/output/actionbar/initials_selector.php new file mode 100644 index 00000000000..f77a3dfb488 --- /dev/null +++ b/course/classes/output/actionbar/initials_selector.php @@ -0,0 +1,111 @@ +. + +namespace core_course\output\actionbar; + +use core\output\comboboxsearch; +use stdClass; + +/** + * Renderable class for the initial selector element in the action bar. + * + * @package core_course + * @copyright 2024 Kevin Percy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class initials_selector extends comboboxsearch { + + /** + * The class constructor. + * + * @param stdClass $course The course object. + * @param string $targeturl The target URL to send the form to. + * @param string $firstinitial The selected first initial. + * @param string $lastinitial The selected last initial. + * @param string $firstinitialparam The parameter name for the first initial. + * @param string $lastinitialparam The parameter name for the last initial. + * @param array $additionalparams Any additional parameters required for the form submission URL. + */ + public function __construct( + protected stdClass $course, + protected string $targeturl, + protected string $firstinitial = '', + protected string $lastinitial = '', + protected string $firstinitialparam = 'sifirst', + protected string $lastinitialparam = 'silast', + protected array $additionalparams = [] + ) { + // The second and third arguments (buttoncontent and dropdowncontent) need to be rendered here, since the comboboxsearch + // template expects HTML in its respective context properties. Ideally, children of comboboxsearch would leverage Mustache's + // blocks pragma, meaning a child template could extend the comboboxsearch, allowing rendering of the child component, + // instead of needing to inject the child's content HTML as part of rendering the comboboxsearch parent, as is the case + // here. Achieving this, however, requires a refactor of comboboxsearch. For now, this must be pre-rendered and injected. + $filterstatestring = $this->get_current_filter_state_string(); + parent::__construct( + false, + $filterstatestring !== '' ? $filterstatestring : get_string('filterbyname', 'course'), + $this->render_initials_dropdown_form(), + 'initials-selector', + 'initialswidget', + 'initialsdropdown', + $filterstatestring !== '' ? get_string('name') : null, + true, + get_string('filterbyname', 'course'), + 'nameinitials', + json_encode([ + 'first' => $firstinitial, + 'last' => $lastinitial, + ]) + ); + } + + /** + * Method to generate the current filter string for the initial selector label. + * + * @return string the HTML string representing the current initials filter state. E.g. "First (A)", or empty if none selected. + */ + private function get_current_filter_state_string(): string { + if ($this->firstinitial !== '' && $this->lastinitial !== '') { + return get_string('filterbothactive', 'course', ['first' => $this->firstinitial, 'last' => $this->lastinitial]); + } else if ($this->firstinitial !== '') { + return get_string('filterfirstactive', 'course', ['first' => $this->firstinitial]); + } else if ($this->lastinitial !== '') { + return get_string('filterlastactive', 'course', ['last' => $this->lastinitial]); + } + + return ''; + } + + /** + * Method to generate the output for the initial selector. + * + * @return string the rendered HTML content. + */ + private function render_initials_dropdown_form(): string { + global $PAGE; + + $initialsdropdownform = new initials_dropdown_form( + $this->course, + $this->targeturl, + $this->firstinitial, + $this->lastinitial, + $this->firstinitialparam, + $this->lastinitialparam, + $this->additionalparams + ); + return $PAGE->get_renderer('core', 'course')->render($initialsdropdownform); + } +} diff --git a/course/classes/output/actionbar/renderer.php b/course/classes/output/actionbar/renderer.php deleted file mode 100644 index adc5b7e89a2..00000000000 --- a/course/classes/output/actionbar/renderer.php +++ /dev/null @@ -1,49 +0,0 @@ -. - -namespace core_course\output\actionbar; - -/** - * Renderer class for the action bar. - * - * @package core_course - * @copyright 2024 Shamim Rezaie - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class renderer extends \plugin_renderer_base { - - /** - * Renders the user selector trigger element in the action bar. - * - * @param user_selector $userselector The user selector object. - * @return string The HTML output. - */ - public function render_user_selector(user_selector $userselector): string { - $data = $userselector->export_for_template($this); - return parent::render_from_template($userselector->get_template(), $data); - } - - /** - * Renders the group selector trigger element in the action bar. - * - * @param group_selector $groupselector The group selector object. - * @return string The HTML output. - */ - protected function render_group_selector(group_selector $groupselector) { - $data = $groupselector->export_for_template($this); - return parent::render_from_template($groupselector->get_template(), $data); - } -} diff --git a/course/classes/output/actionbar/user_selector.php b/course/classes/output/actionbar/user_selector.php index 6afba7196c2..9eed7dd7da8 100644 --- a/course/classes/output/actionbar/user_selector.php +++ b/course/classes/output/actionbar/user_selector.php @@ -40,57 +40,38 @@ class user_selector extends comboboxsearch { * @param int|null $instanceid The instance ID. */ public function __construct( - stdClass $course, - moodle_url $resetlink, - ?int $userid = null, - ?int $groupid = null, - string $usersearch = '', - ?int $instanceid = null + private stdClass $course, + private moodle_url $resetlink, + private ?int $userid = null, + private ?int $groupid = null, + private string $usersearch = '', + private ?int $instanceid = null ) { - - $userselectorontent = $this->user_selector_output($course, $resetlink, $userid, $groupid, $usersearch, $instanceid); - parent::__construct(true, $userselectorontent, null, 'user-search d-flex', + // The second argument (buttoncontent) needs to be rendered here, since the comboboxsearch + // template expects HTML in its respective context properties. Ideally, children of comboboxsearch would leverage Mustache's + // blocks pragma, meaning a child template could extend the comboboxsearch, allowing rendering of the child component, + // instead of needing to inject the child's content HTML as part of rendering the comboboxsearch parent, as is the case + // here. Achieving this, however, requires a refactor of comboboxsearch. For now, this must be pre-rendered and injected. + parent::__construct(true, $this->user_selector_output(), null, 'user-search d-flex', null, 'usersearchdropdown overflow-auto', null, false); } /** * Method that generates the output for the user selector. * - * @param stdClass $course The course object. - * @param moodle_url|null $resetlink The reset link. - * @param int|null $userid The user ID. - * @param int|null $groupid The group ID. - * @param string $usersearch The user search query. - * @param int|null $instanceid The instance ID. * @return string The HTML output. */ - private function user_selector_output( - stdClass $course, - ?moodle_url $resetlink = null, - ?int $userid = null, - ?int $groupid = null, - string $usersearch = '', - ?int $instanceid = null - ): string { - global $OUTPUT; + private function user_selector_output(): string { + global $PAGE; - // If the user ID is set, it indicates that a user has been selected. In this case, override the user search - // string with the full name of the selected user. - if ($userid) { - $usersearch = fullname(\core_user::get_user($userid)); - } - - return $OUTPUT->render_from_template('core_user/comboboxsearch/user_selector', [ - 'currentvalue' => $usersearch, - 'courseid' => $course->id, - 'instance' => $instanceid ?? rand(), - 'resetlink' => $resetlink->out(false), - 'group' => $groupid ?? 0, - 'name' => 'usersearch', - 'value' => json_encode([ - 'userid' => $userid, - 'search' => $usersearch, - ]), - ]); + $userselectordropdown = new user_selector_button( + $this->course, + $this->resetlink, + $this->userid, + $this->groupid, + $this->usersearch, + $this->instanceid + ); + return $PAGE->get_renderer('core', 'course')->render($userselectordropdown); } } diff --git a/course/classes/output/actionbar/user_selector_button.php b/course/classes/output/actionbar/user_selector_button.php new file mode 100644 index 00000000000..1b522475eb0 --- /dev/null +++ b/course/classes/output/actionbar/user_selector_button.php @@ -0,0 +1,73 @@ +. + +namespace core_course\output\actionbar; + +use core\output\named_templatable; +use core\output\renderable; +use core\output\renderer_base; +use core\url; +use stdClass; + +/** + * Renderable class for the user_selector_button. + * + * This is the button content for the user_selector renderable, which itself is an extension of the comboboxsearch component. + * {@see initials_selector}. + * + * @package core_course + * @copyright 2024 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_selector_button implements renderable, named_templatable { + + /** + * The class constructor. + */ + public function __construct( + private stdClass $course, + private url $resetlink, + private ?int $userid = null, + private ?int $groupid = null, + private string $usersearch = '', + private ?int $instanceid = null + ) { + // If the user ID is set, it indicates that a user has been selected. In this case, override the user search + // string with the full name of the selected user. + if ($this->userid) { + $this->usersearch = fullname(\core_user::get_user($this->userid)); + } + } + + public function export_for_template(renderer_base $output) { + return [ + 'currentvalue' => $this->usersearch, + 'courseid' => $this->course->id, + 'instance' => $this->instanceid ?? rand(), + 'resetlink' => $this->resetlink->out(false), + 'group' => $this->groupid ?? 0, + 'name' => 'usersearch', + 'value' => json_encode([ + 'userid' => $this->userid, + 'search' => $this->usersearch, + ]), + ]; + } + + public function get_template_name(renderer_base $renderer): string { + return 'core_user/comboboxsearch/user_selector'; + } +} diff --git a/course/templates/initials_dropdown_form.mustache b/course/templates/initials_dropdown_form.mustache new file mode 100644 index 00000000000..d8ae89c95ff --- /dev/null +++ b/course/templates/initials_dropdown_form.mustache @@ -0,0 +1,38 @@ +{{! + 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 . +}} +{{! + @template core_course/initials_dropdown_form + + The form component of our dropdown within the tertiary navigation. + + Context variables required for this template: + * courseid - The course ID. + * initialsbars - The rendered mini renderer from the user renderer. + + Example context (json): + { + "courseid": "2", + "initialsbars": "Output from renderer method" + } +}} +
+ {{{initialsbars}}} +
+ + +
+
diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index 3de5741c50e..26452572235 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -59,6 +59,10 @@ class behat_course extends behat_base { "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' tab-pane ')]" ] ), + new behat_component_named_selector( + 'initials bar', + [".//*[contains(concat(' ', @class, ' '), ' initialbar ')]//span[contains(., %locator%)]/parent::div"] + ), ]; } diff --git a/grade/amd/build/searchwidget/initials.min.js b/grade/amd/build/searchwidget/initials.min.js index fe40a9e7f85..4fbde753536 100644 --- a/grade/amd/build/searchwidget/initials.min.js +++ b/grade/amd/build/searchwidget/initials.min.js @@ -5,6 +5,8 @@ define("core_grades/searchwidget/initials",["exports","core/pending","core/url", * @module core_grades/searchwidget/initials * @copyright 2022 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated since Moodle 4.5 - please use core_course/actionbar/initials instead. + * @todo Final deprecation in Moodle 6.0. See MDL-82421. */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Url=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}(Url),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);let registered=!1;const selectors_pageListItem="page-item",selectors_pageClickableItem=".page-link",selectors_activeItem="active",selectors_formDropdown=".initialsdropdownform",selectors_parentDomNode=".initials-selector",selectors_firstInitial="firstinitial",selectors_lastInitial="lastinitial",selectors_initialBars=".initialbar",selectors_targetButton="initialswidget",selectors_formItems={type:"submit",save:"save",cancel:"cancel"};_exports.init=function(callingLink){let gpr_userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,gpr_search=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(callingLink,gpr_userid,gpr_search),(0,_jquery.default)(selectors_parentDomNode).on("shown.bs.dropdown",(()=>{document.querySelector(selectors_pageClickableItem).focus({preventScroll:!0})})),pendingPromise.resolve(),registered=!0};const registerListenerEvents=function(callingLink){let gpr_userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,gpr_search=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(e=>{let{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}=onClickVariables(),itemToReset="";if(e.target.closest(selectors_formDropdown)&&e.preventDefault(),e.target.closest("".concat(selectors_formDropdown," .").concat(selectors_pageListItem))){if(e.target.classList.contains(selectors_pageListItem))return;e.target.closest(selectors_initialBars).classList.contains(selectors_firstInitial)?(sifirst=e.target,itemToReset=firstActive):(silast=e.target,itemToReset=lastActive),swapActiveItems(itemToReset,e)}if(e.target.closest("".concat(selectors_formDropdown))&&e.target.type===selectors_formItems.type){if(e.target.dataset.action===selectors_formItems.save){const params={id:e.target.closest(selectors_formDropdown).dataset.courseid,gpr_search:null!==gpr_search?gpr_search:"",sifirst:sifirst.parentElement.classList.contains("initialbarall")?"":sifirst.value,silast:silast.parentElement.classList.contains("initialbarall")?"":silast.value};null!==gpr_userid&&(params.gpr_userid=gpr_userid),window.location=Url.relativeUrl(callingLink,params)}e.target.dataset.action===selectors_formItems.cancel&&(0,_jquery.default)(".".concat(selectors_targetButton)).dropdown("toggle")}}))}))},onClickVariables=()=>{const firstItems=[...document.querySelectorAll(".".concat(selectors_firstInitial," li"))],lastItems=[...document.querySelectorAll(".".concat(selectors_lastInitial," li"))],firstActive=firstItems.filter((item=>item.classList.contains(selectors_activeItem)))[0],lastActive=lastItems.filter((item=>item.classList.contains(selectors_activeItem)))[0];let sifirst=firstActive.querySelector(selectors_pageClickableItem),silast=lastActive.querySelector(selectors_pageClickableItem);return{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}},swapActiveItems=(itemToReset,e)=>{itemToReset.classList.remove(selectors_activeItem),itemToReset.querySelector(selectors_pageClickableItem).ariaCurrent=!1;e.target.parentElement.classList.add(selectors_activeItem),e.target.ariaCurrent=!0}})); //# sourceMappingURL=initials.min.js.map \ No newline at end of file diff --git a/grade/amd/build/searchwidget/initials.min.js.map b/grade/amd/build/searchwidget/initials.min.js.map index 364043b7e07..b2a47a7aa3b 100644 --- a/grade/amd/build/searchwidget/initials.min.js.map +++ b/grade/amd/build/searchwidget/initials.min.js.map @@ -1 +1 @@ -{"version":3,"file":"initials.min.js","sources":["../../src/searchwidget/initials.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A small dropdown to filter users within the gradebook.\n *\n * @module core_grades/searchwidget/initials\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Url from 'core/url';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport $ from 'jquery';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n pageListItem: 'page-item',\n pageClickableItem: '.page-link',\n activeItem: 'active',\n formDropdown: '.initialsdropdownform',\n parentDomNode: '.initials-selector',\n firstInitial: 'firstinitial',\n lastInitial: 'lastinitial',\n initialBars: '.initialbar', // Both first and last name use this class.\n targetButton: 'initialswidget',\n formItems: {\n type: 'submit',\n save: 'save',\n cancel: 'cancel'\n }\n};\n\n/**\n * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {Null|Number} gpr_userid The user id to filter by.\n * @param {Null|String} gpr_search The search value to filter by.\n */\nexport const init = (callingLink, gpr_userid = null, gpr_search = null) => {\n if (registered) {\n return;\n }\n const pendingPromise = new Pending();\n registerListenerEvents(callingLink, gpr_userid, gpr_search);\n // BS events always bubble so, we need to listen for the event higher up the chain.\n $(selectors.parentDomNode).on('shown.bs.dropdown', () => {\n document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});\n });\n pendingPromise.resolve();\n registered = true;\n};\n\n/**\n * Register event listeners.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {Null|Number} gpr_userid The user id to filter by.\n * @param {Null|String} gpr_search The search value to filter by.\n */\nconst registerListenerEvents = (callingLink, gpr_userid = null, gpr_search = null) => {\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n // Register events.\n events.forEach((event) => {\n document.addEventListener(event, (e) => {\n // Always fetch the latest information when we click as state is a fickle thing.\n let {firstActive, lastActive, sifirst, silast} = onClickVariables();\n let itemToReset = '';\n\n // Prevent the usual form behaviour.\n if (e.target.closest(selectors.formDropdown)) {\n e.preventDefault();\n }\n\n // Handle the state of active initials before form submission.\n if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {\n // Ensure the li items don't cause weird clicking emptying out the form.\n if (e.target.classList.contains(selectors.pageListItem)) {\n return;\n }\n\n const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.\n\n // We want to find the current active item in the menu area the user selected.\n // We also want to fetch the raw item out of the array for instant manipulation.\n if (initialsBar.classList.contains(selectors.firstInitial)) {\n sifirst = e.target;\n itemToReset = firstActive;\n } else {\n silast = e.target;\n itemToReset = lastActive;\n }\n swapActiveItems(itemToReset, e);\n }\n\n // Handle form submissions.\n if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) {\n if (e.target.dataset.action === selectors.formItems.save) {\n // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.\n // Then we will redirect the user back onto the page with new filters applied.\n const params = {\n 'id': e.target.closest(selectors.formDropdown).dataset.courseid,\n 'gpr_search': gpr_search !== null ? gpr_search : '',\n 'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,\n 'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,\n };\n if (gpr_userid !== null) {\n params.gpr_userid = gpr_userid;\n }\n window.location = Url.relativeUrl(callingLink, params);\n }\n if (e.target.dataset.action === selectors.formItems.cancel) {\n $(`.${selectors.targetButton}`).dropdown('toggle');\n }\n }\n });\n });\n};\n\n/**\n * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.\n *\n * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}\n */\nconst onClickVariables = () => {\n // Ensure we have an up-to-date initials bar.\n const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];\n const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];\n const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n // Ensure we retain both of the selections from a previous instance.\n let sifirst = firstActive.querySelector(selectors.pageClickableItem);\n let silast = lastActive.querySelector(selectors.pageClickableItem);\n return {firstActive, lastActive, sifirst, silast};\n};\n\n/**\n * Given we are provided the old li and current click event, swap around the active properties.\n *\n * @param {HTMLElement} itemToReset\n * @param {Event} e\n */\nconst swapActiveItems = (itemToReset, e) => {\n itemToReset.classList.remove(selectors.activeItem);\n itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;\n\n // Set the select item as the current item.\n const itemToSetActive = e.target.parentElement;\n itemToSetActive.classList.add(selectors.activeItem);\n e.target.ariaCurrent = true;\n};\n"],"names":["registered","selectors","type","save","cancel","callingLink","gpr_userid","gpr_search","pendingPromise","Pending","registerListenerEvents","on","document","querySelector","focus","preventScroll","resolve","events","CustomEvents","activate","keyboardActivate","define","forEach","event","addEventListener","e","firstActive","lastActive","sifirst","silast","onClickVariables","itemToReset","target","closest","preventDefault","classList","contains","swapActiveItems","dataset","action","params","courseid","parentElement","value","window","location","Url","relativeUrl","dropdown","firstItems","querySelectorAll","lastItems","filter","item","remove","ariaCurrent","add"],"mappings":";;;;;;;44BAiCIA,YAAa,QAGXC,uBACY,YADZA,4BAEiB,aAFjBA,qBAGU,SAHVA,uBAIY,wBAJZA,wBAKa,qBALbA,uBAMY,eANZA,sBAOW,cAPXA,sBAQW,cARXA,uBASY,iBATZA,oBAUS,CACPC,KAAM,SACNC,KAAM,OACNC,OAAQ,wBAWI,SAACC,iBAAaC,kEAAa,KAAMC,kEAAa,QAC1DP,wBAGEQ,eAAiB,IAAIC,iBAC3BC,uBAAuBL,YAAaC,WAAYC,gCAE9CN,yBAAyBU,GAAG,qBAAqB,KAC/CC,SAASC,cAAcZ,6BAA6Ba,MAAM,CAACC,eAAe,OAE9EP,eAAeQ,UACfhB,YAAa,SAUXU,uBAAyB,SAACL,iBAAaC,kEAAa,KAAMC,kEAAa,WACnEU,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOT,SAAUK,QAG9BA,OAAOK,SAASC,QACZX,SAASY,iBAAiBD,OAAQE,QAE1BC,YAACA,YAADC,WAAcA,WAAdC,QAA0BA,QAA1BC,OAAmCA,QAAUC,mBAC7CC,YAAc,MAGdN,EAAEO,OAAOC,QAAQhC,yBACjBwB,EAAES,iBAIFT,EAAEO,OAAOC,kBAAWhC,oCAA2BA,yBAA2B,IAEtEwB,EAAEO,OAAOG,UAAUC,SAASnC,+BAIZwB,EAAEO,OAAOC,QAAQhC,uBAIrBkC,UAAUC,SAASnC,yBAC/B2B,QAAUH,EAAEO,OACZD,YAAcL,cAEdG,OAASJ,EAAEO,OACXD,YAAcJ,YAElBU,gBAAgBN,YAAaN,MAI7BA,EAAEO,OAAOC,kBAAWhC,0BAA6BwB,EAAEO,OAAO9B,OAASD,oBAAoBC,KAAM,IACzFuB,EAAEO,OAAOM,QAAQC,SAAWtC,oBAAoBE,KAAM,OAGhDqC,OAAS,IACLf,EAAEO,OAAOC,QAAQhC,wBAAwBqC,QAAQG,oBAC1B,OAAflC,WAAsBA,WAAa,WACtCqB,QAAQc,cAAcP,UAAUC,SAAS,iBAAmB,GAAKR,QAAQe,aAC1Ed,OAAOa,cAAcP,UAAUC,SAAS,iBAAmB,GAAKP,OAAOc,OAElE,OAAfrC,aACAkC,OAAOlC,WAAaA,YAExBsC,OAAOC,SAAWC,IAAIC,YAAY1C,YAAamC,QAE/Cf,EAAEO,OAAOM,QAAQC,SAAWtC,oBAAoBG,uCAC1CH,yBAA0B+C,SAAS,kBAYvDlB,iBAAmB,WAEfmB,WAAa,IAAIrC,SAASsC,4BAAqBjD,gCAC/CkD,UAAY,IAAIvC,SAASsC,4BAAqBjD,+BAC9CyB,YAAcuB,WAAWG,QAAQC,MAASA,KAAKlB,UAAUC,SAASnC,wBAAuB,GACzF0B,WAAawB,UAAUC,QAAQC,MAASA,KAAKlB,UAAUC,SAASnC,wBAAuB,OAEzF2B,QAAUF,YAAYb,cAAcZ,6BACpC4B,OAASF,WAAWd,cAAcZ,mCAC/B,CAACyB,YAAAA,YAAaC,WAAAA,WAAYC,QAAAA,QAASC,OAAAA,SASxCQ,gBAAkB,CAACN,YAAaN,KAClCM,YAAYI,UAAUmB,OAAOrD,sBAC7B8B,YAAYlB,cAAcZ,6BAA6BsD,aAAc,EAG7C9B,EAAEO,OAAOU,cACjBP,UAAUqB,IAAIvD,sBAC9BwB,EAAEO,OAAOuB,aAAc"} \ No newline at end of file +{"version":3,"file":"initials.min.js","sources":["../../src/searchwidget/initials.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A small dropdown to filter users within the gradebook.\n *\n * @module core_grades/searchwidget/initials\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @deprecated since Moodle 4.5 - please use core_course/actionbar/initials instead.\n * @todo Final deprecation in Moodle 6.0. See MDL-82421.\n */\n\nimport Pending from 'core/pending';\nimport * as Url from 'core/url';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport $ from 'jquery';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n pageListItem: 'page-item',\n pageClickableItem: '.page-link',\n activeItem: 'active',\n formDropdown: '.initialsdropdownform',\n parentDomNode: '.initials-selector',\n firstInitial: 'firstinitial',\n lastInitial: 'lastinitial',\n initialBars: '.initialbar', // Both first and last name use this class.\n targetButton: 'initialswidget',\n formItems: {\n type: 'submit',\n save: 'save',\n cancel: 'cancel'\n }\n};\n\n/**\n * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {Null|Number} gpr_userid The user id to filter by.\n * @param {Null|String} gpr_search The search value to filter by.\n */\nexport const init = (callingLink, gpr_userid = null, gpr_search = null) => {\n if (registered) {\n return;\n }\n const pendingPromise = new Pending();\n registerListenerEvents(callingLink, gpr_userid, gpr_search);\n // BS events always bubble so, we need to listen for the event higher up the chain.\n $(selectors.parentDomNode).on('shown.bs.dropdown', () => {\n document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});\n });\n pendingPromise.resolve();\n registered = true;\n};\n\n/**\n * Register event listeners.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n * @param {Null|Number} gpr_userid The user id to filter by.\n * @param {Null|String} gpr_search The search value to filter by.\n */\nconst registerListenerEvents = (callingLink, gpr_userid = null, gpr_search = null) => {\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n // Register events.\n events.forEach((event) => {\n document.addEventListener(event, (e) => {\n // Always fetch the latest information when we click as state is a fickle thing.\n let {firstActive, lastActive, sifirst, silast} = onClickVariables();\n let itemToReset = '';\n\n // Prevent the usual form behaviour.\n if (e.target.closest(selectors.formDropdown)) {\n e.preventDefault();\n }\n\n // Handle the state of active initials before form submission.\n if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {\n // Ensure the li items don't cause weird clicking emptying out the form.\n if (e.target.classList.contains(selectors.pageListItem)) {\n return;\n }\n\n const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.\n\n // We want to find the current active item in the menu area the user selected.\n // We also want to fetch the raw item out of the array for instant manipulation.\n if (initialsBar.classList.contains(selectors.firstInitial)) {\n sifirst = e.target;\n itemToReset = firstActive;\n } else {\n silast = e.target;\n itemToReset = lastActive;\n }\n swapActiveItems(itemToReset, e);\n }\n\n // Handle form submissions.\n if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) {\n if (e.target.dataset.action === selectors.formItems.save) {\n // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.\n // Then we will redirect the user back onto the page with new filters applied.\n const params = {\n 'id': e.target.closest(selectors.formDropdown).dataset.courseid,\n 'gpr_search': gpr_search !== null ? gpr_search : '',\n 'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,\n 'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,\n };\n if (gpr_userid !== null) {\n params.gpr_userid = gpr_userid;\n }\n window.location = Url.relativeUrl(callingLink, params);\n }\n if (e.target.dataset.action === selectors.formItems.cancel) {\n $(`.${selectors.targetButton}`).dropdown('toggle');\n }\n }\n });\n });\n};\n\n/**\n * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.\n *\n * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}\n */\nconst onClickVariables = () => {\n // Ensure we have an up-to-date initials bar.\n const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];\n const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];\n const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n // Ensure we retain both of the selections from a previous instance.\n let sifirst = firstActive.querySelector(selectors.pageClickableItem);\n let silast = lastActive.querySelector(selectors.pageClickableItem);\n return {firstActive, lastActive, sifirst, silast};\n};\n\n/**\n * Given we are provided the old li and current click event, swap around the active properties.\n *\n * @param {HTMLElement} itemToReset\n * @param {Event} e\n */\nconst swapActiveItems = (itemToReset, e) => {\n itemToReset.classList.remove(selectors.activeItem);\n itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;\n\n // Set the select item as the current item.\n const itemToSetActive = e.target.parentElement;\n itemToSetActive.classList.add(selectors.activeItem);\n e.target.ariaCurrent = true;\n};\n"],"names":["registered","selectors","type","save","cancel","callingLink","gpr_userid","gpr_search","pendingPromise","Pending","registerListenerEvents","on","document","querySelector","focus","preventScroll","resolve","events","CustomEvents","activate","keyboardActivate","define","forEach","event","addEventListener","e","firstActive","lastActive","sifirst","silast","onClickVariables","itemToReset","target","closest","preventDefault","classList","contains","swapActiveItems","dataset","action","params","courseid","parentElement","value","window","location","Url","relativeUrl","dropdown","firstItems","querySelectorAll","lastItems","filter","item","remove","ariaCurrent","add"],"mappings":";;;;;;;;;44BAmCIA,YAAa,QAGXC,uBACY,YADZA,4BAEiB,aAFjBA,qBAGU,SAHVA,uBAIY,wBAJZA,wBAKa,qBALbA,uBAMY,eANZA,sBAOW,cAPXA,sBAQW,cARXA,uBASY,iBATZA,oBAUS,CACPC,KAAM,SACNC,KAAM,OACNC,OAAQ,wBAWI,SAACC,iBAAaC,kEAAa,KAAMC,kEAAa,QAC1DP,wBAGEQ,eAAiB,IAAIC,iBAC3BC,uBAAuBL,YAAaC,WAAYC,gCAE9CN,yBAAyBU,GAAG,qBAAqB,KAC/CC,SAASC,cAAcZ,6BAA6Ba,MAAM,CAACC,eAAe,OAE9EP,eAAeQ,UACfhB,YAAa,SAUXU,uBAAyB,SAACL,iBAAaC,kEAAa,KAAMC,kEAAa,WACnEU,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOT,SAAUK,QAG9BA,OAAOK,SAASC,QACZX,SAASY,iBAAiBD,OAAQE,QAE1BC,YAACA,YAADC,WAAcA,WAAdC,QAA0BA,QAA1BC,OAAmCA,QAAUC,mBAC7CC,YAAc,MAGdN,EAAEO,OAAOC,QAAQhC,yBACjBwB,EAAES,iBAIFT,EAAEO,OAAOC,kBAAWhC,oCAA2BA,yBAA2B,IAEtEwB,EAAEO,OAAOG,UAAUC,SAASnC,+BAIZwB,EAAEO,OAAOC,QAAQhC,uBAIrBkC,UAAUC,SAASnC,yBAC/B2B,QAAUH,EAAEO,OACZD,YAAcL,cAEdG,OAASJ,EAAEO,OACXD,YAAcJ,YAElBU,gBAAgBN,YAAaN,MAI7BA,EAAEO,OAAOC,kBAAWhC,0BAA6BwB,EAAEO,OAAO9B,OAASD,oBAAoBC,KAAM,IACzFuB,EAAEO,OAAOM,QAAQC,SAAWtC,oBAAoBE,KAAM,OAGhDqC,OAAS,IACLf,EAAEO,OAAOC,QAAQhC,wBAAwBqC,QAAQG,oBAC1B,OAAflC,WAAsBA,WAAa,WACtCqB,QAAQc,cAAcP,UAAUC,SAAS,iBAAmB,GAAKR,QAAQe,aAC1Ed,OAAOa,cAAcP,UAAUC,SAAS,iBAAmB,GAAKP,OAAOc,OAElE,OAAfrC,aACAkC,OAAOlC,WAAaA,YAExBsC,OAAOC,SAAWC,IAAIC,YAAY1C,YAAamC,QAE/Cf,EAAEO,OAAOM,QAAQC,SAAWtC,oBAAoBG,uCAC1CH,yBAA0B+C,SAAS,kBAYvDlB,iBAAmB,WAEfmB,WAAa,IAAIrC,SAASsC,4BAAqBjD,gCAC/CkD,UAAY,IAAIvC,SAASsC,4BAAqBjD,+BAC9CyB,YAAcuB,WAAWG,QAAQC,MAASA,KAAKlB,UAAUC,SAASnC,wBAAuB,GACzF0B,WAAawB,UAAUC,QAAQC,MAASA,KAAKlB,UAAUC,SAASnC,wBAAuB,OAEzF2B,QAAUF,YAAYb,cAAcZ,6BACpC4B,OAASF,WAAWd,cAAcZ,mCAC/B,CAACyB,YAAAA,YAAaC,WAAAA,WAAYC,QAAAA,QAASC,OAAAA,SASxCQ,gBAAkB,CAACN,YAAaN,KAClCM,YAAYI,UAAUmB,OAAOrD,sBAC7B8B,YAAYlB,cAAcZ,6BAA6BsD,aAAc,EAG7C9B,EAAEO,OAAOU,cACjBP,UAAUqB,IAAIvD,sBAC9BwB,EAAEO,OAAOuB,aAAc"} \ No newline at end of file diff --git a/grade/amd/src/searchwidget/initials.js b/grade/amd/src/searchwidget/initials.js index 0d3c5bc6dc5..32f2739d987 100644 --- a/grade/amd/src/searchwidget/initials.js +++ b/grade/amd/src/searchwidget/initials.js @@ -19,6 +19,8 @@ * @module core_grades/searchwidget/initials * @copyright 2022 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated since Moodle 4.5 - please use core_course/actionbar/initials instead. + * @todo Final deprecation in Moodle 6.0. See MDL-82421. */ import Pending from 'core/pending'; diff --git a/grade/renderer.php b/grade/renderer.php index d3f772d7f7b..698132bf1a3 100644 --- a/grade/renderer.php +++ b/grade/renderer.php @@ -47,13 +47,13 @@ class core_grades_renderer extends plugin_renderer_base { * @param object $course The course object. * @param string|null $groupactionbaseurl This parameter has been deprecated since 4.4 and should not be used anymore. * @return string|null The raw HTML to render. - * @deprecated since 4.5. Use \core_course\output\actionbar\renderer' instead. + * @deprecated since 4.5. See replacement renderable \core_course\output\actionbar\group_selector instead. * @todo Final deprecation in Moodle 6.0. See MDL-82116. */ #[\core\attribute\deprecated( replacement: null, since: '4.5', - reason: 'Moved to \core_course\output\actionbar\renderer.' + reason: 'See replacement renderable \core_course\output\actionbar\group_selector.' )] public function group_selector(object $course, ?string $groupactionbaseurl = null): ?string { global $USER; @@ -112,7 +112,7 @@ class core_grades_renderer extends plugin_renderer_base { 'group', $activegroup ); - return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this)); + return $this->render($groupdropdown); } /** @@ -124,13 +124,22 @@ class core_grades_renderer extends plugin_renderer_base { * @param context $context Our current context. * @param string $slug The slug for the report that called this function. * @return stdClass The data to output. + * @deprecated since 4.5. See replacement renderable \core_course\output\actionbar\initials_selector instead. + * @todo Final deprecation in Moodle 6.0. See MDL-82421. */ + #[\core\attribute\deprecated( + replacement: null, + since: '4.5', + reason: 'See replacement renderable \core_course\output\actionbar\initials_selector.' + )] public function initials_selector( object $course, context $context, string $slug ): stdClass { global $SESSION, $COURSE; + + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); // User search. $searchvalue = optional_param('gpr_search', null, PARAM_NOTAGS); $userid = optional_param('grp_userid', null, PARAM_INT); diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index d63b4678d47..9a71f1dc2ab 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -17,6 +17,9 @@ namespace gradereport_grader\output; use core\output\comboboxsearch; +use core_course\output\actionbar\group_selector; +use core_course\output\actionbar\initials_selector; +use core_course\output\actionbar\user_selector; use core_grades\output\general_action_bar; use moodle_url; @@ -69,7 +72,7 @@ class action_bar extends \core_grades\output\action_bar { * @throws \moodle_exception */ public function export_for_template(\renderer_base $output): array { - global $PAGE, $OUTPUT, $SESSION, $USER; + global $SESSION, $USER; // If in the course context, we should display the general navigation selector in gradebook. $courseid = $this->context->instanceid; // Get the data used to output the general navigation selector. @@ -82,50 +85,40 @@ class action_bar extends \core_grades\output\action_bar { // and the view mode selector (if applicable). if (has_capability('moodle/grade:viewall', $this->context)) { $course = get_course($courseid); - $gradesrenderer = $PAGE->get_renderer('core_grades'); - - $initialscontent = $gradesrenderer->initials_selector( - $course, - $this->context, - '/grade/report/grader/index.php' - ); $firstnameinitial = $SESSION->gradereport["filterfirstname-{$this->context->id}"] ?? ''; $lastnameinitial = $SESSION->gradereport["filtersurname-{$this->context->id}"] ?? ''; + $additionalparams = []; - $initialselector = new comboboxsearch( - false, - $initialscontent->buttoncontent, - $initialscontent->dropdowncontent, - 'initials-selector', - 'initialswidget', - 'initialsdropdown', - $initialscontent->buttonheader, - true, - get_string('filterbyname', 'core_grades'), - 'nameinitials', - json_encode([ - 'first' => $firstnameinitial, - 'last' => $lastnameinitial, - ]) + if ($this->userid > 0) { + $additionalparams['gpr_userid'] = $this->userid; + } else if (!empty($this->usersearch)) { + $additionalparams['gpr_search'] = $this->usersearch; + } + + $initialselector = new initials_selector( + course: $course, + targeturl: '/grade/report/grader/index.php', + firstinitial: $firstnameinitial, + lastinitial: $lastnameinitial, + additionalparams: $additionalparams, ); $data['initialselector'] = $initialselector->export_for_template($output); if ($course->groupmode) { - $actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar'); - $data['groupselector'] = $actionbarrenderer->render( - new \core_course\output\actionbar\group_selector(null, $this->context)); + $gs = new group_selector($this->context); + $data['groupselector'] = $gs->export_for_template($output); } $resetlink = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]); - $userselectorrenderer = new \core_course\output\actionbar\user_selector( + $userselector = new user_selector( course: $course, resetlink: $resetlink, userid: $this->userid, groupid: 0, usersearch: $this->usersearch ); - $data['searchdropdown'] = $userselectorrenderer->export_for_template($output); + $data['searchdropdown'] = $userselector->export_for_template($output); // The collapsed column dialog is aligned to the edge of the screen, we need to place it such that it also aligns. $collapsemenudirection = right_to_left() ? 'dropdown-menu-left' : 'dropdown-menu-right'; diff --git a/grade/report/grader/templates/action_bar.mustache b/grade/report/grader/templates/action_bar.mustache index 9f5a0ea8aba..c8479f05f0a 100644 --- a/grade/report/grader/templates/action_bar.mustache +++ b/grade/report/grader/templates/action_bar.mustache @@ -60,7 +60,9 @@ } ] }, - "groupselector": "
", + "searchdropdown": {"search dropdown": "context here"}, + "groupselector": {"group selector": "context here"}, + "initialselector": {"initial selector": "context here"}, "collapsedcolumns": "
", "pagereset": "http://moodle.local/grade/report/grader/index.php?id=2&userid=2&reset=1" } @@ -81,7 +83,7 @@ {{/searchdropdown}} {{#groupselector}} {{/groupselector}} diff --git a/grade/report/grader/tests/behat/tertiary_name_filter.feature b/grade/report/grader/tests/behat/tertiary_name_filter.feature index ec8cc66df05..30f65fba51d 100644 --- a/grade/report/grader/tests/behat/tertiary_name_filter.feature +++ b/grade/report/grader/tests/behat/tertiary_name_filter.feature @@ -31,15 +31,15 @@ Feature: Within the grader report, test that we can open our generic filter drop Scenario: A teacher can open the filter component Given I should see "Filter by name" When I click on "Filter by name" "combobox" - Then I should see "27" node occurrences of type "input" in the "First name" "core_grades > initials bar" - And I should see "27" node occurrences of type "input" in the "Last name" "core_grades > initials bar" + Then I should see "27" node occurrences of type "input" in the "First name" "core_course > initials bar" + And I should see "27" node occurrences of type "input" in the "Last name" "core_course > initials bar" And "input[data-action=cancel]" "css_element" should exist And "input[data-action=save]" "css_element" should exist Scenario: A teacher can filter the grader report to limit users reported Given I click on "Filter by name" "combobox" And I wait until "input[data-action=save]" "css_element" exists - When I select "D" in the "First name" "core_grades > initials bar" + When I select "D" in the "First name" "core_course > initials bar" And I press "Apply" And I wait to be redirected # We should only have one user that matches the "D" first name @@ -57,8 +57,8 @@ Feature: Within the grader report, test that we can open our generic filter drop # Test filtering on last name # Business logic: If all is selected, we will not show it i.e. First (D) and NOT First (D) Last (All) And I click on "First (D)" "combobox" - And I select "All" in the "First name" "core_grades > initials bar" - And I select "M" in the "Last name" "core_grades > initials bar" + And I select "All" in the "First name" "core_course > initials bar" + And I select "M" in the "Last name" "core_course > initials bar" And I press "Apply" And I wait to be redirected # We should only have one user that matches the "T" first name @@ -75,8 +75,8 @@ Feature: Within the grader report, test that we can open our generic filter drop # Test filtering on first && last name And I click on "Last (M)" "combobox" - And I select "U" in the "First name" "core_grades > initials bar" - And I select "T" in the "Last name" "core_grades > initials bar" + And I select "U" in the "First name" "core_course > initials bar" + And I select "T" in the "Last name" "core_course > initials bar" And I press "Apply" And I wait to be redirected # We should only have one user that matches the "T" first name @@ -96,7 +96,7 @@ Feature: Within the grader report, test that we can open our generic filter drop Scenario: A teacher can quickly tell that a filter is applied to the current table Given I click on "Filter by name" "combobox" And I wait until "input[data-action=save]" "css_element" exists - When I select "T" in the "First name" "core_grades > initials bar" + When I select "T" in the "First name" "core_course > initials bar" And I press "Apply" And I wait to be redirected Then I should see "First (T)" @@ -104,7 +104,7 @@ Feature: Within the grader report, test that we can open our generic filter drop # Check if removing the filter, removes the highlight and user notice of applied filters And I click on "First (T)" "combobox" And I wait until "input[data-action=save]" "css_element" exists - And I select "All" in the "First name" "core_grades > initials bar" + And I select "All" in the "First name" "core_course > initials bar" And I press "Apply" And I wait to be redirected # Check if the name button indicates if a filter is active @@ -130,13 +130,13 @@ Feature: Within the grader report, test that we can open our generic filter drop | core | all | すべて | And I click on "Filter by name" "combobox" And "input[data-action=save]" "css_element" should be visible - And I select "T" in the "First name" "core_grades > initials bar" + And I select "T" in the "First name" "core_course > initials bar" And I press "Apply" And I wait to be redirected And I click on "First (T)" "combobox" And I wait until "input[data-action=save]" "css_element" exists - When I select "すべて" in the "First name" "core_grades > initials bar" + When I select "すべて" in the "First name" "core_course > initials bar" And I press "Apply" And I wait to be redirected Then I should not see "First (すべて) Last (すべて)" @@ -152,7 +152,7 @@ Feature: Within the grader report, test that we can open our generic filter drop Given I set the field "Search users" to "Student 1" And I click on "Student 1" in the "Search users" search combo box And I click on "Filter by name" "combobox" - And I select "S" in the "First name" "core_grades > initials bar" + And I select "S" in the "First name" "core_course > initials bar" When I press "Apply" And the field "Search users" matches value "Student 1" Then the following should exist in the "user-grades" table: @@ -166,7 +166,7 @@ Feature: Within the grader report, test that we can open our generic filter drop | User Test | student4@example.com | - | | Turtle Manatee | student5@example.com | - | And I click on "First (S)" "combobox" - And I select "M" in the "First name" "core_grades > initials bar" + And I select "M" in the "First name" "core_course > initials bar" And I press "Apply" And the following should not exist in the "user-grades" table: | -1- | -2- | -3- | @@ -191,7 +191,7 @@ Feature: Within the grader report, test that we can open our generic filter drop | Teacher 1 | teacher1@example.com | - | | Turtle Manatee | student5@example.com | - | When I click on "Filter by name" "combobox" - And I select "E" in the "Last name" "core_grades > initials bar" + And I select "E" in the "Last name" "core_course > initials bar" And I press "Apply" Then the following should exist in the "user-grades" table: | -1- | -2- | -3- | @@ -212,8 +212,8 @@ Feature: Within the grader report, test that we can open our generic filter drop And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests And I click on "Filter by name" "combobox" And "input[data-action=save]" "css_element" should be visible - And the focused element is "All" "button" in the "First name" "core_grades > initials bar" + And the focused element is "All" "button" in the "First name" "core_course > initials bar" When I press the tab key - Then the focused element is "input[value=A]" "css_element" in the "First name" "core_grades > initials bar" + Then the focused element is "input[value=A]" "css_element" in the "First name" "core_course > initials bar" And I press the tab key - And the focused element is "input[value=B]" "css_element" in the "First name" "core_grades > initials bar" + And the focused element is "input[value=B]" "css_element" in the "First name" "core_course > initials bar" diff --git a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature index fba2ed8c8b9..95586dd209c 100644 --- a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature +++ b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature @@ -297,8 +297,8 @@ Feature: Within the grader report, test that we can search for users Scenario: Once a teacher searches, it'll apply the currently set filters and inform the teacher as such # Set up a basic filtering case. Given I click on "Filter by name" "combobox" - And I select "U" in the "First name" "core_grades > initials bar" - And I select "E" in the "Last name" "core_grades > initials bar" + And I select "U" in the "First name" "core_course > initials bar" + And I select "E" in the "Last name" "core_course > initials bar" And I press "Apply" And I wait until the page is ready And the following should exist in the "user-grades" table: @@ -324,8 +324,8 @@ Feature: Within the grader report, test that we can search for users | -1- | | Turtle Manatee | And I click on "Filter by name" "combobox" - And I select "T" in the "First name" "core_grades > initials bar" - And I select "M" in the "Last name" "core_grades > initials bar" + And I select "T" in the "First name" "core_course > initials bar" + And I select "M" in the "Last name" "core_course > initials bar" And the following should exist in the "user-grades" table: | -1- | | Turtle Manatee | diff --git a/grade/report/singleview/classes/report/singleview.php b/grade/report/singleview/classes/report/singleview.php index 974baf0a552..843d3fc8535 100644 --- a/grade/report/singleview/classes/report/singleview.php +++ b/grade/report/singleview/classes/report/singleview.php @@ -146,8 +146,7 @@ class singleview extends grade_report { protected static function groups_course_menu(stdClass $course) { global $PAGE; - $renderer = $PAGE->get_renderer('core_course', 'actionbar'); - return $renderer->render(new \core_course\output\actionbar\group_selector(null, $PAGE->context)); + return $PAGE->get_renderer('core', 'course')->render(new \core_course\output\actionbar\group_selector($PAGE->context)); } /** diff --git a/grade/report/singleview/renderer.php b/grade/report/singleview/renderer.php index 57217313440..8853f861a9c 100644 --- a/grade/report/singleview/renderer.php +++ b/grade/report/singleview/renderer.php @@ -45,7 +45,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base { * @return string The raw HTML to render. */ public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string { - $actionbarrenderer = $this->page->get_renderer('core_course', 'actionbar'); + $courserenderer = $this->page->get_renderer('core', 'course'); $resetlink = new moodle_url('/grade/report/singleview/index.php', ['id' => $course->id, 'group' => $groupid ?? 0]); $usersearch = ''; @@ -54,7 +54,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base { $usersearch = fullname($user); } - return $actionbarrenderer->render( + return $courserenderer->render( new \core_course\output\actionbar\user_selector( course: $course, resetlink: $resetlink, @@ -107,7 +107,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base { 'itemid', $gradeitemid ); - return $this->render_from_template($dropdown->get_template(), $dropdown->export_for_template($this)); + return $this->render($dropdown); } /** diff --git a/grade/report/user/classes/output/action_bar.php b/grade/report/user/classes/output/action_bar.php index ceede74769a..fbdebc3f92b 100644 --- a/grade/report/user/classes/output/action_bar.php +++ b/grade/report/user/classes/output/action_bar.php @@ -91,25 +91,46 @@ class action_bar extends \core_grades\output\action_bar { // If the user has the capability to view all grades, display the group selector (if applicable), the user selector // and the view mode selector (if applicable). if (has_capability('moodle/grade:viewall', $this->context)) { - $userreportrenderer = $PAGE->get_renderer('gradereport_user'); $course = get_course($courseid); if ($course->groupmode) { - $groupselector = new \core_course\output\actionbar\group_selector(null, $this->context); - $data['groupselector'] = $PAGE->get_renderer('core_course', 'actionbar')->render($groupselector); + $groupselector = new \core_course\output\actionbar\group_selector($this->context); + $data['groupselector'] = $groupselector->export_for_template($output); } + + $resetlink = new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'group' => 0]); + $baseurl = new moodle_url('/grade/report/user/index.php', ['id' => $courseid]); + $PAGE->requires->js_call_amd('gradereport_user/user', 'init', [$baseurl->out(false)]); + + $userselector = new \core_course\output\actionbar\user_selector( + course: $course, + resetlink: $resetlink, + userid: $this->userid, + groupid: $this->currentgroupid, + usersearch: $this->usersearch + ); $data['userselector'] = [ 'courseid' => $courseid, - 'content' => $userreportrenderer->users_selector( - course: get_course($courseid), - userid: $this->userid, - groupid: $this->currentgroupid, - usersearch: $this->usersearch - ), + 'content' => $userselector->export_for_template($output), ]; // Do not output the 'view mode' selector when in zero state or when the current user is viewing its own report. if (!is_null($this->userid) && $USER->id != $this->userid) { - $data['viewasselector'] = $userreportrenderer->view_mode_selector($this->userid, $this->userview, $courseid); + $viewasotheruser = new moodle_url('/grade/report/user/index.php', + ['id' => $courseid, 'userid' => $this->userid, 'userview' => GRADE_REPORT_USER_VIEW_USER]); + $viewasmyself = new moodle_url('/grade/report/user/index.php', + ['id' => $courseid, 'userid' => $this->userid, 'userview' => GRADE_REPORT_USER_VIEW_SELF]); + + $selectoroptions = [ + $viewasotheruser->out(false) => get_string('otheruser', 'core_grades'), + $viewasmyself->out(false) => get_string('myself', 'core_grades') + ]; + + $selectoractiveurl = $this->userview === GRADE_REPORT_USER_VIEW_USER ? $viewasotheruser : $viewasmyself; + + $viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false)); + $viewasselect->set_label(get_string('viewas', 'core_grades')); + + $data['viewasselector'] = $viewasselect->export_for_template($output); } } diff --git a/grade/report/user/renderer.php b/grade/report/user/renderer.php index e403a51ce0d..a53109d9d49 100644 --- a/grade/report/user/renderer.php +++ b/grade/report/user/renderer.php @@ -91,20 +91,25 @@ class gradereport_user_renderer extends plugin_renderer_base { * @param string $usersearch Search string. * @return string The raw HTML to render. * @throws coding_exception + * @deprecated since Moodle 4.5. See user_selector use in \gradereport_user\output\action_bar::export_for_template. */ public function users_selector(object $course, ?int $userid = null, ?int $groupid = null, string $usersearch = ''): string { - $actionbarrenderer = $this->page->get_renderer('core_course', 'actionbar'); + + debugging('users_selector is deprecated.', DEBUG_DEVELOPER); + + $courserenderer = $this->page->get_renderer('core', 'course'); $resetlink = new moodle_url('/grade/report/user/index.php', ['id' => $course->id, 'group' => 0]); $baseurl = new moodle_url('/grade/report/user/index.php', ['id' => $course->id]); $this->page->requires->js_call_amd('gradereport_user/user', 'init', [$baseurl->out(false)]); - $userselector = new \core_course\output\actionbar\user_selector( - course: $course, - resetlink: $resetlink, - userid: $userid, - groupid: $groupid, - usersearch: $usersearch + return $courserenderer->render( + new \core_course\output\actionbar\user_selector( + course: $course, + resetlink: $resetlink, + userid: $userid, + groupid: $groupid, + usersearch: $usersearch + ) ); - return $actionbarrenderer->render($userselector); } /** @@ -167,9 +172,12 @@ class gradereport_user_renderer extends plugin_renderer_base { * @param int $userview The current view user setting constant * @param int $courseid The course ID. * @return string The raw HTML to render. + * @deprecated since Moodle 4.5 See select_menu use in \gradereport_user\output\action_bar::export_for_template. */ public function view_mode_selector(int $userid, int $userview, int $courseid): string { + debugging('view_mode_selector is deprecated.', DEBUG_DEVELOPER); + $viewasotheruser = new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $userid, 'userview' => GRADE_REPORT_USER_VIEW_USER]); $viewasmyself = new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $userid, diff --git a/grade/report/user/templates/action_bar.mustache b/grade/report/user/templates/action_bar.mustache index 33db5f28e70..6a1768cb411 100644 --- a/grade/report/user/templates/action_bar.mustache +++ b/grade/report/user/templates/action_bar.mustache @@ -80,7 +80,7 @@ {{/generalnavselector}} {{#groupselector}} {{/groupselector}} @@ -88,7 +88,7 @@ @@ -97,7 +97,7 @@ {{#viewasselector}} {{/viewasselector}} diff --git a/grade/templates/initials_dropdown_form.mustache b/grade/templates/initials_dropdown_form.mustache index a363213fc6e..f2abfa832c5 100644 --- a/grade/templates/initials_dropdown_form.mustache +++ b/grade/templates/initials_dropdown_form.mustache @@ -17,6 +17,9 @@ {{! @template core_grades/initials_dropdown_form + @deprecated since Moodle 4.5 - please use core_course/initials_dropdown_form instead. + @todo Final deprecation on Moodle 6.0. See MDL-82421. + The form component of our dropdown within the tertiary navigation. Context variables required for this template: diff --git a/group/templates/comboboxsearch/group_selector.mustache b/group/templates/comboboxsearch/group_selector.mustache index 9d0c02be7ab..13b07f59da5 100644 --- a/group/templates/comboboxsearch/group_selector.mustache +++ b/group/templates/comboboxsearch/group_selector.mustache @@ -34,10 +34,10 @@
- - + + {{selectedgroup}}
diff --git a/lang/en/course.php b/lang/en/course.php index b3da22e6ef2..66db85dba71 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -96,6 +96,10 @@ The setting has no effect on mobile app content download for offline usage.'; $string['enabledownloadcoursecontent'] = 'Enable download course content'; $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.'; $string['favourite'] = 'Starred course'; +$string['filterbothactive'] = 'First ({$a->first}) Last ({$a->last})'; +$string['filterbyname'] = 'Filter by name'; +$string['filterfirstactive'] = 'First ({$a->first})'; +$string['filterlastactive'] = 'Last ({$a->last})'; $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).'; $string['hideendedcoursestask'] = 'Hide courses on end date'; $string['informationformodule'] = 'Information about the {$a} activity'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index f8121dbed38..5d8b2e53361 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -130,3 +130,7 @@ cannotsetpassword,core_error registerwithmoodleorginfoapp,core_hub registration_help,core_admin coursemanagementoptions,core_my +filterbothactive,core_grades +filterbyname,core_grades +filterfirstactive,core_grades +filterlastactive,core_grades diff --git a/lang/en/grades.php b/lang/en/grades.php index ac1620e57c3..015628ef864 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -241,10 +241,6 @@ $string['feedbacks'] = 'Feedbacks'; $string['feedbacksaved'] = 'Feedback saved'; $string['feedbackview'] = 'View feedback'; $string['finalgrade'] = 'Final grade'; -$string['filterbothactive'] = 'First ({$a->first}) Last ({$a->last})'; -$string['filterbyname'] = 'Filter by name'; -$string['filterfirstactive'] = 'First ({$a->first})'; -$string['filterlastactive'] = 'Last ({$a->last})'; $string['finalgrade_help'] = 'If the overridden checkbox is ticked, a grade may be added or amended.'; $string['fixedstudents'] = 'Static students column'; $string['fixedstudents_help'] = 'Allows grades to scroll horizontally without losing sight of the students column, by making it static.'; @@ -917,3 +913,9 @@ $string['gradeitemadvanced'] = 'Advanced grade item options'; $string['gradeitemadvanced_help'] = 'Select all elements that should be displayed as advanced when editing grade items.'; $string['aria-toggledropdown'] = 'Toggle the following dropdown'; $string['aria:dropdowngrades'] = 'Grade items found'; + +// Deprecated since Moodle 4.5. +$string['filterbothactive'] = 'First ({$a->first}) Last ({$a->last})'; +$string['filterbyname'] = 'Filter by name'; +$string['filterfirstactive'] = 'First ({$a->first})'; +$string['filterlastactive'] = 'Last ({$a->last})'; diff --git a/lib/classes/output/comboboxsearch.php b/lib/classes/output/comboboxsearch.php index f0120c8874a..834366d89cd 100644 --- a/lib/classes/output/comboboxsearch.php +++ b/lib/classes/output/comboboxsearch.php @@ -25,7 +25,7 @@ use core\exception\moodle_exception; * @copyright 2022 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class comboboxsearch implements renderable, templatable { +class comboboxsearch implements renderable, named_templatable { /** @var bool $renderlater Should the dropdown render straightaway? We sometimes need to output the component without all of the * data and leave the rendering of any defaults and actual data to the caller. We will give you a basic placeholder that can * then be easily replaced.*/ @@ -154,9 +154,16 @@ class comboboxsearch implements renderable, templatable { /** * Returns the standard template for the dropdown. * + * @deprecated since Moodle 4.5. {@see named_templatable::get_template_name() instead} * @return string */ public function get_template(): string { + debugging('get_template is deprecated. Please use get_template_name instead'); + + return 'core/comboboxsearch'; + } + + public function get_template_name(renderer_base $renderer): string { return 'core/comboboxsearch'; } } diff --git a/lib/tests/behat/alpha_chooser.feature b/lib/tests/behat/alpha_chooser.feature index f1c9f61ad55..cbcfa049095 100644 --- a/lib/tests/behat/alpha_chooser.feature +++ b/lib/tests/behat/alpha_chooser.feature @@ -64,12 +64,14 @@ Feature: Initials bar | student23 | C1 | student | | student24 | C1 | student | + @javascript Scenario: Filter users on assignment submission page Given the following "activities" exist: | activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | | assign | C1 | assign1 | TestAssignment | Test assignment description | 0 | 0 | And I am on the "assign1" "Activity" page logged in as "teacher" When I navigate to "Submissions" in current page administration + 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" @@ -77,15 +79,19 @@ Feature: Initials bar And I should see "Astudent Astudent" And I should see "Bstudent Astudent" And I should see "Cstudent Cstudent" - And I click on "A" "link" in the ".initialbar.lastinitial .page-item.A" "css_element" + And I select "A" in the "Last name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element" + And I press "Apply" And I should see "Astudent Astudent" And I should see "Bstudent Astudent" And I should not see "Cstudent Cstudent" - And I click on "B" "link" in the ".initialbar.firstinitial .page-item.B" "css_element" + And I click on "Last (A)" "combobox" + And I select "B" in the "First name" "core_course > initials bar" + And I press "Apply" + And I wait until the page is ready And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element" And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element" @@ -102,19 +108,25 @@ 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 click on "All" "link" in the ".initialbar.firstinitial" "css_element" + And I click on "First (B) Last (A)" "combobox" + And I select "All" in the "First name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element" + And I press "Apply" + And I wait until the page is ready And I should see "Astudent Astudent" And I should see "Bstudent Astudent" And I should not see "Cstudent Cstudent" - And I click on "All" "link" in the ".initialbar.lastinitial" "css_element" + And I click on "Last (A)" "combobox" + And I select "All" in the "Last name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element" + And I press "Apply" + And I wait until the page is ready And I should see "Astudent Astudent" And I should see "Bstudent Astudent" And I should see "Cstudent Cstudent" @@ -135,7 +147,7 @@ Feature: Initials bar And I should see "Astudent Astudent" And I should see "Bstudent Astudent" And I should see "Cstudent Cstudent" - And I select "A" in the "Last name" "core_grades > initials bar" + And I select "A" in the "Last name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" @@ -145,7 +157,7 @@ Feature: Initials bar And I should see "Bstudent Astudent" And I should not see "Cstudent Cstudent" And I click on "Last (A)" "combobox" - And I select "B" in the "First name" "core_grades > initials bar" + And I select "B" in the "First name" "core_course > initials bar" And I press "Apply" And I wait until the page is ready And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" @@ -166,7 +178,7 @@ Feature: Initials bar And I should see "Bstudent Astudent" And I should not see "Cstudent Cstudent" And I click on "First (B) Last (A)" "combobox" - And I select "All" in the "First name" "core_grades > initials bar" + And I select "All" in the "First name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" @@ -177,7 +189,7 @@ Feature: Initials bar And I should see "Bstudent Astudent" And I should not see "Cstudent Cstudent" And I click on "Last (A)" "combobox" - And I select "All" in the "Last name" "core_grades > initials bar" + And I select "All" in the "Last name" "core_course > 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" And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element" diff --git a/mod/assign/classes/output/grading_actionmenu.php b/mod/assign/classes/output/grading_actionmenu.php index da5e4d6e6f8..b02ce48f9d5 100644 --- a/mod/assign/classes/output/grading_actionmenu.php +++ b/mod/assign/classes/output/grading_actionmenu.php @@ -26,6 +26,8 @@ namespace mod_assign\output; use assign; use context_module; +use core_course\output\actionbar\group_selector; +use core_course\output\actionbar\user_selector; use templatable; use renderable; use moodle_url; @@ -48,9 +50,13 @@ class grading_actionmenu implements templatable, renderable { protected int $submissioncount; /** @var assign The assign instance. */ protected assign $assign; + /** @var bool If there are submissions to download. */ protected bool $showdownload; + /** @var array Applied user initials filters, containing 'firstname' and 'lastname'. **/ + protected array $userinitials; + /** * Constructor for this object. * @@ -58,12 +64,14 @@ class grading_actionmenu implements templatable, renderable { * @param null|bool $submissionpluginenabled This parameter has been deprecated since 4.5 and should not be used anymore. * @param null|int $submissioncount This parameter has been deprecated since 4.5 and should not be used anymore. * @param assign|null $assign The assign instance. If not provided, it will be loaded based on the cmid. + * @param array $userinitials The user initials to filter the table by. */ public function __construct( int $cmid, ?bool $submissionpluginenabled = null, ?int $submissioncount = null, - ?assign $assign = null + ?assign $assign = null, + array $userinitials = [] ) { $this->cmid = $cmid; if (!$assign) { @@ -72,6 +80,7 @@ class grading_actionmenu implements templatable, renderable { } $this->assign = $assign; $this->showdownload = $this->assign->is_any_submission_plugin_enabled() && $this->assign->count_submissions(); + $this->userinitials = $userinitials; } /** @@ -81,12 +90,10 @@ class grading_actionmenu implements templatable, renderable { * @return array Data to render. */ public function export_for_template(\renderer_base $output): array { - global $PAGE, $OUTPUT; + global $PAGE; $course = $this->assign->get_course(); $cm = get_coursemodule_from_id('assign', $this->cmid); - $actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar'); - $data = []; $userid = optional_param('userid', null, PARAM_INT); @@ -96,7 +103,7 @@ class grading_actionmenu implements templatable, renderable { $resetlink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grading']); $groupid = groups_get_course_group($course, true); - $userselector = new \core_course\output\actionbar\user_selector( + $userselector = new user_selector( course: $course, resetlink: $resetlink, userid: $userid, @@ -104,22 +111,43 @@ class grading_actionmenu implements templatable, renderable { usersearch: $usersearch, instanceid: $this->assign->get_instance()->id ); - $data['userselector'] = $actionbarrenderer->render($userselector); + $data['userselector'] = $userselector->export_for_template($output); + + $hasinitials = !empty($this->userinitials['firstname']) || !empty($this->userinitials['lastname']); + $additionalparams = ['action' => 'grading', 'id' => $this->cmid]; + + if (!empty($userid)) { + $additionalparams['userid'] = $userid; + } else if (!empty($usersearch)) { + $additionalparams['search'] = $usersearch; + } + + $initialselector = new \core_course\output\actionbar\initials_selector( + course: $course, + targeturl: 'mod/assign/view.php', + firstinitial: $this->userinitials['firstname'] ?? '', + lastinitial: $this->userinitials['lastname'] ?? '', + firstinitialparam: 'tifirst', + lastinitialparam: 'tilast', + additionalparams: $additionalparams + ); + + $data['initialselector'] = $initialselector->export_for_template($output); if (groups_get_activity_groupmode($cm, $course)) { - $data['groupselector'] = $actionbarrenderer->render( - new \core_course\output\actionbar\group_selector(null, $PAGE->context)); + $gs = new group_selector($PAGE->context); + $data['groupselector'] = $gs->export_for_template($output); } if ($extrafiltersdropdown = $this->get_extra_filters_dropdown()) { $PAGE->requires->js_call_amd('mod_assign/actionbar/grading/extra_filters_dropdown', 'init', []); - $data['extrafiltersdropdown'] = $OUTPUT->render($extrafiltersdropdown); + $data['extrafiltersdropdown'] = $extrafiltersdropdown->export_for_template($output); } $activitygroup = groups_get_activity_group($cm); $hasuserfilter = get_user_preferences('assign_filter'); $hasextrafilters = $this->get_applied_extra_filters_count() > 0; - if ($activitygroup || $hasuserfilter || $hasextrafilters) { + if ($activitygroup || $hasuserfilter || $hasextrafilters || $hasinitials) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->cmid, 'action' => 'grading', @@ -128,6 +156,8 @@ class grading_actionmenu implements templatable, renderable { 'workflowfilter' => '', 'markingallocationfilter' => '', 'suspendedparticipantsfilter' => 0, + 'tifirst' => '', + 'tilast' => '', ]); $data['pagereset'] = $url->out(false); } diff --git a/mod/assign/classes/output/renderer.php b/mod/assign/classes/output/renderer.php index e8a15489639..f67b0ea9341 100644 --- a/mod/assign/classes/output/renderer.php +++ b/mod/assign/classes/output/renderer.php @@ -1131,7 +1131,7 @@ class renderer extends \plugin_renderer_base { $o .= $this->output->box_start('boxaligncenter gradingtable position-relative'); $this->page->requires->js_init_call('M.mod_assign.init_grading_table', array()); - $o .= $this->flexible_table($table, $table->get_rows_per_page(), true); + $o .= $this->flexible_table($table, $table->get_rows_per_page(), false); $o .= $this->output->box_end(); return $o; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 2801b16bc13..46588ccbd1f 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -4545,7 +4545,21 @@ class assign { $markingworkflow = $this->get_instance()->markingworkflow; - $buttons = new \mod_assign\output\grading_actionmenu(cmid: $this->get_course_module()->id, assign: $this); + // Load the table of submissions, to be printed further down. + $usequickgrading = $showquickgrading && $quickgrading; + $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, $usequickgrading); + $gradingtable->responsive = false; + $table = $this->get_renderer()->render($gradingtable); + // This initialises the selected first/last initials for the action menu. + $gradingtable->initialbars(true); + $buttons = new \mod_assign\output\grading_actionmenu( + cmid: $this->get_course_module()->id, + assign: $this, + userinitials: [ + 'firstname' => $gradingtable->get_initial_first(), + 'lastname' => $gradingtable->get_initial_last(), + ] + ); $actionformtext = $this->get_renderer()->render($buttons); $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']); $PAGE->activityheader->set_attrs(['hidecompletion' => true]); @@ -4573,11 +4587,7 @@ class assign { $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage'); } - // Load and print the table of submissions. - $usequickgrading = $showquickgrading && $quickgrading; - $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, $usequickgrading); - $gradingtable->responsive = false; - $table = $this->get_renderer()->render($gradingtable); + // Print the table of submissions. $footerdata = [ 'perpage' => $gradingtable->get_paging_selector(), 'pagingbar' => $gradingtable->get_paging_bar(), diff --git a/mod/assign/styles.css b/mod/assign/styles.css index 176e9ea8a45..537101e5c20 100644 --- a/mod/assign/styles.css +++ b/mod/assign/styles.css @@ -1236,3 +1236,11 @@ body.tox-fullscreen.path-mod-assign [data-region="grade-panel"] { body.path-mod-assign [data-region="grade-panel"] .mform .fitem.has-popout .felement[data-fieldtype="editor"] { overflow: unset; } + +.path-mod-assign .initialsdropdown { + width: 460px; +} + +.path-mod-assign .initialsdropdown::after { + content: ''; +} diff --git a/mod/assign/templates/grading_actionmenu.mustache b/mod/assign/templates/grading_actionmenu.mustache index 6653fff89ed..0e3b7f4e794 100644 --- a/mod/assign/templates/grading_actionmenu.mustache +++ b/mod/assign/templates/grading_actionmenu.mustache @@ -35,8 +35,9 @@ Example context (json): { - "userselector": "", - "groupselector": "
", + "userselector": {"user selector": "context here"}, + "groupselector": {"group selector": "context here"}, + "initialselector": {"initial selector": "context here"}, "statusselector": { "name": "filter", "value": "submitted", @@ -74,16 +75,22 @@ {{#userselector}} {{/userselector}} {{#groupselector}} {{/groupselector}} + {{#initialselector}} + + + {{/initialselector}} {{#statusselector}}