first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,11 @@
define("core_grades/bulkactions/edit/tree/bulk_actions",["exports","core/bulkactions/bulk_actions","core_grades/bulkactions/edit/tree/move"],(function(_exports,_bulk_actions,_move){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_actions=_interopRequireDefault(_bulk_actions),_move=_interopRequireDefault(_move);
/**
* Class for defining the bulk actions area in the gradebook setup page.
*
* @module core_grades/bulkactions/edit/tree/bulk_actions
* @copyright 2023 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors_selectBulkItemCheckbox='input[type="checkbox"].itemselect';class GradebookEditTreeBulkActions extends _bulk_actions.default{static init(courseID){return new this(courseID)}constructor(courseID){var obj,key,value;super(),value=null,(key="courseID")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.courseID=courseID}getBulkActions(){return[new _move.default(this.courseID)]}getSelectedItems(){return document.querySelectorAll("".concat(Selectors_selectBulkItemCheckbox,":checked"))}registerItemSelectChangeEvent(eventHandler){document.querySelectorAll(Selectors_selectBulkItemCheckbox).forEach((checkbox=>{checkbox.addEventListener("change",eventHandler.bind(this))}))}}return _exports.default=GradebookEditTreeBulkActions,_exports.default}));
//# sourceMappingURL=bulk_actions.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"bulk_actions.min.js","sources":["../../../../src/bulkactions/edit/tree/bulk_actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport BulkActions from \"core/bulkactions/bulk_actions\";\nimport GradebookEditTreeBulkMove from \"core_grades/bulkactions/edit/tree/move\";\n\n/**\n * Class for defining the bulk actions area in the gradebook setup page.\n *\n * @module core_grades/bulkactions/edit/tree/bulk_actions\n * @copyright 2023 Mihail Geshoski <mihail@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n selectBulkItemCheckbox: 'input[type=\"checkbox\"].itemselect'\n};\n\nexport default class GradebookEditTreeBulkActions extends BulkActions {\n\n /** @property {int|null} courseID The course ID. */\n courseID = null;\n\n /**\n * Returns the instance of the class.\n *\n * @param {int} courseID\n * @returns {GradebookEditTreeBulkActions}\n */\n static init(courseID) {\n return new this(courseID);\n }\n\n /**\n * The class constructor.\n *\n * @param {int} courseID The course ID.\n * @returns {void}\n */\n constructor(courseID) {\n super();\n this.courseID = courseID;\n }\n\n /**\n * Returns the array of the relevant bulk action objects for the gradebook setup page.\n *\n * @method getBulkActions\n * @returns {Array}\n */\n getBulkActions() {\n return [\n new GradebookEditTreeBulkMove(this.courseID)\n ];\n }\n\n /**\n * Returns the array of selected items.\n *\n * @method getSelectedItems\n * @returns {Array}\n */\n getSelectedItems() {\n return document.querySelectorAll(`${Selectors.selectBulkItemCheckbox}:checked`);\n }\n\n /**\n * Adds the listener for the item select change event.\n *\n * @method registerItemSelectChangeEvent\n * @param {function} eventHandler The event handler function.\n * @returns {void}\n */\n registerItemSelectChangeEvent(eventHandler) {\n const itemSelectCheckboxes = document.querySelectorAll(Selectors.selectBulkItemCheckbox);\n itemSelectCheckboxes.forEach((checkbox) => {\n checkbox.addEventListener('change', eventHandler.bind(this));\n });\n }\n}\n"],"names":["Selectors","GradebookEditTreeBulkActions","BulkActions","courseID","this","constructor","getBulkActions","GradebookEditTreeBulkMove","getSelectedItems","document","querySelectorAll","registerItemSelectChangeEvent","eventHandler","forEach","checkbox","addEventListener","bind"],"mappings":";;;;;;;;MA0BMA,iCACsB,0CAGPC,qCAAqCC,kCAW1CC,iBACD,IAAIC,KAAKD,UASpBE,YAAYF,0CAlBD,6IAoBFA,SAAWA,SASpBG,uBACW,CACH,IAAIC,cAA0BH,KAAKD,WAU3CK,0BACWC,SAASC,2BAAoBV,8CAUxCW,8BAA8BC,cACGH,SAASC,iBAAiBV,kCAClCa,SAASC,WAC1BA,SAASC,iBAAiB,SAAUH,aAAaI,KAAKZ"}
+3
View File
@@ -0,0 +1,3 @@
define("core_grades/bulkactions/edit/tree/move",["exports","core/bulkactions/bulk_action","core/str","core/modal_save_cancel","core/templates","core/ajax","core/modal_events","core_grades/bulkactions/edit/tree/move_options_tree"],(function(_exports,_bulk_action,_str,_modal_save_cancel,_templates,_ajax,_modal_events,_move_options_tree){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_bulk_action=_interopRequireDefault(_bulk_action),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_templates=_interopRequireDefault(_templates),_ajax=_interopRequireDefault(_ajax),_modal_events=_interopRequireDefault(_modal_events),_move_options_tree=_interopRequireDefault(_move_options_tree);const Selectors_editTreeForm="#gradetreeform",Selectors_bulkMoveInput='input[name="bulkmove"]',Selectors_bulkMoveAfterInput='input[name="moveafter"]';class GradebookEditTreeBulkMove extends _bulk_action.default{constructor(courseId){super(),_defineProperty(this,"courseId",null),_defineProperty(this,"moveOptionsTree",null),_defineProperty(this,"gradeTree",null),this.courseId=courseId}getBulkActionTriggerSelector(){return'button[data-action="move"]'}async triggerBulkAction(){const modal=await this.showModal();this.registerCustomListenerEvents(modal)}async renderBulkActionTrigger(){return _templates.default.render("core_grades/bulkactions/edit/tree/bulk_move_trigger",{})}async registerCustomListenerEvents(modal){await modal.getBody(),modal.getRoot().on(_modal_events.default.shown,(()=>{this.moveOptionsTree=new _move_options_tree.default((()=>{modal.setButtonDisabled("save",!1)}))})),modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy()})),modal.getRoot().on(_modal_events.default.save,(()=>{this.moveOptionsTree&&this.moveOptionsTree.selectedMoveOption&&(document.querySelector(Selectors_bulkMoveInput).value=1,document.querySelector(Selectors_bulkMoveAfterInput).value=this.moveOptionsTree.selectedMoveOption.dataset.id,document.querySelector(Selectors_editTreeForm).submit())}))}fetchGradeTree(){const request={methodname:"core_grades_get_grade_tree",args:{courseid:this.courseId}};return _ajax.default.call([request])[0]}async renderModalBody(){return null===this.gradeTree&&(this.gradeTree=await this.fetchGradeTree()),_templates.default.render("core_grades/bulkactions/edit/tree/bulk_move_grade_tree",JSON.parse(this.gradeTree))}async showModal(){const modal=await _modal_save_cancel.default.create({title:await(0,_str.get_string)("movesitems","grades"),body:await this.renderModalBody(),buttons:{save:await(0,_str.get_string)("move")},large:!0});return modal.setButtonDisabled("save",!0),modal.show(),modal}}return _exports.default=GradebookEditTreeBulkMove,_exports.default}));
//# sourceMappingURL=move.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
define("core_grades/bulkactions/edit/tree/move_options_tree",["exports","core/tree","core/normalise"],(function(_exports,_tree,_normalise){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_tree=(obj=_tree)&&obj.__esModule?obj:{default:obj};const Selectors_moveOptionsTree='#destination-selector [role="tree"]',Selectors_moveOption='#destination-selector [role="treeitem"]',Selectors_toggleGroupLink="#destination-selector .collapse-list-link";class MoveOptionsTree extends _tree.default{constructor(afterSelectMoveOptionCallback){super(Selectors_moveOptionsTree),_defineProperty(this,"afterSelectMoveOptionCallback",null),_defineProperty(this,"selectedMoveOption",null),this.afterSelectMoveOptionCallback=afterSelectMoveOptionCallback}handleKeyDown(e){e.keyCode===this.keys.enter||e.keyCode===this.keys.space?this.selectMoveOption(e.target):super.handleKeyDown(e)}handleItemClick(event,item){event.target.closest(Selectors_toggleGroupLink)?super.handleItemClick(event,item):this.selectMoveOption((0,_normalise.getList)(item)[0])}selectMoveOption(moveOption){this.refreshVisibleItemsCache(),document.querySelectorAll(Selectors_moveOption).forEach((item=>{item.dataset.selected="false"})),moveOption.dataset.selected="true",this.selectedMoveOption=moveOption,moveOption.focus(),"function"==typeof this.afterSelectMoveOptionCallback&&this.afterSelectMoveOptionCallback()}}return _exports.default=MoveOptionsTree,_exports.default}));
//# sourceMappingURL=move_options_tree.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/edittree_index",["exports","core/localstorage","core/loadingicon","core/notification","core/pending"],(function(_exports,_localstorage,_loadingicon,_notification,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Enhance the gradebook tree setup with various facilities.
*
* @module core_grades/edittree_index
* @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_localstorage=_interopRequireDefault(_localstorage),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS_CATEGORY_TOGGLE=".toggle-category",SELECTORS_GRADEBOOK_SETUP_TABLE=".setup-grades",SELECTORS_WEIGHT_OVERRIDE_CHECKBOX=".weightoverride",SELECTORS_BULK_MOVE_SELECT="#menumoveafter",SELECTORS_BULK_MOVE_INPUT="#bulkmoveinput",SELECTORS_GRADEBOOK_SETUP_WRAPPER=".gradetree-wrapper",SELECTORS_GRADEBOOK_SETUP_BOX=".gradetreebox",toggleWeightInput=weightOverrideCheckbox=>{const row=weightOverrideCheckbox.closest("tr"),itemId=row.dataset.itemid;row.querySelector('input[name="weight_'.concat(itemId,'"]')).disabled=!weightOverrideCheckbox.checked},submitBulkMoveForm=bulkMoveSelect=>{const form=bulkMoveSelect.closest("form");form.querySelector(SELECTORS_BULK_MOVE_INPUT).value=1,form.submit()},toggleCategory=(toggleElement,courseId,userId,storeCollapsedState)=>{const target=toggleElement.dataset.target,category=toggleElement.dataset.category,isCollapsing="true"===toggleElement.getAttribute("aria-expanded"),gradebookSetup=toggleElement.closest(SELECTORS_GRADEBOOK_SETUP_TABLE),targetRows=gradebookSetup.querySelectorAll(target),maxGradeCell=toggleElement.closest("tr").querySelector(".column-range");if(isCollapsing){if(toggleElement.setAttribute("aria-expanded","false"),toggleElement.dataset.target="[data-hidden-by='".concat(category,"']"),maxGradeCell){const relatedCategoryAggregationRow=gradebookSetup.querySelector("[data-aggregationforcategory='".concat(category,"']"));maxGradeCell.innerHTML=relatedCategoryAggregationRow.querySelector(".column-range").innerHTML}}else toggleElement.setAttribute("aria-expanded","true"),toggleElement.dataset.target=".".concat(category,"[data-hidden='false']"),maxGradeCell&&(maxGradeCell.innerHTML="");storeCollapsedState&&((category,courseId,userId,isCollapsing)=>{const currentStoredCollapsedCategories=_localstorage.default.get("core_grade_collapsedgradecategories_".concat(courseId,"_").concat(userId));let collapsedCategories=currentStoredCollapsedCategories?JSON.parse(currentStoredCollapsedCategories):[];isCollapsing?collapsedCategories.push(category):collapsedCategories=collapsedCategories.filter((cat=>cat!==category)),_localstorage.default.set("core_grade_collapsedgradecategories_".concat(courseId,"_").concat(userId),JSON.stringify(collapsedCategories))})(category,courseId,userId,isCollapsing),targetRows.forEach((row=>{isCollapsing?(row.dataset.hidden="true",row.dataset.hiddenBy=category):(row.dataset.hidden="false",row.dataset.hiddenBy="")})),updateParentCategoryRowspans(toggleElement,targetRows.length)},updateParentCategoryRowspans=(toggleElement,num)=>{const gradebookSetup=toggleElement.closest(SELECTORS_GRADEBOOK_SETUP_TABLE);toggleElement.closest("tr").classList.forEach((className=>{const parentCategoryToggleElement=gradebookSetup.querySelector('[data-target=".'.concat(className,"[data-hidden='false']\""));if(parentCategoryToggleElement){const categoryRowSpanElement=parentCategoryToggleElement.closest("tr").nextElementSibling.querySelector("[rowspan]");"true"===toggleElement.getAttribute("aria-expanded")?categoryRowSpanElement.rowSpan=categoryRowSpanElement.rowSpan+num:categoryRowSpanElement.rowSpan=categoryRowSpanElement.rowSpan-num}}))};_exports.init=(courseId,userId)=>{const pendingPromise=new _pending.default,gradebookSetupBox=document.querySelector(SELECTORS_GRADEBOOK_SETUP_BOX);(0,_loadingicon.addIconToContainer)(gradebookSetupBox).then((loader=>{setTimeout((()=>{((courseId,userId)=>{const gradebookSetup=document.querySelector(SELECTORS_GRADEBOOK_SETUP_TABLE),storedCollapsedCategories=_localstorage.default.get("core_grade_collapsedgradecategories_".concat(courseId,"_").concat(userId));storedCollapsedCategories&&JSON.parse(storedCollapsedCategories).forEach((category=>{const categoryToggleElement=gradebookSetup.querySelector("".concat(SELECTORS_CATEGORY_TOGGLE,'[data-category="').concat(category,'"'));categoryToggleElement&&toggleCategory(categoryToggleElement,courseId,userId,!1)}))})(courseId,userId),loader.remove(),document.querySelector(SELECTORS_GRADEBOOK_SETUP_WRAPPER).classList.remove("d-none"),pendingPromise.resolve()}),150)})).fail(_notification.default.exception),((courseId,userId)=>{document.addEventListener("change",(e=>{e.target.matches(SELECTORS_WEIGHT_OVERRIDE_CHECKBOX)&&toggleWeightInput(e.target),e.target.matches(SELECTORS_BULK_MOVE_SELECT)&&submitBulkMoveForm(e.target)})),document.querySelector(SELECTORS_GRADEBOOK_SETUP_TABLE).addEventListener("click",(e=>{const toggle=e.target.closest(SELECTORS_CATEGORY_TOGGLE);toggle&&(e.preventDefault(),toggleCategory(toggle,courseId,userId,!0))}))})(courseId,userId)}}));
//# sourceMappingURL=edittree_index.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/gradebooksetup_forms",["exports","core_form/modalform","core/str","core/notification","core_form/changechecker","core/pending"],(function(_exports,_modalform,_str,_notification,FormChangeChecker,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Prints the add item gradebook form
*
* @module core_grades
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),FormChangeChecker=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}(FormChangeChecker),_pending=_interopRequireDefault(_pending);const Selectors_advancedFormLink="a.showadvancedform";_exports.init=()=>{document.addEventListener("click",(event=>{const triggerData=(event=>{if(event.target.closest('[data-trigger="add-item-form"]')){const trigger=event.target.closest('[data-trigger="add-item-form"]');return{trigger:trigger,formClass:"core_grades\\form\\add_item",titleKey:"-1"===trigger.getAttribute("data-itemid")?"newitem":"itemsedit",args:{itemid:trigger.getAttribute("data-itemid")}}}if(event.target.closest('[data-trigger="add-category-form"]')){const trigger=event.target.closest('[data-trigger="add-category-form"]');return{trigger:trigger,formClass:"core_grades\\form\\add_category",titleKey:"-1"===trigger.getAttribute("data-category")?"newcategory":"categoryedit",args:{category:trigger.getAttribute("data-category")}}}if(event.target.closest('[data-trigger="add-outcome-form"]')){const trigger=event.target.closest('[data-trigger="add-outcome-form"]');return{trigger:trigger,formClass:"core_grades\\form\\add_outcome",titleKey:"-1"===trigger.getAttribute("data-itemid")?"newoutcomeitem":"outcomeitemsedit",args:{itemid:trigger.getAttribute("data-itemid")}}}return null})(event);if(triggerData){event.preventDefault();const pendingPromise=new _pending.default("core_grades:add_item:".concat(triggerData.args.itemid)),{trigger:trigger,formClass:formClass,titleKey:titleKey,args:args}=triggerData;args.courseid=trigger.getAttribute("data-courseid"),args.gpr_plugin=trigger.getAttribute("data-gprplugin");const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)(titleKey,"core_grades")},formClass:formClass,args:args,saveButtonText:(0,_str.getString)("save","core"),returnFocus:trigger});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?(new _pending.default("core_grades:form_submitted"),window.location.assign(event.detail.url)):_notification.default.addNotification({type:"error",message:(0,_str.getString)("saving_failed","core_grades")})})),modalForm.show(),pendingPromise.resolve()}const showAdvancedForm=event.target.closest(Selectors_advancedFormLink);if(showAdvancedForm){event.preventDefault(),new _pending.default("core_grades:show_advanced_form");const form=event.target.closest("form");form.action=showAdvancedForm.href,FormChangeChecker.disableAllChecks(),form.submit()}}))}}));
//# sourceMappingURL=gradebooksetup_forms.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
define("core_grades/grades/grader/gradingpanel/comparison",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.fillInitialValues=_exports.compareData=void 0;
/**
* Compare a given form's values and its previously set data attributes.
*
* @module core_grades/grades/grader/gradingpanel/comparison
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const fillInitialValues=form=>{Array.prototype.forEach.call(form.elements,(input=>{"submit"!==input.type&&"button"!==input.type&&("radio"===input.type||"checkbox"===input.type?input.dataset.initialValue=JSON.stringify(input.checked):void 0!==input.value?input.dataset.initialValue=JSON.stringify(input.value):"select-one"===input.type&&Array.prototype.forEach.call(input.options,(option=>{option.dataset.initialValue=JSON.stringify(option.selected)})))}))};_exports.fillInitialValues=fillInitialValues;_exports.compareData=form=>{const result=Array.prototype.some.call(form.elements,(input=>{if("submit"===input.type||"button"===input.type)return!1;if("radio"===input.type||"checkbox"===input.type){if(void 0!==input.dataset.initialValue)return input.dataset.initialValue!==JSON.stringify(input.checked)}else if(void 0!==input.value){if(void 0!==input.dataset.initialValue)return input.dataset.initialValue!==JSON.stringify(input.value)}else if("select-one"===input.type)return Array.prototype.some.call(input.options,(option=>void 0!==option.dataset.initialValue&&option.dataset.initialValue!==JSON.stringify(option.selected)));return!0}));return fillInitialValues(form),result}}));
//# sourceMappingURL=comparison.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"comparison.min.js","sources":["../../../../src/grades/grader/gradingpanel/comparison.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Compare a given form's values and its previously set data attributes.\n *\n * @module core_grades/grades/grader/gradingpanel/comparison\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const fillInitialValues = (form) => {\n Array.prototype.forEach.call(form.elements, (input) => {\n if (input.type === 'submit' || input.type === 'button') {\n return;\n } else if (input.type === 'radio' || input.type === 'checkbox') {\n input.dataset.initialValue = JSON.stringify(input.checked);\n } else if (typeof input.value !== 'undefined') {\n input.dataset.initialValue = JSON.stringify(input.value);\n } else if (input.type === 'select-one') {\n Array.prototype.forEach.call(input.options, (option) => {\n option.dataset.initialValue = JSON.stringify(option.selected);\n });\n }\n });\n};\n\n\n/**\n * Compare the form data with the initial form data from when the form was set up.\n *\n * If values have changed, return a truthy value.\n *\n * @param {HTMLElement} form\n * @return {Boolean}\n */\nexport const compareData = (form) => {\n const result = Array.prototype.some.call(form.elements, (input) => {\n if (input.type === 'submit' || input.type === 'button') {\n return false;\n } else if (input.type === 'radio' || input.type === 'checkbox') {\n if (typeof input.dataset.initialValue !== 'undefined') {\n return input.dataset.initialValue !== JSON.stringify(input.checked);\n }\n } else if (typeof input.value !== 'undefined') {\n if (typeof input.dataset.initialValue !== 'undefined') {\n return input.dataset.initialValue !== JSON.stringify(input.value);\n }\n } else if (input.type === 'select-one') {\n return Array.prototype.some.call(input.options, (option) => {\n if (typeof option.dataset.initialValue !== 'undefined') {\n return option.dataset.initialValue !== JSON.stringify(option.selected);\n }\n\n return false;\n });\n }\n\n // No value found to check. Assume that there were changes.\n return true;\n });\n\n // Fill the initial values again as the form may not be reloaded.\n fillInitialValues(form);\n\n return result;\n};\n"],"names":["fillInitialValues","form","Array","prototype","forEach","call","elements","input","type","dataset","initialValue","JSON","stringify","checked","value","options","option","selected","result","some"],"mappings":";;;;;;;;MAuBaA,kBAAqBC,OAC9BC,MAAMC,UAAUC,QAAQC,KAAKJ,KAAKK,UAAWC,QACtB,WAAfA,MAAMC,MAAoC,WAAfD,MAAMC,OAEX,UAAfD,MAAMC,MAAmC,aAAfD,MAAMC,KACvCD,MAAME,QAAQC,aAAeC,KAAKC,UAAUL,MAAMM,cACpB,IAAhBN,MAAMO,MACpBP,MAAME,QAAQC,aAAeC,KAAKC,UAAUL,MAAMO,OAC5B,eAAfP,MAAMC,MACbN,MAAMC,UAAUC,QAAQC,KAAKE,MAAMQ,SAAUC,SACzCA,OAAOP,QAAQC,aAAeC,KAAKC,UAAUI,OAAOC,oFAexChB,aAClBiB,OAAShB,MAAMC,UAAUgB,KAAKd,KAAKJ,KAAKK,UAAWC,WAClC,WAAfA,MAAMC,MAAoC,WAAfD,MAAMC,YAC1B,EACJ,GAAmB,UAAfD,MAAMC,MAAmC,aAAfD,MAAMC,cACG,IAA/BD,MAAME,QAAQC,oBACdH,MAAME,QAAQC,eAAiBC,KAAKC,UAAUL,MAAMM,cAE5D,QAA2B,IAAhBN,MAAMO,eACsB,IAA/BP,MAAME,QAAQC,oBACdH,MAAME,QAAQC,eAAiBC,KAAKC,UAAUL,MAAMO,YAE5D,GAAmB,eAAfP,MAAMC,YACNN,MAAMC,UAAUgB,KAAKd,KAAKE,MAAMQ,SAAUC,aACF,IAAhCA,OAAOP,QAAQC,cACfM,OAAOP,QAAQC,eAAiBC,KAAKC,UAAUI,OAAOC,mBAQlE,YAIXjB,kBAAkBC,MAEXiB"}
@@ -0,0 +1,3 @@
define("core_grades/grades/grader/gradingpanel/normalise",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.normaliseResult=_exports.invalidResult=_exports.failedUpdate=void 0;_exports.normaliseResult=result=>({result:result,failed:!!result.warnings.length,success:!result.warnings.length,error:null});_exports.invalidResult=()=>({success:!1,failed:!1,result:{},error:null});_exports.failedUpdate=error=>({success:!1,failed:!0,result:{},error:error})}));
//# sourceMappingURL=normalise.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"normalise.min.js","sources":["../../../../src/grades/grader/gradingpanel/normalise.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Error handling and normalisation of provided data.\n *\n * @module core_grades/grades/grader/gradingpanel/normalise\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Normalise a resultset for consumption by the grader.\n *\n * @param {Object} result The result returned from a grading web service\n * @return {Object}\n */\nexport const normaliseResult = result => {\n return {\n result,\n failed: !!result.warnings.length,\n success: !result.warnings.length,\n error: null,\n };\n};\n\n/**\n * Return the resultset used to describe an invalid result.\n *\n * @return {Object}\n */\nexport const invalidResult = () => {\n return {\n success: false,\n failed: false,\n result: {},\n error: null,\n };\n};\n\n/**\n * Return the resultset used to describe a failed update.\n *\n * @param {Object} error\n * @return {Object}\n */\nexport const failedUpdate = error => {\n return {\n success: false,\n failed: true,\n result: {},\n error,\n };\n};\n"],"names":["result","failed","warnings","length","success","error"],"mappings":"wPA6B+BA,SACpB,CACHA,OAAAA,OACAC,SAAUD,OAAOE,SAASC,OAC1BC,SAAUJ,OAAOE,SAASC,OAC1BE,MAAO,8BASc,KAClB,CACHD,SAAS,EACTH,QAAQ,EACRD,OAAQ,GACRK,MAAO,6BAUaA,QACjB,CACHD,SAAS,EACTH,QAAQ,EACRD,OAAQ,GACRK,MAAAA"}
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/grades/grader/gradingpanel/point",["exports","./repository","core_grades/grades/grader/gradingpanel/comparison","jquery","./normalise"],(function(_exports,_repository,_comparison,_jquery,_normalise){var obj;
/**
* Grading panel for simple direct grading.
*
* @module core_grades/grades/grader/gradingpanel/point
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=function(){return(0,_repository.fetchGrade)("point")(...arguments)};_exports.storeCurrentGrade=async(component,context,itemname,userId,notifyUser,rootNode)=>{const form=rootNode.querySelector("form"),grade=form.querySelector('input[name="grade"]');return grade.checkValidity()&&grade.value.trim()?!0===(0,_comparison.compareData)(form)?await(0,_repository.saveGrade)("point")(component,context,itemname,userId,notifyUser,(0,_jquery.default)(form).serialize()):"":_normalise.invalidResult}}));
//# sourceMappingURL=point.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"point.min.js","sources":["../../../../src/grades/grader/gradingpanel/point.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Grading panel for simple direct grading.\n *\n * @module core_grades/grades/grader/gradingpanel/point\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {saveGrade, fetchGrade} from './repository';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\nimport {invalidResult} from './normalise';\n\n/**\n * Fetch the current grade for a user.\n *\n * @param {object} args\n * @param {String} args.component\n * @param {Number} args.context\n * @param {String} args.itemname\n * @param {Number} args.userId\n * @param {Element} args.rootNode\n * @returns {Object}\n */\nexport const fetchCurrentGrade = (...args) => fetchGrade('point')(...args);\n\n/**\n * Store a new grade for a user.\n *\n * @param {String} component\n * @param {Number} context\n * @param {String} itemname\n * @param {Number} userId\n * @param {Boolean} notifyUser\n * @param {Element} rootNode\n * @returns {Object}\n */\nexport const storeCurrentGrade = async(component, context, itemname, userId, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n const grade = form.querySelector('input[name=\"grade\"]');\n\n if (!grade.checkValidity() || !grade.value.trim()) {\n return invalidResult;\n }\n\n if (compareData(form) === true) {\n return await saveGrade('point')(component, context, itemname, userId, notifyUser, jQuery(form).serialize());\n } else {\n return '';\n }\n};\n"],"names":["async","component","context","itemname","userId","notifyUser","rootNode","form","querySelector","grade","checkValidity","value","trim","serialize","invalidResult"],"mappings":";;;;;;;6MAwCiC,kBAAa,0BAAW,QAAX,2CAabA,MAAMC,UAAWC,QAASC,SAAUC,OAAQC,WAAYC,kBAC/EC,KAAOD,SAASE,cAAc,QAC9BC,MAAQF,KAAKC,cAAc,8BAE5BC,MAAMC,iBAAoBD,MAAME,MAAMC,QAIjB,KAAtB,2BAAYL,YACC,yBAAU,QAAV,CAAmBN,UAAWC,QAASC,SAAUC,OAAQC,YAAY,mBAAOE,MAAMM,aAExF,GANAC"}
@@ -0,0 +1,3 @@
define("core_grades/grades/grader/gradingpanel/repository",["exports","core/ajax","./normalise"],(function(_exports,_ajax,_normalise){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.saveGrade=_exports.fetchGrade=void 0;_exports.fetchGrade=type=>(component,contextid,itemname,gradeduserid)=>(0,_ajax.call)([{methodname:"core_grades_grader_gradingpanel_".concat(type,"_fetch"),args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid}}])[0];_exports.saveGrade=type=>async(component,contextid,itemname,gradeduserid,notifyUser,formdata)=>(0,_normalise.normaliseResult)(await(0,_ajax.call)([{methodname:"core_grades_grader_gradingpanel_".concat(type,"_store"),args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid,notifyuser:notifyUser,formdata:formdata}}])[0])}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../../../src/grades/grader/gradingpanel/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Repository for simple direct grading panel.\n *\n * @module core_grades/grades/grader/gradingpanel/repository\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {call as fetchMany} from 'core/ajax';\nimport {normaliseResult} from './normalise';\n\nexport const fetchGrade = type => (component, contextid, itemname, gradeduserid) => {\n return fetchMany([{\n methodname: `core_grades_grader_gradingpanel_${type}_fetch`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n },\n }])[0];\n};\n\nexport const saveGrade = type => async(component, contextid, itemname, gradeduserid, notifyUser, formdata) => {\n return normaliseResult(await fetchMany([{\n methodname: `core_grades_grader_gradingpanel_${type}_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n notifyuser: notifyUser,\n formdata,\n },\n }])[0]);\n};\n"],"names":["type","component","contextid","itemname","gradeduserid","methodname","args","async","notifyUser","formdata","notifyuser"],"mappings":"gQAyB0BA,MAAQ,CAACC,UAAWC,UAAWC,SAAUC,gBACxD,cAAU,CAAC,CACdC,qDAA+CL,eAC/CM,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,iBAEJ,sBAGiBJ,MAAQO,MAAMN,UAAWC,UAAWC,SAAUC,aAAcI,WAAYC,YACtF,oCAAsB,cAAU,CAAC,CACpCJ,qDAA+CL,eAC/CM,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,aACAM,WAAYF,WACZC,SAAAA,aAEJ"}
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/grades/grader/gradingpanel/scale",["exports","./repository","core_grades/grades/grader/gradingpanel/comparison","jquery","./normalise"],(function(_exports,_repository,_comparison,_jquery,_normalise){var obj;
/**
* Grading panel for simple direct grading.
*
* @module core_grades/grades/grader/gradingpanel/scale
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=function(){return(0,_repository.fetchGrade)("scale")(...arguments)};_exports.storeCurrentGrade=(component,context,itemname,userId,notifyUser,rootNode)=>{const form=rootNode.querySelector("form"),grade=form.querySelector('select[name="grade"]');return grade.checkValidity()&&grade.value.trim()?!0===(0,_comparison.compareData)(form)?(0,_repository.saveGrade)("scale")(component,context,itemname,userId,notifyUser,(0,_jquery.default)(form).serialize()):"":_normalise.invalidResult}}));
//# sourceMappingURL=scale.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"scale.min.js","sources":["../../../../src/grades/grader/gradingpanel/scale.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Grading panel for simple direct grading.\n *\n * @module core_grades/grades/grader/gradingpanel/scale\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {saveGrade, fetchGrade} from './repository';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\nimport {invalidResult} from './normalise';\n\nexport const fetchCurrentGrade = (...args) => fetchGrade('scale')(...args);\n\nexport const storeCurrentGrade = (component, context, itemname, userId, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n const grade = form.querySelector('select[name=\"grade\"]');\n\n if (!grade.checkValidity() || !grade.value.trim()) {\n return invalidResult;\n }\n\n if (compareData(form) === true) {\n return saveGrade('scale')(component, context, itemname, userId, notifyUser, jQuery(form).serialize());\n } else {\n return '';\n }\n};\n"],"names":["component","context","itemname","userId","notifyUser","rootNode","form","querySelector","grade","checkValidity","value","trim","serialize","invalidResult"],"mappings":";;;;;;;6MA6BiC,kBAAa,0BAAW,QAAX,2CAEb,CAACA,UAAWC,QAASC,SAAUC,OAAQC,WAAYC,kBAC1EC,KAAOD,SAASE,cAAc,QAC9BC,MAAQF,KAAKC,cAAc,+BAE5BC,MAAMC,iBAAoBD,MAAME,MAAMC,QAIjB,KAAtB,2BAAYL,OACL,yBAAU,QAAV,CAAmBN,UAAWC,QAASC,SAAUC,OAAQC,YAAY,mBAAOE,MAAMM,aAElF,GANAC"}
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core_grades/searchwidget/selectors","core/notification","core/log"],(function(_exports,_utils,Templates,Selectors,_notification,_log){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* A widget to search users or grade items within the gradebook.
*
* @module core_grades/searchwidget/basewidget
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=_interopRequireDefault(_notification),_log=_interopRequireDefault(_log);_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,afterSelect=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;_log.default.debug("The core_grades/searchwidget/basewidget component is deprecated. Please refer to core/search_combobox() instead."),bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc,afterSelect)})).catch(_notification.default.exception)};const registerListenerEvents=function(widgetContentContainer,data,searchFunc){let afterSelect=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),searchInput.removeAttribute("aria-activedescendant"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}));const inputElement=document.getElementById(searchInput.dataset.inputElement);inputElement&&afterSelect&&inputElement.addEventListener("change",(e=>{widgetContentContainer.querySelector(Selectors.elements.getSearchWidgetSelectOption(searchInput))&&afterSelect(e.target.value)})),widgetContentContainer.addEventListener("click",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption)if(inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}})),widgetContentContainer.addEventListener("keydown",(e=>{const deprecatedOption=e.target.closest('a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])');if(deprecatedOption&&(" "===e.key||"Enter"===e.key))if(e.preventDefault(),inputElement&&afterSelect)afterSelect(deprecatedOption.dataset.value);else{const url=(data.find((object=>object.id==deprecatedOption.dataset.value))||{url:""}).url;location.href=url}}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{container.innerHTML="";const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);if(await Templates.replaceNodeContents(searchResultsContainer,html,js),"listbox"!==searchResultsContainer.getAttribute("role")){const deprecatedOptions=searchResultsContainer.querySelectorAll('a.dropdown-item[role="menuitem"][href=""], .dropdown-item[role="option"]:not([href])');for(const option of deprecatedOptions)option.tabIndex=0,option.removeAttribute("href")}};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
//# sourceMappingURL=basewidget.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/searchwidget/initials",["exports","core/pending","core/url","core/custom_interaction_events","jquery"],(function(_exports,_pending,Url,_custom_interaction_events,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A small dropdown to filter users within the gradebook.
*
* @module core_grades/searchwidget/initials
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Url=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Url),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);let registered=!1;const selectors_pageListItem="page-item",selectors_pageClickableItem=".page-link",selectors_activeItem="active",selectors_formDropdown=".initialsdropdownform",selectors_parentDomNode=".initials-selector",selectors_firstInitial="firstinitial",selectors_lastInitial="lastinitial",selectors_initialBars=".initialbar",selectors_targetButton="initialswidget",selectors_formItems={type:"submit",save:"save",cancel:"cancel"};_exports.init=function(callingLink){let gpr_userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,gpr_search=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(callingLink,gpr_userid,gpr_search),(0,_jquery.default)(selectors_parentDomNode).on("shown.bs.dropdown",(()=>{document.querySelector(selectors_pageClickableItem).focus({preventScroll:!0})})),pendingPromise.resolve(),registered=!0};const registerListenerEvents=function(callingLink){let gpr_userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,gpr_search=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(e=>{let{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}=onClickVariables(),itemToReset="";if(e.target.closest(selectors_formDropdown)&&e.preventDefault(),e.target.closest("".concat(selectors_formDropdown," .").concat(selectors_pageListItem))){if(e.target.classList.contains(selectors_pageListItem))return;e.target.closest(selectors_initialBars).classList.contains(selectors_firstInitial)?(sifirst=e.target,itemToReset=firstActive):(silast=e.target,itemToReset=lastActive),swapActiveItems(itemToReset,e)}if(e.target.closest("".concat(selectors_formDropdown))&&e.target.type===selectors_formItems.type){if(e.target.dataset.action===selectors_formItems.save){const params={id:e.target.closest(selectors_formDropdown).dataset.courseid,gpr_search:null!==gpr_search?gpr_search:"",sifirst:sifirst.parentElement.classList.contains("initialbarall")?"":sifirst.value,silast:silast.parentElement.classList.contains("initialbarall")?"":silast.value};null!==gpr_userid&&(params.gpr_userid=gpr_userid),window.location=Url.relativeUrl(callingLink,params)}e.target.dataset.action===selectors_formItems.cancel&&(0,_jquery.default)(".".concat(selectors_targetButton)).dropdown("toggle")}}))}))},onClickVariables=()=>{const firstItems=[...document.querySelectorAll(".".concat(selectors_firstInitial," li"))],lastItems=[...document.querySelectorAll(".".concat(selectors_lastInitial," li"))],firstActive=firstItems.filter((item=>item.classList.contains(selectors_activeItem)))[0],lastActive=lastItems.filter((item=>item.classList.contains(selectors_activeItem)))[0];let sifirst=firstActive.querySelector(selectors_pageClickableItem),silast=lastActive.querySelector(selectors_pageClickableItem);return{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}},swapActiveItems=(itemToReset,e)=>{itemToReset.classList.remove(selectors_activeItem),itemToReset.querySelector(selectors_pageClickableItem).ariaCurrent=!1;e.target.parentElement.classList.add(selectors_activeItem),e.target.ariaCurrent=!0}}));
//# sourceMappingURL=initials.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_grades/searchwidget/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A repo for the search widget.
*
* @module core_grades/searchwidget/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=_exports.gradeitemFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.gradeitemFetch=courseid=>{const request={methodname:"gradereport_singleview_get_grade_items_for_search_widget",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.userFetch=(courseid,groupId)=>{const request={methodname:"core_grades_get_enrolled_users_for_selector",args:{courseid:courseid,groupid:groupId}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../src/searchwidget/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the search widget.\n *\n * @module core_grades/searchwidget/repository\n * @copyright 2022 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.\n * Note: This will be worked upon in the single view issue.\n *\n * @method gradeitemFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const gradeitemFetch = (courseid) => {\n const request = {\n methodname: 'gradereport_singleview_get_grade_items_for_search_widget',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a course ID, we want to fetch the enrolled learners, so we may fetch their reports.\n *\n * @method userFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @param {int} groupId ID of the group to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const userFetch = (courseid, groupId) => {\n const request = {\n methodname: 'core_grades_get_enrolled_users_for_selector',\n args: {\n courseid: courseid,\n groupid: groupId,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call","groupId","groupid"],"mappings":";;;;;;;2LAiC+BA,iBACrBC,QAAU,CACZC,WAAY,2DACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU,uBAWP,CAACD,SAAUM,iBAC1BL,QAAU,CACZC,WAAY,8CACZC,KAAM,CACFH,SAAUA,SACVO,QAASD,iBAGVF,cAAKC,KAAK,CAACJ,UAAU"}
+3
View File
@@ -0,0 +1,3 @@
define("core_grades/searchwidget/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={regions:{searchResults:'[data-region="search-results-container-widget"]',unsearchableContent:'[data-region="unsearchable-content-container-widget"]'},actions:{search:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]'},elements:{getSearchWidgetSelector:searchtype=>'.search-widget[data-searchtype="'.concat(searchtype,'"]'),getSearchWidgetDropdownSelector:searchtype=>'.search-widget[data-searchtype="'.concat(searchtype,'"] .dropdown-menu'),getSearchWidgetSelectOption:searchInput=>"#".concat(searchInput.getAttribute("aria-controls"),' [role="option"][aria-selected="true"]')}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../../src/searchwidget/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Selectors for the search widget.\n *\n * @module core_grades/searchwidget/selectors\n * @copyright 2022 Mihail Geshoski <mihail@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n regions: {\n searchResults: '[data-region=\"search-results-container-widget\"]',\n unsearchableContent: '[data-region=\"unsearchable-content-container-widget\"]',\n },\n actions: {\n search: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n },\n elements: {\n getSearchWidgetSelector: searchtype => `.search-widget[data-searchtype=\"${searchtype}\"]`,\n getSearchWidgetDropdownSelector: searchtype => `.search-widget[data-searchtype=\"${searchtype}\"] .dropdown-menu`,\n getSearchWidgetSelectOption:\n searchInput => `#${searchInput.getAttribute('aria-controls')} [role=\"option\"][aria-selected=\"true\"]`,\n },\n};\n"],"names":["regions","searchResults","unsearchableContent","actions","search","clearSearch","elements","getSearchWidgetSelector","searchtype","getSearchWidgetDropdownSelector","getSearchWidgetSelectOption","searchInput","getAttribute"],"mappings":"oLAuBe,CACXA,QAAS,CACLC,cAAe,kDACfC,oBAAqB,yDAEzBC,QAAS,CACLC,OAAQ,yBACRC,YAAa,+BAEjBC,SAAU,CACNC,wBAAyBC,sDAAiDA,iBAC1EC,gCAAiCD,sDAAiDA,gCAClFE,4BACIC,wBAAmBA,YAAYC,aAAa"}
@@ -0,0 +1,92 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import BulkActions from "core/bulkactions/bulk_actions";
import GradebookEditTreeBulkMove from "core_grades/bulkactions/edit/tree/move";
/**
* Class for defining the bulk actions area in the gradebook setup page.
*
* @module core_grades/bulkactions/edit/tree/bulk_actions
* @copyright 2023 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors = {
selectBulkItemCheckbox: 'input[type="checkbox"].itemselect'
};
export default class GradebookEditTreeBulkActions extends BulkActions {
/** @property {int|null} courseID The course ID. */
courseID = null;
/**
* Returns the instance of the class.
*
* @param {int} courseID
* @returns {GradebookEditTreeBulkActions}
*/
static init(courseID) {
return new this(courseID);
}
/**
* The class constructor.
*
* @param {int} courseID The course ID.
* @returns {void}
*/
constructor(courseID) {
super();
this.courseID = courseID;
}
/**
* Returns the array of the relevant bulk action objects for the gradebook setup page.
*
* @method getBulkActions
* @returns {Array}
*/
getBulkActions() {
return [
new GradebookEditTreeBulkMove(this.courseID)
];
}
/**
* Returns the array of selected items.
*
* @method getSelectedItems
* @returns {Array}
*/
getSelectedItems() {
return document.querySelectorAll(`${Selectors.selectBulkItemCheckbox}:checked`);
}
/**
* Adds the listener for the item select change event.
*
* @method registerItemSelectChangeEvent
* @param {function} eventHandler The event handler function.
* @returns {void}
*/
registerItemSelectChangeEvent(eventHandler) {
const itemSelectCheckboxes = document.querySelectorAll(Selectors.selectBulkItemCheckbox);
itemSelectCheckboxes.forEach((checkbox) => {
checkbox.addEventListener('change', eventHandler.bind(this));
});
}
}
+177
View File
@@ -0,0 +1,177 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class that defines the bulk move action in the gradebook setup page.
*
* @module core_grades/bulkactions/edit/tree/move
* @copyright 2023 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import BulkAction from 'core/bulkactions/bulk_action';
import {get_string as getString} from 'core/str';
import ModalSaveCancel from 'core/modal_save_cancel';
import Templates from 'core/templates';
import Ajax from 'core/ajax';
import ModalEvents from 'core/modal_events';
import MoveOptionsTree from 'core_grades/bulkactions/edit/tree/move_options_tree';
/** @constant {Object} The object containing the relevant selectors. */
const Selectors = {
editTreeForm: '#gradetreeform',
bulkMoveInput: 'input[name="bulkmove"]',
bulkMoveAfterInput: 'input[name="moveafter"]'
};
export default class GradebookEditTreeBulkMove extends BulkAction {
/** @property {int|null} courseId The course ID. */
courseId = null;
/** @property {MoveOptionsTree|null} moveOptionsTree The move options tree object. */
moveOptionsTree = null;
/** @property {string|null} gradeTree The grade tree structure. */
gradeTree = null;
/**
* The class constructor.
*
* @param {int} courseId The course ID.
* @returns {void}
*/
constructor(courseId) {
super();
this.courseId = courseId;
}
/**
* Defines the selector of the element that triggers the bulk move action.
*
* @returns {string} The bulk move action trigger selector.
*/
getBulkActionTriggerSelector() {
return 'button[data-action="move"]';
}
/**
* Defines the behavior once the bulk move action is triggered.
*
* @method executeBulkAction
* @returns {void}
*/
async triggerBulkAction() {
const modal = await this.showModal();
this.registerCustomListenerEvents(modal);
}
/**
* Renders the bulk move action trigger element.
*
* @method renderBulkActionTrigger
* @returns {Promise} The bulk move action trigger promise
*/
async renderBulkActionTrigger() {
return Templates.render('core_grades/bulkactions/edit/tree/bulk_move_trigger', {});
}
/**
* Register custom event listeners.
*
* @method registerCustomClickListenerEvents
* @param {Object} modal The modal object.
* @returns {void}
*/
async registerCustomListenerEvents(modal) {
await modal.getBody();
// Initialize the move options tree once the modal is shown.
modal.getRoot().on(ModalEvents.shown, () => {
this.moveOptionsTree = new MoveOptionsTree(() => {
// Enable the 'Move' action button once something is selected.
modal.setButtonDisabled('save', false);
});
});
// Destroy the modal once it is hidden.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
// Define the move action event.
modal.getRoot().on(ModalEvents.save, () => {
// Make sure that a move option is selected.
if (this.moveOptionsTree && this.moveOptionsTree.selectedMoveOption) {
// Set the relevant form values.
document.querySelector(Selectors.bulkMoveInput).value = 1;
document.querySelector(Selectors.bulkMoveAfterInput).value = this.moveOptionsTree.selectedMoveOption.dataset.id;
// Submit the form.
document.querySelector(Selectors.editTreeForm).submit();
}
});
}
/**
* Fetch the grade tree structure for the current course.
*
* @method fetchGradeTree
* @returns {Promise} The grade tree promise
*/
fetchGradeTree() {
const request = {
methodname: 'core_grades_get_grade_tree',
args: {
courseid: this.courseId,
},
};
return Ajax.call([request])[0];
}
/**
* Renders the bulk move modal body.
*
* @method renderModalBody
* @returns {Promise} The modal body promise
*/
async renderModalBody() {
// We need to fetch the grade tree structure only once.
if (this.gradeTree === null) {
this.gradeTree = await this.fetchGradeTree();
}
return Templates.render('core_grades/bulkactions/edit/tree/bulk_move_grade_tree',
JSON.parse(this.gradeTree));
}
/**
* Show the bulk move modal.
*
* @method showModal
* @returns {Promise} The modal promise
*/
async showModal() {
const modal = await ModalSaveCancel.create({
title: await getString('movesitems', 'grades'),
body: await this.renderModalBody(),
buttons: {
save: await getString('move')
},
large: true,
});
// Disable the 'Move' action button until something is selected.
modal.setButtonDisabled('save', true);
modal.show();
return modal;
}
}
@@ -0,0 +1,109 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Keyboard navigation and aria-tree compatibility for the grade move options.
*
* @module core_grades/bulkactions/edit/tree/move_options_tree
* @copyright 2023 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Tree from 'core/tree';
import {getList} from 'core/normalise';
/** @constant {Object} The object containing the relevant selectors. */
const Selectors = {
moveOptionsTree: '#destination-selector [role="tree"]',
moveOption: '#destination-selector [role="treeitem"]',
toggleGroupLink: '#destination-selector .collapse-list-link',
};
export default class MoveOptionsTree extends Tree {
/** @property {function|null} afterSelectMoveOptionCallback Callback function to run after selecting a move option. */
afterSelectMoveOptionCallback = null;
/** @property {HTMLElement|null} selectedMoveOption The selected move option. */
selectedMoveOption = null;
/**
* The class constructor.
*
* @param {function|null} afterSelectMoveOptionCallback Callback function used to define actions that should be run
* after selecting a move option.
* @returns {void}
*/
constructor(afterSelectMoveOptionCallback) {
super(Selectors.moveOptionsTree);
this.afterSelectMoveOptionCallback = afterSelectMoveOptionCallback;
}
/**
* Handle a key down event.
*
* @method handleKeyDown
* @param {Event} e The event.
*/
handleKeyDown(e) {
// If the user presses enter or space, select the item.
if (e.keyCode === this.keys.enter || e.keyCode === this.keys.space) {
this.selectMoveOption(e.target);
} else { // Otherwise, let the default behaviour happen.
super.handleKeyDown(e);
}
}
/**
* Handle an item click.
*
* @param {Event} event The click event.
* @param {jQuery} item The item clicked.
* @returns {void}
*/
handleItemClick(event, item) {
const isToggleGroupLink = event.target.closest(Selectors.toggleGroupLink);
// If the click is on the toggle group (chevron) link, let the default behaviour happen.
if (isToggleGroupLink) {
super.handleItemClick(event, item);
return;
}
// If the click is on the item itself, select it.
this.selectMoveOption(getList(item)[0]);
}
/**
* Select a move option.
*
* @method selectMoveOption
* @param {HTMLElement} moveOption The move option to select.
*/
selectMoveOption(moveOption) {
// Create the cache of the visible items.
this.refreshVisibleItemsCache();
// Deselect all the move options.
document.querySelectorAll(Selectors.moveOption).forEach(item => {
item.dataset.selected = "false";
});
// Select and set the focus on the specified move option.
moveOption.dataset.selected = "true";
this.selectedMoveOption = moveOption;
moveOption.focus();
// Call the callback function if it is defined.
if (typeof this.afterSelectMoveOptionCallback === 'function') {
this.afterSelectMoveOptionCallback();
}
}
}
+245
View File
@@ -0,0 +1,245 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Allow the user to search for grades within the grade area.
*
* @module core_grades/comboboxsearch/grade
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import search_combobox from 'core/comboboxsearch/search_combobox';
import * as Repository from 'core_grades/searchwidget/repository';
import {renderForPromise, replaceNodeContents} from 'core/templates';
import {debounce} from 'core/utils';
export default class GradeItemSearch extends search_combobox {
courseID;
constructor() {
super();
// Define our standard lookups.
this.selectors = {
...this.selectors,
courseid: '[data-region="courseid"]',
placeholder: '.gradesearchdropdown [data-region="searchplaceholder"]',
};
const component = document.querySelector(this.componentSelector());
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;
const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);
searchValueElement.addEventListener('change', () => {
this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);
if (valueElement.value !== searchValueElement.value) {
valueElement.value = searchValueElement.value;
valueElement.dispatchEvent(new Event('change', {bubbles: true}));
}
searchValueElement.value = '';
});
this.$component.on('hide.bs.dropdown', () => {
this.searchInput.removeAttribute('aria-activedescendant');
const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role="listbox"]`);
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
option.classList.remove('active');
});
listbox.scrollTop = 0;
// Use setTimeout to make sure the following code is executed after the click event is handled.
setTimeout(() => {
if (this.searchInput.value !== '') {
this.searchInput.value = '';
this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
}
});
});
this.renderDefault();
}
static init() {
return new GradeItemSearch();
}
/**
* The overall div that contains the searching widget.
*
* @returns {string}
*/
componentSelector() {
return '.grade-search';
}
/**
* The dropdown div that contains the searching widget result space.
*
* @returns {string}
*/
dropdownSelector() {
return '.gradesearchdropdown';
}
/**
* Build the content then replace the node.
*/
async renderDropdown() {
const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {
instance: this.instance,
results: this.getMatchedResults(),
hasresults: this.getMatchedResults().length > 0,
searchterm: this.getSearchTerm(),
});
replaceNodeContents(this.selectors.placeholder, html, js);
// Remove aria-activedescendant when the available options change.
this.searchInput.removeAttribute('aria-activedescendant');
}
/**
* Build the content then replace the node by default we want our form to exist.
*/
async renderDefault() {
this.setMatchedResults(await this.filterDataset(await this.getDataset()));
this.filterMatchDataset();
await this.renderDropdown();
this.updateNodes();
this.registerInputEvents();
}
/**
* Get the data we will be searching against in this component.
*
* @returns {Promise<*>}
*/
async fetchDataset() {
return await Repository.gradeitemFetch(this.courseID).then((r) => r.gradeitems);
}
/**
* Dictate to the search component how and what we want to match upon.
*
* @param {Array} filterableData
* @returns {Array} The users that match the given criteria.
*/
async filterDataset(filterableData) {
// Sometimes we just want to show everything.
if (this.getPreppedSearchTerm() === '') {
return filterableData;
}
return filterableData.filter((grade) => Object.keys(grade).some((key) => {
if (grade[key] === "") {
return false;
}
return grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
}));
}
/**
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
*/
filterMatchDataset() {
this.setMatchedResults(
this.getMatchedResults().map((grade) => {
return {
id: grade.id,
name: grade.name,
};
})
);
}
/**
* Handle any keyboard inputs.
*/
registerInputEvents() {
// Register & handle the text input.
this.searchInput.addEventListener('input', debounce(async() => {
this.setSearchTerms(this.searchInput.value);
// We can also require a set amount of input before search.
if (this.searchInput.value === '') {
// Hide the "clear" search button in the search bar.
this.clearSearchButton.classList.add('d-none');
} else {
// Display the "clear" search button in the search bar.
this.clearSearchButton.classList.remove('d-none');
}
// User has given something for us to filter against.
await this.filterrenderpipe();
}, 300));
}
/**
* The handler for when a user interacts with the component.
*
* @param {MouseEvent} e The triggering event that we are working with.
*/
async clickHandler(e) {
if (e.target.closest(this.selectors.clearSearch)) {
e.stopPropagation();
// Clear the entered search query in the search bar.
this.searchInput.value = '';
this.setSearchTerms(this.searchInput.value);
this.searchInput.focus();
this.clearSearchButton.classList.add('d-none');
// Display results.
await this.filterrenderpipe();
}
}
/**
* The handler for when a user changes the value of the component (selects an option from the dropdown).
*
* @param {Event} e The change event.
*/
changeHandler(e) {
window.location = this.selectOneLink(e.target.value);
}
/**
* Override the input event listener for the text input area.
*/
registerInputHandlers() {
// Register & handle the text input.
this.searchInput.addEventListener('input', debounce(() => {
this.setSearchTerms(this.searchInput.value);
// We can also require a set amount of input before search.
if (this.getSearchTerm() === '') {
// Hide the "clear" search button in the search bar.
this.clearSearchButton.classList.add('d-none');
} else {
// Display the "clear" search button in the search bar.
this.clearSearchButton.classList.remove('d-none');
}
}, 300));
}
/**
* Build up the view all link that is dedicated to a particular result.
* We will call this function when a user interacts with the combobox to redirect them to show their results in the page.
*
* @param {Number} gradeID The ID of the grade item selected.
*/
selectOneLink(gradeID) {
throw new Error(`selectOneLink(${gradeID}) must be implemented in ${this.constructor.name}`);
}
}
+268
View File
@@ -0,0 +1,268 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Enhance the gradebook tree setup with various facilities.
*
* @module core_grades/edittree_index
* @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import storage from 'core/localstorage';
import {addIconToContainer} from 'core/loadingicon';
import Notification from 'core/notification';
import Pending from 'core/pending';
const SELECTORS = {
CATEGORY_TOGGLE: '.toggle-category',
GRADEBOOK_SETUP_TABLE: '.setup-grades',
WEIGHT_OVERRIDE_CHECKBOX: '.weightoverride',
BULK_MOVE_SELECT: '#menumoveafter',
BULK_MOVE_INPUT: '#bulkmoveinput',
GRADEBOOK_SETUP_WRAPPER: '.gradetree-wrapper',
GRADEBOOK_SETUP_BOX: '.gradetreebox'
};
/**
* Register related event listeners.
*
* @method registerListenerEvents
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
const registerListenerEvents = (courseId, userId) => {
document.addEventListener('change', e => {
// Toggle the availability of the weight input field based on the changed state (checked/unchecked) of the
// related checkbox element.
if (e.target.matches(SELECTORS.WEIGHT_OVERRIDE_CHECKBOX)) {
toggleWeightInput(e.target);
}
// Submit the bulk move form when the selected option in the bulk move select element has been changed.
if (e.target.matches(SELECTORS.BULK_MOVE_SELECT)) {
submitBulkMoveForm(e.target);
}
});
const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
gradebookSetup.addEventListener('click', e => {
const toggle = e.target.closest(SELECTORS.CATEGORY_TOGGLE);
// Collapse or expand the grade category when the visibility toggle button is activated.
if (toggle) {
e.preventDefault();
toggleCategory(toggle, courseId, userId, true);
}
});
};
/**
* Toggle the weight input field based on its checkbox.
*
* @method toggleWeightInput
* @param {object} weightOverrideCheckbox The weight override checkbox element.
*/
const toggleWeightInput = (weightOverrideCheckbox) => {
const row = weightOverrideCheckbox.closest('tr');
const itemId = row.dataset.itemid;
const weightOverrideInput = row.querySelector(`input[name="weight_${itemId}"]`);
weightOverrideInput.disabled = !weightOverrideCheckbox.checked;
};
/**
* Submit the bulk move form.
*
* @method toggleWeightInput
* @param {object} bulkMoveSelect The bulk move select element.
*/
const submitBulkMoveForm = (bulkMoveSelect) => {
const form = bulkMoveSelect.closest('form');
const bulkMoveInput = form.querySelector(SELECTORS.BULK_MOVE_INPUT);
bulkMoveInput.value = 1;
form.submit();
};
/**
* Method that collapses all relevant grade categories based on the locally stored state of collapsed grade categories
* for a given user.
*
* @method collapseGradeCategories
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
const collapseGradeCategories = (courseId, userId) => {
const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
const storedCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
if (storedCollapsedCategories) {
// Fetch all grade categories that are locally stored as collapsed and re-apply the collapse action.
const collapsedCategories = JSON.parse(storedCollapsedCategories);
collapsedCategories.forEach((category) => {
const categoryToggleElement =
gradebookSetup.querySelector(`${SELECTORS.CATEGORY_TOGGLE}[data-category="${category}"`);
if (categoryToggleElement) {
toggleCategory(categoryToggleElement, courseId, userId, false);
}
});
}
};
/**
* Method that updates the locally stored state of collapsed grade categories based on a performed toggle action on a
* given grade category.
*
* @method updateCollapsedCategoriesStoredState
* @param {string} category The category to be added or removed from the collapsed grade categories local storage.
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
* @param {boolean} isCollapsing Whether the category is being collapsed or not.
*/
const updateCollapsedCategoriesStoredState = (category, courseId, userId, isCollapsing) => {
const currentStoredCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
let collapsedCategories = currentStoredCollapsedCategories ?
JSON.parse(currentStoredCollapsedCategories) : [];
if (isCollapsing) {
collapsedCategories.push(category);
} else {
collapsedCategories = collapsedCategories.filter(cat => cat !== category);
}
storage.set(`core_grade_collapsedgradecategories_${courseId}_${userId}`, JSON.stringify(collapsedCategories));
};
/**
* Method that handles the grade category toggle action.
*
* @method toggleCategory
* @param {object} toggleElement The category toggle node that was clicked.
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
* @param {boolean} storeCollapsedState Whether to store (local storage) the state of collapsed grade categories.
*/
const toggleCategory = (toggleElement, courseId, userId, storeCollapsedState) => {
const target = toggleElement.dataset.target;
const category = toggleElement.dataset.category;
// Whether the toggle action is collapsing the category or not.
const isCollapsing = toggleElement.getAttribute('aria-expanded') === "true";
const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
// Find all targeted 'children' rows of the toggled category.
const targetRows = gradebookSetup.querySelectorAll(target);
// Find the maximum grade cell in the grade category that is being collapsed/expanded.
const toggleElementRow = toggleElement.closest('tr');
const maxGradeCell = toggleElementRow.querySelector('.column-range');
if (isCollapsing) {
toggleElement.setAttribute('aria-expanded', 'false');
// Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
// to expand this category we only target rows which have been hidden by this category toggle action.
toggleElement.dataset.target = `[data-hidden-by='${category}']`;
if (maxGradeCell) {
const relatedCategoryAggregationRow = gradebookSetup.querySelector(`[data-aggregationforcategory='${category}']`);
maxGradeCell.innerHTML = relatedCategoryAggregationRow.querySelector('.column-range').innerHTML;
}
} else {
toggleElement.setAttribute('aria-expanded', 'true');
// Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
// to collapse this category we only target rows which are children of this category and are not currently hidden.
toggleElement.dataset.target = `.${category}[data-hidden='false']`;
if (maxGradeCell) {
maxGradeCell.innerHTML = '';
}
}
// If explicitly instructed, update accordingly the locally stored state of collapsed categories based on the
// toggle action performed on the given grade category.
if (storeCollapsedState) {
updateCollapsedCategoriesStoredState(category, courseId, userId, isCollapsing);
}
// Loop through all targeted child row elements and update the required data attributes to either hide or show
// them depending on the toggle action (collapsing or expanding).
targetRows.forEach((row) => {
if (isCollapsing) {
row.dataset.hidden = 'true';
row.dataset.hiddenBy = category;
} else {
row.dataset.hidden = 'false';
row.dataset.hiddenBy = '';
}
});
// Since the user report is presented in an HTML table, rowspans are used under each category to create a visual
// hierarchy between categories and grading items. When expanding or collapsing a category we need to also update
// (subtract or add) the rowspan values associated to each parent category row to preserve the correct visual
// hierarchy in the table.
updateParentCategoryRowspans(toggleElement, targetRows.length);
};
/**
* Method that updates the rowspan value of all 'parent' category rows of a given category node.
*
* @method updateParentCategoryRowspans
* @param {object} toggleElement The category toggle node that was clicked.
* @param {int} num The number we want to add or subtract from the rowspan value of the 'parent' category row elements.
*/
const updateParentCategoryRowspans = (toggleElement, num) => {
const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
// Get the row element which contains the category toggle node.
const rowElement = toggleElement.closest('tr');
// Loop through the class list of the toggle category row element.
// The list contains classes which identify all parent categories of the toggled category.
rowElement.classList.forEach((className) => {
// Find the toggle node of the 'parent' category that is identified by the given class name.
const parentCategoryToggleElement = gradebookSetup.querySelector(`[data-target=".${className}[data-hidden='false']"`);
if (parentCategoryToggleElement) {
// Get the row element which contains the parent category toggle node.
const categoryRowElement = parentCategoryToggleElement.closest('tr');
// Find the rowspan element associated to this parent category.
const categoryRowSpanElement = categoryRowElement.nextElementSibling.querySelector('[rowspan]');
// Depending on whether the toggle action has expanded or collapsed the category, either add or
// subtract from the 'parent' category rowspan.
if (toggleElement.getAttribute('aria-expanded') === "true") {
categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan + num;
} else { // The category has been collapsed.
categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan - num;
}
}
});
};
/**
* Initialize module.
*
* @method init
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
export const init = (courseId, userId) => {
const pendingPromise = new Pending();
const gradebookSetupBox = document.querySelector(SELECTORS.GRADEBOOK_SETUP_BOX);
// Display a loader while the relevant grade categories are being re-collapsed on page load (based on the locally
// stored state for the given user).
addIconToContainer(gradebookSetupBox).then((loader) => {
setTimeout(() => {
collapseGradeCategories(courseId, userId);
// Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.
loader.remove();
document.querySelector(SELECTORS.GRADEBOOK_SETUP_WRAPPER).classList.remove('d-none');
pendingPromise.resolve();
}, 150);
return;
}).fail(Notification.exception);
registerListenerEvents(courseId, userId);
};
+312
View File
@@ -0,0 +1,312 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This module provides functionality for managing weight calculations and adjustments for grade items.
*
* @module core_grades/edittree_weight
* @copyright 2023 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getString} from 'core/str';
import {prefetchStrings} from 'core/prefetch';
/**
* Selectors.
*
* @type {Object}
*/
const selectors = {
weightOverrideCheckbox: 'input[type="checkbox"][name^="weightoverride_"]',
weightOverrideInput: 'input[type="text"][name^="weight_"]',
aggregationForCategory: category => `[data-aggregationforcategory='${category}']`,
childrenByCategory: category => `tr[data-parent-category="${category}"]`,
categoryByIdentifier: identifier => `tr.category[data-category="${identifier}"]`,
};
/**
* An object representing grading-related constants.
* The same as what's defined in lib/grade/constants.php.
*
* @type {Object}
* @property {Object} aggregation Aggregation settings.
* @property {number} aggregation.sum Aggregation method: sum.
* @property {Object} type Grade type settings.
* @property {number} type.none Grade type: none.
* @property {number} type.value Grade type: value.
* @property {number} type.scale Grade type: scale.
*/
const grade = {
aggregation: {
sum: 13,
},
};
/**
* The character used as the decimal separator for number formatting.
*
* @type {string}
*/
let decimalSeparator;
/**
* This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
* Even though the old algorithm has bugs in it, we need to preserve existing grades.
*
* @type {boolean}
*/
let oldExtraCreditCalculation;
/**
* Recalculates the natural weights for grade items within a given category.
*
* @param {HTMLElement} categoryElement The DOM element representing the category.
*/
// Suppress 'complexity' linting rule to keep this function as close to grade_category::auto_update_weights.
// eslint-disable-next-line complexity
const recalculateNaturalWeights = (categoryElement) => {
const childElements = document.querySelectorAll(selectors.childrenByCategory(categoryElement.dataset.category));
// Calculate the sum of the grademax's of all the items within this category.
let totalGradeMax = 0;
// Out of 100, how much weight has been manually overridden by a user?
let totalOverriddenWeight = 0;
let totalOverriddenGradeMax = 0;
// Has every assessment in this category been overridden?
let automaticGradeItemsPresent = false;
// Does the grade item require normalising?
let requiresNormalising = false;
// Is there an error in the weight calculations?
let erroneous = false;
// This array keeps track of the id and weight of every grade item that has been overridden.
const overrideArray = {};
for (const childElement of childElements) {
const weightInput = childElement.querySelector(selectors.weightOverrideInput);
const weightCheckbox = childElement.querySelector(selectors.weightOverrideCheckbox);
// There are cases where a grade item should be excluded from calculations:
// - If the item's grade type is 'text' or 'none'.
// - If the grade item is an outcome item and the settings are set to not aggregate outcome items.
// - If the item's grade type is 'scale' and the settings are set to ignore scales in aggregations.
// All these cases are already taken care of in the backend, and no 'weight' input element is rendered on the page
// if a grade item should not have a weight.
if (!weightInput) {
continue;
}
const itemWeight = parseWeight(weightInput.value);
const itemAggregationCoefficient = parseInt(childElement.dataset.aggregationcoef);
const itemGradeMax = parseFloat(childElement.dataset.grademax);
// Record the ID and the weight for this grade item.
overrideArray[childElement.dataset.itemid] = {
extraCredit: itemAggregationCoefficient,
weight: itemWeight,
weightOverride: weightCheckbox.checked,
};
// If this item has had its weight overridden then set the flag to true, but
// only if all previous items were also overridden. Note that extra credit items
// are counted as overridden grade items.
if (!weightCheckbox.checked && itemAggregationCoefficient === 0) {
automaticGradeItemsPresent = true;
}
if (itemAggregationCoefficient > 0) {
// An extra credit grade item doesn't contribute to totalOverriddenGradeMax.
continue;
} else if (weightCheckbox.checked && itemWeight <= 0) {
// An overridden item that defines a weight of 0 does not contribute to totalOverriddenGradeMax.
continue;
}
totalGradeMax += itemGradeMax;
if (weightCheckbox.checked) {
totalOverriddenWeight += itemWeight;
totalOverriddenGradeMax += itemGradeMax;
}
}
// Initialise this variable (used to keep track of the weight override total).
let normaliseTotal = 0;
// Keep a record of how much the override total is to see if it is above 100. If it is then we need to set the
// other weights to zero and normalise the others.
let overriddenTotal = 0;
// Total up all the weights.
for (const gradeItemDetail of Object.values(overrideArray)) {
// Exclude grade items with extra credit or negative weights (which will be set to zero later).
if (!gradeItemDetail.extraCredit && gradeItemDetail.weight > 0) {
normaliseTotal += gradeItemDetail.weight;
}
// The overridden total includes items that are marked as overridden, not extra credit, and have a positive weight.
if (gradeItemDetail.weightOverride && !gradeItemDetail.extraCredit && gradeItemDetail.weight > 0) {
// Add overridden weights up to see if they are greater than 1.
overriddenTotal += gradeItemDetail.weight;
}
}
if (overriddenTotal > 100) {
// Make sure that this category of weights gets normalised.
requiresNormalising = true;
// The normalised weights are only the overridden weights, so we just use the total of those.
normaliseTotal = overriddenTotal;
}
const totalNonOverriddenGradeMax = totalGradeMax - totalOverriddenGradeMax;
for (const childElement of childElements) {
const weightInput = childElement.querySelector(selectors.weightOverrideInput);
const weightCheckbox = childElement.querySelector(selectors.weightOverrideCheckbox);
const itemAggregationCoefficient = parseInt(childElement.dataset.aggregationcoef);
const itemGradeMax = parseFloat(childElement.dataset.grademax);
if (!weightInput) {
continue;
} else if (!oldExtraCreditCalculation && itemAggregationCoefficient > 0 && weightCheckbox.checked) {
// For an item with extra credit ignore other weights and overrides but do not change anything at all
// if its weight was already overridden.
continue;
}
// Remove any error messages and classes.
weightInput.classList.remove('is-invalid');
const errorArea = weightInput.closest('td').querySelector('.invalid-feedback');
errorArea.textContent = '';
if (!oldExtraCreditCalculation && itemAggregationCoefficient > 0 && !weightCheckbox.checked) {
// For an item with extra credit ignore other weights and overrides.
weightInput.value = totalGradeMax ? formatFloat(itemGradeMax * 100 / totalGradeMax) : 0;
} else if (!weightCheckbox.checked) {
// Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
if (totalOverriddenWeight >= 100 || totalNonOverriddenGradeMax === 0 || itemGradeMax === 0) {
// There is no more weight to distribute.
weightInput.value = formatFloat(0);
} else {
// Calculate this item's weight as a percentage of the non-overridden total grade maxes
// then convert it to a proportion of the available non-overridden weight.
weightInput.value = formatFloat((itemGradeMax / totalNonOverriddenGradeMax) * (100 - totalOverriddenWeight));
}
} else if ((!automaticGradeItemsPresent && normaliseTotal !== 100) || requiresNormalising ||
overrideArray[childElement.dataset.itemid].weight < 0) {
if (overrideArray[childElement.dataset.itemid].weight < 0) {
weightInput.value = formatFloat(0);
}
// Zero is a special case. If the total is zero then we need to set the weight of the parent category to zero.
if (normaliseTotal !== 0) {
erroneous = true;
const error = normaliseTotal > 100 ? 'erroroverweight' : 'errorunderweight';
// eslint-disable-next-line promise/always-return,promise/catch-or-return
getString(error, 'core_grades').then((errorString) => {
errorArea.textContent = errorString;
});
weightInput.classList.add('is-invalid');
}
}
}
if (!erroneous) {
const categoryGradeMax = parseFloat(categoryElement.dataset.grademax);
if (categoryGradeMax !== totalGradeMax) {
// The category grade max is not the same as the total grade max, so we need to update the category grade max.
categoryElement.dataset.grademax = totalGradeMax;
const relatedCategoryAggregationRow = document.querySelector(
selectors.aggregationForCategory(categoryElement.dataset.category)
);
relatedCategoryAggregationRow.querySelector('.column-range').innerHTML = formatFloat(totalGradeMax, 2, 2);
const parentCategory = document.querySelector(selectors.categoryByIdentifier(categoryElement.dataset.parentCategory));
if (parentCategory && (parseInt(parentCategory.dataset.aggregation) === grade.aggregation.sum)) {
recalculateNaturalWeights(parentCategory);
}
}
}
};
/**
* Formats a floating-point number as a string with the specified number of decimal places.
* Unnecessary trailing zeros are removed up to the specified minimum number of decimal places.
*
* @param {number} number The float value to be formatted.
* @param {number} [decimalPoints=3] The number of decimal places to use.
* @param {number} [minDecimals=1] The minimum number of decimal places to use.
* @returns {string} The formatted weight value with the specified decimal places.
*/
const formatFloat = (number, decimalPoints = 3, minDecimals = 1) => {
return number.toFixed(decimalPoints)
.replace(new RegExp(`0{0,${decimalPoints - minDecimals}}$`), '')
.replace('.', decimalSeparator);
};
/**
* Parses a weight string and returns a normalized float value.
*
* @param {string} weightString The weight as a string, possibly with localized formatting.
* @returns {number} The parsed weight as a float. If parsing fails, returns 0.
*/
const parseWeight = (weightString) => {
const normalizedWeightString = weightString.replace(decimalSeparator, '.');
return isNaN(Number(normalizedWeightString)) ? 0 : parseFloat(normalizedWeightString || 0);
};
/**
* Initializes the weight management module with optional configuration.
*
* @param {string} decSep The character used as the decimal separator for number formatting.
* @param {boolean} oldCalculation A flag indicating whether to use the old (pre MDL-49257) extra credit calculation.
*/
export const init = (decSep, oldCalculation) => {
decimalSeparator = decSep;
oldExtraCreditCalculation = oldCalculation;
prefetchStrings('core_grades', ['erroroverweight', 'errorunderweight']);
document.addEventListener('change', e => {
// Update the weights of all grade items in the category when the weight of any grade item in the category is changed.
if (e.target.matches(selectors.weightOverrideInput) || e.target.matches(selectors.weightOverrideCheckbox)) {
// The following is named gradeItemRow, but it may also be a row that's representing a grade category.
// It's ok because it serves as the categories associated grade item in our calculations.
const gradeItemRow = e.target.closest('tr');
const categoryElement = document.querySelector(selectors.categoryByIdentifier(gradeItemRow.dataset.parentCategory));
// This is only required if we are using natural weights.
if (parseInt(categoryElement.dataset.aggregation) === grade.aggregation.sum) {
const weightElement = gradeItemRow.querySelector(selectors.weightOverrideInput);
weightElement.value = formatFloat(Math.max(0, parseWeight(weightElement.value)));
recalculateNaturalWeights(categoryElement);
}
}
});
document.addEventListener('submit', e => {
// If the form is being submitted, then we need to ensure that the weight input fields are all set to
// a valid value.
if (e.target.matches('#gradetreeform')) {
const firstInvalidWeightInput = e.target.querySelector('input.is-invalid');
if (firstInvalidWeightInput) {
const firstFocusableInvalidWeightInput = e.target.querySelector('input.is-invalid:enabled');
if (firstFocusableInvalidWeightInput) {
firstFocusableInvalidWeightInput.focus();
} else {
firstInvalidWeightInput.scrollIntoView({block: 'center'});
}
e.preventDefault();
}
}
});
};
+128
View File
@@ -0,0 +1,128 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Prints the add item gradebook form
*
* @module core_grades
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
import ModalForm from 'core_form/modalform';
import {getString} from 'core/str';
import Notification from 'core/notification';
import * as FormChangeChecker from 'core_form/changechecker';
import PendingPromise from 'core/pending';
const Selectors = {
advancedFormLink: 'a.showadvancedform'
};
const getDetailsFromEvent = (event) => {
if (event.target.closest('[data-trigger="add-item-form"]')) {
const trigger = event.target.closest('[data-trigger="add-item-form"]');
return {
trigger,
formClass: 'core_grades\\form\\add_item',
titleKey: trigger.getAttribute('data-itemid') === '-1' ? 'newitem' : 'itemsedit',
args: {
itemid: trigger.getAttribute('data-itemid'),
},
};
} else if (event.target.closest('[data-trigger="add-category-form"]')) {
const trigger = event.target.closest('[data-trigger="add-category-form"]');
return {
trigger,
formClass: 'core_grades\\form\\add_category',
titleKey: trigger.getAttribute('data-category') === '-1' ? 'newcategory' : 'categoryedit',
args: {
category: trigger.getAttribute('data-category'),
},
};
} else if (event.target.closest('[data-trigger="add-outcome-form"]')) {
const trigger = event.target.closest('[data-trigger="add-outcome-form"]');
return {
trigger,
formClass: 'core_grades\\form\\add_outcome',
titleKey: trigger.getAttribute('data-itemid') === '-1' ? 'newoutcomeitem' : 'outcomeitemsedit',
args: {
itemid: trigger.getAttribute('data-itemid'),
},
};
}
return null;
};
/**
* Initialize module
*/
export const init = () => {
// Sometimes the trigger does not exist, so lets conditionally add it.
document.addEventListener('click', event => {
const triggerData = getDetailsFromEvent(event);
if (triggerData) {
event.preventDefault();
const pendingPromise = new PendingPromise(`core_grades:add_item:${triggerData.args.itemid}`);
const {trigger, formClass, titleKey, args} = triggerData;
args.courseid = trigger.getAttribute('data-courseid');
args.gpr_plugin = trigger.getAttribute('data-gprplugin');
const modalForm = new ModalForm({
modalConfig: {
title: getString(titleKey, 'core_grades'),
},
formClass: formClass,
args: args,
saveButtonText: getString('save', 'core'),
returnFocus: trigger,
});
// Show a toast notification when the form is submitted.
modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {
if (event.detail.result) {
new PendingPromise('core_grades:form_submitted');
window.location.assign(event.detail.url);
} else {
Notification.addNotification({
type: 'error',
message: getString('saving_failed', 'core_grades')
});
}
});
modalForm.show();
pendingPromise.resolve();
}
const showAdvancedForm = event.target.closest(Selectors.advancedFormLink);
if (showAdvancedForm) {
// Navigate to the advanced form page and cary over any entered data.
event.preventDefault();
// Do not resolve this pendingPromise - it will be cleared when the page changes.
new PendingPromise('core_grades:show_advanced_form');
const form = event.target.closest('form');
form.action = showAdvancedForm.href;
// Disable the form change checker as we are going to carry over the data to the advanced form.
FormChangeChecker.disableAllChecks();
form.submit();
}
});
};
@@ -0,0 +1,79 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Compare a given form's values and its previously set data attributes.
*
* @module core_grades/grades/grader/gradingpanel/comparison
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export const fillInitialValues = (form) => {
Array.prototype.forEach.call(form.elements, (input) => {
if (input.type === 'submit' || input.type === 'button') {
return;
} else if (input.type === 'radio' || input.type === 'checkbox') {
input.dataset.initialValue = JSON.stringify(input.checked);
} else if (typeof input.value !== 'undefined') {
input.dataset.initialValue = JSON.stringify(input.value);
} else if (input.type === 'select-one') {
Array.prototype.forEach.call(input.options, (option) => {
option.dataset.initialValue = JSON.stringify(option.selected);
});
}
});
};
/**
* Compare the form data with the initial form data from when the form was set up.
*
* If values have changed, return a truthy value.
*
* @param {HTMLElement} form
* @return {Boolean}
*/
export const compareData = (form) => {
const result = Array.prototype.some.call(form.elements, (input) => {
if (input.type === 'submit' || input.type === 'button') {
return false;
} else if (input.type === 'radio' || input.type === 'checkbox') {
if (typeof input.dataset.initialValue !== 'undefined') {
return input.dataset.initialValue !== JSON.stringify(input.checked);
}
} else if (typeof input.value !== 'undefined') {
if (typeof input.dataset.initialValue !== 'undefined') {
return input.dataset.initialValue !== JSON.stringify(input.value);
}
} else if (input.type === 'select-one') {
return Array.prototype.some.call(input.options, (option) => {
if (typeof option.dataset.initialValue !== 'undefined') {
return option.dataset.initialValue !== JSON.stringify(option.selected);
}
return false;
});
}
// No value found to check. Assume that there were changes.
return true;
});
// Fill the initial values again as the form may not be reloaded.
fillInitialValues(form);
return result;
};
@@ -0,0 +1,66 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Error handling and normalisation of provided data.
*
* @module core_grades/grades/grader/gradingpanel/normalise
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Normalise a resultset for consumption by the grader.
*
* @param {Object} result The result returned from a grading web service
* @return {Object}
*/
export const normaliseResult = result => {
return {
result,
failed: !!result.warnings.length,
success: !result.warnings.length,
error: null,
};
};
/**
* Return the resultset used to describe an invalid result.
*
* @return {Object}
*/
export const invalidResult = () => {
return {
success: false,
failed: false,
result: {},
error: null,
};
};
/**
* Return the resultset used to describe a failed update.
*
* @param {Object} error
* @return {Object}
*/
export const failedUpdate = error => {
return {
success: false,
failed: true,
result: {},
error,
};
};
@@ -0,0 +1,67 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Grading panel for simple direct grading.
*
* @module core_grades/grades/grader/gradingpanel/point
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {saveGrade, fetchGrade} from './repository';
import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';
// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
import jQuery from 'jquery';
import {invalidResult} from './normalise';
/**
* Fetch the current grade for a user.
*
* @param {object} args
* @param {String} args.component
* @param {Number} args.context
* @param {String} args.itemname
* @param {Number} args.userId
* @param {Element} args.rootNode
* @returns {Object}
*/
export const fetchCurrentGrade = (...args) => fetchGrade('point')(...args);
/**
* Store a new grade for a user.
*
* @param {String} component
* @param {Number} context
* @param {String} itemname
* @param {Number} userId
* @param {Boolean} notifyUser
* @param {Element} rootNode
* @returns {Object}
*/
export const storeCurrentGrade = async(component, context, itemname, userId, notifyUser, rootNode) => {
const form = rootNode.querySelector('form');
const grade = form.querySelector('input[name="grade"]');
if (!grade.checkValidity() || !grade.value.trim()) {
return invalidResult;
}
if (compareData(form) === true) {
return await saveGrade('point')(component, context, itemname, userId, notifyUser, jQuery(form).serialize());
} else {
return '';
}
};
@@ -0,0 +1,50 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Repository for simple direct grading panel.
*
* @module core_grades/grades/grader/gradingpanel/repository
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
import {normaliseResult} from './normalise';
export const fetchGrade = type => (component, contextid, itemname, gradeduserid) => {
return fetchMany([{
methodname: `core_grades_grader_gradingpanel_${type}_fetch`,
args: {
component,
contextid,
itemname,
gradeduserid,
},
}])[0];
};
export const saveGrade = type => async(component, contextid, itemname, gradeduserid, notifyUser, formdata) => {
return normaliseResult(await fetchMany([{
methodname: `core_grades_grader_gradingpanel_${type}_store`,
args: {
component,
contextid,
itemname,
gradeduserid,
notifyuser: notifyUser,
formdata,
},
}])[0]);
};
@@ -0,0 +1,45 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Grading panel for simple direct grading.
*
* @module core_grades/grades/grader/gradingpanel/scale
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {saveGrade, fetchGrade} from './repository';
import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';
// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
import jQuery from 'jquery';
import {invalidResult} from './normalise';
export const fetchCurrentGrade = (...args) => fetchGrade('scale')(...args);
export const storeCurrentGrade = (component, context, itemname, userId, notifyUser, rootNode) => {
const form = rootNode.querySelector('form');
const grade = form.querySelector('select[name="grade"]');
if (!grade.checkValidity() || !grade.value.trim()) {
return invalidResult;
}
if (compareData(form) === true) {
return saveGrade('scale')(component, context, itemname, userId, notifyUser, jQuery(form).serialize());
} else {
return '';
}
};
+261
View File
@@ -0,0 +1,261 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A widget to search users or grade items within the gradebook.
*
* @module core_grades/searchwidget/basewidget
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {debounce} from 'core/utils';
import * as Templates from 'core/templates';
import * as Selectors from 'core_grades/searchwidget/selectors';
import Notification from 'core/notification';
import Log from 'core/log';
/**
* Build the base searching widget.
*
* @method init
* @param {HTMLElement} widgetContentContainer The selector for the widget container element.
* @param {Promise} bodyPromise The promise from the callee of the contents to place in the widget container.
* @param {Array} data An array of all the data generated by the callee.
* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
* @param {string|null} unsearchableContent The content rendered in a non-searchable area.
* @param {Function|null} afterSelect Callback executed after an item is selected.
*/
export const init = async(
widgetContentContainer,
bodyPromise,
data,
searchFunc,
unsearchableContent = null,
afterSelect = null,
) => {
Log.debug('The core_grades/searchwidget/basewidget component is deprecated. Please refer to core/search_combobox() instead.');
bodyPromise.then(async(bodyContent) => {
// Render the body content.
widgetContentContainer.innerHTML = bodyContent;
// Render the unsearchable content if defined.
if (unsearchableContent) {
const unsearchableContentContainer = widgetContentContainer.querySelector(Selectors.regions.unsearchableContent);
unsearchableContentContainer.innerHTML += unsearchableContent;
}
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
// Display a loader until the search results are rendered.
await showLoader(searchResultsContainer);
// Render the search results.
await renderSearchResults(searchResultsContainer, data);
registerListenerEvents(widgetContentContainer, data, searchFunc, afterSelect);
}).catch(Notification.exception);
};
/**
* Register the event listeners for the search widget.
*
* @method registerListenerEvents
* @param {HTMLElement} widgetContentContainer The selector for the widget container element.
* @param {Array} data An array of all the data generated by the callee.
* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
* @param {Function|null} afterSelect Callback executed after an item is selected.
*/
export const registerListenerEvents = (widgetContentContainer, data, searchFunc, afterSelect = null) => {
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);
if (!searchInput) {
// Too late. The widget is already closed and its content is empty.
return;
}
// We want to focus on the first known user interable element within the dropdown.
searchInput.focus();
const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);
// The search input is triggered.
searchInput.addEventListener('input', debounce(async() => {
// If search query is present display the 'clear search' button, otherwise hide it.
if (searchInput.value.length > 0) {
clearSearchButton.classList.remove('d-none');
} else {
clearSearchButton.classList.add('d-none');
}
// Remove aria-activedescendant when the available options change.
searchInput.removeAttribute('aria-activedescendant');
// Display the search results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
}, 300));
// Clear search is triggered.
clearSearchButton.addEventListener('click', async(e) => {
e.stopPropagation();
// Clear the entered search query in the search bar.
searchInput.value = "";
searchInput.focus();
clearSearchButton.classList.add('d-none');
// Remove aria-activedescendant when the available options change.
searchInput.removeAttribute('aria-activedescendant');
// Display all results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
});
const inputElement = document.getElementById(searchInput.dataset.inputElement);
if (inputElement && afterSelect) {
inputElement.addEventListener('change', e => {
const selectedOption = widgetContentContainer.querySelector(
Selectors.elements.getSearchWidgetSelectOption(searchInput),
);
if (selectedOption) {
afterSelect(e.target.value);
}
});
}
// Backward compatibility. Handle the click event for the following cases:
// - When we have <li> tags without an afterSelect callback function being provided (old js).
// - When we have <a> tags without href (old template).
widgetContentContainer.addEventListener('click', e => {
const deprecatedOption = e.target.closest(
'a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])'
);
if (deprecatedOption) {
// We are in one of these situations:
// - We have <li> tags without an afterSelect callback function being provided.
// - We have <a> tags without href.
if (inputElement && afterSelect) {
afterSelect(deprecatedOption.dataset.value);
} else {
const url = (data.find(object => object.id == deprecatedOption.dataset.value) || {url: ''}).url;
location.href = url;
}
}
});
// Backward compatibility. Handle the keydown event for the following cases:
// - When we have <li> tags without an afterSelect callback function being provided (old js).
// - When we have <a> tags without href (old template).
widgetContentContainer.addEventListener('keydown', e => {
const deprecatedOption = e.target.closest(
'a.dropdown-item[role="menuitem"]:not([href]), .dropdown-item[role="option"]:not([href])'
);
if (deprecatedOption && (e.key === ' ' || e.key === 'Enter')) {
// We are in one of these situations:
// - We have <li> tags without an afterSelect callback function being provided.
// - We have <a> tags without href.
e.preventDefault();
if (inputElement && afterSelect) {
afterSelect(deprecatedOption.dataset.value);
} else {
const url = (data.find(object => object.id == deprecatedOption.dataset.value) || {url: ''}).url;
location.href = url;
}
}
});
};
/**
* Renders the loading placeholder for the search widget.
*
* @method showLoader
* @param {HTMLElement} container The DOM node where we'll render the loading placeholder.
*/
export const showLoader = async(container) => {
container.innerHTML = '';
const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/loading', {});
Templates.replaceNodeContents(container, html, js);
};
/**
* We have a small helper that'll call the curried search function allowing callers to filter
* the data set however we want rather than defining how data must be filtered.
*
* @method debounceCallee
* @param {String} searchValue The input from the user that we'll search against.
* @param {Array} data An array of all the data generated by the callee.
* @param {Function} searchFunction Partially applied function we need to manage search the passed dataset.
* @return {Array} The filtered subset of the provided data that we'll then render into the results.
*/
const debounceCallee = (searchValue, data, searchFunction) => {
if (searchValue.length > 0) { // Search query is present.
return searchFunction(data, searchValue);
}
return data;
};
/**
* Given the output of the callers' search function, render out the results into the search results container.
*
* @method renderSearchResults
* @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results.
* @param {Array} searchResultsData The filtered subset of the provided data that we'll then render into the results.
*/
const renderSearchResults = async(searchResultsContainer, searchResultsData) => {
const templateData = {
'searchresults': searchResultsData,
};
// Build up the html & js ready to place into the help section.
const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/searchresults', templateData);
await Templates.replaceNodeContents(searchResultsContainer, html, js);
// Backward compatibility.
if (searchResultsContainer.getAttribute('role') !== 'listbox') {
const deprecatedOptions = searchResultsContainer.querySelectorAll(
'a.dropdown-item[role="menuitem"][href=""], .dropdown-item[role="option"]:not([href])'
);
for (const option of deprecatedOptions) {
option.tabIndex = 0;
option.removeAttribute('href');
}
}
};
/**
* We want to create the basic promises and hooks that the caller will implement, so we can build the search widget
* ahead of time and allow the caller to resolve their promises once complete.
*
* @method promisesAndResolvers
* @returns {{bodyPromise: Promise, bodyPromiseResolver}}
*/
export const promisesAndResolvers = () => {
// We want to show the widget instantly but loading whilst waiting for our data.
let bodyPromiseResolver;
const bodyPromise = new Promise(resolve => {
bodyPromiseResolver = resolve;
});
return {bodyPromiseResolver, bodyPromise};
};
+178
View File
@@ -0,0 +1,178 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A small dropdown to filter users within the gradebook.
*
* @module core_grades/searchwidget/initials
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Pending from 'core/pending';
import * as Url from 'core/url';
import CustomEvents from "core/custom_interaction_events";
import $ from 'jquery';
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
// Contain our selectors within this file until they could be of use elsewhere.
const selectors = {
pageListItem: 'page-item',
pageClickableItem: '.page-link',
activeItem: 'active',
formDropdown: '.initialsdropdownform',
parentDomNode: '.initials-selector',
firstInitial: 'firstinitial',
lastInitial: 'lastinitial',
initialBars: '.initialbar', // Both first and last name use this class.
targetButton: 'initialswidget',
formItems: {
type: 'submit',
save: 'save',
cancel: 'cancel'
}
};
/**
* Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.
*
* @param {String} callingLink The link to redirect upon form submission.
* @param {Null|Number} gpr_userid The user id to filter by.
* @param {Null|String} gpr_search The search value to filter by.
*/
export const init = (callingLink, gpr_userid = null, gpr_search = null) => {
if (registered) {
return;
}
const pendingPromise = new Pending();
registerListenerEvents(callingLink, gpr_userid, gpr_search);
// BS events always bubble so, we need to listen for the event higher up the chain.
$(selectors.parentDomNode).on('shown.bs.dropdown', () => {
document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});
});
pendingPromise.resolve();
registered = true;
};
/**
* Register event listeners.
*
* @param {String} callingLink The link to redirect upon form submission.
* @param {Null|Number} gpr_userid The user id to filter by.
* @param {Null|String} gpr_search The search value to filter by.
*/
const registerListenerEvents = (callingLink, gpr_userid = null, gpr_search = null) => {
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
// Register events.
events.forEach((event) => {
document.addEventListener(event, (e) => {
// Always fetch the latest information when we click as state is a fickle thing.
let {firstActive, lastActive, sifirst, silast} = onClickVariables();
let itemToReset = '';
// Prevent the usual form behaviour.
if (e.target.closest(selectors.formDropdown)) {
e.preventDefault();
}
// Handle the state of active initials before form submission.
if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {
// Ensure the li items don't cause weird clicking emptying out the form.
if (e.target.classList.contains(selectors.pageListItem)) {
return;
}
const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.
// We want to find the current active item in the menu area the user selected.
// We also want to fetch the raw item out of the array for instant manipulation.
if (initialsBar.classList.contains(selectors.firstInitial)) {
sifirst = e.target;
itemToReset = firstActive;
} else {
silast = e.target;
itemToReset = lastActive;
}
swapActiveItems(itemToReset, e);
}
// Handle form submissions.
if (e.target.closest(`${selectors.formDropdown}`) && e.target.type === selectors.formItems.type) {
if (e.target.dataset.action === selectors.formItems.save) {
// Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.
// Then we will redirect the user back onto the page with new filters applied.
const params = {
'id': e.target.closest(selectors.formDropdown).dataset.courseid,
'gpr_search': gpr_search !== null ? gpr_search : '',
'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,
'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,
};
if (gpr_userid !== null) {
params.gpr_userid = gpr_userid;
}
window.location = Url.relativeUrl(callingLink, params);
}
if (e.target.dataset.action === selectors.formItems.cancel) {
$(`.${selectors.targetButton}`).dropdown('toggle');
}
}
});
});
};
/**
* A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.
*
* @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}
*/
const onClickVariables = () => {
// Ensure we have an up-to-date initials bar.
const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];
const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];
const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];
const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];
// Ensure we retain both of the selections from a previous instance.
let sifirst = firstActive.querySelector(selectors.pageClickableItem);
let silast = lastActive.querySelector(selectors.pageClickableItem);
return {firstActive, lastActive, sifirst, silast};
};
/**
* Given we are provided the old li and current click event, swap around the active properties.
*
* @param {HTMLElement} itemToReset
* @param {Event} e
*/
const swapActiveItems = (itemToReset, e) => {
itemToReset.classList.remove(selectors.activeItem);
itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;
// Set the select item as the current item.
const itemToSetActive = e.target.parentElement;
itemToSetActive.classList.add(selectors.activeItem);
e.target.ariaCurrent = true;
};
+61
View File
@@ -0,0 +1,61 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A repo for the search widget.
*
* @module core_grades/searchwidget/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.
* Note: This will be worked upon in the single view issue.
*
* @method gradeitemFetch
* @param {int} courseid ID of the course to fetch the users of.
* @return {object} jQuery promise
*/
export const gradeitemFetch = (courseid) => {
const request = {
methodname: 'gradereport_singleview_get_grade_items_for_search_widget',
args: {
courseid: courseid,
},
};
return ajax.call([request])[0];
};
/**
* Given a course ID, we want to fetch the enrolled learners, so we may fetch their reports.
*
* @method userFetch
* @param {int} courseid ID of the course to fetch the users of.
* @param {int} groupId ID of the group to fetch the users of.
* @return {object} jQuery promise
*/
export const userFetch = (courseid, groupId) => {
const request = {
methodname: 'core_grades_get_enrolled_users_for_selector',
args: {
courseid: courseid,
groupid: groupId,
},
};
return ajax.call([request])[0];
};
+39
View File
@@ -0,0 +1,39 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Selectors for the search widget.
*
* @module core_grades/searchwidget/selectors
* @copyright 2022 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
regions: {
searchResults: '[data-region="search-results-container-widget"]',
unsearchableContent: '[data-region="unsearchable-content-container-widget"]',
},
actions: {
search: '[data-action="search"]',
clearSearch: '[data-action="clearsearch"]',
},
elements: {
getSearchWidgetSelector: searchtype => `.search-widget[data-searchtype="${searchtype}"]`,
getSearchWidgetDropdownSelector: searchtype => `.search-widget[data-searchtype="${searchtype}"] .dropdown-menu`,
getSearchWidgetSelectOption:
searchInput => `#${searchInput.getAttribute('aria-controls')} [role="option"][aria-selected="true"]`,
},
};