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
+11
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
define("core_course/copy_modal",["exports","core/str","core/modal","core/ajax","core/fragment","core/notification","core/config"],(function(_exports,_str,_modal,ajax,Fragment,_notification,Config){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* This module provides the course copy modal from the course and
* category management screen.
*
* @module core_course/copy_modal
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.9
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),ajax=_interopRequireWildcard(ajax),Fragment=_interopRequireWildcard(Fragment),_notification=_interopRequireDefault(_notification),Config=_interopRequireWildcard(Config);class CopyModal{static init(context){return new CopyModal(context)}constructor(context){this.contextid=context,this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(e=>{const copyAction=e.target.closest(".action-copy");if(!copyAction)return;e.preventDefault();const url=new URL(copyAction.href),params=new URLSearchParams(url.search);this.fetchCourseData(params.get("id")).then((_ref=>{let[course]=_ref;return this.createModal(course)})).catch((error=>_notification.default.exception(error)))}))}fetchCourseData(courseid){return ajax.call([{methodname:"core_course_get_courses",args:{options:{ids:[courseid]}}}])[0]}submitBackupRequest(jsonformdata){return ajax.call([{methodname:"core_backup_submit_copy_form",args:{jsonformdata:jsonformdata}}])[0]}createModal(course){let formdata=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const params={jsonformdata:JSON.stringify(formdata),courseid:course.id};return _modal.default.create({title:(0,_str.get_string)("copycoursetitle","backup",course.shortname),body:Fragment.loadFragment("course","new_base_form",this.contextid,params),large:!0,show:!0,removeOnClose:!0}).then((modal=>(modal.getRoot().on("click","#id_submitreturn",(e=>{this.processModalForm(course,modal,e)})),modal.getRoot().on("click","#id_cancel",(e=>{e.preventDefault(),modal.destroy()})),modal.getRoot().on("click","#id_submitdisplay",(e=>{e.formredirect=!0,this.processModalForm(course,modal,e)})),modal)))}processModalForm(course,modal,e){e.preventDefault();const copyform=modal.getRoot().find("form").serialize(),formjson=JSON.stringify(copyform),invalid=modal.getRoot()[0].querySelectorAll('[aria-invalid="true"], .error');invalid.length?invalid[0].focus():(modal.destroy(),this.submitBackupRequest(formjson).then((()=>{if(1==e.formredirect){const redirect="".concat(Config.wwwroot,"/backup/copyprogress.php?id=").concat(course.id);window.location.assign(redirect)}})).catch((()=>{this.createModal(course,copyform)})))}}return _exports.default=CopyModal,_exports.default}));
//# sourceMappingURL=copy_modal.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/downloadcontent",["exports","core/config","core/custom_interaction_events","core/modal_save_cancel","jquery","core/pending","core/key_codes"],(function(_exports,_config,_custom_interaction_events,_modal_save_cancel,_jquery,_pending,_key_codes){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Functions related to downloading course content.
*
* @module core_course/downloadcontent
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_config=_interopRequireDefault(_config),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);_exports.init=()=>{const pendingPromise=new _pending.default;(0,_jquery.default)("[data-downloadcourse]").on("click keydown",(e=>{"click"!==e.type&&e.which!==_key_codes.enter&&e.which!==_key_codes.space||(e.preventDefault(),displayDownloadConfirmation(e.currentTarget))})),pendingPromise.resolve()};const displayDownloadConfirmation=downloadModalTrigger=>_modal_save_cancel.default.create({title:downloadModalTrigger.dataset.downloadTitle,body:"<p>".concat(downloadModalTrigger.dataset.downloadBody,"</p>"),buttons:{save:downloadModalTrigger.dataset.downloadButtonText},templateContext:{classes:"downloadcoursecontentmodal"}}).then((modal=>{modal.show();const saveButton=document.querySelector('.modal .downloadcoursecontentmodal [data-action="save"]'),cancelButton=document.querySelector('.modal .downloadcoursecontentmodal [data-action="cancel"]'),modalContainer=document.querySelector('.modal[data-region="modal-container"]');return(0,_jquery.default)(saveButton).on(_custom_interaction_events.default.events.activate,(e=>downloadContent(e,downloadModalTrigger,modal))),(0,_jquery.default)(cancelButton).on(_custom_interaction_events.default.events.activate,(()=>{modal.destroy()})),modalContainer.querySelector(".downloadcoursecontentmodal")&&(0,_jquery.default)(modalContainer).on(_custom_interaction_events.default.events.activate,(()=>{modal.destroy()})),modal})),downloadContent=(e,downloadModalTrigger,modal)=>{e.preventDefault();const downloadForm=document.createElement("form");downloadForm.action=downloadModalTrigger.dataset.downloadLink,downloadForm.method="POST",downloadForm.target="_blank";const downloadSesskey=document.createElement("input");downloadSesskey.name="sesskey",downloadSesskey.value=_config.default.sesskey,downloadForm.appendChild(downloadSesskey),downloadForm.style.display="none",document.body.appendChild(downloadForm),downloadForm.submit(),document.body.removeChild(downloadForm),modal.destroy()}}));
//# sourceMappingURL=downloadcontent.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("core_course/events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled",stateChanged:"core_course:stateChanged",sectionRefreshed:"core_course:sectionRefreshed"},_exports.default}));
//# sourceMappingURL=events.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"events.min.js","sources":["../src/events.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 * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @copyright 2018 Simey Lameze <simey@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n stateChanged: 'core_course:stateChanged',\n sectionRefreshed: 'core_course:sectionRefreshed',\n};\n"],"names":["favourited","unfavorited","manualCompletionToggled","stateChanged","sectionRefreshed"],"mappings":"oKAsBe,CACXA,WAAY,yBACZC,YAAa,0BACbC,wBAAyB,sCACzBC,aAAc,2BACdC,iBAAkB"}
+12
View File
@@ -0,0 +1,12 @@
define("core_course/formatchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Course format selection handler.
*
* @module core_course/formatchooser
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
const Selectors_fields={selector:'[data-formatchooser-field="selector"]',updateButton:'[data-formatchooser-field="updateButton"]'};_exports.init=()=>{document.querySelector(Selectors_fields.selector).addEventListener("change",(e=>{const form=e.target.closest("form"),updateButton=form.querySelector(Selectors_fields.updateButton),fieldset=updateButton.closest("fieldset"),url=new URL(form.action);url.hash=fieldset.id,form.action=url.toString(),updateButton.click()}))}}));
//# sourceMappingURL=formatchooser.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"formatchooser.min.js","sources":["../src/formatchooser.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\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 * Course format selection handler.\n *\n * @module core_course/formatchooser\n * @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-formatchooser-field=\"selector\"]',\n updateButton: '[data-formatchooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the format chooser.\n */\nexport const init = () => {\n document.querySelector(Selectors.fields.selector).addEventListener('change', e => {\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n const fieldset = updateButton.closest('fieldset');\n\n const url = new URL(form.action);\n url.hash = fieldset.id;\n\n form.action = url.toString();\n updateButton.click();\n });\n};\n"],"names":["Selectors","selector","updateButton","document","querySelector","addEventListener","e","form","target","closest","fieldset","url","URL","action","hash","id","toString","click"],"mappings":";;;;;;;;;MAuBMA,iBACM,CACJC,SAAU,wCACVC,aAAc,2DAOF,KAChBC,SAASC,cAAcJ,iBAAiBC,UAAUI,iBAAiB,UAAUC,UACnEC,KAAOD,EAAEE,OAAOC,QAAQ,QACxBP,aAAeK,KAAKH,cAAcJ,iBAAiBE,cACnDQ,SAAWR,aAAaO,QAAQ,YAEhCE,IAAM,IAAIC,IAAIL,KAAKM,QACzBF,IAAIG,KAAOJ,SAASK,GAEpBR,KAAKM,OAASF,IAAIK,WAClBd,aAAae"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
define("core_course/local/activitychooser/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle user AJAX actions.
*
* @module core_course/local/activitychooser/repository
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unfavouriteModule=_exports.fetchFooterData=_exports.favouriteModule=_exports.activityModules=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.activityModules=courseid=>{const request={methodname:"core_course_get_course_content_items",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.favouriteModule=(modName,modID)=>{const request={methodname:"core_course_add_content_item_to_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.unfavouriteModule=(modName,modID)=>{const request={methodname:"core_course_remove_content_item_from_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.fetchFooterData=(courseid,sectionid)=>{const request={methodname:"core_course_get_activity_chooser_footer",args:{courseid:courseid,sectionid:sectionid}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../../src/local/activitychooser/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 javascript module to handle user AJAX actions.\n *\n * @module core_course/local/activitychooser/repository\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method activityModules\n * @param {Number} courseid What course to fetch the modules for\n * @return {object} jQuery promise\n */\nexport const activityModules = (courseid) => {\n const request = {\n methodname: 'core_course_get_course_content_items',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is a users' favourite.\n *\n * @method favouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const favouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_add_content_item_to_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is no longer a users' favourite.\n *\n * @method unfavouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const unfavouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_remove_content_item_from_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchFooterData\n * @param {Number} courseid What course to fetch the data for\n * @param {Number} sectionid What section to fetch the data for\n * @return {object} jQuery promise\n */\nexport const fetchFooterData = (courseid, sectionid) => {\n const request = {\n methodname: 'core_course_get_activity_chooser_footer',\n args: {\n courseid: courseid,\n sectionid: sectionid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call","modName","modID","componentname","contentitemid","sectionid"],"mappings":";;;;;;;uPA+BgCA,iBACtBC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU,6BAYD,CAACK,QAASC,eAC/BN,QAAU,CACZC,WAAY,kDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,+BAYC,CAACK,QAASC,eACjCN,QAAU,CACZC,WAAY,uDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,6BAWD,CAACD,SAAUU,mBAChCT,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFH,SAAUA,SACVU,UAAWA,mBAGZN,cAAKC,KAAK,CAACJ,UAAU"}
+11
View File
@@ -0,0 +1,11 @@
define("core_course/local/activitychooser/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module core_course/local/activitychooser/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{chooser:getDataSelector("region","chooser-container"),getSectionChooserOptions:containerid=>"".concat(containerid," ").concat(getDataSelector("region","chooser-options-container")),chooserOption:{container:getDataSelector("region","chooser-option-container"),actions:getDataSelector("region","chooser-option-actions-container"),info:getDataSelector("region","chooser-option-info-container")},chooserSummary:{container:getDataSelector("region","chooser-option-summary-container"),content:getDataSelector("region","chooser-option-summary-content-container"),header:getDataSelector("region","summary-header"),actions:getDataSelector("region","chooser-option-summary-actions-container")},carousel:getDataSelector("region","carousel"),help:getDataSelector("region","help"),modules:getDataSelector("region","modules"),favouriteTabNav:getDataSelector("region","favourite-tab-nav"),defaultTabNav:getDataSelector("region","default-tab-nav"),activityTabNav:getDataSelector("region","activity-tab-nav"),favouriteTab:getDataSelector("region","favourites"),recommendedTab:getDataSelector("region","recommended"),defaultTab:getDataSelector("region","default"),activityTab:getDataSelector("region","activity"),resourceTab:getDataSelector("region","resources"),getModuleSelector:modname=>'[role="menuitem"][data-modname="'.concat(modname,'"]'),searchResults:getDataSelector("region","search-results-container"),searchResultItems:getDataSelector("region","search-result-items-container")},actions:{optionActions:{showSummary:getDataSelector("action","show-option-summary"),manageFavourite:getDataSelector("action","manage-module-favourite")},addChooser:getDataSelector("action","add-chooser-option"),closeOption:getDataSelector("action","close-chooser-option-summary"),hide:getDataSelector("action","hide"),search:getDataSelector("action","search"),clearSearch:getDataSelector("action","clearsearch")},render:{favourites:getDataSelector("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:'a[data-toggle="tab"]',activetab:'a[data-toggle="tab"][aria-selected="true"]',visibletabs:'a[data-toggle="tab"]:not(.d-none)'}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=selectors.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("core_course/manual_completion_toggle",["exports","core/templates","core/notification","core_course/repository","core_course/events","core/pending"],(function(_exports,_templates,_notification,_repository,CourseEvents,_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}}
/**
* Provides the functionality for toggling the manual completion state of a course module through
* the manual completion button.
*
* @module core_course/manual_completion_toggle
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=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}(CourseEvents),_pending=_interopRequireDefault(_pending);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const pendingPromise=new _pending.default("core_course:toggleManualCompletionState"),originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE;_templates.default.renderForPromise("core/loading",{}).then((loadingHtml=>{_templates.default.replaceNodeContents(toggleButton,loadingHtml,"")})).catch((()=>{}));try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}pendingPromise.resolve()}}));
//# sourceMappingURL=manual_completion_toggle.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/recommendations",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A javascript module to handle toggling activity chooser recommendations.
*
* @module core_course/recommendations
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);const toggleRecommendation=e=>{let data={methodname:"core_course_toggle_activity_recommendation",args:{area:e.currentTarget.dataset.area,id:e.currentTarget.dataset.id}};_ajax.default.call([data])[0].fail(_notification.default.exception)};_exports.init=()=>{document.querySelectorAll("[data-area]").forEach((checkbox=>{checkbox.addEventListener("change",toggleRecommendation)}))}}));
//# sourceMappingURL=recommendations.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"recommendations.min.js","sources":["../src/recommendations.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 javascript module to handle toggling activity chooser recommendations.\n *\n * @module core_course/recommendations\n * @copyright 2020 Adrian Greeve <adrian@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Do an ajax call to toggle the recommendation\n *\n * @param {object} e The event\n * @return {void}\n */\nconst toggleRecommendation = (e) => {\n let data = {\n methodname: 'core_course_toggle_activity_recommendation',\n args: {\n area: e.currentTarget.dataset.area,\n id: e.currentTarget.dataset.id\n }\n };\n Ajax.call([data])[0].fail(Notification.exception);\n};\n\n/**\n * Initialisation function\n *\n * @return {void}\n */\nexport const init = () => {\n const checkboxelements = document.querySelectorAll(\"[data-area]\");\n checkboxelements.forEach((checkbox) => {\n checkbox.addEventListener('change', toggleRecommendation);\n });\n};\n"],"names":["toggleRecommendation","e","data","methodname","args","area","currentTarget","dataset","id","call","fail","Notification","exception","document","querySelectorAll","forEach","checkbox","addEventListener"],"mappings":";;;;;;;gLAgCMA,qBAAwBC,QACtBC,KAAO,CACPC,WAAY,6CACZC,KAAM,CACFC,KAAMJ,EAAEK,cAAcC,QAAQF,KAC9BG,GAAIP,EAAEK,cAAcC,QAAQC,mBAG/BC,KAAK,CAACP,OAAO,GAAGQ,KAAKC,sBAAaC,0BAQvB,KACSC,SAASC,iBAAiB,eAClCC,SAASC,WACtBA,SAASC,iBAAiB,SAAUjB"}
+10
View File
@@ -0,0 +1,10 @@
define("core_course/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle course ajax actions.
*
* @module core_course/repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:function(cmid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},getGradableUsersFromCourseID:function(courseid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"core_grades_get_gradable_users",args:{courseid:courseid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=repository.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_course/view",["exports","core_course/events"],(function(_exports,CourseEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CourseEvents=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}
/**
* JS module for the course homepage.
*
* @module core_course/view
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(CourseEvents);let registered=!1;_exports.init=()=>{registered||(document.addEventListener(CourseEvents.manualCompletionToggled,(e=>{parseInt(e.detail.withAvailability)&&window.location.reload()})),registered=!0)}}));
//# sourceMappingURL=view.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"view.min.js","sources":["../src/view.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 * JS module for the course homepage.\n *\n * @module core_course/view\n * @copyright 2021 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as CourseEvents from 'core_course/events';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n/**\n * Function to intialise and register event listeners for this module.\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n // Listen for toggled manual completion states of activities.\n document.addEventListener(CourseEvents.manualCompletionToggled, (e) => {\n const withAvailability = parseInt(e.detail.withAvailability);\n if (withAvailability) {\n // Reload the page when the toggled manual completion button has availability conditions linked to it.\n window.location.reload();\n }\n });\n registered = true;\n};\n"],"names":["registered","document","addEventListener","CourseEvents","manualCompletionToggled","e","parseInt","detail","withAvailability","window","location","reload"],"mappings":";;;;;;;wBA8BIA,YAAa,gBAKG,KACZA,aAIJC,SAASC,iBAAiBC,aAAaC,yBAA0BC,IACpCC,SAASD,EAAEE,OAAOC,mBAGvCC,OAAOC,SAASC,YAGxBX,YAAa"}
File diff suppressed because it is too large Load Diff
+421
View File
@@ -0,0 +1,421 @@
// 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 type of dialogue used as for choosing modules in a course.
*
* @module core_course/activitychooser
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';
import * as Repository from 'core_course/local/activitychooser/repository';
import selectors from 'core_course/local/activitychooser/selectors';
import CustomEvents from 'core/custom_interaction_events';
import * as Templates from 'core/templates';
import {getString} from 'core/str';
import Modal from 'core/modal';
import Pending from 'core/pending';
// Set up some JS module wide constants that can be added to in the future.
// Tab config options.
const ALLACTIVITIESRESOURCES = 0;
const ACTIVITIESRESOURCES = 2;
const ALLACTIVITIESRESOURCESREC = 3;
const ONLYALLREC = 4;
const ACTIVITIESRESOURCESREC = 5;
// Module types.
const ACTIVITY = 0;
const RESOURCE = 1;
let initialized = false;
/**
* Set up the activity chooser.
*
* @method init
* @param {Number} courseId Course ID to use later on in fetchModules()
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
*/
export const init = (courseId, chooserConfig) => {
const pendingPromise = new Pending();
registerListenerEvents(courseId, chooserConfig);
pendingPromise.resolve();
};
/**
* Once a selection has been made make the modal & module information and pass it along
*
* @method registerListenerEvents
* @param {Number} courseId
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
*/
const registerListenerEvents = (courseId, chooserConfig) => {
// Ensure we only add our listeners once.
if (initialized) {
return;
}
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
const fetchModuleData = (() => {
let innerPromise = null;
return () => {
if (!innerPromise) {
innerPromise = new Promise((resolve) => {
resolve(Repository.activityModules(courseId));
});
}
return innerPromise;
};
})();
const fetchFooterData = (() => {
let footerInnerPromise = null;
return (sectionId) => {
if (!footerInnerPromise) {
footerInnerPromise = new Promise((resolve) => {
resolve(Repository.fetchFooterData(courseId, sectionId));
});
}
return footerInnerPromise;
};
})();
CustomEvents.define(document, events);
// Display module chooser event listeners.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
if (e.target.closest(selectors.elements.sectionmodchooser)) {
let caller;
// We need to know who called this.
// Standard courses use the ID in the main section info.
const sectionDiv = e.target.closest(selectors.elements.section);
// Front page courses need some special handling.
const button = e.target.closest(selectors.elements.sectionmodchooser);
// If we don't have a section ID use the fallback ID.
// We always want the sectionDiv caller first as it keeps track of section ID's after DnD changes.
// The button attribute is always just a fallback for us as the section div is not always available.
// A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.
if (sectionDiv !== null && sectionDiv.hasAttribute('data-sectionid')) {
// We check for attributes just in case of outdated contrib course formats.
caller = sectionDiv;
} else {
caller = button;
}
// We want to show the modal instantly but loading whilst waiting for our data.
let bodyPromiseResolver;
const bodyPromise = new Promise(resolve => {
bodyPromiseResolver = resolve;
});
const footerData = await fetchFooterData(caller.dataset.sectionid);
const sectionModal = buildModal(bodyPromise, footerData);
// Now we have a modal we should start fetching data.
// If an error occurs while fetching the data, display the error within the modal.
const data = await fetchModuleData().catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));
});
// Early return if there is no module data.
if (!data) {
return;
}
// Apply the section id to all the module instance links.
const builtModuleData = sectionIdMapper(
data,
caller.dataset.sectionid,
caller.dataset.sectionreturnid,
caller.dataset.beforemod
);
ChooserDialogue.displayChooser(
sectionModal,
builtModuleData,
partiallyAppliedFavouriteManager(data, caller.dataset.sectionid),
footerData,
);
bodyPromiseResolver(await Templates.render(
'core_course/activitychooser',
templateDataBuilder(builtModuleData, chooserConfig)
));
}
});
});
initialized = true;
};
/**
* Given the web service data and an ID we want to make a deep copy
* of the WS data then add on the section ID to the addoption URL
*
* @method sectionIdMapper
* @param {Object} webServiceData Our original data from the Web service call
* @param {Number} id The ID of the section we need to append to the links
* @param {Number|null} sectionreturnid The ID of the section return we need to append to the links
* @param {Number|null} beforemod The ID of the cm we need to append to the links
* @return {Array} [modules] with URL's built
*/
const sectionIdMapper = (webServiceData, id, sectionreturnid, beforemod) => {
// We need to take a fresh deep copy of the original data as an object is a reference type.
const newData = JSON.parse(JSON.stringify(webServiceData));
newData.content_items.forEach((module) => {
module.link += '&section=' + id + '&beforemod=' + (beforemod ?? 0);
if (sectionreturnid) {
module.link += '&sr=' + sectionreturnid;
}
});
return newData.content_items;
};
/**
* Given an array of modules we want to figure out where & how to place them into our template object
*
* @method templateDataBuilder
* @param {Array} data our modules to manipulate into a Templatable object
* @param {Object} chooserConfig Any PHP config settings that we may need to reference
* @return {Object} Our built object ready to render out
*/
const templateDataBuilder = (data, chooserConfig) => {
// Setup of various bits and pieces we need to mutate before throwing it to the wolves.
let activities = [];
let resources = [];
let showAll = true;
let showActivities = false;
let showResources = false;
// Tab mode can be the following [All, Resources & Activities, All & Activities & Resources].
const tabMode = parseInt(chooserConfig.tabmode);
// Filter the incoming data to find favourite & recommended modules.
const favourites = data.filter(mod => mod.favourite === true);
const recommended = data.filter(mod => mod.recommended === true);
// Whether the activities and resources tabs should be displayed or not.
const showActivitiesAndResources = (tabMode) => {
const acceptableModes = [
ALLACTIVITIESRESOURCES,
ALLACTIVITIESRESOURCESREC,
ACTIVITIESRESOURCES,
ACTIVITIESRESOURCESREC,
];
return acceptableModes.indexOf(tabMode) !== -1;
};
// These modes need Activity & Resource tabs.
if (showActivitiesAndResources(tabMode)) {
// Filter the incoming data to find activities then resources.
activities = data.filter(mod => mod.archetype === ACTIVITY);
resources = data.filter(mod => mod.archetype === RESOURCE);
showActivities = true;
showResources = true;
// We want all of the previous information but no 'All' tab.
if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) {
showAll = false;
}
}
const recommendedBeforeTabs = [
ALLACTIVITIESRESOURCESREC,
ONLYALLREC,
ACTIVITIESRESOURCESREC,
];
// Whether the recommended tab should be displayed before the All/Activities/Resources tabs.
const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1;
// Given the results of the above filters lets figure out what tab to set active.
// We have some favourites.
const favouritesFirst = !!favourites.length;
const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length;
// We are in tabMode 2 without any favourites.
const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false;
// We have nothing fallback to show all modules.
const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false;
return {
'default': data,
showAll: showAll,
activities: activities,
showActivities: showActivities,
activitiesFirst: activitiesFirst,
resources: resources,
showResources: showResources,
favourites: favourites,
recommended: recommended,
recommendedFirst: recommendedFirst,
recommendedBeginning: recommendedBeginning,
favouritesFirst: favouritesFirst,
fallback: fallback,
};
};
/**
* Given an object we want to build a modal ready to show
*
* @method buildModal
* @param {Promise} body
* @param {String|Boolean} footer Either a footer to add or nothing
* @return {Object} The modal ready to display immediately and render body in later.
*/
const buildModal = (body, footer) => Modal.create({
body,
title: getString('addresourceoractivity'),
footer: footer.customfootertemplate,
large: true,
scrollable: false,
templateContext: {
classes: 'modchooser'
},
show: true,
});
/**
* A small helper function to handle the case where there are no more favourites
* and we need to mess a bit with the available tabs in the chooser
*
* @method nullFavouriteDomManager
* @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav
* @param {HTMLElement} modalBody Our current modals' body
*/
const nullFavouriteDomManager = (favouriteTabNav, modalBody) => {
favouriteTabNav.tabIndex = -1;
favouriteTabNav.classList.add('d-none');
// Need to set active to an available tab.
if (favouriteTabNav.classList.contains('active')) {
favouriteTabNav.classList.remove('active');
favouriteTabNav.setAttribute('aria-selected', 'false');
const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);
favouriteTab.classList.remove('active');
const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);
const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav);
if (defaultTabNav.classList.contains('d-none') === false) {
defaultTabNav.classList.add('active');
defaultTabNav.setAttribute('aria-selected', 'true');
defaultTabNav.tabIndex = 0;
defaultTabNav.focus();
const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);
defaultTab.classList.add('active');
} else {
activitiesTabNav.classList.add('active');
activitiesTabNav.setAttribute('aria-selected', 'true');
activitiesTabNav.tabIndex = 0;
activitiesTabNav.focus();
const activitiesTab = modalBody.querySelector(selectors.regions.activityTab);
activitiesTab.classList.add('active');
}
}
};
/**
* Export a curried function where the builtModules has been applied.
* We have our array of modules so we can rerender the favourites area and have all of the items sorted.
*
* @method partiallyAppliedFavouriteManager
* @param {Array} moduleData This is our raw WS data that we need to manipulate
* @param {Number} sectionId We need this to add the sectionID to the URL's in the faves area after rerender
* @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array
*/
const partiallyAppliedFavouriteManager = (moduleData, sectionId) => {
/**
* Curried function that is being returned.
*
* @param {String} internal Internal name of the module to manage
* @param {Boolean} favourite Is the caller adding a favourite or removing one?
* @param {HTMLElement} modalBody What we need to update whilst we are here
*/
return async(internal, favourite, modalBody) => {
const favouriteArea = modalBody.querySelector(selectors.render.favourites);
// eslint-disable-next-line max-len
const favouriteButtons = modalBody.querySelectorAll(`[data-internal="${internal}"] ${selectors.actions.optionActions.manageFavourite}`);
const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);
const result = moduleData.content_items.find(({name}) => name === internal);
const newFaves = {};
if (result) {
if (favourite) {
result.favourite = true;
// eslint-disable-next-line camelcase
newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);
const builtFaves = sectionIdMapper(newFaves, sectionId);
const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites',
{favourites: builtFaves});
await Templates.replaceNodeContents(favouriteArea, html, js);
Array.from(favouriteButtons).forEach((element) => {
element.classList.remove('text-muted');
element.classList.add('text-primary');
element.dataset.favourited = 'true';
element.setAttribute('aria-pressed', true);
element.firstElementChild.classList.remove('fa-star-o');
element.firstElementChild.classList.add('fa-star');
});
favouriteTabNav.classList.remove('d-none');
} else {
result.favourite = false;
const nodeToRemove = favouriteArea.querySelector(`[data-internal="${internal}"]`);
nodeToRemove.parentNode.removeChild(nodeToRemove);
Array.from(favouriteButtons).forEach((element) => {
element.classList.add('text-muted');
element.classList.remove('text-primary');
element.dataset.favourited = 'false';
element.setAttribute('aria-pressed', false);
element.firstElementChild.classList.remove('fa-star');
element.firstElementChild.classList.add('fa-star-o');
});
const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);
if (newFaves.length === 0) {
nullFavouriteDomManager(favouriteTabNav, modalBody);
}
}
}
};
};
+150
View File
@@ -0,0 +1,150 @@
// 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 the course copy modal from the course and
* category management screen.
*
* @module core_course/copy_modal
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.9
*/
import {get_string as getString} from 'core/str';
import Modal from 'core/modal';
import * as ajax from 'core/ajax';
import * as Fragment from 'core/fragment';
import Notification from 'core/notification';
import * as Config from 'core/config';
export default class CopyModal {
static init(context) {
return new CopyModal(context);
}
constructor(context) {
this.contextid = context;
this.registerEventListeners();
}
registerEventListeners() {
document.addEventListener('click', (e) => {
const copyAction = e.target.closest('.action-copy');
if (!copyAction) {
return;
}
e.preventDefault(); // Stop. Hammer time.
const url = new URL(copyAction.href);
const params = new URLSearchParams(url.search);
this.fetchCourseData(params.get('id'))
.then(([course]) => this.createModal(course))
.catch((error) => Notification.exception(error));
});
}
fetchCourseData(courseid) {
return ajax.call([{
methodname: 'core_course_get_courses',
args: {
options: {
ids: [courseid],
},
},
}])[0];
}
submitBackupRequest(jsonformdata) {
return ajax.call([{
methodname: 'core_backup_submit_copy_form',
args: {
jsonformdata,
},
}])[0];
}
createModal(
course,
formdata = {},
) {
const params = {
jsonformdata: JSON.stringify(formdata),
courseid: course.id,
};
// Create the Modal.
return Modal.create({
title: getString('copycoursetitle', 'backup', course.shortname),
body: Fragment.loadFragment('course', 'new_base_form', this.contextid, params),
large: true,
show: true,
removeOnClose: true,
})
.then((modal) => {
// Explicitly handle form click events.
modal.getRoot().on('click', '#id_submitreturn', (e) => {
this.processModalForm(course, modal, e);
});
modal.getRoot().on('click', '#id_cancel', (e) => {
e.preventDefault();
modal.destroy();
});
modal.getRoot().on('click', '#id_submitdisplay', (e) => {
e.formredirect = true;
this.processModalForm(course, modal, e);
});
return modal;
});
}
processModalForm(course, modal, e) {
e.preventDefault(); // Stop modal from closing.
// Form data.
const copyform = modal.getRoot().find('form').serialize();
const formjson = JSON.stringify(copyform);
// Handle invalid form fields for better UX.
const invalid = modal.getRoot()[0].querySelectorAll('[aria-invalid="true"], .error');
if (invalid.length) {
invalid[0].focus();
return;
}
modal.destroy();
// Submit form via ajax.
this.submitBackupRequest(formjson)
.then(() => {
if (e.formredirect == true) {
// We are redirecting to copy progress display.
const redirect = `${Config.wwwroot}/backup/copyprogress.php?id=${course.id}`;
window.location.assign(redirect);
}
return;
})
.catch(() => {
// Form submission failed server side, redisplay with errors.
this.createModal(course, copyform);
});
}
}
+125
View File
@@ -0,0 +1,125 @@
// 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/>.
/**
* Functions related to downloading course content.
*
* @module core_course/downloadcontent
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Config from 'core/config';
import CustomEvents from 'core/custom_interaction_events';
import SaveCancelModal from 'core/modal_save_cancel';
import jQuery from 'jquery';
import Pending from 'core/pending';
import {enter, space} from 'core/key_codes';
/**
* Set up listener to trigger the download course content modal.
*
* @return {void}
*/
export const init = () => {
const pendingPromise = new Pending();
// Add event listeners for click and enter/space keys.
jQuery('[data-downloadcourse]').on('click keydown', (e) => {
if (e.type === 'click' || e.which === enter || e.which === space) {
e.preventDefault();
displayDownloadConfirmation(e.currentTarget);
}
});
pendingPromise.resolve();
};
/**
* Display the download course content modal.
*
* @method displayDownloadConfirmation
* @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
* @return {void}
*/
const displayDownloadConfirmation = (downloadModalTrigger) => {
return SaveCancelModal.create({
title: downloadModalTrigger.dataset.downloadTitle,
body: `<p>${downloadModalTrigger.dataset.downloadBody}</p>`,
buttons: {
save: downloadModalTrigger.dataset.downloadButtonText
},
templateContext: {
classes: 'downloadcoursecontentmodal'
}
})
.then((modal) => {
// Display the modal.
modal.show();
const saveButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="save"]');
const cancelButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="cancel"]');
const modalContainer = document.querySelector('.modal[data-region="modal-container"]');
// Create listener to trigger the download when the "Download" button is pressed.
jQuery(saveButton).on(CustomEvents.events.activate, (e) => downloadContent(e, downloadModalTrigger, modal));
// Create listener to destroy the modal when closing modal by cancelling.
jQuery(cancelButton).on(CustomEvents.events.activate, () => {
modal.destroy();
});
// Create listener to destroy the modal when closing modal by clicking outside of it.
if (modalContainer.querySelector('.downloadcoursecontentmodal')) {
jQuery(modalContainer).on(CustomEvents.events.activate, () => {
modal.destroy();
});
}
return modal;
});
};
/**
* Trigger downloading of course content.
*
* @method downloadContent
* @param {Event} e The event triggering the download.
* @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
* @param {Object} modal The modal object.
* @return {void}
*/
const downloadContent = (e, downloadModalTrigger, modal) => {
e.preventDefault();
// Create a form to submit the file download request, so we can avoid sending sesskey over GET.
const downloadForm = document.createElement('form');
downloadForm.action = downloadModalTrigger.dataset.downloadLink;
downloadForm.method = 'POST';
// Open download in a new tab, so current course view is not disrupted.
downloadForm.target = '_blank';
const downloadSesskey = document.createElement('input');
downloadSesskey.name = 'sesskey';
downloadSesskey.value = Config.sesskey;
downloadForm.appendChild(downloadSesskey);
downloadForm.style.display = 'none';
document.body.appendChild(downloadForm);
downloadForm.submit();
document.body.removeChild(downloadForm);
// Destroy the modal to prevent duplicates if reopened later.
modal.destroy();
};
+29
View File
@@ -0,0 +1,29 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contain the events the course component can trigger.
*
* @module core_course/events
* @copyright 2018 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
favourited: 'core_course:favourited',
unfavorited: 'core_course:unfavorited',
manualCompletionToggled: 'core_course:manualcompletiontoggled',
stateChanged: 'core_course:stateChanged',
sectionRefreshed: 'core_course:sectionRefreshed',
};
+46
View File
@@ -0,0 +1,46 @@
// 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/>.
/**
* Course format selection handler.
*
* @module core_course/formatchooser
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
const Selectors = {
fields: {
selector: '[data-formatchooser-field="selector"]',
updateButton: '[data-formatchooser-field="updateButton"]',
},
};
/**
* Initialise the format chooser.
*/
export const init = () => {
document.querySelector(Selectors.fields.selector).addEventListener('change', e => {
const form = e.target.closest('form');
const updateButton = form.querySelector(Selectors.fields.updateButton);
const fieldset = updateButton.closest('fieldset');
const url = new URL(form.action);
url.hash = fieldset.id;
form.action = url.toString();
updateButton.click();
});
};
@@ -0,0 +1,524 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A type of dialogue used as for choosing options.
*
* @module core_course/local/activitychooser/dialogue
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import * as ModalEvents from 'core/modal_events';
import selectors from 'core_course/local/activitychooser/selectors';
import * as Templates from 'core/templates';
import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
import {addIconToContainer} from 'core/loadingicon';
import * as Repository from 'core_course/local/activitychooser/repository';
import Notification from 'core/notification';
import {debounce} from 'core/utils';
const getPlugin = pluginName => import(pluginName);
/**
* Given an event from the main module 'page' navigate to it's help section via a carousel.
*
* @method showModuleHelp
* @param {jQuery} carousel Our initialized carousel to manipulate
* @param {Object} moduleData Data of the module to carousel to
* @param {jQuery} modal We need to figure out if the current modal has a footer.
*/
const showModuleHelp = (carousel, moduleData, modal = null) => {
// If we have a real footer then we need to change temporarily.
if (modal !== null && moduleData.showFooter === true) {
modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));
}
const help = carousel.find(selectors.regions.help)[0];
help.innerHTML = '';
help.classList.add('m-auto');
// Add a spinner.
const spinnerPromise = addIconToContainer(help);
// Used later...
let transitionPromiseResolver = null;
const transitionPromise = new Promise(resolve => {
transitionPromiseResolver = resolve;
});
// Build up the html & js ready to place into the help section.
const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);
// Wait for the content to be ready, and for the transition to be complet.
Promise.all([contentPromise, spinnerPromise, transitionPromise])
.then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))
.then(() => {
help.querySelector(selectors.regions.chooserSummary.header).focus();
return help;
})
.catch(Notification.exception);
// Move to the next slide, and resolve the transition promise when it's done.
carousel.one('slid.bs.carousel', () => {
transitionPromiseResolver();
});
// Trigger the transition between 'pages'.
carousel.carousel('next');
};
/**
* Given a user wants to change the favourite state of a module we either add or remove the status.
* We also propergate this change across our map of modals.
*
* @method manageFavouriteState
* @param {HTMLElement} modalBody The DOM node of the modal to manipulate
* @param {HTMLElement} caller
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
*/
const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
const isFavourite = caller.dataset.favourited;
const id = caller.dataset.id;
const name = caller.dataset.name;
const internal = caller.dataset.internal;
// Switch on fave or not.
if (isFavourite === 'true') {
await Repository.unfavouriteModule(name, id);
partialFavourite(internal, false, modalBody);
} else {
await Repository.favouriteModule(name, id);
partialFavourite(internal, true, modalBody);
}
};
/**
* Register chooser related event listeners.
*
* @method registerListenerEvents
* @param {Promise} modal Our modal that we are working with
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
* @param {Object} footerData Our base footer object.
*/
const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {
const bodyClickListener = async(e) => {
if (e.target.closest(selectors.actions.optionActions.showSummary)) {
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
const module = e.target.closest(selectors.regions.chooserOption.container);
const moduleName = module.dataset.modname;
const moduleData = mappedModules.get(moduleName);
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
moduleData.showFooter = modal.hasFooterContent();
showModuleHelp(carousel, moduleData, modal);
}
if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href");
const sectionChooserOptions = modal.getBody()[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = sectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);
}
// From the help screen go back to the module overview.
if (e.target.matches(selectors.actions.closeOption)) {
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
// Trigger the transition between 'pages'.
carousel.carousel('prev');
carousel.on('slid.bs.carousel', () => {
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));
caller.focus();
});
}
// The "clear search" button is triggered.
if (e.target.closest(selectors.actions.clearSearch)) {
// Clear the entered search query in the search bar and hide the search results container.
const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);
searchInput.value = "";
searchInput.focus();
toggleSearchResultsView(modal, mappedModules, searchInput.value);
}
};
// We essentially have two types of footer.
// A fake one that is handled within the template for chooser_help and then all of the stuff for
// modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we
// need to manage. The below code handles a real footer going to a mnet carousel item.
const footerClickListener = async(e) => {
if (footerData.footer === true) {
const footerjs = await getPlugin(footerData.customfooterjs);
await footerjs.footerClickListener(e, footerData, modal);
}
};
modal.getBodyPromise()
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
.then(body => body[0])
// Set up the carousel.
.then(body => {
$(body.querySelector(selectors.regions.carousel))
.carousel({
interval: false,
pause: true,
keyboard: false
});
return body;
})
// Add the listener for clicks on the body.
.then(body => {
body.addEventListener('click', bodyClickListener);
return body;
})
// Add a listener for an input change in the activity chooser's search bar.
.then(body => {
const searchInput = body.querySelector(selectors.actions.search);
// The search input is triggered.
searchInput.addEventListener('input', debounce(() => {
// Display the search results.
toggleSearchResultsView(modal, mappedModules, searchInput.value);
}, 300));
return body;
})
// Register event listeners related to the keyboard navigation controls.
.then(body => {
// Get the active chooser options section.
const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute("href");
const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
return body;
})
.catch();
modal.getFooterPromise()
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
.then(footer => footer[0])
// Add the listener for clicks on the footer.
.then(footer => {
footer.addEventListener('click', footerClickListener);
return footer;
})
.catch();
};
/**
* Initialise the keyboard navigation controls for the chooser options.
*
* @method initChooserOptionsKeyboardNavigation
* @param {HTMLElement} body Our modal that we are working with
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items
* @param {Object} modal Our created modal for the section
*/
const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {
const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
Array.from(chooserOptions).forEach((element) => {
return element.addEventListener('keydown', (e) => {
// Check for enter/ space triggers for showing the help.
if (e.keyCode === enter || e.keyCode === space) {
if (e.target.matches(selectors.actions.optionActions.showSummary)) {
e.preventDefault();
const module = e.target.closest(selectors.regions.chooserOption.container);
const moduleName = module.dataset.modname;
const moduleData = mappedModules.get(moduleName);
const carousel = $(body.querySelector(selectors.regions.carousel));
carousel.carousel({
interval: false,
pause: true,
keyboard: false
});
// We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
moduleData.showFooter = modal.hasFooterContent();
showModuleHelp(carousel, moduleData, modal);
}
}
// Next.
if (e.keyCode === arrowRight) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const nextOption = currentOption.nextElementSibling;
const firstOption = chooserOptionsContainer.firstElementChild;
const toFocusOption = clickErrorHandler(nextOption, firstOption);
focusChooserOption(toFocusOption, currentOption);
}
// Previous.
if (e.keyCode === arrowLeft) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const previousOption = currentOption.previousElementSibling;
const lastOption = chooserOptionsContainer.lastElementChild;
const toFocusOption = clickErrorHandler(previousOption, lastOption);
focusChooserOption(toFocusOption, currentOption);
}
if (e.keyCode === home) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const firstOption = chooserOptionsContainer.firstElementChild;
focusChooserOption(firstOption, currentOption);
}
if (e.keyCode === end) {
e.preventDefault();
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
const lastOption = chooserOptionsContainer.lastElementChild;
focusChooserOption(lastOption, currentOption);
}
});
});
};
/**
* Focus on a chooser option element and remove the previous chooser element from the focus order
*
* @method focusChooserOption
* @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus
* @param {HTMLElement|null} previousChooserOption The previous focused option element
*/
const focusChooserOption = (currentChooserOption, previousChooserOption = null) => {
if (previousChooserOption !== null) {
toggleFocusableChooserOption(previousChooserOption, false);
}
toggleFocusableChooserOption(currentChooserOption, true);
currentChooserOption.focus();
};
/**
* Add or remove a chooser option from the focus order.
*
* @method toggleFocusableChooserOption
* @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order
* @param {Boolean} isFocusable Whether the chooser element is focusable or not
*/
const toggleFocusableChooserOption = (chooserOption, isFocusable) => {
const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);
const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);
const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);
if (isFocusable) {
// Set tabindex to 0 to add current chooser option element to the focus order.
chooserOption.tabIndex = 0;
chooserOptionLink.tabIndex = 0;
chooserOptionHelp.tabIndex = 0;
chooserOptionFavourite.tabIndex = 0;
} else {
// Set tabindex to -1 to remove the previous chooser option element from the focus order.
chooserOption.tabIndex = -1;
chooserOptionLink.tabIndex = -1;
chooserOptionHelp.tabIndex = -1;
chooserOptionFavourite.tabIndex = -1;
}
};
/**
* Small error handling function to make sure the navigated to object exists
*
* @method clickErrorHandler
* @param {HTMLElement} item What we want to check exists
* @param {HTMLElement} fallback If we dont match anything fallback the focus
* @return {HTMLElement}
*/
const clickErrorHandler = (item, fallback) => {
if (item !== null) {
return item;
} else {
return fallback;
}
};
/**
* Render the search results in a defined container
*
* @method renderSearchResults
* @param {HTMLElement} searchResultsContainer The container where the data should be rendered
* @param {Object} searchResultsData Data containing the module items that satisfy the search criteria
*/
const renderSearchResults = async(searchResultsContainer, searchResultsData) => {
const templateData = {
'searchresultsnumber': searchResultsData.length,
'searchresults': searchResultsData
};
// Build up the html & js ready to place into the help section.
const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);
await Templates.replaceNodeContents(searchResultsContainer, html, js);
};
/**
* Toggle (display/hide) the search results depending on the value of the search query
*
* @method toggleSearchResultsView
* @param {Object} modal Our created modal for the section
* @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
* @param {String} searchQuery The search query
*/
const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
const modalBody = modal.getBody()[0];
const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);
if (searchQuery.length > 0) { // Search query is present.
const searchResultsData = searchModules(mappedModules, searchQuery);
await renderSearchResults(searchResultsContainer, searchResultsData);
const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);
const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);
if (firstSearchResultItem) {
// Set the first result item to be focusable.
toggleFocusableChooserOption(firstSearchResultItem, true);
// Register keyboard events on the created search result items.
initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
}
// Display the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.remove('d-none');
// Hide the default chooser options container.
chooserContainer.setAttribute('hidden', 'hidden');
// Display the search results container.
searchResultsContainer.removeAttribute('hidden');
} else { // Search query is not present.
// Hide the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.add('d-none');
// Hide the search results container.
searchResultsContainer.setAttribute('hidden', 'hidden');
// Display the default chooser options container.
chooserContainer.removeAttribute('hidden');
}
};
/**
* Return the list of modules which have a name or description that matches the given search term.
*
* @method searchModules
* @param {Array} modules List of available modules
* @param {String} searchTerm The search term to match
* @return {Array}
*/
const searchModules = (modules, searchTerm) => {
if (searchTerm === '') {
return modules;
}
searchTerm = searchTerm.toLowerCase();
const searchResults = [];
modules.forEach((activity) => {
const activityName = activity.title.toLowerCase();
const activityDesc = activity.help.toLowerCase();
if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {
searchResults.push(activity);
}
});
return searchResults;
};
/**
* Set up our tabindex information across the chooser.
*
* @method setupKeyboardAccessibility
* @param {Promise} modal Our created modal for the section
* @param {Map} mappedModules A map of all of the built module information
*/
const setupKeyboardAccessibility = (modal, mappedModules) => {
modal.getModal()[0].tabIndex = -1;
modal.getBodyPromise().then(body => {
$(selectors.elements.tab).on('shown.bs.tab', (e) => {
const activeSectionId = e.target.getAttribute("href");
const activeSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
const firstChooserOption = activeSectionChooserOptions
.querySelector(selectors.regions.chooserOption.container);
const prevActiveSectionId = e.relatedTarget.getAttribute("href");
const prevActiveSectionChooserOptions = body[0]
.querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
// Disable the focus of every chooser option in the previous active section.
disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
// Enable the focus of the first chooser option in the current active section.
toggleFocusableChooserOption(firstChooserOption, true);
initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);
});
return;
}).catch(Notification.exception);
};
/**
* Disable the focus of all chooser options in a specific container (section).
*
* @method disableFocusAllChooserOptions
* @param {HTMLElement} sectionChooserOptions The section that contains the chooser items
*/
const disableFocusAllChooserOptions = (sectionChooserOptions) => {
const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);
allChooserOptions.forEach((chooserOption) => {
toggleFocusableChooserOption(chooserOption, false);
});
};
/**
* Display the module chooser.
*
* @method displayChooser
* @param {Promise} modalPromise Our created modal for the section
* @param {Array} sectionModules An array of all of the built module information
* @param {Function} partialFavourite Partially applied function we need to manage favourite status
* @param {Object} footerData Our base footer object.
*/
export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {
// Make a map so we can quickly fetch a specific module's object for either rendering or searching.
const mappedModules = new Map();
sectionModules.forEach((module) => {
mappedModules.set(module.componentname + '_' + module.link, module);
});
// Register event listeners.
modalPromise.then(modal => {
registerListenerEvents(modal, mappedModules, partialFavourite, footerData);
// We want to focus on the first chooser option element as soon as the modal is opened.
setupKeyboardAccessibility(modal, mappedModules);
// We want to focus on the action select when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
return modal;
}).catch();
};
@@ -0,0 +1,99 @@
// 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 javascript module to handle user AJAX actions.
*
* @module core_course/local/activitychooser/repository
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Fetch all the information on modules we'll need in the activity chooser.
*
* @method activityModules
* @param {Number} courseid What course to fetch the modules for
* @return {object} jQuery promise
*/
export const activityModules = (courseid) => {
const request = {
methodname: 'core_course_get_course_content_items',
args: {
courseid: courseid,
},
};
return ajax.call([request])[0];
};
/**
* Given a module name, module ID & the current course we want to specify that the module
* is a users' favourite.
*
* @method favouriteModule
* @param {String} modName Frankenstyle name of the component to add favourite
* @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
* @return {object} jQuery promise
*/
export const favouriteModule = (modName, modID) => {
const request = {
methodname: 'core_course_add_content_item_to_user_favourites',
args: {
componentname: modName,
contentitemid: modID,
},
};
return ajax.call([request])[0];
};
/**
* Given a module name, module ID & the current course we want to specify that the module
* is no longer a users' favourite.
*
* @method unfavouriteModule
* @param {String} modName Frankenstyle name of the component to add favourite
* @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names
* @return {object} jQuery promise
*/
export const unfavouriteModule = (modName, modID) => {
const request = {
methodname: 'core_course_remove_content_item_from_user_favourites',
args: {
componentname: modName,
contentitemid: modID,
},
};
return ajax.call([request])[0];
};
/**
* Fetch all the information on modules we'll need in the activity chooser.
*
* @method fetchFooterData
* @param {Number} courseid What course to fetch the data for
* @param {Number} sectionid What section to fetch the data for
* @return {object} jQuery promise
*/
export const fetchFooterData = (courseid, sectionid) => {
const request = {
methodname: 'core_course_get_activity_chooser_footer',
args: {
courseid: courseid,
sectionid: sectionid,
},
};
return ajax.call([request])[0];
};
@@ -0,0 +1,88 @@
// 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/>.
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module core_course/local/activitychooser/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* A small helper function to build queryable data selectors.
* @method getDataSelector
* @param {String} name
* @param {String} value
* @return {string}
*/
const getDataSelector = (name, value) => {
return `[data-${name}="${value}"]`;
};
export default {
regions: {
chooser: getDataSelector('region', 'chooser-container'),
getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,
chooserOption: {
container: getDataSelector('region', 'chooser-option-container'),
actions: getDataSelector('region', 'chooser-option-actions-container'),
info: getDataSelector('region', 'chooser-option-info-container'),
},
chooserSummary: {
container: getDataSelector('region', 'chooser-option-summary-container'),
content: getDataSelector('region', 'chooser-option-summary-content-container'),
header: getDataSelector('region', 'summary-header'),
actions: getDataSelector('region', 'chooser-option-summary-actions-container'),
},
carousel: getDataSelector('region', 'carousel'),
help: getDataSelector('region', 'help'),
modules: getDataSelector('region', 'modules'),
favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
defaultTabNav: getDataSelector('region', 'default-tab-nav'),
activityTabNav: getDataSelector('region', 'activity-tab-nav'),
favouriteTab: getDataSelector('region', 'favourites'),
recommendedTab: getDataSelector('region', 'recommended'),
defaultTab: getDataSelector('region', 'default'),
activityTab: getDataSelector('region', 'activity'),
resourceTab: getDataSelector('region', 'resources'),
getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`,
searchResults: getDataSelector('region', 'search-results-container'),
searchResultItems: getDataSelector('region', 'search-result-items-container'),
},
actions: {
optionActions: {
showSummary: getDataSelector('action', 'show-option-summary'),
manageFavourite: getDataSelector('action', 'manage-module-favourite'),
},
addChooser: getDataSelector('action', 'add-chooser-option'),
closeOption: getDataSelector('action', 'close-chooser-option-summary'),
hide: getDataSelector('action', 'hide'),
search: getDataSelector('action', 'search'),
clearSearch: getDataSelector('action', 'clearsearch'),
},
render: {
favourites: getDataSelector('render', 'favourites-area'),
},
elements: {
section: '.section',
sectionmodchooser: 'button.section-modchooser-link',
sitemenu: '.block_site_main_menu',
sitetopic: 'div.sitetopic',
tab: 'a[data-toggle="tab"]',
activetab: 'a[data-toggle="tab"][aria-selected="true"]',
visibletabs: 'a[data-toggle="tab"]:not(.d-none)'
},
};
+143
View File
@@ -0,0 +1,143 @@
// 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/>.
/**
* Provides the functionality for toggling the manual completion state of a course module through
* the manual completion button.
*
* @module core_course/manual_completion_toggle
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Notification from 'core/notification';
import {toggleManualCompletion} from 'core_course/repository';
import * as CourseEvents from 'core_course/events';
import Pending from 'core/pending';
/**
* Selectors in the manual completion template.
*
* @type {{MANUAL_TOGGLE: string}}
*/
const SELECTORS = {
MANUAL_TOGGLE: 'button[data-action=toggle-manual-completion]',
};
/**
* Toggle type values for the data-toggletype attribute in the core_course/completion_manual template.
*
* @type {{TOGGLE_UNDO: string, TOGGLE_MARK_DONE: string}}
*/
const TOGGLE_TYPES = {
TOGGLE_MARK_DONE: 'manual:mark-done',
TOGGLE_UNDO: 'manual:undo',
};
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
/**
* Registers the click event listener for the manual completion toggle button.
*/
export const init = () => {
if (registered) {
return;
}
document.addEventListener('click', (e) => {
const toggleButton = e.target.closest(SELECTORS.MANUAL_TOGGLE);
if (toggleButton) {
e.preventDefault();
toggleManualCompletionState(toggleButton).catch(Notification.exception);
}
});
registered = true;
};
/**
* Toggles the manual completion state of the module for the given user.
*
* @param {HTMLElement} toggleButton
* @returns {Promise<void>}
*/
const toggleManualCompletionState = async(toggleButton) => {
const pendingPromise = new Pending('core_course:toggleManualCompletionState');
// Make a copy of the original content of the button.
const originalInnerHtml = toggleButton.innerHTML;
// Disable the button to prevent double clicks.
toggleButton.setAttribute('disabled', 'disabled');
// Get button data.
const toggleType = toggleButton.getAttribute('data-toggletype');
const cmid = toggleButton.getAttribute('data-cmid');
const activityname = toggleButton.getAttribute('data-activityname');
// Get the target completion state.
const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE;
// Replace the button contents with the loading icon.
Templates.renderForPromise('core/loading', {})
.then((loadingHtml) => {
Templates.replaceNodeContents(toggleButton, loadingHtml, '');
return;
}).catch(() => {});
try {
// Call the webservice to update the manual completion status.
await toggleManualCompletion(cmid, completed);
// All good so far. Refresh the manual completion button to reflect its new state by re-rendering the template.
const templateContext = {
cmid: cmid,
activityname: activityname,
overallcomplete: completed,
overallincomplete: !completed,
istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.
};
const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);
// Replace the toggle button with the newly loaded template.
const replacedNode = await Templates.replaceNode(toggleButton, renderObject.html, renderObject.js);
const newToggleButton = replacedNode.pop();
// Build manualCompletionToggled custom event.
const withAvailability = toggleButton.getAttribute('data-withavailability');
const toggledEvent = new CustomEvent(CourseEvents.manualCompletionToggled, {
bubbles: true,
detail: {
cmid,
activityname,
completed,
withAvailability,
}
});
// Dispatch the manualCompletionToggled custom event.
newToggleButton.dispatchEvent(toggledEvent);
} catch (exception) {
// In case of an error, revert the original state and appearance of the button.
toggleButton.removeAttribute('disabled');
toggleButton.innerHTML = originalInnerHtml;
// Show the exception.
Notification.exception(exception);
}
pendingPromise.resolve();
};
+54
View File
@@ -0,0 +1,54 @@
// 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 javascript module to handle toggling activity chooser recommendations.
*
* @module core_course/recommendations
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Notification from 'core/notification';
/**
* Do an ajax call to toggle the recommendation
*
* @param {object} e The event
* @return {void}
*/
const toggleRecommendation = (e) => {
let data = {
methodname: 'core_course_toggle_activity_recommendation',
args: {
area: e.currentTarget.dataset.area,
id: e.currentTarget.dataset.id
}
};
Ajax.call([data])[0].fail(Notification.exception);
};
/**
* Initialisation function
*
* @return {void}
*/
export const init = () => {
const checkboxelements = document.querySelectorAll("[data-area]");
checkboxelements.forEach((checkbox) => {
checkbox.addEventListener('change', toggleRecommendation);
});
};
+199
View File
@@ -0,0 +1,199 @@
// 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 javascript module to handle course ajax actions.
*
* @module core_course/repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Get the list of courses that the logged in user is enrolled in for a given
* timeline classification.
*
* @param {string} classification past, inprogress, or future
* @param {int} limit Only return this many results
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @return {object} jQuery promise resolved with courses.
*/
const getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {
const args = {
classification: classification
};
if (typeof limit !== 'undefined') {
args.limit = limit;
}
if (typeof offset !== 'undefined') {
args.offset = offset;
}
if (typeof sort !== 'undefined') {
args.sort = sort;
}
const request = {
methodname: 'core_course_get_enrolled_courses_by_timeline_classification',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get a list of courses that the logged in user is enrolled in, where they have at least one action event,
* for a given timeline classification.
*
* @param {string} classification past, inprogress, or future
* @param {int} limit The maximum number of courses to return
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @param {string} searchValue Optional text search value
* @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in
* @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in
* @return {object} jQuery promise resolved with courses.
*/
const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null,
searchValue = null, eventsFrom = null, eventsTo = null) => {
const args = {
classification: classification,
limit: limit,
offset: offset,
sort: sort,
eventsfrom: eventsFrom,
eventsto: eventsTo,
searchvalue: searchValue,
};
const request = {
methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get the list of courses that the user has most recently accessed.
*
* @method getLastAccessedCourses
* @param {int} userid User from which the courses will be obtained
* @param {int} limit Only return this many results
* @param {int} offset Skip this many results from the start of the result set
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
* @return {promise} Resolved with an array of courses
*/
const getLastAccessedCourses = (userid, limit, offset, sort) => {
const args = {};
if (typeof userid !== 'undefined') {
args.userid = userid;
}
if (typeof limit !== 'undefined') {
args.limit = limit;
}
if (typeof offset !== 'undefined') {
args.offset = offset;
}
if (typeof sort !== 'undefined') {
args.sort = sort;
}
const request = {
methodname: 'core_course_get_recent_courses',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get the list of users enrolled in this cmid.
*
* @param {Number} cmid Course Module from which the users will be obtained
* @param {Number} groupID Group ID from which the users will be obtained
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
* @returns {Promise} Promise containing a list of users
*/
const getEnrolledUsersFromCourseModuleID = (cmid, groupID, onlyActive = false) => {
var request = {
methodname: 'core_course_get_enrolled_users_by_cmid',
args: {
cmid: cmid,
groupid: groupID,
onlyactive: onlyActive,
},
};
return Ajax.call([request])[0];
};
/**
* Get the list of gradable users enrolled in this course.
*
* @param {Number} courseid Course ID from which the users will be obtained
* @param {Number} groupID Group ID from which the users will be obtained
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
* @returns {Promise} Promise containing a list of users
*/
const getGradabaleUsersFromCourseID = (courseid, groupID, onlyActive = false) => {
const request = {
methodname: 'core_grades_get_gradable_users',
args: {
courseid: courseid,
groupid: groupID,
onlyactive: onlyActive,
},
};
return Ajax.call([request])[0];
};
/**
* Toggle the completion state of an activity with manual completion.
*
* @param {Number} cmid The course module ID.
* @param {Boolean} completed Whether to set as complete or not.
* @returns {object} jQuery promise
*/
const toggleManualCompletion = (cmid, completed) => {
const request = {
methodname: 'core_completion_update_activity_completion_status_manually',
args: {
cmid,
completed,
}
};
return Ajax.call([request])[0];
};
export default {
getEnrolledCoursesByTimelineClassification,
getLastAccessedCourses,
getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,
getGradableUsersFromCourseID: getGradabaleUsersFromCourseID,
toggleManualCompletion,
getEnrolledCoursesWithEventsByTimelineClassification,
};
+49
View File
@@ -0,0 +1,49 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* JS module for the course homepage.
*
* @module core_course/view
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as CourseEvents from 'core_course/events';
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
/**
* Function to intialise and register event listeners for this module.
*/
export const init = () => {
if (registered) {
return;
}
// Listen for toggled manual completion states of activities.
document.addEventListener(CourseEvents.manualCompletionToggled, (e) => {
const withAvailability = parseInt(e.detail.withAvailability);
if (withAvailability) {
// Reload the page when the toggled manual completion button has availability conditions linked to it.
window.location.reload();
}
});
registered = true;
};