diff --git a/grade/classes/external/get_gradeitems.php b/grade/classes/external/get_gradeitems.php new file mode 100644 index 0000000000000..881d00870ce6f --- /dev/null +++ b/grade/classes/external/get_gradeitems.php @@ -0,0 +1,106 @@ +. + +namespace core_grades\external; + +defined('MOODLE_INTERNAL') || die; + +use context_course; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_warnings; +use core_external\restricted_context_exception; +use grade_item; + +require_once($CFG->libdir . '/gradelib.php'); + +/** + * External grade get gradeitems API implementation + * + * @package core_grades + * @copyright 2023 Mathew May + * @category external + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_gradeitems extends external_api { + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters ( + [ + 'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED) + ] + ); + } + + /** + * Given a course ID find the grading objects and return their names & IDs. + * + * @param int $courseid + * @return array + * @throws restricted_context_exception + * @throws \invalid_parameter_exception + */ + public static function execute(int $courseid): array { + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'courseid' => $courseid + ] + ); + + $warnings = []; + $context = context_course::instance($params['courseid']); + parent::validate_context($context); + + $allgradeitems = grade_item::fetch_all(['courseid' => $params['courseid']]); + $gradeitems = array_filter($allgradeitems, function($item) { + $item->itemname = $item->get_name(); + $item->category = $item->get_parent_category()->get_name(); + return $item->gradetype != GRADE_TYPE_NONE && !$item->is_category_item() && !$item->is_course_item(); + }); + + return [ + 'gradeItems' => $gradeitems, + 'warnings' => $warnings, + ]; + } + + /** + * Returns description of what gradeitems fetch should return. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'gradeItems' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the grade item', VALUE_REQUIRED), + 'itemname' => new external_value(PARAM_TEXT, 'The full name of the grade item', VALUE_REQUIRED), + 'category' => new external_value(PARAM_TEXT, 'The grade category of the grade item', VALUE_OPTIONAL), + ]) + ), + 'warnings' => new external_warnings(), + ]); + } +} diff --git a/grade/lib.php b/grade/lib.php index 0ff15b7e3e5c8..158772bdc49c3 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -1539,7 +1539,7 @@ public function get_element_type_string(array $element): string { * @param bool $withdescription Show description if defined by this item. * @param bool $fulltotal If the item is a category total, returns $categoryname."total" * instead of "Category total" or "Course total" - * @param moodle_url|null $sortlink Link to sort column. + * @param moodle_url|null $sortlink Link to sort column. * * @return string header */ @@ -1563,19 +1563,25 @@ public function get_element_header(array &$element, bool $withlink = false, bool if ($sortlink) { $url = $sortlink; - $header = html_writer::link($url, $header, - ['title' => $titleunescaped, 'class' => 'gradeitemheader']); - } - - if (!$sortlink) { + $header = html_writer::link($url, $header, [ + 'title' => $titleunescaped, + 'class' => 'gradeitemheader ' + ]); + } else { if ($withlink && $url = $this->get_activity_link($element)) { $a = new stdClass(); $a->name = get_string('modulename', $element['object']->itemmodule); $a->title = $titleunescaped; $title = get_string('linktoactivity', 'grades', $a); - $header = html_writer::link($url, $header, ['title' => $title, 'class' => 'gradeitemheader']); + $header = html_writer::link($url, $header, [ + 'title' => $title, + 'class' => 'gradeitemheader ', + ]); } else { - $header = html_writer::span($header, 'gradeitemheader', ['title' => $titleunescaped, 'tabindex' => '0']); + $header = html_writer::span($header, 'gradeitemheader ', [ + 'title' => $titleunescaped, + 'tabindex' => '0' + ]); } } @@ -2460,7 +2466,7 @@ public function set_grade_status_icons(array $element): string { } } - $class = 'grade_icons'; + $class = 'grade_icons data-collapse_gradeicons'; if (isset($element['type']) && ($element['type'] == 'category')) { $class = 'category_grade_icons'; } @@ -2526,14 +2532,18 @@ public function get_cell_action_menu(array $element, string $mode, grade_plugin_ $element, $gpr, $mode, $context, true); $context->advancedgradingurl = $this->get_advanced_grading_link($element, $gpr); } + $context->divider1 = true; } - if ($element['type'] == 'item') { - $context->divider1 = true; + if (($element['type'] == 'item') || + (($element['type'] == 'userfield') && ($element['name'] !== 'fullname'))) { + $context->divider2 = true; } if (!empty($USER->editing) || $mode == 'setup') { - if (($element['type'] !== 'userfield') && ($mode !== 'setup')) { + if (($element['type'] == 'userfield') && ($element['name'] !== 'fullname')) { + $context->divider2 = true; + } else if (($mode !== 'setup') && ($element['type'] !== 'userfield')) { $context->divider1 = true; $context->divider2 = true; } @@ -2577,6 +2587,10 @@ public function get_cell_action_menu(array $element, string $mode, grade_plugin_ $context->descendingurl = $this->get_sorting_link($sortlink, $gpr, 'desc'); } } + if ($mode !== 'setup') { + $context = grade_report::get_additional_context($this->context, $this->courseid, + $element, $gpr, $mode, $context); + } } else if ($element['type'] == 'category') { $context->datatype = 'category'; if ($mode !== 'setup') { diff --git a/grade/report/grader/amd/build/collapse.min.js b/grade/report/grader/amd/build/collapse.min.js new file mode 100644 index 0000000000000..5b8839d786f7b --- /dev/null +++ b/grade/report/grader/amd/build/collapse.min.js @@ -0,0 +1,3 @@ +define("gradereport_grader/collapse",["exports","gradereport_grader/collapse/repository","gradereport_grader/search/search_class","core/templates","core/utils","jquery","core/str","core/custom_interaction_events","core/localstorage","core/loadingicon","core/notification","core/pending"],(function(_exports,Repository,_search_class,_templates,_utils,_jquery,_str,_custom_interaction_events,_localstorage,_loadingicon,_notification,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository),_search_class=_interopRequireDefault(_search_class),_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_localstorage=_interopRequireDefault(_localstorage),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const selectors_component=".collapse-columns",selectors_formDropdown=".columnsdropdownform",selectors_formItems={cancel:"cancel",save:"save",checked:'input[type="checkbox"]:checked'},selectors_hider="hide",selectors_expand="expand",selectors_colVal="[data-col]",selectors_itemVal="[data-itemid]",selectors_content='[data-collapse="content"]',selectors_sort='[data-collapse="sort"]',selectors_expandbutton='[data-collapse="expandbutton"]',selectors_avgrowcell='[data-collapse="avgrowcell"]',selectors_menu='[data-collapse="menu"]',selectors_icons=".data-collapse_gradeicons",selectors_count='[data-collapse="count"]',selectors_placeholder='.collapsecolumndropdown [data-region="placeholder"]',selectors_fullDropdown=".collapsecolumndropdown",countIndicator=document.querySelector(selectors_count);class ColumnSearch extends _search_class.default{static init(userID,courseID,defaultSort){return new ColumnSearch(userID,courseID,defaultSort)}constructor(userID,courseID,defaultSort){super(),_defineProperty(this,"userID",-1),_defineProperty(this,"courseID",null),_defineProperty(this,"defaultSort",""),_defineProperty(this,"nodes",[]),_defineProperty(this,"gradeStrings",null),_defineProperty(this,"userStrings",null),_defineProperty(this,"stringMap",[]),this.userID=userID,this.courseID=courseID,this.defaultSort=defaultSort,this.component=document.querySelector(selectors_component);const pendingPromise=new _pending.default;(0,_loadingicon.addIconToContainer)(document.querySelector(".gradeparent")).then((loader=>{setTimeout((()=>{this.getDataset().forEach((item=>{this.nodesUpdate(item)})),this.renderDefault(),loader.remove(),document.querySelector(".gradereport-grader-table").classList.remove("d-none")}),10)})).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)}setComponentSelector(){return".collapse-columns"}setDropdownSelector(){return".searchresultitemscontainer"}setTriggerSelector(){return".collapsecolumn"}getDataset(){if(!this.dataset){const cols=this.fetchDataset();this.dataset=JSON.parse(cols)?JSON.parse(cols).split(","):[]}return this.datasetSize=this.dataset.length,this.dataset}fetchDataset(){return _localstorage.default.get("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID))}setPreferences(){_localstorage.default.set("gradereport_grader_collapseditems_".concat(this.courseID,"_").concat(this.userID),JSON.stringify(this.getDataset().join(",")))}registerClickHandlers(){this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",this.docClickHandler.bind(this))}clickHandler(e){super.clickHandler(e),e.target.closest(selectors_fullDropdown)&&e.stopPropagation()}async docClickHandler(e){var _e$target$closest3;if(e.target.dataset.hider===selectors_hider){var _e$target$closest,_e$target$closest2;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest=e.target.closest(selectors_colVal))||void 0===_e$target$closest?void 0:_e$target$closest.dataset.col:null===(_e$target$closest2=e.target.closest(selectors_itemVal))||void 0===_e$target$closest2?void 0:_e$target$closest2.dataset.itemid;-1===this.getDataset().indexOf(desiredToHide)&&this.getDataset().push(desiredToHide),await this.prefcountpippe(),this.nodesUpdate(desiredToHide)}if((null===(_e$target$closest3=e.target.closest("button"))||void 0===_e$target$closest3?void 0:_e$target$closest3.dataset.hider)===selectors_expand){var _e$target$closest4,_e$target$closest5,_e$target$closest6,_e$target$closest7;e.preventDefault();const desiredToHide=e.target.closest(selectors_colVal)?null===(_e$target$closest4=e.target.closest(selectors_colVal))||void 0===_e$target$closest4?void 0:_e$target$closest4.dataset.col:null===(_e$target$closest5=e.target.closest(selectors_itemVal))||void 0===_e$target$closest5?void 0:_e$target$closest5.dataset.itemid,idx=this.getDataset().indexOf(desiredToHide);this.getDataset().splice(idx,1),await this.prefcountpippe(),this.nodesUpdate(null===(_e$target$closest6=e.target.closest(selectors_colVal))||void 0===_e$target$closest6?void 0:_e$target$closest6.dataset.col),this.nodesUpdate(null===(_e$target$closest7=e.target.closest(selectors_colVal))||void 0===_e$target$closest7?void 0:_e$target$closest7.dataset.itemid)}}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}registerFormEvents(){const form=this.component.querySelector(selectors_formDropdown),events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events),events.forEach((event=>{form.addEventListener(event,(e=>{e.stopPropagation();const submitBtn=form.querySelector('[data-action="'.concat(selectors_formItems.save,'"'));if(e.target.closest("input")){const checkedCount=Array.from(form.querySelectorAll(selectors_formItems.checked)).length;submitBtn.disabled=checkedCount<=0}}),!1),this.searchInput.addEventListener(event,(e=>e.stopPropagation())),this.clearSearchButton.addEventListener(event,(async e=>{e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()}))})),form.addEventListener("submit",(async e=>{if(e.preventDefault(),e.submitter.dataset.action===selectors_formItems.cancel)return void(0,_jquery.default)(this.component).dropdown("toggle");[...form.elements].filter((item=>item.checked)).forEach((item=>{const idx=this.getDataset().indexOf(item.dataset.collapse);this.getDataset().splice(idx,1),this.nodesUpdate(item.dataset.collapse)})),await this.prefcountpippe()}))}nodesUpdate(item){const colNodesToHide=[...document.querySelectorAll('[data-col="'.concat(item,'"]'))],itemIDNodesToHide=[...document.querySelectorAll('[data-itemid="'.concat(item,'"]'))];this.nodes=[...colNodesToHide,...itemIDNodesToHide],this.updateDisplay()}async prefcountpippe(){this.setPreferences(),this.countUpdate(),await this.filterrenderpipe()}async filterDataset(filterableData){const stringUserMap=await this.fetchRequiredUserStrings(),stringGradeMap=await this.fetchRequiredGradeStrings(),customFieldMap=this.fetchCustomFieldValues();this.stringMap=new Map([...stringGradeMap,...stringUserMap,...customFieldMap]);const searching=filterableData.map((s=>{var _mapObj$itemname,_mapObj$category;const mapObj=this.stringMap.get(s);return void 0===mapObj?{key:s,string:s}:{key:s,string:null!==(_mapObj$itemname=mapObj.itemname)&&void 0!==_mapObj$itemname?_mapObj$itemname:this.stringMap.get(s),category:null!==(_mapObj$category=mapObj.category)&&void 0!==_mapObj$category?_mapObj$category:""}}));return""===this.getPreppedSearchTerm()?searching:searching.filter((col=>col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm())))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((column=>{var _column$string,_column$category;return{name:column.key,displayName:null!==(_column$string=column.string)&&void 0!==_column$string?_column$string:column.key,category:null!==(_column$category=column.category)&&void 0!==_column$category?_column$category:""}})))}async filterrenderpipe(){this.updateNodes(),this.setMatchedResults(await this.filterDataset(this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}updateDisplay(){this.nodes.forEach((element=>{const content=element.querySelector(selectors_content),sort=element.querySelector(selectors_sort),expandButton=element.querySelector(selectors_expandbutton),avgRowCell=element.querySelector(selectors_avgrowcell),nodeSet=[element.querySelector(selectors_menu),element.querySelector(selectors_icons),content];element.classList.contains("cell")&&(null!==sort&&(window.location=this.defaultSort),null===content?avgRowCell.classList.contains("d-none")?(null==avgRowCell||avgRowCell.classList.remove("d-none"),null==avgRowCell||avgRowCell.setAttribute("aria-hidden","false")):(null==avgRowCell||avgRowCell.classList.add("d-none"),null==avgRowCell||avgRowCell.setAttribute("aria-hidden","true")):content.classList.contains("d-none")?(element.classList.remove("collapsed"),content.classList.add("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.remove("d-none"),null==node||node.setAttribute("aria-hidden","false")})),null==expandButton||expandButton.classList.add("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","true")):(element.classList.add("collapsed"),content.classList.remove("d-flex"),nodeSet.forEach((node=>{null==node||node.classList.add("d-none"),null==node||node.setAttribute("aria-hidden","true")})),null==expandButton||expandButton.classList.remove("d-none"),null==expandButton||expandButton.setAttribute("aria-hidden","false")))}))}countUpdate(){countIndicator.textContent=this.getDatasetSize(),this.getDatasetSize()>0?(this.component.parentElement.classList.add("d-flex"),this.component.parentElement.classList.remove("d-none")):(this.component.parentElement.classList.remove("d-flex"),this.component.parentElement.classList.add("d-none"))}async renderDefault(){this.setMatchedResults(await this.filterDataset(this.getDataset())),this.filterMatchDataset(),this.countUpdate();const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapsebody",{results:this.getMatchedResults(),userid:this.userID});(0,_templates.replaceNode)(selectors_placeholder,html,js),this.updateNodes(),this.registerFormEvents(),this.registerInputEvents()}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/collapse/collapseresults",{results:this.getMatchedResults(),searchTerm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchCustomFieldValues(){return[...document.querySelectorAll("[data-collapse-name]")].map((field=>[field.parentElement.dataset.col,field.dataset.collapseName]))}fetchRequiredUserStrings(){if(!this.userStrings){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.userStrings=(0,_str.get_strings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.userStrings}fetchRequiredGradeStrings(){return this.gradeStrings||(this.gradeStrings=Repository.gradeItems(this.courseID).then((result=>new Map(result.gradeItems.map((key=>[key.id,key])))))),this.gradeStrings}}return _exports.default=ColumnSearch,_exports.default})); + +//# sourceMappingURL=collapse.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/collapse.min.js.map b/grade/report/grader/amd/build/collapse.min.js.map new file mode 100644 index 0000000000000..8976eaf4b2bd3 --- /dev/null +++ b/grade/report/grader/amd/build/collapse.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"collapse.min.js","sources":["../src/collapse.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to show and hide columns of the report at will.\n *\n * @module gradereport_grader/collapse\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Repository from 'gradereport_grader/collapse/repository';\nimport GradebookSearchClass from 'gradereport_grader/search/search_class';\nimport {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport $ from 'jquery';\nimport {get_strings as getStrings} from 'core/str';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport storage from 'core/localstorage';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n component: '.collapse-columns',\n formDropdown: '.columnsdropdownform',\n formItems: {\n cancel: 'cancel',\n save: 'save',\n checked: 'input[type=\"checkbox\"]:checked'\n },\n hider: 'hide',\n expand: 'expand',\n colVal: '[data-col]',\n itemVal: '[data-itemid]',\n content: '[data-collapse=\"content\"]',\n sort: '[data-collapse=\"sort\"]',\n expandbutton: '[data-collapse=\"expandbutton\"]',\n avgrowcell: '[data-collapse=\"avgrowcell\"]',\n menu: '[data-collapse=\"menu\"]',\n icons: '.data-collapse_gradeicons',\n count: '[data-collapse=\"count\"]',\n placeholder: '.collapsecolumndropdown [data-region=\"placeholder\"]',\n fullDropdown: '.collapsecolumndropdown',\n};\n\nconst countIndicator = document.querySelector(selectors.count);\n\nexport default class ColumnSearch extends GradebookSearchClass {\n\n userID = -1;\n courseID = null;\n defaultSort = '';\n\n nodes = [];\n\n gradeStrings = null;\n userStrings = null;\n stringMap = [];\n\n static init(userID, courseID, defaultSort) {\n return new ColumnSearch(userID, courseID, defaultSort);\n }\n\n constructor(userID, courseID, defaultSort) {\n super();\n this.userID = userID;\n this.courseID = courseID;\n this.defaultSort = defaultSort;\n this.component = document.querySelector(selectors.component);\n\n const pendingPromise = new Pending();\n // Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user).\n addIconToContainer(document.querySelector('.gradeparent')).then((loader) => {\n setTimeout(() => {\n // Get the users' checked columns to change.\n this.getDataset().forEach((item) => {\n this.nodesUpdate(item);\n });\n this.renderDefault();\n\n // Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.\n loader.remove();\n document.querySelector('.gradereport-grader-table').classList.remove('d-none');\n }, 10);\n }).then(() => pendingPromise.resolve()).catch(Notification.exception);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n setComponentSelector() {\n return '.collapse-columns';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n setDropdownSelector() {\n return '.searchresultitemscontainer';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n setTriggerSelector() {\n return '.collapsecolumn';\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Array}\n */\n getDataset() {\n if (!this.dataset) {\n const cols = this.fetchDataset();\n this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : [];\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {string}\n */\n fetchDataset() {\n return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`);\n }\n\n /**\n * Given a user performs an action, update the users' preferences.\n */\n setPreferences() {\n storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`,\n JSON.stringify(this.getDataset().join(','))\n );\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\n document.addEventListener('click', this.docClickHandler.bind(this));\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e);\n // Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form.\n if (e.target.closest(selectors.fullDropdown)) {\n e.stopPropagation();\n }\n }\n\n /**\n * Externally defined click function to improve memory handling.\n *\n * @param {MouseEvent} e\n * @returns {Promise}\n */\n async docClickHandler(e) {\n if (e.target.dataset.hider === selectors.hider) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n if (idx === -1) {\n this.getDataset().push(desiredToHide);\n }\n await this.prefcountpippe();\n\n this.nodesUpdate(desiredToHide);\n }\n\n if (e.target.closest('button')?.dataset.hider === selectors.expand) {\n e.preventDefault();\n const desiredToHide = e.target.closest(selectors.colVal) ?\n e.target.closest(selectors.colVal)?.dataset.col :\n e.target.closest(selectors.itemVal)?.dataset.itemid;\n const idx = this.getDataset().indexOf(desiredToHide);\n this.getDataset().splice(idx, 1);\n\n await this.prefcountpippe();\n\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);\n this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);\n }\n }\n\n /**\n * Handle any keyboard inputs.\n */\n registerInputEvents() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.searchInput.value === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Handle the form submission within the dropdown.\n */\n registerFormEvents() {\n const form = this.component.querySelector(selectors.formDropdown);\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n // Register clicks & keyboard form handling.\n events.forEach((event) => {\n form.addEventListener(event, (e) => {\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n const submitBtn = form.querySelector(`[data-action=\"${selectors.formItems.save}\"`);\n if (e.target.closest('input')) {\n const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length;\n // Check if any are clicked or not then change disabled.\n submitBtn.disabled = checkedCount <= 0;\n }\n }, false);\n\n // Stop Bootstrap from being clever.\n this.searchInput.addEventListener(event, e => e.stopPropagation());\n this.clearSearchButton.addEventListener(event, async(e) => {\n e.stopPropagation();\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n await this.filterrenderpipe();\n });\n });\n\n form.addEventListener('submit', async(e) => {\n e.preventDefault();\n if (e.submitter.dataset.action === selectors.formItems.cancel) {\n $(this.component).dropdown('toggle');\n return;\n }\n // Get the users' checked columns to change.\n const checkedItems = [...form.elements].filter(item => item.checked);\n checkedItems.forEach((item) => {\n const idx = this.getDataset().indexOf(item.dataset.collapse);\n this.getDataset().splice(idx, 1);\n this.nodesUpdate(item.dataset.collapse);\n });\n await this.prefcountpippe();\n });\n }\n\n nodesUpdate(item) {\n const colNodesToHide = [...document.querySelectorAll(`[data-col=\"${item}\"]`)];\n const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid=\"${item}\"]`)];\n this.nodes = [...colNodesToHide, ...itemIDNodesToHide];\n this.updateDisplay();\n }\n\n /**\n * Update the user preferences, count display then render the results.\n *\n * @returns {Promise}\n */\n async prefcountpippe() {\n this.setPreferences();\n this.countUpdate();\n await this.filterrenderpipe();\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} An array of objects containing the system reference and the user readable value.\n */\n async filterDataset(filterableData) {\n const stringUserMap = await this.fetchRequiredUserStrings();\n const stringGradeMap = await this.fetchRequiredGradeStrings();\n // Custom user profile fields are not in our string map and need a bit of extra love.\n const customFieldMap = this.fetchCustomFieldValues();\n this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]);\n\n const searching = filterableData.map(s => {\n const mapObj = this.stringMap.get(s);\n if (mapObj === undefined) {\n return {key: s, string: s};\n }\n return {\n key: s,\n string: mapObj.itemname ?? this.stringMap.get(s),\n category: mapObj.category ?? '',\n };\n });\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return searching;\n }\n // Other times we want to actually filter the content.\n return searching.filter((col) => {\n return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm());\n });\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((column) => {\n return {\n name: column.key,\n displayName: column.string ?? column.key,\n category: column.category ?? '',\n };\n })\n );\n }\n\n /**\n * Update any changeable nodes, filter and then render the result.\n *\n * @returns {Promise}\n */\n async filterrenderpipe() {\n this.updateNodes();\n this.setMatchedResults(await this.filterDataset(this.getDataset()));\n this.filterMatchDataset();\n await this.renderDropdown();\n }\n\n /**\n * With an array of nodes, switch their classes and values.\n */\n updateDisplay() {\n this.nodes.forEach((element) => {\n const content = element.querySelector(selectors.content);\n const sort = element.querySelector(selectors.sort);\n const expandButton = element.querySelector(selectors.expandbutton);\n const avgRowCell = element.querySelector(selectors.avgrowcell);\n const nodeSet = [\n element.querySelector(selectors.menu),\n element.querySelector(selectors.icons),\n content\n ];\n\n // This can be further improved to reduce redundant similar calls.\n if (element.classList.contains('cell')) {\n // The column is actively being sorted, lets reset that and reload the page.\n if (sort !== null) {\n window.location = this.defaultSort;\n }\n if (content === null) {\n if (avgRowCell.classList.contains('d-none')) {\n avgRowCell?.classList.remove('d-none');\n avgRowCell?.setAttribute('aria-hidden', 'false');\n } else {\n avgRowCell?.classList.add('d-none');\n avgRowCell?.setAttribute('aria-hidden', 'true');\n }\n } else if (content.classList.contains('d-none')) {\n // We should always have content but some cells do not contain menus or other actions.\n element.classList.remove('collapsed');\n content.classList.add('d-flex');\n nodeSet.forEach(node => {\n node?.classList.remove('d-none');\n node?.setAttribute('aria-hidden', 'false');\n });\n expandButton?.classList.add('d-none');\n expandButton?.setAttribute('aria-hidden', 'true');\n } else {\n element.classList.add('collapsed');\n content.classList.remove('d-flex');\n nodeSet.forEach(node => {\n node?.classList.add('d-none');\n node?.setAttribute('aria-hidden', 'true');\n });\n expandButton?.classList.remove('d-none');\n expandButton?.setAttribute('aria-hidden', 'false');\n }\n }\n });\n }\n\n /**\n * Update the visual count of collapsed columns or hide the count all together.\n */\n countUpdate() {\n countIndicator.textContent = this.getDatasetSize();\n if (this.getDatasetSize() > 0) {\n this.component.parentElement.classList.add('d-flex');\n this.component.parentElement.classList.remove('d-none');\n } else {\n this.component.parentElement.classList.remove('d-flex');\n this.component.parentElement.classList.add('d-none');\n }\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(this.getDataset()));\n this.filterMatchDataset();\n\n // Update the collapsed button pill.\n this.countUpdate();\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {\n 'results': this.getMatchedResults(),\n 'userid': this.userID,\n });\n replaceNode(selectors.placeholder, html, js);\n this.updateNodes();\n\n // Given we now have the body, we can set up more triggers.\n this.registerFormEvents();\n this.registerInputEvents();\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {\n 'results': this.getMatchedResults(),\n 'searchTerm': this.getSearchTerm(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * If we have any custom user profile fields, grab their system & readable names to add to our string map.\n *\n * @returns {[string,*][]} An array of associated string arrays ready for our map.\n */\n fetchCustomFieldValues() {\n const customFields = document.querySelectorAll('[data-collapse-name]');\n // Cast from NodeList to array to grab all the values.\n return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredUserStrings() {\n if (!this.userStrings) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.userStrings = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.userStrings;\n }\n\n /**\n * Given the set of gradable items we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n fetchRequiredGradeStrings() {\n if (!this.gradeStrings) {\n this.gradeStrings = Repository.gradeItems(this.courseID)\n .then((result) => new Map(\n result.gradeItems.map(key => ([key.id, key]))\n ));\n }\n return this.gradeStrings;\n }\n}\n"],"names":["selectors","cancel","save","checked","countIndicator","document","querySelector","ColumnSearch","GradebookSearchClass","userID","courseID","defaultSort","constructor","component","pendingPromise","Pending","then","loader","setTimeout","getDataset","forEach","item","nodesUpdate","renderDefault","remove","classList","resolve","catch","Notification","exception","setComponentSelector","setDropdownSelector","setTriggerSelector","this","dataset","cols","fetchDataset","JSON","parse","split","datasetSize","length","storage","get","setPreferences","set","stringify","join","registerClickHandlers","addEventListener","clickHandler","bind","docClickHandler","e","target","closest","stopPropagation","hider","preventDefault","desiredToHide","_e$target$closest","col","_e$target$closest2","itemid","indexOf","push","prefcountpippe","_e$target$closest4","_e$target$closest5","idx","splice","_e$target$closest6","_e$target$closest7","registerInputEvents","searchInput","async","setSearchTerms","value","clearSearchButton","add","filterrenderpipe","registerFormEvents","form","events","CustomEvents","activate","keyboardActivate","define","event","submitBtn","checkedCount","Array","from","querySelectorAll","disabled","submitter","action","dropdown","elements","filter","collapse","colNodesToHide","itemIDNodesToHide","nodes","updateDisplay","countUpdate","filterableData","stringUserMap","fetchRequiredUserStrings","stringGradeMap","fetchRequiredGradeStrings","customFieldMap","fetchCustomFieldValues","stringMap","Map","searching","map","s","mapObj","undefined","key","string","itemname","category","getPreppedSearchTerm","toString","toLowerCase","includes","filterMatchDataset","setMatchedResults","getMatchedResults","column","name","displayName","updateNodes","filterDataset","renderDropdown","element","content","sort","expandButton","avgRowCell","nodeSet","contains","window","location","setAttribute","node","textContent","getDatasetSize","parentElement","html","js","getSearchTerm","getHTMLElements","searchDropdown","field","collapseName","userStrings","requiredStrings","stringArray","index","gradeStrings","Repository","gradeItems","result","id"],"mappings":"w/DAmCMA,oBACS,oBADTA,uBAEY,uBAFZA,oBAGS,CACPC,OAAQ,SACRC,KAAM,OACNC,QAAS,kCANXH,gBAQK,OARLA,iBASM,SATNA,iBAUM,aAVNA,kBAWO,gBAXPA,kBAYO,4BAZPA,eAaI,yBAbJA,uBAcY,iCAdZA,qBAeU,+BAfVA,eAgBI,yBAhBJA,gBAiBK,4BAjBLA,gBAkBK,0BAlBLA,sBAmBW,sDAnBXA,uBAoBY,0BAGZI,eAAiBC,SAASC,cAAcN,uBAEzBO,qBAAqBC,kCAY1BC,OAAQC,SAAUC,oBACnB,IAAIJ,aAAaE,OAAQC,SAAUC,aAG9CC,YAAYH,OAAQC,SAAUC,oDAdpB,mCACC,yCACG,iCAEN,wCAEO,yCACD,uCACF,SAQHF,OAASA,YACTC,SAAWA,cACXC,YAAcA,iBACdE,UAAYR,SAASC,cAAcN,2BAElCc,eAAiB,IAAIC,qDAERV,SAASC,cAAc,iBAAiBU,MAAMC,SAC7DC,YAAW,UAEFC,aAAaC,SAASC,YAClBC,YAAYD,cAEhBE,gBAGLN,OAAOO,SACPnB,SAASC,cAAc,6BAA6BmB,UAAUD,OAAO,YACtE,OACJR,MAAK,IAAMF,eAAeY,YAAWC,MAAMC,sBAAaC,WAQ/DC,6BACW,oBAQXC,4BACW,8BAQXC,2BACW,kBAQXb,iBACSc,KAAKC,QAAS,OACTC,KAAOF,KAAKG,oBACbF,QAAUG,KAAKC,MAAMH,MAAQE,KAAKC,MAAMH,MAAMI,MAAM,KAAO,eAE/DC,YAAcP,KAAKC,QAAQO,OACzBR,KAAKC,QAQhBE,sBACWM,sBAAQC,gDAAyCV,KAAKvB,qBAAYuB,KAAKxB,SAMlFmC,uCACYC,gDAAyCZ,KAAKvB,qBAAYuB,KAAKxB,QACnE4B,KAAKS,UAAUb,KAAKd,aAAa4B,KAAK,OAO9CC,6BAESnC,UAAUoC,iBAAiB,QAAShB,KAAKiB,aAAaC,KAAKlB,OAEhE5B,SAAS4C,iBAAiB,QAAShB,KAAKmB,gBAAgBD,KAAKlB,OAQjEiB,aAAaG,SACHH,aAAaG,GAEfA,EAAEC,OAAOC,QAAQvD,yBACjBqD,EAAEG,wCAUYH,6BACdA,EAAEC,OAAOpB,QAAQuB,QAAUzD,gBAAiB,0CAC5CqD,EAAEK,uBACIC,cAAgBN,EAAEC,OAAOC,QAAQvD,4CACnCqD,EAAEC,OAAOC,QAAQvD,sDAAjB4D,kBAAoC1B,QAAQ2B,+BAC5CR,EAAEC,OAAOC,QAAQvD,wDAAjB8D,mBAAqC5B,QAAQ6B,QAEpC,IADD9B,KAAKd,aAAa6C,QAAQL,qBAE7BxC,aAAa8C,KAAKN,qBAErB1B,KAAKiC,sBAEN5C,YAAYqC,8CAGjBN,EAAEC,OAAOC,QAAQ,kEAAWrB,QAAQuB,SAAUzD,iBAAkB,iFAChEqD,EAAEK,uBACIC,cAAgBN,EAAEC,OAAOC,QAAQvD,6CACnCqD,EAAEC,OAAOC,QAAQvD,uDAAjBmE,mBAAoCjC,QAAQ2B,+BAC5CR,EAAEC,OAAOC,QAAQvD,wDAAjBoE,mBAAqClC,QAAQ6B,OAC3CM,IAAMpC,KAAKd,aAAa6C,QAAQL,oBACjCxC,aAAamD,OAAOD,IAAK,SAExBpC,KAAKiC,sBAEN5C,uCAAY+B,EAAEC,OAAOC,QAAQvD,uDAAjBuE,mBAAoCrC,QAAQ2B,UACxDvC,uCAAY+B,EAAEC,OAAOC,QAAQvD,uDAAjBwE,mBAAoCtC,QAAQ6B,SAOrEU,2BAESC,YAAYzB,iBAAiB,SAAS,oBAAS0B,eAC3CC,eAAe3C,KAAKyC,YAAYG,OAEN,KAA3B5C,KAAKyC,YAAYG,WAEZC,kBAAkBrD,UAAUsD,IAAI,eAGhCD,kBAAkBrD,UAAUD,OAAO,gBAGtCS,KAAK+C,qBACZ,MAMPC,2BACUC,KAAOjD,KAAKpB,UAAUP,cAAcN,wBACpCmF,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOlF,SAAU8E,QAG9BA,OAAO/D,SAASoE,QACZN,KAAKjC,iBAAiBuC,OAAQnC,IAE1BA,EAAEG,wBACIiC,UAAYP,KAAK5E,sCAA+BN,oBAAoBE,cACtEmD,EAAEC,OAAOC,QAAQ,SAAU,OACrBmC,aAAeC,MAAMC,KAAKV,KAAKW,iBAAiB7F,oBAAoBG,UAAUsC,OAEpFgD,UAAUK,SAAWJ,cAAgB,MAE1C,QAGEhB,YAAYzB,iBAAiBuC,OAAOnC,GAAKA,EAAEG,yBAC3CsB,kBAAkB7B,iBAAiBuC,OAAOb,MAAAA,IAC3CtB,EAAEG,uBACGkB,YAAYG,MAAQ,QACpBD,eAAe3C,KAAKyC,YAAYG,aAC/B5C,KAAK+C,yBAInBE,KAAKjC,iBAAiB,UAAU0B,MAAAA,OAC5BtB,EAAEK,iBACEL,EAAE0C,UAAU7D,QAAQ8D,SAAWhG,oBAAoBC,sCACjDgC,KAAKpB,WAAWoF,SAAS,UAIV,IAAIf,KAAKgB,UAAUC,QAAO9E,MAAQA,KAAKlB,UAC/CiB,SAASC,aACZgD,IAAMpC,KAAKd,aAAa6C,QAAQ3C,KAAKa,QAAQkE,eAC9CjF,aAAamD,OAAOD,IAAK,QACzB/C,YAAYD,KAAKa,QAAQkE,mBAE5BnE,KAAKiC,oBAInB5C,YAAYD,YACFgF,eAAiB,IAAIhG,SAASwF,sCAA+BxE,aAC7DiF,kBAAoB,IAAIjG,SAASwF,yCAAkCxE,kBACpEkF,MAAQ,IAAIF,kBAAmBC,wBAC/BE,4CASA5D,sBACA6D,oBACCxE,KAAK+C,uCASK0B,sBACVC,oBAAsB1E,KAAK2E,2BAC3BC,qBAAuB5E,KAAK6E,4BAE5BC,eAAiB9E,KAAK+E,8BACvBC,UAAY,IAAIC,IAAI,IAAIL,kBAAmBF,iBAAkBI,uBAE5DI,UAAYT,eAAeU,KAAIC,gDAC3BC,OAASrF,KAAKgF,UAAUtE,IAAI0E,eACnBE,IAAXD,OACO,CAACE,IAAKH,EAAGI,OAAQJ,GAErB,CACHG,IAAKH,EACLI,gCAAQH,OAAOI,sDAAYzF,KAAKgF,UAAUtE,IAAI0E,GAC9CM,kCAAUL,OAAOK,sDAAY,aAID,KAAhC1F,KAAK2F,uBACET,UAGJA,UAAUhB,QAAQtC,KACdA,IAAI4D,OAAOI,WAAWC,cAAcC,SAAS9F,KAAK2F,0BAOjEI,0BACSC,kBACDhG,KAAKiG,oBAAoBd,KAAKe,mDACnB,CACHC,KAAMD,OAAOX,IACba,mCAAaF,OAAOV,gDAAUU,OAAOX,IACrCG,kCAAUQ,OAAOR,sDAAY,sCAYpCW,mBACAL,wBAAwBhG,KAAKsG,cAActG,KAAKd,oBAChD6G,2BACC/F,KAAKuG,iBAMfhC,qBACSD,MAAMnF,SAASqH,gBACVC,QAAUD,QAAQnI,cAAcN,mBAChC2I,KAAOF,QAAQnI,cAAcN,gBAC7B4I,aAAeH,QAAQnI,cAAcN,wBACrC6I,WAAaJ,QAAQnI,cAAcN,sBACnC8I,QAAU,CACZL,QAAQnI,cAAcN,gBACtByI,QAAQnI,cAAcN,iBACtB0I,SAIAD,QAAQhH,UAAUsH,SAAS,UAEd,OAATJ,OACAK,OAAOC,SAAWhH,KAAKtB,aAEX,OAAZ+H,QACIG,WAAWpH,UAAUsH,SAAS,WAC9BF,MAAAA,YAAAA,WAAYpH,UAAUD,OAAO,UAC7BqH,MAAAA,YAAAA,WAAYK,aAAa,cAAe,WAExCL,MAAAA,YAAAA,WAAYpH,UAAUsD,IAAI,UAC1B8D,MAAAA,YAAAA,WAAYK,aAAa,cAAe,SAErCR,QAAQjH,UAAUsH,SAAS,WAElCN,QAAQhH,UAAUD,OAAO,aACzBkH,QAAQjH,UAAUsD,IAAI,UACtB+D,QAAQ1H,SAAQ+H,OACZA,MAAAA,MAAAA,KAAM1H,UAAUD,OAAO,UACvB2H,MAAAA,MAAAA,KAAMD,aAAa,cAAe,YAEtCN,MAAAA,cAAAA,aAAcnH,UAAUsD,IAAI,UAC5B6D,MAAAA,cAAAA,aAAcM,aAAa,cAAe,UAE1CT,QAAQhH,UAAUsD,IAAI,aACtB2D,QAAQjH,UAAUD,OAAO,UACzBsH,QAAQ1H,SAAQ+H,OACZA,MAAAA,MAAAA,KAAM1H,UAAUsD,IAAI,UACpBoE,MAAAA,MAAAA,KAAMD,aAAa,cAAe,WAEtCN,MAAAA,cAAAA,aAAcnH,UAAUD,OAAO,UAC/BoH,MAAAA,cAAAA,aAAcM,aAAa,cAAe,cAS1DzC,cACIrG,eAAegJ,YAAcnH,KAAKoH,iBAC9BpH,KAAKoH,iBAAmB,QACnBxI,UAAUyI,cAAc7H,UAAUsD,IAAI,eACtClE,UAAUyI,cAAc7H,UAAUD,OAAO,iBAEzCX,UAAUyI,cAAc7H,UAAUD,OAAO,eACzCX,UAAUyI,cAAc7H,UAAUsD,IAAI,sCAQ1CkD,wBAAwBhG,KAAKsG,cAActG,KAAKd,oBAChD6G,0BAGAvB,oBACC8C,KAACA,KAADC,GAAOA,UAAY,+BAAiB,2CAA4C,SACvEvH,KAAKiG,2BACNjG,KAAKxB,oCAEPT,sBAAuBuJ,KAAMC,SACpClB,mBAGArD,0BACAR,mDAOC8E,KAACA,KAADC,GAAOA,UAAY,+BAAiB,8CAA+C,SAC1EvH,KAAKiG,+BACFjG,KAAKwH,qDAEHxH,KAAKyH,kBAAkBC,eAAgBJ,KAAMC,IAQrExC,+BAGW,IAFc3G,SAASwF,iBAAiB,yBAEtBuB,KAAIwC,OAAS,CAACA,MAAMN,cAAcpH,QAAQ2B,IAAK+F,MAAM1H,QAAQ2H,gBAS1FjD,+BACS3E,KAAK6H,YAAa,OACbC,gBAAkB,CACpB,WACA,YACA,WACA,QACA,OACA,UACA,aACA,cACA,WACA,SACA,eAECD,aAAc,oBAAWC,gBAAgB3C,KAAKI,OAAUA,IAAAA,SACxDxG,MAAMgJ,aAAgB,IAAI9C,IACvB6C,gBAAgB3C,KAAI,CAACI,IAAKyC,QAAW,CAACzC,IAAKwC,YAAYC,oBAG5DhI,KAAK6H,YAShBhD,mCACS7E,KAAKiI,oBACDA,aAAeC,WAAWC,WAAWnI,KAAKvB,UAC1CM,MAAMqJ,QAAW,IAAInD,IAClBmD,OAAOD,WAAWhD,KAAII,KAAQ,CAACA,IAAI8C,GAAI9C,WAG5CvF,KAAKiI"} \ No newline at end of file diff --git a/grade/report/grader/amd/build/collapse/repository.min.js b/grade/report/grader/amd/build/collapse/repository.min.js new file mode 100644 index 0000000000000..c2a90d4354337 --- /dev/null +++ b/grade/report/grader/amd/build/collapse/repository.min.js @@ -0,0 +1,10 @@ +define("gradereport_grader/collapse/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * A repo for the collapsing in the grader report. + * + * @module gradereport_grader/collapse/repository + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.gradeItems=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.gradeItems=courseid=>{const request={methodname:"core_grades_get_gradeitems",args:{courseid:courseid}};return _ajax.default.call([request])[0]}})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/collapse/repository.min.js.map b/grade/report/grader/amd/build/collapse/repository.min.js.map new file mode 100644 index 0000000000000..465f0687b1b8e --- /dev/null +++ b/grade/report/grader/amd/build/collapse/repository.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"repository.min.js","sources":["../../src/collapse/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A repo for the collapsing in the grader report.\n *\n * @module gradereport_grader/collapse/repository\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on gradeitems we'll need in the column collapser.\n *\n * @method gradeItems\n * @param {Number} courseid What course to fetch the gradeitems for\n * @return {object} jQuery promise\n */\nexport const gradeItems = (courseid) => {\n const request = {\n methodname: 'core_grades_get_gradeitems',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAgC2BA,iBACjBC,QAAU,CACZC,WAAY,6BACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file diff --git a/grade/report/grader/amd/build/search/search_class.min.js b/grade/report/grader/amd/build/search/search_class.min.js index d33a1664e2644..90855441b3d09 100644 --- a/grade/report/grader/amd/build/search/search_class.min.js +++ b/grade/report/grader/amd/build/search/search_class.min.js @@ -5,6 +5,6 @@ define("gradereport_grader/search/search_class",["exports","jquery","core/custom * @module gradereport_grader/search/search_class * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery);const events=["keydown",(_custom_interaction_events=_interopRequireDefault(_custom_interaction_events)).default.events.activate,_custom_interaction_events.default.events.keyboardActivate];return _exports.default=class{constructor(){var _this$searchInput$val;_defineProperty(this,"selectors",{component:this.setComponentSelector(),trigger:this.setTriggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.setDropdownSelector(),resultitems:'[role="option"]',viewall:"#select-all"}),_defineProperty(this,"matchedResults",[]),_defineProperty(this,"searchTerm",""),_defineProperty(this,"preppedSearchTerm",null),_defineProperty(this,"resultNodes",[]),_defineProperty(this,"currentNode",null),_defineProperty(this,"currentViewAll",null),_defineProperty(this,"dataset",null),_defineProperty(this,"datasetSize",0),_defineProperty(this,"component",document.querySelector(this.selectors.component)),_defineProperty(this,"searchInput",this.component.querySelector(this.selectors.input)),_defineProperty(this,"searchDropdown",this.component.querySelector(this.selectors.dropdown)),_defineProperty(this,"$searchButton",(0,_jquery.default)(this.selectors.trigger)),_defineProperty(this,"clearSearchButton",this.component.querySelector(this.selectors.clearSearch)),_defineProperty(this,"$component",(0,_jquery.default)(this.component)),_defineProperty(this,"selectNode",(node=>{node.focus({preventScroll:!0}),this.searchDropdown.scrollTop=node.offsetTop-node.clientHeight/2})),_defineProperty(this,"moveToFirstNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[0])})),_defineProperty(this,"moveToLastNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[this.resultNodes.length-1])})),_defineProperty(this,"moveToNode",(index=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[index])})),this.setSearchTerms(null!==(_this$searchInput$val=this.searchInput.value)&&void 0!==_this$searchInput$val?_this$searchInput$val:""),this.registerClickHandlers(),this.registerKeyHandlers(),this.registerInputHandlers()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}filterDataset(dataset){throw new Error("filterDataset(".concat(dataset,") must be implemented in ").concat(this.constructor.name))}filterMatchDataset(){throw new Error("filterMatchDataset() must be implemented in ".concat(this.constructor.name))}renderDropdown(){throw new Error("renderDropdown() must be implemented in ".concat(this.constructor.name))}setComponentSelector(){throw new Error("setComponentSelector() must be implemented in ".concat(this.constructor.name))}setDropdownSelector(){throw new Error("setDropdownSelector() must be implemented in ".concat(this.constructor.name))}setTriggerSelector(){throw new Error("setTriggerSelector() must be implemented in ".concat(this.constructor.name))}async getDataset(){return this.dataset||(this.dataset=await this.fetchDataset()),this.datasetSize=this.dataset.length,this.dataset}getDatasetSize(){return this.datasetSize}getMatchedResults(){return this.matchedResults}setMatchedResults(result){this.matchedResults=result}getSearchTerm(){return this.searchTerm}getPreppedSearchTerm(){return this.preppedSearchTerm}setSearchTerms(result){this.searchTerm=result,this.preppedSearchTerm=result.toLowerCase()}getHTMLElements(){return this.updateNodes(),{searchDropdown:this.searchDropdown,currentViewAll:this.currentViewAll,searchInput:this.searchInput,clearSearchButton:this.clearSearchButton}}closeSearch(){this.toggleDropdown(),this.clearSearchButton.classList.add("d-none"),this.setSearchTerms(""),this.searchInput.value=""}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),this.$searchButton.attr("aria-expanded",on),on?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show()):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide())}updateNodes(){this.resultNodes=[...this.component.querySelectorAll(this.selectors.resultitems)],this.currentNode=this.resultNodes.find((r=>r.id===document.activeElement.id)),this.currentViewAll=this.component.querySelector(this.selectors.viewall)}registerClickHandlers(){this.$searchButton.on("click",(()=>{this.toggleDropdown()})),this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()}))}registerKeyHandlers(){_custom_interaction_events.default.define(document,events),events.forEach((event=>{this.component.addEventListener(event,this.keyHandler.bind(this))}))}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?(this.toggleDropdown(),this.clearSearchButton.classList.add("d-none")):(this.clearSearchButton.classList.remove("d-none"),await this.renderAndShow())}),300))}async renderAndShow(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),await this.filterMatchDataset(),await this.renderDropdown(),this.toggleDropdown(!0)}keyUpDown(direction,e){e.preventDefault(),e.stopPropagation(),document.activeElement===this.searchInput&&this.resultNodes.length>0&&(-1===direction?this.moveToLastNode():this.moveToFirstNode());const index=this.resultNodes.indexOf(this.currentNode);this.currentNode&&(-1===direction?0===index?this.moveToLastNode():this.moveToNode(index-1):index+1>=this.resultNodes.length?this.moveToFirstNode():this.moveToNode(index+1))}async clickHandler(e){this.updateNodes(),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href),e.target.closest(this.selectors.clearSearch)&&0===e.button&&(this.closeSearch(),this.searchInput.focus({preventScroll:!0})),e.target.closest(this.selectors.input)&&""!==this.getSearchTerm()&&0===e.button&&await this.renderAndShow()}keyHandler(e){switch(this.updateNodes(),e.key){case"ArrowUp":this.keyUpDown(-1,e);break;case"ArrowDown":this.keyUpDown(1,e);break;case"Home":e.preventDefault(),this.moveToFirstNode();break;case"End":e.preventDefault(),this.moveToLastNode();break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch()),e.target.closest(this.selectors.viewall)&&this.closeSearch()}}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery);const events=["keydown",(_custom_interaction_events=_interopRequireDefault(_custom_interaction_events)).default.events.activate,_custom_interaction_events.default.events.keyboardActivate];return _exports.default=class{constructor(){var _this$searchInput$val,_this$searchInput;_defineProperty(this,"selectors",{component:this.setComponentSelector(),trigger:this.setTriggerSelector(),input:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]',dropdown:this.setDropdownSelector(),resultitems:'[role="option"]',viewall:"#select-all"}),_defineProperty(this,"matchedResults",[]),_defineProperty(this,"searchTerm",""),_defineProperty(this,"preppedSearchTerm",null),_defineProperty(this,"resultNodes",[]),_defineProperty(this,"currentNode",null),_defineProperty(this,"currentViewAll",null),_defineProperty(this,"dataset",null),_defineProperty(this,"datasetSize",0),_defineProperty(this,"component",document.querySelector(this.selectors.component)),_defineProperty(this,"searchInput",this.component.querySelector(this.selectors.input)),_defineProperty(this,"searchDropdown",this.component.querySelector(this.selectors.dropdown)),_defineProperty(this,"$searchButton",(0,_jquery.default)(this.selectors.trigger)),_defineProperty(this,"clearSearchButton",this.component.querySelector(this.selectors.clearSearch)),_defineProperty(this,"$component",(0,_jquery.default)(this.component)),_defineProperty(this,"selectNode",(node=>{node.focus({preventScroll:!0}),this.searchDropdown.scrollTop=node.offsetTop-node.clientHeight/2})),_defineProperty(this,"moveToFirstNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[0])})),_defineProperty(this,"moveToLastNode",(()=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[this.resultNodes.length-1])})),_defineProperty(this,"moveToNode",(index=>{this.resultNodes.length>0&&this.selectNode(this.resultNodes[index])})),this.setSearchTerms(null!==(_this$searchInput$val=null===(_this$searchInput=this.searchInput)||void 0===_this$searchInput?void 0:_this$searchInput.value)&&void 0!==_this$searchInput$val?_this$searchInput$val:""),this.registerClickHandlers(),this.registerKeyHandlers(),null!==this.searchInput&&this.registerInputHandlers()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}filterDataset(dataset){throw new Error("filterDataset(".concat(dataset,") must be implemented in ").concat(this.constructor.name))}filterMatchDataset(){throw new Error("filterMatchDataset() must be implemented in ".concat(this.constructor.name))}renderDropdown(){throw new Error("renderDropdown() must be implemented in ".concat(this.constructor.name))}setComponentSelector(){throw new Error("setComponentSelector() must be implemented in ".concat(this.constructor.name))}setDropdownSelector(){throw new Error("setDropdownSelector() must be implemented in ".concat(this.constructor.name))}setTriggerSelector(){throw new Error("setTriggerSelector() must be implemented in ".concat(this.constructor.name))}async getDataset(){return this.dataset||(this.dataset=await this.fetchDataset()),this.datasetSize=this.dataset.length,this.dataset}getDatasetSize(){return this.datasetSize}getMatchedResults(){return this.matchedResults}setMatchedResults(result){this.matchedResults=result}getSearchTerm(){return this.searchTerm}getPreppedSearchTerm(){return this.preppedSearchTerm}setSearchTerms(result){this.searchTerm=result,this.preppedSearchTerm=result.toLowerCase()}getHTMLElements(){return this.updateNodes(),{searchDropdown:this.searchDropdown,currentViewAll:this.currentViewAll,searchInput:this.searchInput,clearSearchButton:this.clearSearchButton}}closeSearch(){this.toggleDropdown(),this.clearSearchButton.classList.add("d-none"),this.setSearchTerms(""),this.searchInput.value=""}toggleDropdown(){let on=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.$component.dropdown("toggle"),this.$searchButton.attr("aria-expanded",on),on?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show()):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide())}updateNodes(){this.resultNodes=[...this.component.querySelectorAll(this.selectors.resultitems)],this.currentNode=this.resultNodes.find((r=>r.id===document.activeElement.id)),this.currentViewAll=this.component.querySelector(this.selectors.viewall),this.clearSearchButton=this.component.querySelector(this.selectors.clearSearch),this.searchInput=this.component.querySelector(this.selectors.input),this.searchDropdown=this.component.querySelector(this.selectors.dropdown)}registerClickHandlers(){this.$searchButton.on("click",(()=>{this.toggleDropdown()})),this.component.addEventListener("click",this.clickHandler.bind(this)),document.addEventListener("click",(e=>{!e.target.closest(this.selectors.component)&&this.searchDropdown.classList.contains("show")&&this.toggleDropdown()}))}registerKeyHandlers(){_custom_interaction_events.default.define(document,events),events.forEach((event=>{this.component.addEventListener(event,this.keyHandler.bind(this))}))}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?(this.toggleDropdown(),this.clearSearchButton.classList.add("d-none")):(this.clearSearchButton.classList.remove("d-none"),await this.renderAndShow())}),300))}async renderAndShow(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),await this.filterMatchDataset(),await this.renderDropdown(),this.toggleDropdown(!0)}keyUpDown(direction,e){e.preventDefault(),e.stopPropagation(),document.activeElement===this.searchInput&&this.resultNodes.length>0&&(-1===direction?this.moveToLastNode():this.moveToFirstNode());const index=this.resultNodes.indexOf(this.currentNode);this.currentNode&&(-1===direction?0===index?this.moveToLastNode():this.moveToNode(index-1):index+1>=this.resultNodes.length?this.moveToFirstNode():this.moveToNode(index+1))}async clickHandler(e){this.updateNodes(),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href),e.target.closest(this.selectors.clearSearch)&&0===e.button&&(this.closeSearch(),this.searchInput.focus({preventScroll:!0})),e.target.closest(this.selectors.input)&&""!==this.getSearchTerm()&&0===e.button&&await this.renderAndShow()}keyHandler(e){switch(this.updateNodes(),e.key){case"ArrowUp":this.keyUpDown(-1,e);break;case"ArrowDown":this.keyUpDown(1,e);break;case"Home":e.preventDefault(),this.moveToFirstNode();break;case"End":e.preventDefault(),this.moveToLastNode();break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch()),e.target.closest(this.selectors.viewall)&&this.closeSearch()}}},_exports.default})); //# sourceMappingURL=search_class.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/search/search_class.min.js.map b/grade/report/grader/amd/build/search/search_class.min.js.map index 03492144f0970..550c1f573653e 100644 --- a/grade/report/grader/amd/build/search/search_class.min.js.map +++ b/grade/report/grader/amd/build/search/search_class.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search_class.min.js","sources":["../../src/search/search_class.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport $ from 'jquery';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport {debounce} from 'core/utils';\n\n/**\n * The class that manages the state of the search.\n *\n * @module gradereport_grader/search/search_class\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Reused variables for the class.\nconst events = [\n 'keydown',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n];\nconst UP = -1;\nconst DOWN = 1;\n\nexport default class {\n // Define our standard lookups.\n selectors = {\n component: this.setComponentSelector(),\n trigger: this.setTriggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.setDropdownSelector(),\n resultitems: '[role=\"option\"]',\n viewall: '#select-all',\n };\n\n // The results from the called filter function.\n matchedResults = [];\n\n // What did the user search for?\n searchTerm = '';\n\n // What the user searched for as a lowercase.\n preppedSearchTerm = null;\n\n // The DOM nodes after the dropdown render.\n resultNodes = [];\n\n // Where does the user currently have focus?\n currentNode = null;\n\n // The current node for the view all link.\n currentViewAll = null;\n\n dataset = null;\n\n datasetSize = 0;\n\n // DOM nodes that persist.\n component = document.querySelector(this.selectors.component);\n searchInput = this.component.querySelector(this.selectors.input);\n searchDropdown = this.component.querySelector(this.selectors.dropdown);\n $searchButton = $(this.selectors.trigger);\n clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n $component = $(this.component);\n\n constructor() {\n this.setSearchTerms(this.searchInput.value ?? '');\n // Begin handling the base search component.\n this.registerClickHandlers();\n this.registerKeyHandlers();\n this.registerInputHandlers();\n }\n\n /**\n * Stub out a required function.\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n * @param {Array} dataset\n */\n filterDataset(dataset) {\n throw new Error(`filterDataset(${dataset}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n filterMatchDataset() {\n throw new Error(`filterMatchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n renderDropdown() {\n throw new Error(`renderDropdown() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setComponentSelector() {\n throw new Error(`setComponentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setDropdownSelector() {\n throw new Error(`setDropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setTriggerSelector() {\n throw new Error(`setTriggerSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Promise}\n */\n async getDataset() {\n if (!this.dataset) {\n this.dataset = await this.fetchDataset();\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Return the size of the dataset.\n *\n * @returns {number}\n */\n getDatasetSize() {\n return this.datasetSize;\n }\n\n /**\n * Return the results of the filter upon the dataset.\n *\n * @returns {Array}\n */\n getMatchedResults() {\n return this.matchedResults;\n }\n\n /**\n * Given a filter has been run across the dataset, store the matched results.\n *\n * @param {Array} result\n */\n setMatchedResults(result) {\n this.matchedResults = result;\n }\n\n /**\n * Get the value that the user entered.\n *\n * @returns {string}\n */\n getSearchTerm() {\n return this.searchTerm;\n }\n\n /**\n * Get the transformed search value.\n *\n * @returns {string}\n */\n getPreppedSearchTerm() {\n return this.preppedSearchTerm;\n }\n\n /**\n * When a user searches for something, set our variable to manage it.\n *\n * @param {string} result\n */\n setSearchTerms(result) {\n this.searchTerm = result;\n this.preppedSearchTerm = result.toLowerCase();\n }\n\n /**\n * Return an object containing a handfull of dom nodes that we sometimes need the value of.\n *\n * @returns {object}\n */\n getHTMLElements() {\n this.updateNodes();\n return {\n searchDropdown: this.searchDropdown,\n currentViewAll: this.currentViewAll,\n searchInput: this.searchInput,\n clearSearchButton: this.clearSearchButton\n };\n }\n\n /**\n * When called, close the dropdown and reset the input field attributes.\n */\n closeSearch() {\n this.toggleDropdown();\n // Hide the \"clear\" search button search bar.\n this.clearSearchButton.classList.add('d-none');\n // Clear the entered search query in the search bar and hide the search results container.\n this.setSearchTerms('');\n this.searchInput.value = \"\";\n }\n\n /**\n * When called, update the dropdown fields.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n this.$component.dropdown('toggle');\n this.$searchButton.attr('aria-expanded', on);\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n }\n }\n\n /**\n * These class members change when a new result set is rendered. So update for fresh data.\n */\n updateNodes() {\n this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)];\n this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id);\n this.currentViewAll = this.component.querySelector(this.selectors.viewall);\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Prevent the click triggering the dropdown.\n this.$searchButton.on('click', () => {\n this.toggleDropdown();\n });\n\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n }\n\n /**\n * Register key event listeners.\n */\n registerKeyHandlers() {\n CustomEvents.define(document, events);\n\n // Register click events.\n events.forEach((event) => {\n this.component.addEventListener(event, this.keyHandler.bind(this));\n });\n }\n\n /**\n * Register input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n this.toggleDropdown();\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n await this.renderAndShow();\n }\n }, 300));\n }\n\n /**\n * A combo method to take the matching fields and render out the results.\n *\n * @returns {Promise}\n */\n async renderAndShow() {\n // User has given something for us to filter against.\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n await this.filterMatchDataset();\n // Replace the dropdown node contents and show the results.\n await this.renderDropdown();\n // Set the dropdown to open.\n this.toggleDropdown(true);\n }\n\n /**\n * Set the current focus either on the preceding or next result item.\n *\n * @param {Number} direction Is the user moving up or down the resultset?\n * @param {KeyboardEvent} e The JS event from the event handler.\n */\n keyUpDown(direction, e) {\n e.preventDefault();\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n // Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.\n if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\n }\n }\n const index = this.resultNodes.indexOf(this.currentNode);\n if (this.currentNode) {\n if (direction === UP) {\n if (index === 0) {\n this.moveToLastNode();\n } else {\n this.moveToNode(index - 1);\n }\n } else {\n if (index + 1 >= this.resultNodes.length) {\n this.moveToFirstNode();\n } else {\n this.moveToNode(index + 1);\n }\n }\n }\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n this.updateNodes();\n\n // Prevent normal key presses activating this.\n if (e.target.closest('.dropdown-item') && e.button === 0) {\n window.location = e.target.closest('.dropdown-item').href;\n }\n // The \"clear search\" button is triggered.\n if (e.target.closest(this.selectors.clearSearch) && e.button === 0) {\n this.closeSearch();\n this.searchInput.focus({preventScroll: true});\n }\n // User may have accidentally clicked off the dropdown and wants to reopen it.\n if (e.target.closest(this.selectors.input) && this.getSearchTerm() !== '' && e.button === 0) {\n await this.renderAndShow();\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n this.updateNodes();\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n this.keyUpDown(UP, e);\n break;\n case 'ArrowDown':\n this.keyUpDown(DOWN, e);\n break;\n case 'Home':\n e.preventDefault();\n this.moveToFirstNode();\n break;\n case 'End':\n e.preventDefault();\n this.moveToLastNode();\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n // If the current focus is on the view all link, then close the widget then set focus on the next tertiary nav item.\n if (e.target.closest(this.selectors.viewall)) {\n this.closeSearch();\n }\n break;\n }\n }\n\n /**\n * Set focus on a given node after parsed through the calling functions.\n *\n * @param {HTMLElement} node The node to set focus upon.\n */\n selectNode = (node) => {\n node.focus({preventScroll: true});\n this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);\n };\n\n /**\n * Set the focus on the first node within the array.\n */\n moveToFirstNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[0]);\n }\n };\n\n /**\n * Set the focus to the final node within the array.\n */\n moveToLastNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[this.resultNodes.length - 1]);\n }\n };\n\n /**\n * Set focus on any given specified node within the node array.\n *\n * @param {Number} index Which item within the array to set focus upon.\n */\n moveToNode = (index) => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[index]);\n }\n };\n}\n"],"names":["events","activate","CustomEvents","keyboardActivate","constructor","component","this","setComponentSelector","trigger","setTriggerSelector","input","clearSearch","dropdown","setDropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","fetchDataset","Error","name","filterDataset","dataset","filterMatchDataset","renderDropdown","datasetSize","getDatasetSize","getMatchedResults","matchedResults","setMatchedResults","result","getSearchTerm","searchTerm","getPreppedSearchTerm","preppedSearchTerm","toLowerCase","getHTMLElements","updateNodes","currentViewAll","clearSearchButton","closeSearch","toggleDropdown","classList","add","on","$component","$searchButton","attr","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","e","target","closest","contains","define","forEach","event","keyHandler","async","renderAndShow","getDataset","keyUpDown","direction","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","button","window","location","href","key"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"search_class.min.js","sources":["../../src/search/search_class.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport $ from 'jquery';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport {debounce} from 'core/utils';\n\n/**\n * The class that manages the state of the search.\n *\n * @module gradereport_grader/search/search_class\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Reused variables for the class.\nconst events = [\n 'keydown',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n];\nconst UP = -1;\nconst DOWN = 1;\n\nexport default class {\n // Define our standard lookups.\n selectors = {\n component: this.setComponentSelector(),\n trigger: this.setTriggerSelector(),\n input: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n dropdown: this.setDropdownSelector(),\n resultitems: '[role=\"option\"]',\n viewall: '#select-all',\n };\n\n // The results from the called filter function.\n matchedResults = [];\n\n // What did the user search for?\n searchTerm = '';\n\n // What the user searched for as a lowercase.\n preppedSearchTerm = null;\n\n // The DOM nodes after the dropdown render.\n resultNodes = [];\n\n // Where does the user currently have focus?\n currentNode = null;\n\n // The current node for the view all link.\n currentViewAll = null;\n\n dataset = null;\n\n datasetSize = 0;\n\n // DOM nodes that persist.\n component = document.querySelector(this.selectors.component);\n searchInput = this.component.querySelector(this.selectors.input);\n searchDropdown = this.component.querySelector(this.selectors.dropdown);\n $searchButton = $(this.selectors.trigger);\n clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n $component = $(this.component);\n\n constructor() {\n // If we have a search input, try to get the value otherwise fallback.\n this.setSearchTerms(this.searchInput?.value ?? '');\n // Begin handling the base search component.\n this.registerClickHandlers();\n this.registerKeyHandlers();\n // Conditionally set up the input handler since we don't know exactly how we were called.\n if (this.searchInput !== null) {\n this.registerInputHandlers();\n }\n }\n\n /**\n * Stub out a required function.\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n * @param {Array} dataset\n */\n filterDataset(dataset) {\n throw new Error(`filterDataset(${dataset}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n filterMatchDataset() {\n throw new Error(`filterMatchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n renderDropdown() {\n throw new Error(`renderDropdown() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setComponentSelector() {\n throw new Error(`setComponentSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setDropdownSelector() {\n throw new Error(`setDropdownSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Stub out a required function.\n */\n setTriggerSelector() {\n throw new Error(`setTriggerSelector() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Return the dataset that we will be searching upon.\n *\n * @returns {Promise}\n */\n async getDataset() {\n if (!this.dataset) {\n this.dataset = await this.fetchDataset();\n }\n this.datasetSize = this.dataset.length;\n return this.dataset;\n }\n\n /**\n * Return the size of the dataset.\n *\n * @returns {number}\n */\n getDatasetSize() {\n return this.datasetSize;\n }\n\n /**\n * Return the results of the filter upon the dataset.\n *\n * @returns {Array}\n */\n getMatchedResults() {\n return this.matchedResults;\n }\n\n /**\n * Given a filter has been run across the dataset, store the matched results.\n *\n * @param {Array} result\n */\n setMatchedResults(result) {\n this.matchedResults = result;\n }\n\n /**\n * Get the value that the user entered.\n *\n * @returns {string}\n */\n getSearchTerm() {\n return this.searchTerm;\n }\n\n /**\n * Get the transformed search value.\n *\n * @returns {string}\n */\n getPreppedSearchTerm() {\n return this.preppedSearchTerm;\n }\n\n /**\n * When a user searches for something, set our variable to manage it.\n *\n * @param {string} result\n */\n setSearchTerms(result) {\n this.searchTerm = result;\n this.preppedSearchTerm = result.toLowerCase();\n }\n\n /**\n * Return an object containing a handfull of dom nodes that we sometimes need the value of.\n *\n * @returns {object}\n */\n getHTMLElements() {\n this.updateNodes();\n return {\n searchDropdown: this.searchDropdown,\n currentViewAll: this.currentViewAll,\n searchInput: this.searchInput,\n clearSearchButton: this.clearSearchButton\n };\n }\n\n /**\n * When called, close the dropdown and reset the input field attributes.\n */\n closeSearch() {\n this.toggleDropdown();\n // Hide the \"clear\" search button search bar.\n this.clearSearchButton.classList.add('d-none');\n // Clear the entered search query in the search bar and hide the search results container.\n this.setSearchTerms('');\n this.searchInput.value = \"\";\n }\n\n /**\n * When called, update the dropdown fields.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n this.$component.dropdown('toggle');\n this.$searchButton.attr('aria-expanded', on);\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n }\n }\n\n /**\n * These class members change when a new result set is rendered. So update for fresh data.\n */\n updateNodes() {\n this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)];\n this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id);\n this.currentViewAll = this.component.querySelector(this.selectors.viewall);\n this.clearSearchButton = this.component.querySelector(this.selectors.clearSearch);\n this.searchInput = this.component.querySelector(this.selectors.input);\n this.searchDropdown = this.component.querySelector(this.selectors.dropdown);\n }\n\n /**\n * Register clickable event listeners.\n */\n registerClickHandlers() {\n // Prevent the click triggering the dropdown.\n this.$searchButton.on('click', () => {\n this.toggleDropdown();\n });\n\n // Register click events within the component.\n this.component.addEventListener('click', this.clickHandler.bind(this));\n\n // Register a small click event onto the document since we need to check if they are clicking off the component.\n document.addEventListener('click', (e) => {\n // Since we are handling dropdowns manually, ensure we can close it when clicking off.\n if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {\n this.toggleDropdown();\n }\n });\n }\n\n /**\n * Register key event listeners.\n */\n registerKeyHandlers() {\n CustomEvents.define(document, events);\n\n // Register click events.\n events.forEach((event) => {\n this.component.addEventListener(event, this.keyHandler.bind(this));\n });\n }\n\n /**\n * Register input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n this.toggleDropdown();\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n await this.renderAndShow();\n }\n }, 300));\n }\n\n /**\n * A combo method to take the matching fields and render out the results.\n *\n * @returns {Promise}\n */\n async renderAndShow() {\n // User has given something for us to filter against.\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n await this.filterMatchDataset();\n // Replace the dropdown node contents and show the results.\n await this.renderDropdown();\n // Set the dropdown to open.\n this.toggleDropdown(true);\n }\n\n /**\n * Set the current focus either on the preceding or next result item.\n *\n * @param {Number} direction Is the user moving up or down the resultset?\n * @param {KeyboardEvent} e The JS event from the event handler.\n */\n keyUpDown(direction, e) {\n e.preventDefault();\n // Stop Bootstrap from being clever.\n e.stopPropagation();\n // Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.\n if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {\n if (direction === UP) {\n this.moveToLastNode();\n } else {\n this.moveToFirstNode();\n }\n }\n const index = this.resultNodes.indexOf(this.currentNode);\n if (this.currentNode) {\n if (direction === UP) {\n if (index === 0) {\n this.moveToLastNode();\n } else {\n this.moveToNode(index - 1);\n }\n } else {\n if (index + 1 >= this.resultNodes.length) {\n this.moveToFirstNode();\n } else {\n this.moveToNode(index + 1);\n }\n }\n }\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n this.updateNodes();\n\n // Prevent normal key presses activating this.\n if (e.target.closest('.dropdown-item') && e.button === 0) {\n window.location = e.target.closest('.dropdown-item').href;\n }\n // The \"clear search\" button is triggered.\n if (e.target.closest(this.selectors.clearSearch) && e.button === 0) {\n this.closeSearch();\n this.searchInput.focus({preventScroll: true});\n }\n // User may have accidentally clicked off the dropdown and wants to reopen it.\n if (e.target.closest(this.selectors.input) && this.getSearchTerm() !== '' && e.button === 0) {\n await this.renderAndShow();\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n this.updateNodes();\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n this.keyUpDown(UP, e);\n break;\n case 'ArrowDown':\n this.keyUpDown(DOWN, e);\n break;\n case 'Home':\n e.preventDefault();\n this.moveToFirstNode();\n break;\n case 'End':\n e.preventDefault();\n this.moveToLastNode();\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n // If the current focus is on the view all link, then close the widget then set focus on the next tertiary nav item.\n if (e.target.closest(this.selectors.viewall)) {\n this.closeSearch();\n }\n break;\n }\n }\n\n /**\n * Set focus on a given node after parsed through the calling functions.\n *\n * @param {HTMLElement} node The node to set focus upon.\n */\n selectNode = (node) => {\n node.focus({preventScroll: true});\n this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);\n };\n\n /**\n * Set the focus on the first node within the array.\n */\n moveToFirstNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[0]);\n }\n };\n\n /**\n * Set the focus to the final node within the array.\n */\n moveToLastNode = () => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[this.resultNodes.length - 1]);\n }\n };\n\n /**\n * Set focus on any given specified node within the node array.\n *\n * @param {Number} index Which item within the array to set focus upon.\n */\n moveToNode = (index) => {\n if (this.resultNodes.length > 0) {\n this.selectNode(this.resultNodes[index]);\n }\n };\n}\n"],"names":["events","activate","CustomEvents","keyboardActivate","constructor","component","this","setComponentSelector","trigger","setTriggerSelector","input","clearSearch","dropdown","setDropdownSelector","resultitems","viewall","document","querySelector","selectors","node","focus","preventScroll","searchDropdown","scrollTop","offsetTop","clientHeight","resultNodes","length","selectNode","index","setSearchTerms","searchInput","_this$searchInput","value","registerClickHandlers","registerKeyHandlers","registerInputHandlers","fetchDataset","Error","name","filterDataset","dataset","filterMatchDataset","renderDropdown","datasetSize","getDatasetSize","getMatchedResults","matchedResults","setMatchedResults","result","getSearchTerm","searchTerm","getPreppedSearchTerm","preppedSearchTerm","toLowerCase","getHTMLElements","updateNodes","currentViewAll","clearSearchButton","closeSearch","toggleDropdown","classList","add","on","$component","$searchButton","attr","show","remove","hide","querySelectorAll","currentNode","find","r","id","activeElement","addEventListener","clickHandler","bind","e","target","closest","contains","define","forEach","event","keyHandler","async","renderAndShow","getDataset","keyUpDown","direction","preventDefault","stopPropagation","moveToLastNode","moveToFirstNode","indexOf","moveToNode","button","window","location","href","key"],"mappings":"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"} \ No newline at end of file diff --git a/grade/report/grader/amd/src/collapse.js b/grade/report/grader/amd/src/collapse.js new file mode 100644 index 0000000000000..b29489a249d32 --- /dev/null +++ b/grade/report/grader/amd/src/collapse.js @@ -0,0 +1,524 @@ +// 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 . + +/** + * Allow the user to show and hide columns of the report at will. + * + * @module gradereport_grader/collapse + * @copyright 2023 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import * as Repository from 'gradereport_grader/collapse/repository'; +import GradebookSearchClass from 'gradereport_grader/search/search_class'; +import {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates'; +import {debounce} from 'core/utils'; +import $ from 'jquery'; +import {get_strings as getStrings} from 'core/str'; +import CustomEvents from "core/custom_interaction_events"; +import storage from 'core/localstorage'; +import {addIconToContainer} from 'core/loadingicon'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; + +// Contain our selectors within this file until they could be of use elsewhere. +const selectors = { + component: '.collapse-columns', + formDropdown: '.columnsdropdownform', + formItems: { + cancel: 'cancel', + save: 'save', + checked: 'input[type="checkbox"]:checked' + }, + hider: 'hide', + expand: 'expand', + colVal: '[data-col]', + itemVal: '[data-itemid]', + content: '[data-collapse="content"]', + sort: '[data-collapse="sort"]', + expandbutton: '[data-collapse="expandbutton"]', + avgrowcell: '[data-collapse="avgrowcell"]', + menu: '[data-collapse="menu"]', + icons: '.data-collapse_gradeicons', + count: '[data-collapse="count"]', + placeholder: '.collapsecolumndropdown [data-region="placeholder"]', + fullDropdown: '.collapsecolumndropdown', +}; + +const countIndicator = document.querySelector(selectors.count); + +export default class ColumnSearch extends GradebookSearchClass { + + userID = -1; + courseID = null; + defaultSort = ''; + + nodes = []; + + gradeStrings = null; + userStrings = null; + stringMap = []; + + static init(userID, courseID, defaultSort) { + return new ColumnSearch(userID, courseID, defaultSort); + } + + constructor(userID, courseID, defaultSort) { + super(); + this.userID = userID; + this.courseID = courseID; + this.defaultSort = defaultSort; + this.component = document.querySelector(selectors.component); + + const pendingPromise = new Pending(); + // Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user). + addIconToContainer(document.querySelector('.gradeparent')).then((loader) => { + setTimeout(() => { + // Get the users' checked columns to change. + this.getDataset().forEach((item) => { + this.nodesUpdate(item); + }); + this.renderDefault(); + + // Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content. + loader.remove(); + document.querySelector('.gradereport-grader-table').classList.remove('d-none'); + }, 10); + }).then(() => pendingPromise.resolve()).catch(Notification.exception); + } + + /** + * The overall div that contains the searching widget. + * + * @returns {string} + */ + setComponentSelector() { + return '.collapse-columns'; + } + + /** + * The dropdown div that contains the searching widget result space. + * + * @returns {string} + */ + setDropdownSelector() { + return '.searchresultitemscontainer'; + } + + /** + * The triggering div that contains the searching widget. + * + * @returns {string} + */ + setTriggerSelector() { + return '.collapsecolumn'; + } + + /** + * Return the dataset that we will be searching upon. + * + * @returns {Array} + */ + getDataset() { + if (!this.dataset) { + const cols = this.fetchDataset(); + this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : []; + } + this.datasetSize = this.dataset.length; + return this.dataset; + } + + /** + * Get the data we will be searching against in this component. + * + * @returns {string} + */ + fetchDataset() { + return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`); + } + + /** + * Given a user performs an action, update the users' preferences. + */ + setPreferences() { + storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`, + JSON.stringify(this.getDataset().join(',')) + ); + } + + /** + * Register clickable event listeners. + */ + registerClickHandlers() { + // Register click events within the component. + this.component.addEventListener('click', this.clickHandler.bind(this)); + + document.addEventListener('click', this.docClickHandler.bind(this)); + } + + /** + * The handler for when a user interacts with the component. + * + * @param {MouseEvent} e The triggering event that we are working with. + */ + clickHandler(e) { + super.clickHandler(e); + // Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form. + if (e.target.closest(selectors.fullDropdown)) { + e.stopPropagation(); + } + } + + /** + * Externally defined click function to improve memory handling. + * + * @param {MouseEvent} e + * @returns {Promise} + */ + async docClickHandler(e) { + if (e.target.dataset.hider === selectors.hider) { + e.preventDefault(); + const desiredToHide = e.target.closest(selectors.colVal) ? + e.target.closest(selectors.colVal)?.dataset.col : + e.target.closest(selectors.itemVal)?.dataset.itemid; + const idx = this.getDataset().indexOf(desiredToHide); + if (idx === -1) { + this.getDataset().push(desiredToHide); + } + await this.prefcountpippe(); + + this.nodesUpdate(desiredToHide); + } + + if (e.target.closest('button')?.dataset.hider === selectors.expand) { + e.preventDefault(); + const desiredToHide = e.target.closest(selectors.colVal) ? + e.target.closest(selectors.colVal)?.dataset.col : + e.target.closest(selectors.itemVal)?.dataset.itemid; + const idx = this.getDataset().indexOf(desiredToHide); + this.getDataset().splice(idx, 1); + + await this.prefcountpippe(); + + this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col); + this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid); + } + } + + /** + * Handle any keyboard inputs. + */ + registerInputEvents() { + // Register & handle the text input. + this.searchInput.addEventListener('input', debounce(async() => { + this.setSearchTerms(this.searchInput.value); + // We can also require a set amount of input before search. + if (this.searchInput.value === '') { + // Hide the "clear" search button in the search bar. + this.clearSearchButton.classList.add('d-none'); + } else { + // Display the "clear" search button in the search bar. + this.clearSearchButton.classList.remove('d-none'); + } + // User has given something for us to filter against. + await this.filterrenderpipe(); + }, 300)); + } + + /** + * Handle the form submission within the dropdown. + */ + registerFormEvents() { + const form = this.component.querySelector(selectors.formDropdown); + const events = [ + 'click', + CustomEvents.events.activate, + CustomEvents.events.keyboardActivate + ]; + CustomEvents.define(document, events); + + // Register clicks & keyboard form handling. + events.forEach((event) => { + form.addEventListener(event, (e) => { + // Stop Bootstrap from being clever. + e.stopPropagation(); + const submitBtn = form.querySelector(`[data-action="${selectors.formItems.save}"`); + if (e.target.closest('input')) { + const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length; + // Check if any are clicked or not then change disabled. + submitBtn.disabled = checkedCount <= 0; + } + }, false); + + // Stop Bootstrap from being clever. + this.searchInput.addEventListener(event, e => e.stopPropagation()); + this.clearSearchButton.addEventListener(event, async(e) => { + e.stopPropagation(); + this.searchInput.value = ''; + this.setSearchTerms(this.searchInput.value); + await this.filterrenderpipe(); + }); + }); + + form.addEventListener('submit', async(e) => { + e.preventDefault(); + if (e.submitter.dataset.action === selectors.formItems.cancel) { + $(this.component).dropdown('toggle'); + return; + } + // Get the users' checked columns to change. + const checkedItems = [...form.elements].filter(item => item.checked); + checkedItems.forEach((item) => { + const idx = this.getDataset().indexOf(item.dataset.collapse); + this.getDataset().splice(idx, 1); + this.nodesUpdate(item.dataset.collapse); + }); + await this.prefcountpippe(); + }); + } + + nodesUpdate(item) { + const colNodesToHide = [...document.querySelectorAll(`[data-col="${item}"]`)]; + const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid="${item}"]`)]; + this.nodes = [...colNodesToHide, ...itemIDNodesToHide]; + this.updateDisplay(); + } + + /** + * Update the user preferences, count display then render the results. + * + * @returns {Promise} + */ + async prefcountpippe() { + this.setPreferences(); + this.countUpdate(); + await this.filterrenderpipe(); + } + + /** + * Dictate to the search component how and what we want to match upon. + * + * @param {Array} filterableData + * @returns {Array} An array of objects containing the system reference and the user readable value. + */ + async filterDataset(filterableData) { + const stringUserMap = await this.fetchRequiredUserStrings(); + const stringGradeMap = await this.fetchRequiredGradeStrings(); + // Custom user profile fields are not in our string map and need a bit of extra love. + const customFieldMap = this.fetchCustomFieldValues(); + this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]); + + const searching = filterableData.map(s => { + const mapObj = this.stringMap.get(s); + if (mapObj === undefined) { + return {key: s, string: s}; + } + return { + key: s, + string: mapObj.itemname ?? this.stringMap.get(s), + category: mapObj.category ?? '', + }; + }); + // Sometimes we just want to show everything. + if (this.getPreppedSearchTerm() === '') { + return searching; + } + // Other times we want to actually filter the content. + return searching.filter((col) => { + return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm()); + }); + } + + /** + * Given we have a subset of the dataset, set the field that we matched upon to inform the end user. + */ + filterMatchDataset() { + this.setMatchedResults( + this.getMatchedResults().map((column) => { + return { + name: column.key, + displayName: column.string ?? column.key, + category: column.category ?? '', + }; + }) + ); + } + + /** + * Update any changeable nodes, filter and then render the result. + * + * @returns {Promise} + */ + async filterrenderpipe() { + this.updateNodes(); + this.setMatchedResults(await this.filterDataset(this.getDataset())); + this.filterMatchDataset(); + await this.renderDropdown(); + } + + /** + * With an array of nodes, switch their classes and values. + */ + updateDisplay() { + this.nodes.forEach((element) => { + const content = element.querySelector(selectors.content); + const sort = element.querySelector(selectors.sort); + const expandButton = element.querySelector(selectors.expandbutton); + const avgRowCell = element.querySelector(selectors.avgrowcell); + const nodeSet = [ + element.querySelector(selectors.menu), + element.querySelector(selectors.icons), + content + ]; + + // This can be further improved to reduce redundant similar calls. + if (element.classList.contains('cell')) { + // The column is actively being sorted, lets reset that and reload the page. + if (sort !== null) { + window.location = this.defaultSort; + } + if (content === null) { + if (avgRowCell.classList.contains('d-none')) { + avgRowCell?.classList.remove('d-none'); + avgRowCell?.setAttribute('aria-hidden', 'false'); + } else { + avgRowCell?.classList.add('d-none'); + avgRowCell?.setAttribute('aria-hidden', 'true'); + } + } else if (content.classList.contains('d-none')) { + // We should always have content but some cells do not contain menus or other actions. + element.classList.remove('collapsed'); + content.classList.add('d-flex'); + nodeSet.forEach(node => { + node?.classList.remove('d-none'); + node?.setAttribute('aria-hidden', 'false'); + }); + expandButton?.classList.add('d-none'); + expandButton?.setAttribute('aria-hidden', 'true'); + } else { + element.classList.add('collapsed'); + content.classList.remove('d-flex'); + nodeSet.forEach(node => { + node?.classList.add('d-none'); + node?.setAttribute('aria-hidden', 'true'); + }); + expandButton?.classList.remove('d-none'); + expandButton?.setAttribute('aria-hidden', 'false'); + } + } + }); + } + + /** + * Update the visual count of collapsed columns or hide the count all together. + */ + countUpdate() { + countIndicator.textContent = this.getDatasetSize(); + if (this.getDatasetSize() > 0) { + this.component.parentElement.classList.add('d-flex'); + this.component.parentElement.classList.remove('d-none'); + } else { + this.component.parentElement.classList.remove('d-flex'); + this.component.parentElement.classList.add('d-none'); + } + } + + /** + * Build the content then replace the node by default we want our form to exist. + */ + async renderDefault() { + this.setMatchedResults(await this.filterDataset(this.getDataset())); + this.filterMatchDataset(); + + // Update the collapsed button pill. + this.countUpdate(); + const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', { + 'results': this.getMatchedResults(), + 'userid': this.userID, + }); + replaceNode(selectors.placeholder, html, js); + this.updateNodes(); + + // Given we now have the body, we can set up more triggers. + this.registerFormEvents(); + this.registerInputEvents(); + } + + /** + * Build the content then replace the node. + */ + async renderDropdown() { + const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', { + 'results': this.getMatchedResults(), + 'searchTerm': this.getSearchTerm(), + }); + replaceNodeContents(this.getHTMLElements().searchDropdown, html, js); + } + + /** + * If we have any custom user profile fields, grab their system & readable names to add to our string map. + * + * @returns {[string,*][]} An array of associated string arrays ready for our map. + */ + fetchCustomFieldValues() { + const customFields = document.querySelectorAll('[data-collapse-name]'); + // Cast from NodeList to array to grab all the values. + return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]); + } + + /** + * Given the set of profile fields we can possibly search, fetch their strings, + * so we can report to screen readers the field that matched. + * + * @returns {Promise} + */ + fetchRequiredUserStrings() { + if (!this.userStrings) { + const requiredStrings = [ + 'username', + 'firstname', + 'lastname', + 'email', + 'city', + 'country', + 'department', + 'institution', + 'idnumber', + 'phone1', + 'phone2', + ]; + this.userStrings = getStrings(requiredStrings.map((key) => ({key}))) + .then((stringArray) => new Map( + requiredStrings.map((key, index) => ([key, stringArray[index]])) + )); + } + return this.userStrings; + } + + /** + * Given the set of gradable items we can possibly search, fetch their strings, + * so we can report to screen readers the field that matched. + * + * @returns {Promise} + */ + fetchRequiredGradeStrings() { + if (!this.gradeStrings) { + this.gradeStrings = Repository.gradeItems(this.courseID) + .then((result) => new Map( + result.gradeItems.map(key => ([key.id, key])) + )); + } + return this.gradeStrings; + } +} diff --git a/grade/report/grader/amd/src/collapse/repository.js b/grade/report/grader/amd/src/collapse/repository.js new file mode 100644 index 0000000000000..994883ce375db --- /dev/null +++ b/grade/report/grader/amd/src/collapse/repository.js @@ -0,0 +1,41 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * A repo for the collapsing in the grader report. + * + * @module gradereport_grader/collapse/repository + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import ajax from 'core/ajax'; + +/** + * Fetch all the information on gradeitems we'll need in the column collapser. + * + * @method gradeItems + * @param {Number} courseid What course to fetch the gradeitems for + * @return {object} jQuery promise + */ +export const gradeItems = (courseid) => { + const request = { + methodname: 'core_grades_get_gradeitems', + args: { + courseid: courseid, + }, + }; + return ajax.call([request])[0]; +}; diff --git a/grade/report/grader/amd/src/search/search_class.js b/grade/report/grader/amd/src/search/search_class.js index f1dd47679966b..f03fa15f78aa2 100644 --- a/grade/report/grader/amd/src/search/search_class.js +++ b/grade/report/grader/amd/src/search/search_class.js @@ -76,11 +76,15 @@ export default class { $component = $(this.component); constructor() { - this.setSearchTerms(this.searchInput.value ?? ''); + // If we have a search input, try to get the value otherwise fallback. + this.setSearchTerms(this.searchInput?.value ?? ''); // Begin handling the base search component. this.registerClickHandlers(); this.registerKeyHandlers(); - this.registerInputHandlers(); + // Conditionally set up the input handler since we don't know exactly how we were called. + if (this.searchInput !== null) { + this.registerInputHandlers(); + } } /** @@ -252,6 +256,9 @@ export default class { this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)]; this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id); this.currentViewAll = this.component.querySelector(this.selectors.viewall); + this.clearSearchButton = this.component.querySelector(this.selectors.clearSearch); + this.searchInput = this.component.querySelector(this.selectors.input); + this.searchDropdown = this.component.querySelector(this.selectors.dropdown); } /** diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index a83cd11ec2cec..b391fd53e945a 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -107,6 +107,21 @@ public function export_for_template(\renderer_base $output): array { false, ); $data['searchdropdown'] = $searchdropdown->export_for_template($output); + + $collapse = new gradebook_dropdown( + true, + get_string('collapsedcolumns', 'gradereport_grader', 0), + null, + 'collapse-columns', + 'collapsecolumn', + 'collapsecolumndropdown p-3 flex-column', + null, + true, + ); + $data['collapsedcolumns'] = [ + 'classes' => 'd-none', + 'content' => $collapse->export_for_template($output) + ]; } return $data; diff --git a/grade/report/grader/index.php b/grade/report/grader/index.php index 7cc86d1e31eb5..3ba7858adacbf 100644 --- a/grade/report/grader/index.php +++ b/grade/report/grader/index.php @@ -127,6 +127,14 @@ $sort = strtoupper($sort); } $report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid, $sort); + +// We call this a little later since we need some info from the grader report. +$PAGE->requires->js_call_amd('gradereport_grader/collapse', 'init', [ + 'userID' => $USER->id, + 'courseID' => $courseid, + 'defaultSort' => $report->get_default_sortable() +]); + $numusers = $report->get_numusers(true, true); $actionbar = new \gradereport_grader\output\action_bar($context, $report, $numusers); diff --git a/grade/report/grader/lang/en/gradereport_grader.php b/grade/report/grader/lang/en/gradereport_grader.php index cf8a12c492d15..a5c2ab8c78d50 100644 --- a/grade/report/grader/lang/en/gradereport_grader.php +++ b/grade/report/grader/lang/en/gradereport_grader.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['collapsedcolumns'] = 'Collapsed columns {$a}'; $string['eventgradereportviewed'] = 'Grader report viewed'; $string['grader:manage'] = 'Manage the grader report'; $string['grader:view'] = 'View grader report'; @@ -43,6 +44,7 @@ $string['privacy:metadata:preference:grade_report_showuserimage'] = 'Whether to show the user\'s profile image next to the name'; $string['privacy:metadata:preference:grade_report_studentsperpage'] = 'The number of students displayed per page in the grader report'; $string['privacy:request:preference:grade_report_grader_collapsed_categories'] = 'You have some gradebook categories collapsed in the "{$a->name}" course'; +$string['reopencolumn'] = 'Reopen {$a} column'; $string['summarygrader'] = 'A table with the names of students in the first column, with assessable activities grouped by course and category across the top.'; $string['showingxofy'] = 'Showing {$a->found} of {$a->total}'; $string['useractivitygrade'] = '{$a} grade'; diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index c538f2856e2fe..c56d33611af45 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -666,11 +666,19 @@ public function get_left_rows($displayaverages) { foreach ($extrafields as $field) { $fieldheader = new html_table_cell(); $fieldheader->attributes['class'] = 'userfield user' . $field; + $fieldheader->attributes['data-col'] = $field; $fieldheader->scope = 'col'; $fieldheader->header = true; + + $collapsecontext = ['field' => $field, 'name' => $field]; + + $collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext); + // Need to wrap the button into a div with our hooking element for user items, gradeitems already have this. + $collapsedicon = html_writer::div($collapsedicon, 'd-none', ['data-collapse' => 'expandbutton']); + $element = ['type' => 'userfield', 'name' => $field]; $fieldheader->text = $arrows[$field] . - $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl); + $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl) . $collapsedicon; $headerrow->cells[] = $fieldheader; } @@ -723,8 +731,12 @@ public function get_left_rows($displayaverages) { foreach ($extrafields as $field) { $fieldcell = new html_table_cell(); $fieldcell->attributes['class'] = 'userfield user' . $field; + $fieldcell->attributes['data-col'] = $field; $fieldcell->header = false; - $fieldcell->text = s($user->{$field}); + $fieldcell->text = html_writer::tag('div', s($user->{$field}), [ + 'data-collapse' => 'content' + ]); + $userrow->cells[] = $fieldcell; } @@ -836,6 +848,15 @@ public function get_right_rows(bool $displayaverages) : array { } } + $collapsecontext = [ + 'field' => $element['object']->id, + 'name' => $element['object']->get_name(), + ]; + $collapsedicon = ''; + // We do not want grade category total items to be hidden away as it is controlled by something else. + if (!$element['object']->is_aggregate_item()) { + $collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext); + } $headerlink = $this->gtree->get_element_header($element, true, true, false, false, true, $sortlink); @@ -871,6 +892,7 @@ public function get_right_rows(bool $displayaverages) : array { $context->arrow = $arrow; $context->singleview = $singleview; $context->statusicons = $statusicons; + $context->collapsedicon = $collapsedicon; $itemcell->text = $OUTPUT->render_from_template('gradereport_grader/headercell', $context); @@ -1166,7 +1188,7 @@ public function get_grade_table($displayaverages = false) { $html = ''; $fulltable = new html_table(); - $fulltable->attributes['class'] = 'gradereport-grader-table'; + $fulltable->attributes['class'] = 'gradereport-grader-table d-none'; $fulltable->id = 'user-grades'; $fulltable->caption = get_string('summarygrader', 'gradereport_grader'); $fulltable->captionhide = true; @@ -1370,7 +1392,6 @@ public function get_right_range_row($rows=array()) { */ public function get_right_avg_row($rows=array(), $grouponly=false) { global $USER, $DB, $OUTPUT, $CFG; - if (!$this->canviewhidden) { // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered // better not show them at all if user can not see all hidden grades. @@ -1518,9 +1539,9 @@ public function get_right_avg_row($rows=array(), $grouponly=false) { if (!isset($sumarray[$item->id]) || $meancount == 0) { $avgcell = new html_table_cell(); $avgcell->attributes['class'] = $gradetypeclass . ' i'. $itemid; - $avgcell->text = '-'; + $avgcell->attributes['data-itemid'] = $itemid; + $avgcell->text = html_writer::div('-', '', ['data-collapse' => 'avgrowcell']); $avgrow->cells[] = $avgcell; - } else { $sum = $sumarray[$item->id]; $avgradeval = $sum/$meancount; @@ -1533,7 +1554,8 @@ public function get_right_avg_row($rows=array(), $grouponly=false) { $avgcell = new html_table_cell(); $avgcell->attributes['class'] = $gradetypeclass . ' i'. $itemid; - $avgcell->text = $gradehtml.$numberofgrades; + $avgcell->attributes['data-itemid'] = $itemid; + $avgcell->text = html_writer::div($gradehtml.$numberofgrades, '', ['data-collapse' => 'avgrowcell']); $avgrow->cells[] = $avgcell; } } @@ -1839,7 +1861,7 @@ public static function do_process_action($target, $action, $courseid = null) { * user idnumber * @return array An associative array of HTML sorting links+arrows */ - public function get_sort_arrows(array $extrafields = array()) { + public function get_sort_arrows(array $extrafields = []) { global $CFG; $arrows = array(); $sortlink = clone($this->baseurl); @@ -1879,8 +1901,15 @@ public function get_sort_arrows(array $extrafields = array()) { } foreach ($extrafields as $field) { - $fieldlink = html_writer::link(new moodle_url($this->baseurl, - array('sortitemid' => $field)), \core_user\fields::get_display_name($field)); + $attributes = [ + 'data-collapse' => 'content' + ]; + // With additional user profile fields, we can't grab the name via WS, so conditionally add it to rip out of the DOM. + if (preg_match(\core_user\fields::PROFILE_FIELD_REGEX, $field)) { + $attributes['data-collapse-name'] = \core_user\fields::get_display_name($field); + } + $fieldlink = html_writer::link(new moodle_url($this->baseurl, ['sortitemid' => $field]), + \core_user\fields::get_display_name($field), $attributes); $arrows[$field] = $fieldlink; if ($field == $this->sortitemid) { @@ -1925,6 +1954,38 @@ public function get_category_view_mode_link(moodle_url $url, string $title, stri return html_writer::link($urlnew, $title, ['class' => 'dropdown-item', 'aria-label' => $title, 'aria-current' => $active, 'role' => 'menuitem']); } + + /** + * Return the link to allow the field to collapse from the users view. + * + * @return string Dropdown menu link that'll trigger the collapsing functionality. + * @throws coding_exception + * @throws moodle_exception + */ + public function get_hide_show_link(): string { + $link = new moodle_url('#', []); + return html_writer::link( + $link->out(false), + get_string('collapse'), + ['class' => 'dropdown-item', 'data-hider' => 'hide', 'aria-label' => get_string('collapse'), 'role' => 'menuitem'], + ); + } + + /** + * Return the base report link with some default sorting applied. + * + * @return string + * @throws moodle_exception + */ + public function get_default_sortable(): string { + $sortlink = new moodle_url('/grade/report/grader/index.php', [ + 'id' => $this->courseid, + 'sortitemid' => 'firstname', + 'sort' => 'asc' + ]); + $this->gpr->add_url_params($sortlink); + return $sortlink->out(false); + } } /** @@ -1941,12 +2002,12 @@ public function get_category_view_mode_link(moodle_url $url, string $title, stri function gradereport_grader_get_report_link(context_course $context, int $courseid, array $element, grade_plugin_return $gpr, string $mode, ?stdClass $templatecontext): ?stdClass { - if ($mode == 'category') { - static $report = null; - if (!$report) { - $report = new grade_report_grader($courseid, $gpr, $context); - } + static $report = null; + if (!$report) { + $report = new grade_report_grader($courseid, $gpr, $context); + } + if ($mode == 'category') { if (!isset($templatecontext)) { $templatecontext = new stdClass(); } @@ -1977,6 +2038,17 @@ function gradereport_grader_get_report_link(context_course $context, int $course $templatecontext->fullmodeurl = $report->get_category_view_mode_link($url, $strswitchwhole, 'switch_whole', $fullmode); return $templatecontext; + } else if ($mode == 'gradeitem') { + if (($element['type'] == 'userfield') && ($element['name'] !== 'fullname')) { + $templatecontext->columncollapse = $report->get_hide_show_link(); + $templatecontext->dataid = $element['name']; + } + + // We do not want grade category total items to be hidden away as it is controlled by something else. + if (isset($element['object']->id) && !$element['object']->is_aggregate_item()) { + $templatecontext->columncollapse = $report->get_hide_show_link(); + } + return $templatecontext; } return null; } diff --git a/grade/report/grader/styles.css b/grade/report/grader/styles.css index c785551e9aa7c..d8805ddeeb958 100644 --- a/grade/report/grader/styles.css +++ b/grade/report/grader/styles.css @@ -110,6 +110,10 @@ min-width: 200px; } +.path-grade-report-grader .gradeparent .highlightable.cell.collapsed { + min-width: unset; +} + .path-grade-report-grader .gradeparent .user.cell .userpicture { border: none; vertical-align: middle; @@ -249,3 +253,8 @@ border-top: 1px solid #dee2e6; font-size: 90%; } + +.collapsecolumndropdown.show { + width: 275px; + max-height: 300px; +} diff --git a/grade/report/grader/templates/action_bar.mustache b/grade/report/grader/templates/action_bar.mustache index 96514c985d28c..a87795fa50231 100644 --- a/grade/report/grader/templates/action_bar.mustache +++ b/grade/report/grader/templates/action_bar.mustache @@ -60,7 +60,8 @@ } ] }, - "groupselector": "
" + "groupselector": "
", + "collapsedcolumns": "
" } }}
@@ -89,5 +90,12 @@
{{/initialselector}} + {{#collapsedcolumns}} + + {{/collapsedcolumns}} diff --git a/grade/report/grader/templates/cell.mustache b/grade/report/grader/templates/cell.mustache index 05cda44db0762..52b8910a844b2 100644 --- a/grade/report/grader/templates/cell.mustache +++ b/grade/report/grader/templates/cell.mustache @@ -33,7 +33,7 @@ "name": "grade[313][624]" } }} -
+
{{#iseditable}} diff --git a/grade/report/grader/templates/collapse/collapsebody.mustache b/grade/report/grader/templates/collapse/collapsebody.mustache new file mode 100644 index 0000000000000..88f036a15abb1 --- /dev/null +++ b/grade/report/grader/templates/collapse/collapsebody.mustache @@ -0,0 +1,53 @@ +{{! + 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 gradereport_grader/collapse/collapsebody + + The body of the column collapsing dropdown that contains the form and subsequent results from the search. + + Example context (json): + { + "results": [ + { + "name": "42", + "displayName": "The meaning of life", + "category": "Hitchhikers grade category" + } + ], + "searchTerm": "Meaning of", + "userid": "42" + } +}} +
+ + + {{< core/search_input_auto }} + {{$label}}{{#str}} + searchcollapsedcolumns, core_grades + {{/str}}{{/label}} + {{$placeholder}}{{#str}} + searchcollapsedcolumns, core_grades + {{/str}}{{/placeholder}} + {{/ core/search_input_auto }} + +
+ +
+ + +
+
+
diff --git a/grade/report/grader/templates/collapse/collapseresultitems.mustache b/grade/report/grader/templates/collapse/collapseresultitems.mustache new file mode 100644 index 0000000000000..6174c01128285 --- /dev/null +++ b/grade/report/grader/templates/collapse/collapseresultitems.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_grader/collapse/collapseresultitems + + Context variables required for this template: + * name - The 'system' name that we search against. + * displayName - The 'end user' name that the user can search against. + * category - The category that a gradable item is within. + + Example context (json): + { + "name": "42", + "displayName": "The meaning of life", + "category": "Hitchhikers grade category" + } +}} +
  • + + +
  • diff --git a/grade/report/grader/templates/collapse/collapseresults.mustache b/grade/report/grader/templates/collapse/collapseresults.mustache new file mode 100644 index 0000000000000..0ae1bb4f62ae0 --- /dev/null +++ b/grade/report/grader/templates/collapse/collapseresults.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 gradereport_grader/collapse/collapseresults + + Context variables required for this template: + * results - An array of found columns that are currently hidden. + * searchTerm - What the user is currently searching for. + + Example context (json): + { + "results": [ + { + "name": "42", + "displayName": "The meaning of life", + "category": "Hitchhikers grade category" + } + ], + "searchTerm": "Meaning of" + } +}} +{{#results}} + {{>gradereport_grader/collapse/collapseresultitems}} +{{/results}} +{{^results}} + {{#str}} noresultsfor, core_grades, {{searchTerm}}{{/str}} +{{/results}} diff --git a/grade/report/grader/templates/collapse/icon.mustache b/grade/report/grader/templates/collapse/icon.mustache new file mode 100644 index 0000000000000..c465330d65761 --- /dev/null +++ b/grade/report/grader/templates/collapse/icon.mustache @@ -0,0 +1,29 @@ +{{! + 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 gradereport_grader/collapse/icon + + Context variables required for this template: + * field - Either the shortname of the user field or a grade item ID. + + Example context (json): + { + "field": "42", + "name": "Meaning of life" + } +}} + diff --git a/grade/report/grader/templates/headercell.mustache b/grade/report/grader/templates/headercell.mustache index f6b1493c3bab7..9f49696282994 100644 --- a/grade/report/grader/templates/headercell.mustache +++ b/grade/report/grader/templates/headercell.mustache @@ -28,7 +28,10 @@ } }}
    -
    +
    + {{{collapsedicon}}} +
    +
    {{{headerlink}}} {{{arrow}}} diff --git a/grade/report/grader/templates/search/resultitem.mustache b/grade/report/grader/templates/search/resultitem.mustache index 8abc6f0e77b22..eb13cdd1f1a45 100644 --- a/grade/report/grader/templates/search/resultitem.mustache +++ b/grade/report/grader/templates/search/resultitem.mustache @@ -45,10 +45,10 @@ {{/profileimageurl}}
    - + {{fullname}} -