Merge branch 'MDL-76432-master-v02' of https://github.com/ferranrecio/moodle

This commit is contained in:
Jun Pataleta 2023-02-06 22:30:40 +08:00
commit 08a061ad5d
66 changed files with 2610 additions and 44 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
define("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive"],(function(_exports,_reactive){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
define("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive","core/str","core/prefetch","core/templates"],(function(_exports,_reactive,_str,_prefetch,_templates){var obj;
/**
* Course index section component.
*
@ -9,7 +9,6 @@ define("core_courseformat/local/courseeditor/dndsection",["exports","core/reacti
* @class core_courseformat/local/courseeditor/dndsection
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type))return!0;if("section"===(null==dropdata?void 0:dropdata.type)){const sectionzeroid=this.course.sectionlist[0];return(null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.id)!=sectionzeroid&&this.id!=sectionzeroid}return!1}showDropZone(dropdata){var _this$getLastCm;"cm"==dropdata.type&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.section.number>dropdata.number?(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN)):(this.element.classList.add(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)}drop(dropdata,event){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMove",[dropdata.id],this.id)}}return _exports.default=_default,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core",["addfilehere"]);class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){if("files"===(null==dropdata?void 0:dropdata.type))return!0;if("cm"===(null==dropdata?void 0:dropdata.type))return!0;if("section"===(null==dropdata?void 0:dropdata.type)){const sectionzeroid=this.course.sectionlist[0];return(null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.id)!=sectionzeroid&&this.id!=sectionzeroid}return!1}showDropZone(dropdata){var _this$getLastCm;("files"==dropdata.type&&this.addOverlay({content:(0,_str.get_string)("addfilehere","core"),icon:_templates.default.renderPix("t/download","core")}).then((()=>{var _this$dragdrop;null!==(_this$dragdrop=this.dragdrop)&&void 0!==_this$dragdrop&&_this$dragdrop.isDropzoneVisible()||this.removeOverlay()})).catch((error=>{throw error})),"cm"==dropdata.type)&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.section.number>dropdata.number?(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN)):(this.element.classList.add(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN),this.removeOverlay()}drop(dropdata,event){if("files"!=dropdata.type){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMove",[dropdata.id],this.id)}else this.reactive.uploadFiles(this.section.id,this.section.number,dropdata.files)}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=dndsection.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,6 @@ define("core_courseformat/local/courseeditor/exporter",["exports"],(function(_ex
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig,_state$course$section;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){return{...cminfo,isactive:!1}}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;const statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}},_exports.default}));
class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig,_state$course$section;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){return{...cminfo,isactive:!1}}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;const statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}},_exports.default}));
//# sourceMappingURL=exporter.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/section",["exports","core_courseform
* @class core_courseformat/local/courseindex/section
* @copyright 2021 Ferran Recio <ferran@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,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&amp;/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}}return _exports.default=Component,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&amp;/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default}));
//# sourceMappingURL=section.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ import Exporter from 'core_courseformat/local/courseeditor/exporter';
import log from 'core/log';
import ajax from 'core/ajax';
import * as Storage from 'core/sessionstorage';
import {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';
/**
* Main course editor module.
@ -82,6 +83,7 @@ export default class extends Reactive {
// Default view format setup.
this._editing = false;
this._supportscomponents = false;
this._fileHandlers = null;
this.courseId = courseId;
@ -125,6 +127,49 @@ export default class extends Reactive {
}
this.stateKey = Storage.get(`course/${courseId}/stateKey`);
}
this._loadFileHandlers();
}
/**
* Load the file hanlders promise.
*/
_loadFileHandlers() {
// Load the course file extensions.
this._fileHandlersPromise = new Promise((resolve) => {
if (!this.isEditing) {
resolve([]);
return;
}
// Check the cache.
const handlersCacheKey = `course/${this.courseId}/fileHandlers`;
const cacheValue = Storage.get(handlersCacheKey);
if (cacheValue) {
try {
const cachedHandlers = JSON.parse(cacheValue);
resolve(cachedHandlers);
return;
} catch (error) {
log.error("ERROR PARSING CACHED FILE HANDLERS");
}
}
// Call file handlers webservice.
ajax.call([{
methodname: 'core_courseformat_file_handlers',
args: {
courseid: this.courseId,
}
}])[0].then((handlers) => {
Storage.set(handlersCacheKey, JSON.stringify(handlers));
resolve(handlers);
return;
}).catch(error => {
log.error(error);
resolve([]);
return;
});
});
}
/**
@ -192,6 +237,28 @@ export default class extends Reactive {
return this._supportscomponents ?? false;
}
/**
* Return the course file handlers promise.
* @returns {Promise} the promise for file handlers.
*/
async getFileHandlersPromise() {
return this._fileHandlersPromise ?? [];
}
/**
* Upload a file list to the course.
*
* This method is a wrapper to the course file uploader.
*
* @param {number} sectionId the section id
* @param {number} sectionNum the section number
* @param {Array} files and array of files
* @return {Promise} the file queue promise
*/
uploadFiles(sectionId, sectionNum, files) {
return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);
}
/**
* Get a value from the course editor static storage if any.
*
@ -242,6 +309,16 @@ export default class extends Reactive {
return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));
}
/**
* Convert a file dragging event into a proper dragging file list.
* @param {DataTransfer} dataTransfer the event to convert
* @return {Array} of file list info.
*/
getFilesDraggableData(dataTransfer) {
const exporter = this.getExporter();
return exporter.fileDraggableData(this.state, dataTransfer);
}
/**
* Dispatch a change in the state.
*

View File

@ -26,6 +26,12 @@
*/
import {BaseComponent, DragDrop} from 'core/reactive';
import {get_string as getString} from 'core/str';
import {prefetchStrings} from 'core/prefetch';
import Templates from 'core/templates';
// Load global strings.
prefetchStrings('core', ['addfilehere']);
export default class extends BaseComponent {
@ -105,6 +111,10 @@ export default class extends BaseComponent {
* @returns {boolean}
*/
validateDropData(dropdata) {
// We accept files.
if (dropdata?.type === 'files') {
return true;
}
// We accept any course module.
if (dropdata?.type === 'cm') {
return true;
@ -123,6 +133,20 @@ export default class extends BaseComponent {
* @param {Object} dropdata the accepted drop data
*/
showDropZone(dropdata) {
if (dropdata.type == 'files') {
this.addOverlay({
content: getString('addfilehere', 'core'),
icon: Templates.renderPix('t/download', 'core'),
}).then(() => {
// Check if we still need the file dropzone.
if (!this.dragdrop?.isDropzoneVisible()) {
this.removeOverlay();
}
return;
}).catch((error) => {
throw error;
});
}
if (dropdata.type == 'cm') {
this.getLastCm()?.classList.add(this.classes.DROPDOWN);
}
@ -145,6 +169,7 @@ export default class extends BaseComponent {
this.getLastCm()?.classList.remove(this.classes.DROPDOWN);
this.element.classList.remove(this.classes.DROPUP);
this.element.classList.remove(this.classes.DROPDOWN);
this.removeOverlay();
}
/**
@ -154,6 +179,15 @@ export default class extends BaseComponent {
* @param {Event} event the drop event
*/
drop(dropdata, event) {
// File handling.
if (dropdata.type == 'files') {
this.reactive.uploadFiles(
this.section.id,
this.section.number,
dropdata.files
);
return;
}
// Call the move mutation.
if (dropdata.type == 'cm') {
const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';

View File

@ -158,7 +158,30 @@ export default class {
}
/**
* Generate a compoetion export data from the cm element.
* Generate a file draggable structure.
*
* This method is used when files are dragged on the browser.
*
* @param {*} state the state object
* @param {*} dataTransfer the current data tranfer data
* @returns {Object|null}
*/
fileDraggableData(state, dataTransfer) {
const files = [];
// Browsers do not provide the file list until the drop event.
if (dataTransfer.files?.length > 0) {
dataTransfer.files.forEach(file => {
files.push(file);
});
}
return {
type: 'files',
files,
};
}
/**
* Generate a completion export data from the cm element.
*
* @param {Object} state the current state.
* @param {Object} cminfo the course module state data.

View File

@ -0,0 +1,558 @@
// 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/>.
/**
* The course file uploader.
*
* This module is used to upload files directly into the course.
*
* @module core_courseformat/local/courseeditor/fileuploader
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* @typedef {Object} Handler
* @property {String} extension the handled extension or * for any
* @property {String} message the handler message
* @property {String} module the module name
*/
import Config from 'core/config';
import ModalFactory from 'core/modal_factory';
import ModalEvents from 'core/modal_events';
import Templates from 'core/templates';
import {getFirst} from 'core/normalise';
import {prefetchStrings} from 'core/prefetch';
import {get_string as getString, get_strings as getStrings} from 'core/str';
import {getCourseEditor} from 'core_courseformat/courseeditor';
import {processMonitor} from 'core/process_monitor';
import {debounce} from 'core/utils';
// Uploading url.
const UPLOADURL = Config.wwwroot + '/course/dndupload.php';
const DEBOUNCETIMER = 500;
const USERCANIGNOREFILESIZELIMITS = -1;
/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */
let uploadQueue = null;
/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */
let handlerManagers = {};
/** @var {Map} courseUpdates the pending course sections updates. */
let courseUpdates = new Map();
/** @var {Object} errors the error messages. */
let errors = null;
// Load global strings.
prefetchStrings('moodle', ['addresourceoractivity', 'upload']);
prefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);
/**
* Class to upload a file into the course.
* @private
*/
class FileUploader {
/**
* Class constructor.
*
* @param {number} courseId the course id
* @param {number} sectionId the section id
* @param {number} sectionNum the section number
* @param {File} fileInfo the file information object
* @param {Handler} handler the file selected file handler
*/
constructor(courseId, sectionId, sectionNum, fileInfo, handler) {
this.courseId = courseId;
this.sectionId = sectionId;
this.sectionNum = sectionNum;
this.fileInfo = fileInfo;
this.handler = handler;
}
/**
* Execute the file upload and update the state in the given process.
*
* @param {LoadingProcess} process the process to store the upload result
*/
execute(process) {
const fileInfo = this.fileInfo;
const xhr = this._createXhrRequest(process);
const formData = this._createUploadFormData();
// Try reading the file to check it is not a folder, before sending it to the server.
const reader = new FileReader();
reader.onload = function() {
// File was read OK - send it to the server.
xhr.open("POST", UPLOADURL, true);
xhr.send(formData);
};
reader.onerror = function() {
// Unable to read the file (it is probably a folder) - display an error message.
process.setError(errors.dndread);
};
if (fileInfo.size > 0) {
// If this is a non-empty file, try reading the first few bytes.
// This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
reader.readAsText(fileInfo.slice(0, 5));
} else {
// If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
// instead of reader.onerror().
// So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
reader.readAsText(fileInfo);
}
}
/**
* Returns the bind version of execute function.
*
* This method is used to queue the process into a ProcessQueue instance.
*
* @returns {Function} the bind function to execute the process
*/
getExecutionFunction() {
return this.execute.bind(this);
}
/**
* Generate a upload XHR file request.
*
* @param {LoadingProcess} process the current process
* @return {XMLHttpRequest} the XHR request
*/
_createXhrRequest(process) {
const xhr = new XMLHttpRequest();
// Update the progress bar as the file is uploaded.
xhr.upload.addEventListener(
'progress',
(event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded * 100) / event.total);
process.setPercentage(percent);
}
},
false
);
// Wait for the AJAX call to complete.
xhr.onreadystatechange = () => {
if (xhr.readyState == 1) {
// Add a 1% just to indicate that it is uploading.
process.setPercentage(1);
}
// State 4 is DONE. Otherwise the connection is still ongoing.
if (xhr.readyState != 4) {
return;
}
if (xhr.status == 200) {
var result = JSON.parse(xhr.responseText);
if (result && result.error == 0) {
// All OK.
this._finishProcess(process);
} else {
process.setError(result.error);
}
} else {
process.setError(errors.dndupload);
}
};
return xhr;
}
/**
* Upload a file into the course.
*
* @return {FormData|null} the new form data object
*/
_createUploadFormData() {
const formData = new FormData();
try {
formData.append('repo_upload_file', this.fileInfo);
} catch (error) {
throw Error(error.dndread);
}
formData.append('sesskey', Config.sesskey);
formData.append('course', this.courseId);
formData.append('section', this.sectionNum);
formData.append('module', this.handler.module);
formData.append('type', 'Files');
return formData;
}
/**
* Finishes the current process.
* @param {LoadingProcess} process the process
*/
_finishProcess(process) {
addRefreshSection(this.courseId, this.sectionId);
process.setPercentage(100);
process.finish();
}
}
/**
* The file handler manager class.
*
* @private
*/
class HandlerManager {
/** @var {Object} lastHandlers the last handlers selected per each file extension. */
lastHandlers = {};
/** @var {Handler[]|null} allHandlers all the available handlers. */
allHandlers = null;
/**
* Class constructor.
*
* @param {Number} courseId
*/
constructor(courseId) {
this.courseId = courseId;
this.lastUploadId = 0;
this.courseEditor = getCourseEditor(courseId);
if (!this.courseEditor) {
throw Error('Unkown course editor');
}
this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;
}
/**
* Load the course file handlers.
*/
async loadHandlers() {
this.allHandlers = await this.courseEditor.getFileHandlersPromise();
}
/**
* Extract the file extension from a fileInfo.
*
* @param {File} fileInfo
* @returns {String} the file extension or an empty string.
*/
getFileExtension(fileInfo) {
let extension = '';
const dotpos = fileInfo.name.lastIndexOf('.');
if (dotpos != -1) {
extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();
}
return extension;
}
/**
* Check if the file is valid.
*
* @param {File} fileInfo the file info
*/
validateFile(fileInfo) {
if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {
throw Error(errors.dndmaxbytes);
}
}
/**
* Get the file handlers of an specific file.
*
* @param {File} fileInfo the file indo
* @return {Array} Array of handlers
*/
filterHandlers(fileInfo) {
const extension = this.getFileExtension(fileInfo);
return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);
}
/**
* Get the Handler to upload a specific file.
*
* It will ask the used if more than one handler is available.
*
* @param {File} fileInfo the file info
* @returns {Promise<Handler|null>} the selected handler or null if the user cancel
*/
async getFileHandler(fileInfo) {
const fileHandlers = this.filterHandlers(fileInfo);
if (fileHandlers.length == 0) {
throw Error(errors.dndunkownfile);
}
let fileHandler = null;
if (fileHandlers.length == 1) {
fileHandler = fileHandlers[0];
} else {
fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);
}
return fileHandler;
}
/**
* Ask the user to select a specific handler.
*
* @param {Handler[]} fileHandlers
* @param {File} fileInfo the file info
* @return {Promise<Handler>} the selected handler
*/
async askHandlerToUser(fileHandlers, fileInfo) {
const extension = this.getFileExtension(fileInfo);
// Build the modal parameters from the event data.
const modalParams = {
title: getString('addresourceoractivity', 'moodle'),
body: Templates.render(
'core_courseformat/fileuploader',
this.getModalData(
fileHandlers,
fileInfo,
this.lastHandlers[extension] ?? null
)
),
type: ModalFactory.types.SAVE_CANCEL,
saveButtonText: getString('upload', 'moodle'),
};
// Create the modal.
const modal = await this.modalBodyRenderedPromise(modalParams);
const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);
// Cancel action.
if (selectedHandler === null) {
return null;
}
// Save last selected handler.
this.lastHandlers[extension] = selectedHandler.module;
return selectedHandler;
}
/**
* Generated the modal template data.
*
* @param {Handler[]} fileHandlers
* @param {File} fileInfo the file info
* @param {String|null} defaultModule the default module if any
* @return {Object} the modal template data.
*/
getModalData(fileHandlers, fileInfo, defaultModule) {
const data = {
filename: fileInfo.name,
uploadid: ++this.lastUploadId,
handlers: [],
};
let hasDefault = false;
fileHandlers.forEach((handler, index) => {
const isDefault = (defaultModule == handler.module);
data.handlers.push({
...handler,
selected: isDefault,
labelid: `fileuploader_${data.uploadid}`,
value: index,
});
hasDefault = hasDefault || isDefault;
});
if (!hasDefault && data.handlers.length > 0) {
const lastHandler = data.handlers.pop();
lastHandler.selected = true;
data.handlers.push(lastHandler);
}
return data;
}
/**
* Get the user handler choice.
*
* Wait for the user answer in the modal and resolve with the selected index.
*
* @param {Modal} modal the modal instance
* @param {Handler[]} fileHandlers the availabvle file handlers
* @return {Promise} with the option selected by the user.
*/
modalUserAnswerPromise(modal, fileHandlers) {
const modalBody = getFirst(modal.getBody());
return new Promise((resolve, reject) => {
modal.getRoot().on(
ModalEvents.save,
event => {
// Get the selected option.
const index = modalBody.querySelector('input:checked').value;
event.preventDefault();
modal.destroy();
if (!fileHandlers[index]) {
reject('Invalid handler selected');
}
resolve(fileHandlers[index]);
}
);
modal.getRoot().on(
ModalEvents.cancel,
() => {
resolve(null);
}
);
});
}
/**
* Create a new modal and return a Promise to the body rendered.
*
* @param {Object} modalParams the modal params
* @returns {Promise} the modal body rendered promise
*/
modalBodyRenderedPromise(modalParams) {
return new Promise((resolve, reject) => {
ModalFactory.create(modalParams).then((modal) => {
modal.setRemoveOnClose(true);
// Handle body loading event.
modal.getRoot().on(ModalEvents.bodyRendered, () => {
resolve(modal);
});
// Configure some extra modal params.
if (modalParams.saveButtonText !== undefined) {
modal.setSaveButtonText(modalParams.saveButtonText);
}
modal.show();
return;
}).catch(() => {
reject(`Cannot load modal content`);
});
});
}
}
/**
* Add a section to refresh.
*
* @param {number} courseId the course id
* @param {number} sectionId the seciton id
*/
function addRefreshSection(courseId, sectionId) {
let refresh = courseUpdates.get(courseId);
if (!refresh) {
refresh = new Set();
}
refresh.add(sectionId);
courseUpdates.set(courseId, refresh);
refreshCourseEditors();
}
/**
* Debounced processing all pending course refreshes.
* @private
*/
const refreshCourseEditors = debounce(
() => {
const refreshes = courseUpdates;
courseUpdates = new Map();
refreshes.forEach((sectionIds, courseId) => {
const courseEditor = getCourseEditor(courseId);
if (!courseEditor) {
return;
}
courseEditor.dispatch('sectionState', [...sectionIds]);
});
},
DEBOUNCETIMER
);
/**
* Load and return the course handler manager instance.
*
* @param {Number} courseId the course Id to load
* @returns {Promise<HandlerManager>} promise of the the loaded handleManager
*/
async function loadCourseHandlerManager(courseId) {
if (handlerManagers[courseId] !== undefined) {
return handlerManagers[courseId];
}
try {
const handlerManager = new HandlerManager(courseId);
await handlerManager.loadHandlers();
handlerManagers[courseId] = handlerManager;
} catch (error) {
throw error;
}
return handlerManagers[courseId];
}
/**
* Load all the erros messages at once in the module "errors" variable.
* @param {Number} courseId the course id
*/
async function loadErrorStrings(courseId) {
if (errors !== null) {
return;
}
const courseEditor = getCourseEditor(courseId);
const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';
errors = {};
const allStrings = [
{key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},
{key: 'dndread', component: 'core_error'},
{key: 'dndupload', component: 'core_error'},
{key: 'dndunkownfile', component: 'core_error'},
];
window.console.log(allStrings);
const loadedStrings = await getStrings(allStrings);
allStrings.forEach(({key}, index) => {
errors[key] = loadedStrings[index];
});
}
/**
* Start a batch file uploading into the course.
*
* @private
* @param {number} courseId the course id.
* @param {number} sectionId the section id.
* @param {number} sectionNum the section number.
* @param {File} fileInfo the file information object
* @param {HandlerManager} handlerManager the course handler manager
*/
const queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {
let handler;
uploadQueue = await processMonitor.createProcessQueue();
try {
handlerManager.validateFile(fileInfo);
handler = await handlerManager.getFileHandler(fileInfo);
} catch (error) {
uploadQueue.addError(fileInfo.name, error.message);
return;
}
// If we don't have a handler means the user cancel the upload.
if (!handler) {
return;
}
const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);
uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());
};
/**
* Upload a file to the course.
*
* This method will show any necesary modal to handle the request.
*
* @param {number} courseId the course id
* @param {number} sectionId the section id
* @param {number} sectionNum the section number
* @param {Array} files and array of files
*/
export const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {
// Get the course handlers.
let handlerManager;
try {
handlerManager = await loadCourseHandlerManager(courseId);
await loadErrorStrings(courseId);
} catch (error) {
throw error;
}
for (let index = 0; index < files.length; index++) {
const fileInfo = files[index];
await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);
}
};

View File

@ -48,6 +48,7 @@ export default class Component extends DndSection {
LOCKED: 'editinprogress',
RESTRICTIONS: 'restrictions',
PAGEITEM: 'pageitem',
OVERLAYBORDERS: 'overlay-preview-borders',
};
// We need our id to watch specific events.
@ -164,4 +165,22 @@ export default class Component extends DndSection {
this.element.scrollIntoView({block: "nearest"});
}
}
/**
* Overridden version of the component addOverlay async method.
*
* The course index is not compatible with overlay elements.
*/
async addOverlay() {
this.element.classList.add(this.classes.OVERLAYBORDERS);
}
/**
* Overridden version of the component removeOverlay.
*
* The course index is not compatible with overlay elements.
*/
removeOverlay() {
this.element.classList.remove(this.classes.OVERLAYBORDERS);
}
}

View File

@ -0,0 +1,95 @@
<?php
// 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/>.
namespace core_courseformat\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/course/dnduploadlib.php');
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use dndupload_handler;
/**
* Class for exporting a course file handlers.
*
* @package core_courseformat
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class file_handlers extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED),
]
);
}
/**
* Return the list of available file handlers.
*
* @param int $courseid the course id
* @return array of file hanlders.
*/
public static function execute(int $courseid): array {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
$params = external_api::validate_parameters(self::execute_parameters(), [
'courseid' => $courseid,
]);
$courseid = $params['courseid'];
self::validate_context(\context_course::instance($courseid));
$format = course_get_format($courseid);
$course = $format->get_course();
$handler = new dndupload_handler($course, null);
$data = $handler->get_js_data();
return $data->filehandlers ?? [];
}
/**
* Webservice returns.
*
* @return external_multiple_structure
*/
public static function execute_returns(): external_multiple_structure {
return new external_multiple_structure(
new external_single_structure([
'extension' => new external_value(PARAM_TEXT, 'File extension'),
'module' => new external_value(PARAM_TEXT, 'Target module'),
'message' => new external_value(PARAM_TEXT, 'Output message'),
])
);
}
}

View File

@ -50,12 +50,16 @@ class course implements renderable {
* @return stdClass data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
global $CFG;
$format = $this->format;
$course = $format->get_course();
$context = $format->get_context();
// State must represent always the most updated version of the course.
$modinfo = course_modinfo::instance($course);
$url = new moodle_url('/course/view.php', ['id' => $course->id]);
$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes);
$data = (object)[
'id' => $course->id,
@ -66,8 +70,11 @@ class course implements renderable {
'maxsections' => $format->get_max_sections(),
'baseurl' => $url->out(),
'statekey' => course_format::session_cache($course),
'maxbytes' => $maxbytes,
'maxbytestext' => display_size($maxbytes),
];
$sections = $modinfo->get_section_info_all();
foreach ($sections as $section) {
if ($format->is_section_visible($section)) {

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/>.
}}
{{!
@template core_courseformat/fileuploader
Displays the complete course format.
Example context (json):
{
"filename": "course-format.html",
"uploadid": "example",
"handlers": [
{
"module": "resource",
"labelid": "resource_file1",
"message": "Create a ressource",
"selected": true,
"value": 1
},
{
"module": "h5pactivity",
"labelid": "resource_file2",
"message": "Create an H5P activity",
"selected": false,
"value": 2
}
]
}
}}
<p>
{{#str}} actionchoice, moodle, {{filename}}{{/str}}
</p>
<div id="dndupload_handlers{{uploadid}}">
{{#handlers}}
<input
type="radio"
name="handler"
value="{{value}}"
id="{{labelid}}"
{{#selected}} checked="checked" {{/selected}}
/>
<label for="{{labelid}}">{{message}}</label>
<br/>
{{/handlers}}
</div>

View File

@ -0,0 +1,84 @@
<?php
// 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/>.
namespace core_courseformat\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use external_api;
use dndupload_handler;
/**
* Tests for the file_hanlders class.
*
* @package core_course
* @category test
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\external\file_handlers
*/
class file_handlers_test extends \externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void { // phpcs:ignore
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/dnduploadlib.php');
}
/**
* Test the behaviour of get_state::execute().
*
* @covers ::execute
*/
public function test_execute(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']);
$this->setAdminUser();
$result = file_handlers::execute($course->id);
$result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result);
$handlers = new dndupload_handler($course, null);
$expected = $handlers->get_js_data();
$this->assertCount(count($expected->filehandlers), $result);
foreach ($expected->filehandlers as $key => $handler) {
$tocompare = $result[$key];
$this->assertEquals($handler->extension, $tocompare['extension']);
}
}
/**
* Test the behaviour of get_state::execute() in a wrong course.
*
* @covers ::execute
*/
public function test_execute_wrong_course(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']);
$this->setAdminUser();
$this->expectException('dml_missing_record_exception');
$result = file_handlers::execute(-1);
$result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result);
}
}

View File

@ -3116,10 +3116,9 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
'ajaxurl' => $config->resourceurl,
'config' => $config,
)), null, true);
}
// Require various strings for the command toolbox
$PAGE->requires->strings_for_js(array(
// Require various strings for the command toolbox.
$PAGE->requires->strings_for_js(array(
'moveleft',
'deletechecktype',
'deletechecktypename',
@ -3146,22 +3145,23 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
'totopofsection',
), 'moodle');
// Include section-specific strings for formats which support sections.
if (course_format_uses_sections($course->format)) {
$PAGE->requires->strings_for_js(array(
'showfromothers',
'hidefromothers',
), 'format_' . $course->format);
}
// Include section-specific strings for formats which support sections.
if (course_format_uses_sections($course->format)) {
$PAGE->requires->strings_for_js(array(
'showfromothers',
'hidefromothers',
), 'format_' . $course->format);
}
// For confirming resource deletion we need the name of the module in question
foreach ($usedmodules as $module => $modname) {
$PAGE->requires->string_for_js('pluginname', $module);
}
// For confirming resource deletion we need the name of the module in question.
foreach ($usedmodules as $module => $modname) {
$PAGE->requires->string_for_js('pluginname', $module);
}
// Load drag and drop upload AJAX.
require_once($CFG->dirroot.'/course/dnduploadlib.php');
dndupload_add_to_course($course, $enabledmodules);
// Load drag and drop upload AJAX.
require_once($CFG->dirroot.'/course/dnduploadlib.php');
dndupload_add_to_course($course, $enabledmodules);
}
$PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));

View File

@ -228,6 +228,10 @@ $string['dmlparseexception'] = 'Error parsing SQL query';
$string['dmlreadexception'] = 'Error reading from database';
$string['dmltransactionexception'] = 'Database transaction error';
$string['dmlwriteexception'] = 'Error writing to database';
$string['dndmaxbytes'] = 'The file is too large. The maximum size allowed is {$a->size}';
$string['dndread'] = 'Error reading the file';
$string['dndupload'] = 'An unknown error ocurred while uploading the file';
$string['dndunkownfile'] = 'This file type is not supported';
$string['downgradedcore'] = 'ERROR!!! The code you are using is OLDER than the version that made these databases!';
$string['downloadedfilecheckfailed'] = 'Downloaded file check failed';
$string['duplicatefieldname'] = 'Duplicate field name "{$a}" detected';

View File

@ -1745,6 +1745,7 @@ $string['private_files_handler_name'] = 'Email to Private files';
$string['proceed'] = 'Proceed';
$string['profile'] = 'Profile';
$string['profilenotshown'] = 'This profile description will not be shown until this person is enrolled in at least one course.';
$string['progress'] = 'Progress';
$string['publicprofile'] = 'Public profile';
$string['publicsitefileswarning'] = 'Note: files placed here can be accessed by anyone';
$string['publicsitefileswarning2'] = 'Note: Files placed here can be accessed by anyone who knows (or can guess) the URL. For security reasons, it is recommended that any backup files are deleted immediately after restoring them.';

View File

@ -0,0 +1,12 @@
define("core/local/process_monitor/events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.dispatchStateChangedEvent=function(detail,target){void 0===target&&(target=document);target.dispatchEvent(new CustomEvent(eventTypes.processMonitorStateChange,{bubbles:!0,detail:detail}))},_exports.eventTypes=void 0;
/**
* Javascript events for the `process_monitor` module.
*
* @module core/local/process_monitor/events
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
const eventTypes={processMonitorStateChange:"core_editor/contentRestored"};_exports.eventTypes=eventTypes}));
//# sourceMappingURL=events.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"events.min.js","sources":["../../../src/local/process_monitor/events.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 * Javascript events for the `process_monitor` module.\n *\n * @module core/local/process_monitor/events\n * @copyright 2022 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\n/**\n * Events for the `core_editor` subsystem.\n *\n * @constant\n * @property {String} processMonitorStateChange See {@link event:processMonitorStateChange}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the monitor state has changed.\n *\n * @event processMonitorStateChange\n */\n processMonitorStateChange: 'core_editor/contentRestored',\n};\n\n/**\n * Trigger a state changed event.\n *\n * @method dispatchStateChangedEvent\n * @param {Object} detail the full state\n * @param {Object} target the custom event target (document if none provided)\n * @param {Function} target.dispatchEvent the component dispatch event method.\n */\nexport function dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(\n eventTypes.processMonitorStateChange,\n {\n bubbles: true,\n detail: detail,\n }\n ));\n}\n"],"names":["detail","target","undefined","document","dispatchEvent","CustomEvent","eventTypes","processMonitorStateChange","bubbles"],"mappings":"+KA8C0CA,OAAQC,aAC/BC,IAAXD,SACAA,OAASE,UAEbF,OAAOG,cAAc,IAAIC,YACrBC,WAAWC,0BACX,CACIC,SAAS,EACTR,OAAQA;;;;;;;;;MAzBPM,WAAa,CAMtBC,0BAA2B"}

View File

@ -0,0 +1,3 @@
define("core/local/process_monitor/loadingprocess",["exports","core/log"],(function(_exports,_log){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.LoadingProcess=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.LoadingProcess=class{constructor(manager,definition){_defineProperty(this,"processData",null),_defineProperty(this,"extraData",null),_defineProperty(this,"manager",null),_defineProperty(this,"finishedCallback",null),_defineProperty(this,"removedCallback",null),_defineProperty(this,"errorCallback",null),this.manager=manager,this.processData={id:manager.generateProcessId(),name:"",percentage:0,url:null,error:null,finished:!1,...definition},this._dispatch("addProcess",this.processData)}_dispatch(action,params){this.manager.getInitialStatePromise().then((()=>{this.manager.dispatch(action,params)})).catch((()=>{_log.default.error("Cannot update process monitor.")}))}onFinish(callback){this.finishedCallback=callback}onRemove(callback){this.removedCallback=callback}onError(callback){this.errorCallback=callback}setPercentage(percentage){this.processData.percentage=percentage,this._dispatch("updateProcess",this.processData)}setExtraData(extraData){this.extraData=extraData}setError(error){this.processData.error=error,null!==this.errorCallback&&this.errorCallback(this),this.processData.finished=!0,null!==this.finishedCallback&&this.finishedCallback(this),this._dispatch("updateProcess",this.processData)}setName(name){this.processData.name=name,this._dispatch("updateProcess",this.processData)}finish(){this.processData.finished=!0,null!==this.finishedCallback&&this.finishedCallback(this),this._dispatch("updateProcess",this.processData)}remove(){null!==this.removedCallback&&this.removedCallback(this),this._dispatch("removeProcess",this.processData.id)}getData(){return{...this.processData}}get name(){return this.processData.name}get id(){return this.processData.id}get data(){return this.extraData}}}));
//# sourceMappingURL=loadingprocess.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
define("core/local/process_monitor/manager",["exports","core/reactive","core/local/process_monitor/events"],(function(_exports,_reactive,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.manager=void 0;
/**
* The reactive file uploader class.
*
* As all the upload queues are reactive, any plugin can implement its own upload monitor.
*
* @module core/local/process_monitor/manager
* @class ProcessMonitorManager
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ProcessMonitorManager extends _reactive.Reactive{constructor(){var obj,key,value;super(...arguments),value=1,(key="nextId")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}generateProcessId(){return this.nextId++}}const mutations={addProcess:function(stateManager,processData){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.add({...processData}),state.display.show=!0,stateManager.setReadOnly(!0)},removeProcess:function(stateManager,processId){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.delete(processId),0===state.queue.size&&(state.display.show=!1),stateManager.setReadOnly(!0)},updateProcess:function(stateManager,processData){if(void 0===processData.id)throw Error("Missing process ID in process data");const state=stateManager.state;stateManager.setReadOnly(!1);const queueItem=state.queue.get(processData.id);if(!queueItem)throw Error("Unkown process with id ".concat(processData.id));for(const[prop,propValue]of Object.entries(processData))queueItem[prop]=propValue;stateManager.setReadOnly(!0)},setShow:function(stateManager,show){const state=stateManager.state;stateManager.setReadOnly(!1),state.display.show=show,show||this.cleanFinishedProcesses(stateManager),stateManager.setReadOnly(!0)},removeAllProcesses:function(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.forEach((element=>{state.queue.delete(element.id)})),state.display.show=!1,stateManager.setReadOnly(!0)},cleanFinishedProcesses:function(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.forEach((element=>{element.finished&&!element.error&&state.queue.delete(element.id)})),0===state.queue.size&&(state.display.show=!1),stateManager.setReadOnly(!0)}},manager=new ProcessMonitorManager({name:"ProcessMonitor",eventName:_events.eventTypes.processMonitorStateChange,eventDispatch:_events.dispatchStateChangedEvent,mutations:mutations,state:{display:{show:!1},queue:[]}});_exports.manager=manager}));
//# sourceMappingURL=manager.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
define("core/local/process_monitor/monitor",["exports","core/templates","core/reactive","core/local/process_monitor/manager"],(function(_exports,_templates,_reactive,_manager){var obj;
/**
* The file upload monitor component.
*
* @module core/local/process_monitor/monitor
* @class core/local/process_monitor/monitor
* @copyright 2022 Ferran Recio <ferran@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,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};class _default extends _reactive.BaseComponent{create(){this.name="process_monitor",this.selectors={QUEUELIST:'[data-for="process-list"]',CLOSE:'[data-action="hide"]'},this.classes={HIDE:"d-none"}}static init(query,selectors){return new this({element:document.querySelector(query),reactive:_manager.manager,selectors:selectors})}stateReady(state){this._updateMonitor({state:state,element:state.display}),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this._closeMonitor),state.queue.forEach((element=>{this._createListItem({state:state,element:element})}))}getWatchers(){return[{watch:"queue:created",handler:this._createListItem},{watch:"display:updated",handler:this._updateMonitor}]}async _createListItem(_ref){let{element:element}=_ref;const{html:html,js:js}=await _templates.default.renderForPromise("core/local/process_monitor/process",{...element}),target=this.getElement(this.selectors.QUEUELIST);_templates.default.appendNodeContents(target,html,js)}_updateMonitor(_ref2){let{element:element}=_ref2;this.element.classList.toggle(this.classes.HIDE,!0!==element.show)}_closeMonitor(){this.reactive.dispatch("setShow",!1)}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=monitor.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
define("core/local/process_monitor/process",["exports","core/reactive","core/local/process_monitor/manager"],(function(_exports,_reactive,_manager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* The process motnitor's process reactive component.
*
* @module core/local/process_monitor/process
* @class core/local/process_monitor/process
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class _default extends _reactive.BaseComponent{create(){this.name="process_monitor_process",this.selectors={CLOSE:'[data-action="closeProcess"]',ERROR:'[data-for="error"]',PROGRESSBAR:"progress",NAME:'[data-for="name"]'},this.classes={HIDE:"d-none"},this.id=this.element.dataset.id}static init(query,selectors){return new this({element:document.querySelector(query),reactive:_manager.manager,selectors:selectors})}stateReady(state){this._refreshItem({state:state,element:state.queue.get(this.id)}),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this._removeProcess)}getWatchers(){return[{watch:"queue[".concat(this.id,"]:updated"),handler:this._refreshItem},{watch:"queue[".concat(this.id,"]:deleted"),handler:this.remove}]}async _refreshItem(_ref){let{element:element}=_ref;this.getElement(this.selectors.NAME).innerHTML=element.name;const progressbar=this.getElement(this.selectors.PROGRESSBAR);progressbar.classList.toggle(this.classes.HIDE,element.finished),progressbar.value=element.percentage;this.getElement(this.selectors.CLOSE).classList.toggle(this.classes.HIDE,!element.error);const error=this.getElement(this.selectors.ERROR);error.innerHTML=element.error,error.classList.toggle(this.classes.HIDE,!element.error)}_removeProcess(){this.reactive.dispatch("removeProcess",this.id)}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=process.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
define("core/local/process_monitor/processqueue",["exports","core/utils","core/local/process_monitor/loadingprocess","core/log"],(function(_exports,_utils,_loadingprocess,_log){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ProcessQueue=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.ProcessQueue=
/**
* A process queue manager.
*
* Adding process to the queue will guarante process are executed in sequence.
*
* @module core/local/process_monitor/processqueue
* @class ProcessQueue
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class{constructor(manager){_defineProperty(this,"pending",[]),_defineProperty(this,"currentProcess",null),this.manager=manager,this.cleanFinishedProcesses=(0,_utils.debounce)((()=>manager.dispatch("cleanFinishedProcesses")),3e3)}addPending(processName,processor){const process=new _loadingprocess.LoadingProcess(this.manager,{name:processName});process.setExtraData({processor:processor}),process.onFinish((uploadedFile=>{var _this$currentProcess;(null===(_this$currentProcess=this.currentProcess)||void 0===_this$currentProcess?void 0:_this$currentProcess.id)===uploadedFile.id&&this._discardCurrent()})),this.pending.push(process),this._continueProcessing()}addError(processName,errorMessage){new _loadingprocess.LoadingProcess(this.manager,{name:processName}).setError(errorMessage)}_discardCurrent(){this.currentProcess&&(this.currentProcess=null),this.cleanFinishedProcesses(),this._continueProcessing()}_currentProcessor(){return this.currentProcess.data.processor}async _continueProcessing(){if(null===this.currentProcess&&0!==this.pending.length){this.currentProcess=this.pending.shift();try{const processor=this._currentProcessor();await processor(this.currentProcess)}catch(error){this.currentProcess.setError(error.message),_log.default.error(error)}}}}}));
//# sourceMappingURL=processqueue.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"processqueue.min.js","sources":["../../../src/local/process_monitor/processqueue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport {debounce} from 'core/utils';\nimport {LoadingProcess} from 'core/local/process_monitor/loadingprocess';\nimport log from 'core/log';\n\nconst TOASTSTIMER = 3000;\n\n/**\n * A process queue manager.\n *\n * Adding process to the queue will guarante process are executed in sequence.\n *\n * @module core/local/process_monitor/processqueue\n * @class ProcessQueue\n * @copyright 2022 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport class ProcessQueue {\n /** @var {Array} pending the pending queue. */\n pending = [];\n\n /** @var {LoadingProcess} current the current uploading process. */\n currentProcess = null;\n\n /**\n * Class constructor.\n * @param {ProcessMonitorManager} manager the monitor manager\n */\n constructor(manager) {\n this.manager = manager;\n this.cleanFinishedProcesses = debounce(\n () => manager.dispatch('cleanFinishedProcesses'),\n TOASTSTIMER\n );\n }\n\n /**\n * Adds a new pending upload to the queue.\n * @param {String} processName the process name\n * @param {Function} processor the execution function\n */\n addPending(processName, processor) {\n const process = new LoadingProcess(this.manager, {name: processName});\n process.setExtraData({\n processor,\n });\n process.onFinish((uploadedFile) => {\n if (this.currentProcess?.id !== uploadedFile.id) {\n return;\n }\n this._discardCurrent();\n });\n this.pending.push(process);\n this._continueProcessing();\n }\n\n /**\n * Adds a new pending upload to the queue.\n * @param {String} processName the file info\n * @param {String} errorMessage the file processor\n */\n addError(processName, errorMessage) {\n const process = new LoadingProcess(this.manager, {name: processName});\n process.setError(errorMessage);\n }\n\n /**\n * Discard the current process and execute the next one if any.\n */\n _discardCurrent() {\n if (this.currentProcess) {\n this.currentProcess = null;\n }\n this.cleanFinishedProcesses();\n this._continueProcessing();\n }\n\n /**\n * Return the current file uploader.\n * @return {FileUploader}\n */\n _currentProcessor() {\n return this.currentProcess.data.processor;\n }\n\n /**\n * Continue the queue processing if no current process is defined.\n */\n async _continueProcessing() {\n if (this.currentProcess !== null || this.pending.length === 0) {\n return;\n }\n this.currentProcess = this.pending.shift();\n try {\n const processor = this._currentProcessor();\n await processor(this.currentProcess);\n } catch (error) {\n this.currentProcess.setError(error.message);\n log.error(error);\n }\n }\n}\n"],"names":["constructor","manager","cleanFinishedProcesses","dispatch","addPending","processName","processor","process","LoadingProcess","this","name","setExtraData","onFinish","uploadedFile","currentProcess","id","_discardCurrent","pending","push","_continueProcessing","addError","errorMessage","setError","_currentProcessor","data","length","shift","error","message"],"mappings":";;;;;;;;;;;MA0CIA,YAAYC,wCATF,0CAGO,WAORA,QAAUA,aACVC,wBAAyB,oBAC1B,IAAMD,QAAQE,SAAS,2BA1Bf,KAoChBC,WAAWC,YAAaC,iBACdC,QAAU,IAAIC,+BAAeC,KAAKR,QAAS,CAACS,KAAML,cACxDE,QAAQI,aAAa,CACjBL,UAAAA,YAEJC,QAAQK,UAAUC,2EACLC,2EAAgBC,MAAOF,aAAaE,SAGxCC,0BAEJC,QAAQC,KAAKX,cACbY,sBAQTC,SAASf,YAAagB,cACF,IAAIb,+BAAeC,KAAKR,QAAS,CAACS,KAAML,cAChDiB,SAASD,cAMrBL,kBACQP,KAAKK,sBACAA,eAAiB,WAErBZ,8BACAiB,sBAOTI,2BACWd,KAAKK,eAAeU,KAAKlB,yCAOJ,OAAxBG,KAAKK,gBAAmD,IAAxBL,KAAKQ,QAAQQ,aAG5CX,eAAiBL,KAAKQ,QAAQS,kBAEzBpB,UAAYG,KAAKc,0BACjBjB,UAAUG,KAAKK,gBACvB,MAAOa,YACAb,eAAeQ,SAASK,MAAMC,sBAC/BD,MAAMA"}

View File

@ -1,4 +1,4 @@
define("core/local/reactive/basecomponent",["exports","core/templates"],(function(_exports,_templates){var obj;
define("core/local/reactive/basecomponent",["exports","core/templates","core/local/reactive/overlay"],(function(_exports,_templates,_overlay){var obj;
/**
* Reactive UI component base class.
*
@ -8,6 +8,6 @@ define("core/local/reactive/basecomponent",["exports","core/templates"],(functio
* @class core/local/reactive/basecomponent
* @copyright 2020 Ferran Recio <ferran@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,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}},_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}async addOverlay(definition,target){var _definition$classes;this._overlay&&this.removeOverlay(),this._overlay=await(0,_overlay.addOverlay)({content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"},null!=target?target:this.element)}removeOverlay(){this._overlay&&((0,_overlay.removeOverlay)(this._overlay),this._overlay=null)}removeAllOverlays(){(0,_overlay.removeAllOverlays)()}},_exports.default}));
//# sourceMappingURL=basecomponent.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
define("core/local/reactive/overlay",["exports","core/templates","core/prefetch"],(function(_exports,_templates,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Element overlay methods.
*
* This module is used to create overlay information on components. For example
* to generate or destroy file drop-zones.
*
* @module core/local/reactive/overlay
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeOverlay=_exports.removeAllOverlays=_exports.addOverlay=void 0,_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch);_prefetch.default.prefetchTemplate("core/local/reactive/overlay");const selectors_OVERLAY="[data-overlay]",selectors_REPOSITION="[data-overlay-dynamic]",selectors_NAVBAR="nav.navbar.fixed-top";_exports.addOverlay=async(definition,parent)=>{var _definition$classes;definition.content&&"string"!=typeof definition.content&&(definition.content=await definition.content),definition.icon&&"string"!=typeof definition.icon&&(definition.icon=await definition.icon);const data={content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"};let overlay;try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/reactive/overlay",data);_templates.default.appendNodeContents(parent,html,js),overlay=parent.querySelector(selectors_OVERLAY),rePositionPreviewInfoElement(overlay),init()}catch(error){throw error}return overlay};const removeOverlay=overlay=>{var _overlay$dataset;overlay&&overlay.parentNode&&(null!==(_overlay$dataset=overlay.dataset)&&void 0!==_overlay$dataset&&_overlay$dataset.overlayPosition&&delete overlay.parentNode.style.position,overlay.parentNode.removeChild(overlay))};_exports.removeOverlay=removeOverlay;_exports.removeAllOverlays=()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{removeOverlay(overlay)}))};const rePositionPreviewInfoElement=function(overlay){var _overlay$parentNode,_overlay$parentNode$s;if(!overlay)throw new Error("Inexistent overlay element");null!==(_overlay$parentNode=overlay.parentNode)&&void 0!==_overlay$parentNode&&null!==(_overlay$parentNode$s=_overlay$parentNode.style)&&void 0!==_overlay$parentNode$s&&_overlay$parentNode$s.position||(overlay.parentNode.style.position="relative",overlay.dataset.overlayPosition="true");const target=overlay.querySelector(selectors_REPOSITION);if(!target)return;const rect=overlay.getBoundingClientRect(),sectionHeight=parseInt(window.getComputedStyle(overlay).height,10),sectionOffset=rect.top,previewHeight=parseInt(window.getComputedStyle(target).height,10)+2*parseInt(window.getComputedStyle(target).padding,10);let top,bottom;if(sectionOffset<0)if(sectionHeight+sectionOffset>=previewHeight){let offSetTop=0-sectionOffset;const navBar=document.querySelector(selectors_NAVBAR);navBar&&(offSetTop+=navBar.offsetHeight),top=offSetTop+"px",bottom="unset"}else top="unset",bottom=0;else top=0,bottom="unset";target.style.top=top,target.style.bottom=bottom},init=()=>{document.addEventListener("scroll",(()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{rePositionPreviewInfoElement(overlay)}))}),!0)}}));
//# sourceMappingURL=overlay.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,6 @@ define("core/normalise",["exports","jquery"],(function(_exports,_jquery){var obj
* @module core/normalise
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getList=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.getList=nodes=>nodes instanceof HTMLElement?[nodes]:nodes instanceof Array?nodes:nodes instanceof NodeList?Array.from(nodes):nodes instanceof _jquery.default?nodes.get():Array.from(nodes)}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getList=_exports.getFirst=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const getList=nodes=>nodes instanceof HTMLElement?[nodes]:nodes instanceof Array?nodes:nodes instanceof NodeList?Array.from(nodes):nodes instanceof _jquery.default?nodes.get():Array.from(nodes);_exports.getList=getList;_exports.getFirst=nodes=>getList(nodes)[0]}));
//# sourceMappingURL=normalise.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"normalise.min.js","sources":["../src/normalise.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Normalisation helpers.\n *\n * @module core/normalise\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\n\n/**\n * Normalise a list of Nodes into an Array of Nodes.\n *\n * @method getList\n * @param {(Array|jQuery|NodeList|HTMLElement)} nodes\n * @returns {HTMLElement[]}\n */\nexport const getList = nodes => {\n if (nodes instanceof HTMLElement) {\n // A single record to conver to a NodeList.\n return [nodes];\n }\n\n if (nodes instanceof Array) {\n // A single record to conver to a NodeList.\n return nodes;\n }\n\n if (nodes instanceof NodeList) {\n // Already a NodeList.\n return Array.from(nodes);\n }\n\n if (nodes instanceof jQuery) {\n // A jQuery object to a NodeList.\n return nodes.get();\n }\n\n // Fallback to just having a go.\n return Array.from(nodes);\n};\n"],"names":["nodes","HTMLElement","Array","NodeList","from","jQuery","get"],"mappings":";;;;;;;8JAgCuBA,OACfA,iBAAiBC,YAEV,CAACD,OAGRA,iBAAiBE,MAEVF,MAGPA,iBAAiBG,SAEVD,MAAME,KAAKJ,OAGlBA,iBAAiBK,gBAEVL,MAAMM,MAIVJ,MAAME,KAAKJ"}
{"version":3,"file":"normalise.min.js","sources":["../src/normalise.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Normalisation helpers.\n *\n * @module core/normalise\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\n\n/**\n * Normalise a list of Nodes into an Array of Nodes.\n *\n * @method getList\n * @param {(Array|jQuery|NodeList|HTMLElement)} nodes\n * @returns {HTMLElement[]}\n */\nexport const getList = nodes => {\n if (nodes instanceof HTMLElement) {\n // A single record to conver to a NodeList.\n return [nodes];\n }\n\n if (nodes instanceof Array) {\n // A single record to conver to a NodeList.\n return nodes;\n }\n\n if (nodes instanceof NodeList) {\n // Already a NodeList.\n return Array.from(nodes);\n }\n\n if (nodes instanceof jQuery) {\n // A jQuery object to a NodeList.\n return nodes.get();\n }\n\n // Fallback to just having a go.\n return Array.from(nodes);\n};\n\n/**\n * Return the first element in a list of normalised Nodes.\n *\n * @param {Array|jQuery|NodeList|HTMLElement} nodes the unmormalised list of nodes\n * @returns {HTMLElement|undefined} the first list element\n */\nexport const getFirst = nodes => {\n const list = getList(nodes);\n return list[0];\n};\n"],"names":["getList","nodes","HTMLElement","Array","NodeList","from","jQuery","get"],"mappings":";;;;;;;qKAgCaA,QAAUC,OACfA,iBAAiBC,YAEV,CAACD,OAGRA,iBAAiBE,MAEVF,MAGPA,iBAAiBG,SAEVD,MAAME,KAAKJ,OAGlBA,iBAAiBK,gBAEVL,MAAMM,MAIVJ,MAAME,KAAKJ,kDASEA,OACPD,QAAQC,OACT"}

10
lib/amd/build/process_monitor.min.js vendored Normal file
View File

@ -0,0 +1,10 @@
define("core/process_monitor",["exports","core/log","core/local/process_monitor/manager","core/local/process_monitor/loadingprocess","core/local/process_monitor/processqueue","core/templates"],(function(_exports,_log,_manager,_loadingprocess,_processqueue,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Process monitor includer.
*
* @module core/process_monitor
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processMonitor=void 0,_log=_interopRequireDefault(_log),_templates=_interopRequireDefault(_templates);let initialized=!1;const processMonitor={addLoadingProcess:function(definition){this.initProcessMonitor();return new _loadingprocess.LoadingProcess(_manager.manager,definition)},removeAllProcesses:function(){_manager.manager.getInitialStatePromise().then((()=>{_manager.manager.dispatch("removeAllProcesses")})).catch((()=>{_log.default.error("Cannot update process monitor.")}))},initProcessMonitor:async function(){if(initialized)return;initialized=!0;const container=null!==(_document$querySelect=document.querySelector("#page"))&&void 0!==_document$querySelect?_document$querySelect:document.body;var _document$querySelect;if(!document.getElementById("#processMonitor"))try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/process_monitor/monitor",{});_templates.default.appendNodeContents(container,html,js)}catch(error){_log.default.error("Cannot load the process monitor")}},getInitialStatePromise:function(){return _manager.manager.getInitialStatePromise()},createProcessQueue:async function(){processMonitor.initProcessMonitor();const processQueue=new _processqueue.ProcessQueue(_manager.manager);return await processMonitor.getInitialStatePromise(),processQueue}};_exports.processMonitor=processMonitor}));
//# sourceMappingURL=process_monitor.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"process_monitor.min.js","sources":["../src/process_monitor.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 * Process monitor includer.\n *\n * @module core/process_monitor\n * @copyright 2022 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\nimport {manager} from 'core/local/process_monitor/manager';\nimport {LoadingProcess} from 'core/local/process_monitor/loadingprocess';\nimport {ProcessQueue} from 'core/local/process_monitor/processqueue';\nimport Templates from 'core/templates';\n\nlet initialized = false;\n\n/**\n * Get the parent container.\n * @private\n * @return {HTMLelement} the process monitor container.\n */\nconst getParentContainer = () => {\n // The footer pop over depends on the theme.\n return document.querySelector(`#page`) ?? document.body;\n};\n\nexport const processMonitor = {\n /**\n * Adds a new process to the monitor.\n * @param {Object} definition the process definition\n * @param {String} definition.name the process name\n * @param {Number} definition.percentage the current percentage (0 - 100)\n * @param {String} definition.error the error message if any\n * @param {String} definition.url possible link url if any\n * @returns {LoadingProcess} the loading process\n */\n addLoadingProcess: function(definition) {\n this.initProcessMonitor();\n const process = new LoadingProcess(manager, definition);\n return process;\n },\n\n /**\n * Remove all processes form the current monitor.\n */\n removeAllProcesses: function() {\n manager.getInitialStatePromise().then(() => {\n manager.dispatch('removeAllProcesses');\n return;\n }).catch(() => {\n log.error(`Cannot update process monitor.`);\n });\n },\n\n /**\n * Initialize the process monitor.\n */\n initProcessMonitor: async function() {\n if (initialized) {\n return;\n }\n initialized = true;\n const container = getParentContainer();\n if (document.getElementById(`#processMonitor`)) {\n return;\n }\n try {\n const {html, js} = await Templates.renderForPromise('core/local/process_monitor/monitor', {});\n Templates.appendNodeContents(container, html, js);\n } catch (error) {\n log.error(`Cannot load the process monitor`);\n }\n },\n\n /**\n * Return the process monitor initial state promise.\n * @returns {Promise} Promise of the initial state fully loaded\n */\n getInitialStatePromise: function() {\n return manager.getInitialStatePromise();\n },\n\n /**\n * Load the load queue monitor.\n *\n * @return {Promise<ProcessQueue>} when the file uploader is ready to be used.\n */\n createProcessQueue: async function() {\n processMonitor.initProcessMonitor();\n const processQueue = new ProcessQueue(manager);\n await processMonitor.getInitialStatePromise();\n return processQueue;\n }\n};\n"],"names":["initialized","processMonitor","addLoadingProcess","definition","initProcessMonitor","LoadingProcess","manager","removeAllProcesses","getInitialStatePromise","then","dispatch","catch","error","async","container","document","querySelector","body","getElementById","html","js","Templates","renderForPromise","appendNodeContents","createProcessQueue","processQueue","ProcessQueue"],"mappings":";;;;;;;gLA6BIA,aAAc,QAYLC,eAAiB,CAU1BC,kBAAmB,SAASC,iBACnBC,4BACW,IAAIC,+BAAeC,iBAASH,aAOhDI,mBAAoB,4BACRC,yBAAyBC,MAAK,sBAC1BC,SAAS,yBAElBC,OAAM,kBACDC,4CAOZR,mBAAoBS,oBACZb,mBAGJA,aAAc,QACRc,wCAvCHC,SAASC,8EAA0BD,SAASE,KAF5B,8BA0CfF,SAASG,4CAIHC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,qCAAsC,uBAChFC,mBAAmBT,UAAWK,KAAMC,IAChD,MAAOR,oBACDA,2CAQZJ,uBAAwB,kBACbF,iBAAQE,0BAQnBgB,mBAAoBX,iBAChBZ,eAAeG,2BACTqB,aAAe,IAAIC,2BAAapB,+BAChCL,eAAeO,yBACdiB"}

View File

@ -0,0 +1,58 @@
// This file is part of Moodle - http://moodle.org/ //
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript events for the `process_monitor` module.
*
* @module core/local/process_monitor/events
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
/**
* Events for the `core_editor` subsystem.
*
* @constant
* @property {String} processMonitorStateChange See {@link event:processMonitorStateChange}
*/
export const eventTypes = {
/**
* An event triggered when the monitor state has changed.
*
* @event processMonitorStateChange
*/
processMonitorStateChange: 'core_editor/contentRestored',
};
/**
* Trigger a state changed event.
*
* @method dispatchStateChangedEvent
* @param {Object} detail the full state
* @param {Object} target the custom event target (document if none provided)
* @param {Function} target.dispatchEvent the component dispatch event method.
*/
export function dispatchStateChangedEvent(detail, target) {
if (target === undefined) {
target = document;
}
target.dispatchEvent(new CustomEvent(
eventTypes.processMonitorStateChange,
{
bubbles: true,
detail: detail,
}
));
}

View File

@ -0,0 +1,211 @@
// 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/>.
/**
* The process wrapper class.
*
* This module is used to update a process in the process monitor.
*
* @module core/local/process_monitor/loadingprocess
* @class LoadingProcess
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import log from 'core/log';
export class LoadingProcess {
/** @var {Map} editorUpdates the courses pending to be updated. */
processData = null;
/** @var {Object} extraData any extra process information to store. */
extraData = null;
/** @var {ProcessMonitorManager} manager the page monitor. */
manager = null;
/** @var {Function} finishedCallback the finished callback if any. */
finishedCallback = null;
/** @var {Function} removedCallback the removed callback if any. */
removedCallback = null;
/** @var {Function} errorCallback the error callback if any. */
errorCallback = null;
/**
* Class constructor
* @param {ProcessMonitorManager} manager the monitor manager
* @param {Object} definition the process definition data
*/
constructor(manager, definition) {
this.manager = manager;
// Add defaults.
this.processData = {
id: manager.generateProcessId(),
name: '',
percentage: 0,
url: null,
error: null,
finished: false,
...definition,
};
// Create a new entry.
this._dispatch('addProcess', this.processData);
}
/**
* Execute a monitor manager mutation when the state is ready.
*
* @private
* @param {String} action the mutation to dispatch
* @param {*} params the mutaiton params
*/
_dispatch(action, params) {
this.manager.getInitialStatePromise().then(() => {
this.manager.dispatch(action, params);
return;
}).catch(() => {
log.error(`Cannot update process monitor.`);
});
}
/**
* Define a finished process callback function.
* @param {Function} callback the callback function
*/
onFinish(callback) {
this.finishedCallback = callback;
}
/**
* Define a removed from monitor process callback function.
* @param {Function} callback the callback function
*/
onRemove(callback) {
this.removedCallback = callback;
}
/**
* Define a error process callback function.
* @param {Function} callback the callback function
*/
onError(callback) {
this.errorCallback = callback;
}
/**
* Set the process percentage.
* @param {Number} percentage
*/
setPercentage(percentage) {
this.processData.percentage = percentage;
this._dispatch('updateProcess', this.processData);
}
/**
* Stores extra information to the process.
*
* This method is used to add information like the course, the user
* or any other needed information.
*
* @param {Object} extraData any extra process information to store
*/
setExtraData(extraData) {
this.extraData = extraData;
}
/**
* Set the process error string.
*
* Note: set the error message will mark the process as finished.
*
* @param {String} error the string message
*/
setError(error) {
this.processData.error = error;
if (this.errorCallback !== null) {
this.errorCallback(this);
}
this.processData.finished = true;
if (this.finishedCallback !== null) {
this.finishedCallback(this);
}
this._dispatch('updateProcess', this.processData);
}
/**
* Rename the process
* @param {String} name the new process name
*/
setName(name) {
this.processData.name = name;
this._dispatch('updateProcess', this.processData);
}
/**
* Mark the process as finished.
*/
finish() {
this.processData.finished = true;
if (this.finishedCallback !== null) {
this.finishedCallback(this);
}
this._dispatch('updateProcess', this.processData);
}
/**
* Remove the process from the monitor.
*/
remove() {
if (this.removedCallback !== null) {
this.removedCallback(this);
}
this._dispatch('removeProcess', this.processData.id);
}
/**
* Returns the current rpocess data.
* @returns {Object} the process data
*/
getData() {
return {...this.processData};
}
/**
* Return the process name
* @return {String}
*/
get name() {
return this.processData.name;
}
/**
* Return the process internal id
* @return {Number}
*/
get id() {
return this.processData.id;
}
/**
* Return the process extra data.
* @return {*} whatever is in extra data
*/
get data() {
return this.extraData;
}
}

View File

@ -0,0 +1,182 @@
// 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/>.
/**
* The course file uploader.
*
* This module is used to upload files directly into the course.
*
* @module core/local/process_monitor/manager
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {Reactive} from 'core/reactive';
import {eventTypes, dispatchStateChangedEvent} from 'core/local/process_monitor/events';
const initialState = {
display: {
show: false,
},
queue: [],
};
/**
* The reactive file uploader class.
*
* As all the upload queues are reactive, any plugin can implement its own upload monitor.
*
* @module core/local/process_monitor/manager
* @class ProcessMonitorManager
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ProcessMonitorManager extends Reactive {
/**
* The next process id to use.
*
* @attribute nextId
* @type number
* @default 1
* @package
*/
nextId = 1;
/**
* Generate a unique process id.
* @return {number} a generated process Id
*/
generateProcessId() {
return this.nextId++;
}
}
/**
* @var {Object} mutations the monitor mutations.
*/
const mutations = {
/**
* Add a new process to the queue.
*
* @param {StateManager} stateManager the current state manager
* @param {Object} processData the upload id to finish
*/
addProcess: function(stateManager, processData) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.queue.add({...processData});
state.display.show = true;
stateManager.setReadOnly(true);
},
/**
* Remove a process from the queue.
*
* @param {StateManager} stateManager the current state manager
* @param {Number} processId the process id
*/
removeProcess: function(stateManager, processId) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.queue.delete(processId);
if (state.queue.size === 0) {
state.display.show = false;
}
stateManager.setReadOnly(true);
},
/**
* Update a process process to the queue.
*
* @param {StateManager} stateManager the current state manager
* @param {Object} processData the upload id to finish
* @param {Number} processData.id the process id
*/
updateProcess: function(stateManager, processData) {
if (processData.id === undefined) {
throw Error(`Missing process ID in process data`);
}
const state = stateManager.state;
stateManager.setReadOnly(false);
const queueItem = state.queue.get(processData.id);
if (!queueItem) {
throw Error(`Unkown process with id ${processData.id}`);
}
for (const [prop, propValue] of Object.entries(processData)) {
queueItem[prop] = propValue;
}
stateManager.setReadOnly(true);
},
/**
* Set the monitor show attribute.
*
* @param {StateManager} stateManager the current state manager
* @param {Boolean} show the show value
*/
setShow: function(stateManager, show) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.display.show = show;
if (!show) {
this.cleanFinishedProcesses(stateManager);
}
stateManager.setReadOnly(true);
},
/**
* Remove a processes from the queue.
*
* @param {StateManager} stateManager the current state manager
*/
removeAllProcesses: function(stateManager) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.queue.forEach((element) => {
state.queue.delete(element.id);
});
state.display.show = false;
stateManager.setReadOnly(true);
},
/**
* Clean all finished processes.
*
* @param {StateManager} stateManager the current state manager
*/
cleanFinishedProcesses: function(stateManager) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.queue.forEach((element) => {
if (element.finished && !element.error) {
state.queue.delete(element.id);
}
});
if (state.queue.size === 0) {
state.display.show = false;
}
stateManager.setReadOnly(true);
},
};
const manager = new ProcessMonitorManager({
name: `ProcessMonitor`,
eventName: eventTypes.processMonitorStateChange,
eventDispatch: dispatchStateChangedEvent,
mutations: mutations,
state: initialState,
});
export {manager};

View File

@ -0,0 +1,120 @@
// 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/>.
/**
* The file upload monitor component.
*
* @module core/local/process_monitor/monitor
* @class core/local/process_monitor/monitor
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {BaseComponent} from 'core/reactive';
import {manager} from 'core/local/process_monitor/manager';
export default class extends BaseComponent {
/**
* Constructor hook.
*/
create() {
// Optional component name for debugging.
this.name = 'process_monitor';
// Default query selectors.
this.selectors = {
QUEUELIST: `[data-for="process-list"]`,
CLOSE: `[data-action="hide"]`,
};
// Default classes to toggle on refresh.
this.classes = {
HIDE: `d-none`,
};
}
/**
* Static method to create a component instance form the mustache template.
*
* @param {string} query the DOM main element query selector
* @param {object} selectors optional css selector overrides
* @return {this}
*/
static init(query, selectors) {
return new this({
element: document.querySelector(query),
reactive: manager,
selectors,
});
}
/**
* Initial state ready method.
*
* @param {Object} state the initial state
*/
stateReady(state) {
this._updateMonitor({state, element: state.display});
this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._closeMonitor);
state.queue.forEach((element) => {
this._createListItem({state, element});
});
}
/**
* Return the component watchers.
*
* @returns {Array} of watchers
*/
getWatchers() {
return [
// State changes that require to reload some course modules.
{watch: `queue:created`, handler: this._createListItem},
{watch: `display:updated`, handler: this._updateMonitor},
];
}
/**
* Create a monitor item.
*
* @param {object} args the watcher arguments
* @param {object} args.element the item state data
*/
async _createListItem({element}) {
const {html, js} = await Templates.renderForPromise(
'core/local/process_monitor/process',
{...element}
);
const target = this.getElement(this.selectors.QUEUELIST);
Templates.appendNodeContents(target, html, js);
}
/**
* Create a monitor item.
*
* @param {object} args the watcher arguments
* @param {object} args.element the display state data
*/
_updateMonitor({element}) {
this.element.classList.toggle(this.classes.HIDE, element.show !== true);
}
/**
* Close the monitor.
*/
_closeMonitor() {
this.reactive.dispatch('setShow', false);
}
}

View File

@ -0,0 +1,115 @@
// 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/>.
/**
* The process motnitor's process reactive component.
*
* @module core/local/process_monitor/process
* @class core/local/process_monitor/process
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {BaseComponent} from 'core/reactive';
import {manager} from 'core/local/process_monitor/manager';
export default class extends BaseComponent {
/**
* Constructor hook.
*/
create() {
// Optional component name for debugging.
this.name = 'process_monitor_process';
// Default query selectors.
this.selectors = {
CLOSE: `[data-action="closeProcess"]`,
ERROR: `[data-for="error"]`,
PROGRESSBAR: `progress`,
NAME: `[data-for="name"]`,
};
// Default classes to toggle on refresh.
this.classes = {
HIDE: `d-none`,
};
this.id = this.element.dataset.id;
}
/**
* Static method to create a component instance form the mustache template.
*
* @param {string} query the DOM main element query selector
* @param {object} selectors optional css selector overrides
* @return {this}
*/
static init(query, selectors) {
return new this({
element: document.querySelector(query),
reactive: manager,
selectors,
});
}
/**
* Initial state ready method.
*
* @param {Object} state the initial state
*/
stateReady(state) {
this._refreshItem({state, element: state.queue.get(this.id)});
this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._removeProcess);
}
/**
* Return the component watchers.
*
* @returns {Array} of watchers
*/
getWatchers() {
return [
{watch: `queue[${this.id}]:updated`, handler: this._refreshItem},
{watch: `queue[${this.id}]:deleted`, handler: this.remove},
];
}
/**
* Create a monitor item.
*
* @param {object} args the watcher arguments
* @param {object} args.element the item state data
*/
async _refreshItem({element}) {
const name = this.getElement(this.selectors.NAME);
name.innerHTML = element.name;
const progressbar = this.getElement(this.selectors.PROGRESSBAR);
progressbar.classList.toggle(this.classes.HIDE, element.finished);
progressbar.value = element.percentage;
const close = this.getElement(this.selectors.CLOSE);
close.classList.toggle(this.classes.HIDE, !element.error);
const error = this.getElement(this.selectors.ERROR);
error.innerHTML = element.error;
error.classList.toggle(this.classes.HIDE, !element.error);
}
/**
* Close the process.
*/
_removeProcess() {
this.reactive.dispatch('removeProcess', this.id);
}
}

View File

@ -0,0 +1,116 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import {debounce} from 'core/utils';
import {LoadingProcess} from 'core/local/process_monitor/loadingprocess';
import log from 'core/log';
const TOASTSTIMER = 3000;
/**
* A process queue manager.
*
* Adding process to the queue will guarante process are executed in sequence.
*
* @module core/local/process_monitor/processqueue
* @class ProcessQueue
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export class ProcessQueue {
/** @var {Array} pending the pending queue. */
pending = [];
/** @var {LoadingProcess} current the current uploading process. */
currentProcess = null;
/**
* Class constructor.
* @param {ProcessMonitorManager} manager the monitor manager
*/
constructor(manager) {
this.manager = manager;
this.cleanFinishedProcesses = debounce(
() => manager.dispatch('cleanFinishedProcesses'),
TOASTSTIMER
);
}
/**
* Adds a new pending upload to the queue.
* @param {String} processName the process name
* @param {Function} processor the execution function
*/
addPending(processName, processor) {
const process = new LoadingProcess(this.manager, {name: processName});
process.setExtraData({
processor,
});
process.onFinish((uploadedFile) => {
if (this.currentProcess?.id !== uploadedFile.id) {
return;
}
this._discardCurrent();
});
this.pending.push(process);
this._continueProcessing();
}
/**
* Adds a new pending upload to the queue.
* @param {String} processName the file info
* @param {String} errorMessage the file processor
*/
addError(processName, errorMessage) {
const process = new LoadingProcess(this.manager, {name: processName});
process.setError(errorMessage);
}
/**
* Discard the current process and execute the next one if any.
*/
_discardCurrent() {
if (this.currentProcess) {
this.currentProcess = null;
}
this.cleanFinishedProcesses();
this._continueProcessing();
}
/**
* Return the current file uploader.
* @return {FileUploader}
*/
_currentProcessor() {
return this.currentProcess.data.processor;
}
/**
* Continue the queue processing if no current process is defined.
*/
async _continueProcessing() {
if (this.currentProcess !== null || this.pending.length === 0) {
return;
}
this.currentProcess = this.pending.shift();
try {
const processor = this._currentProcessor();
await processor(this.currentProcess);
} catch (error) {
this.currentProcess.setError(error.message);
log.error(error);
}
}
}

View File

@ -14,6 +14,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import Templates from 'core/templates';
import {addOverlay, removeOverlay, removeAllOverlays} from 'core/local/reactive/overlay';
/**
* Reactive UI component base class.
@ -488,4 +489,43 @@ export default class {
getElementLocked(target) {
return target.dataset.locked ?? false;
}
/**
* Adds an overlay to a specific page element.
*
* @param {Object} definition the overlay definition.
* @param {String} definition.content an optional overlay content.
* @param {String} definition.classes an optional CSS classes
* @param {Element} target optional parent object (this.element will be used if none provided)
*/
async addOverlay(definition, target) {
if (this._overlay) {
this.removeOverlay();
}
this._overlay = await addOverlay(
{
content: definition.content,
css: definition.classes ?? 'file-drop-zone',
},
target ?? this.element
);
}
/**
* Remove the current overlay.
*/
removeOverlay() {
if (!this._overlay) {
return;
}
removeOverlay(this._overlay);
this._overlay = null;
}
/**
* Remove all page overlais.
*/
removeAllOverlays() {
removeAllOverlays();
}
}

View File

@ -147,6 +147,8 @@ export default class extends BaseComponent {
// Stores if the droparea is shown or not.
this.dropzonevisible = false;
// Stores if the mouse is over the element or not.
this.ismouseover = false;
}
/**
@ -158,6 +160,15 @@ export default class extends BaseComponent {
return this.classes;
}
/**
* Return the current drop-zone visible of the element.
*
* @returns {boolean} if the dropzone should be visible or not
*/
isDropzoneVisible() {
return this.dropzonevisible;
}
/**
* Initial state ready method.
*
@ -174,6 +185,8 @@ export default class extends BaseComponent {
this.addEventListener(this.element, 'dragleave', this._dragLeave);
this.addEventListener(this.element, 'dragover', this._dragOver);
this.addEventListener(this.element, 'drop', this._drop);
this.addEventListener(this.element, 'mouseover', this._mouseOver);
this.addEventListener(this.element, 'mouseleave', this._mouseLeave);
}
// Configure the elements draggable if the parent component has dragable data.
@ -203,6 +216,20 @@ export default class extends BaseComponent {
}
}
/**
* Mouse over handle.
*/
_mouseOver() {
this.ismouseover = true;
}
/**
* Mouse leave handler.
*/
_mouseLeave() {
this.ismouseover = false;
}
/**
* Drag start event handler.
*
@ -278,6 +305,7 @@ export default class extends BaseComponent {
document.body.classList.remove(this.classes.BODYDRAGGING);
this.element.classList.remove(this.classes.DRAGGING);
this.fullregion?.classList.remove(this.classes.DRAGGING);
this.removeAllOverlays();
// We add the total movement to the event in case the component
// wants to move its absolute position.
@ -339,7 +367,7 @@ export default class extends BaseComponent {
const dropdata = this._processEvent(event);
if (dropdata) {
this.entercount--;
if (this.entercount == 0 && this.dropzonevisible) {
if (this.entercount <= 0 && this.dropzonevisible) {
this.dropzonevisible = false;
this.element.classList.remove(this.classes.DRAGOVER);
this._callParentMethod('hideDropZone', dropdata, event);
@ -363,6 +391,7 @@ export default class extends BaseComponent {
this._callParentMethod('hideDropZone', dropdata, event);
}
this.element.classList.remove(this.classes.DRAGOVER);
this.removeAllOverlays();
this._callParentMethod('drop', dropdata, event);
// An accepted drop resets the initial position.
// Save the starting point.
@ -443,7 +472,12 @@ export default class extends BaseComponent {
* @returns {Object|undefined} with the dragged data (or undefined if none)
*/
_getDropData(event) {
if (this._containsOnlyFiles(event)) {
this._isOnlyFilesDragging = this._containsOnlyFiles(event);
if (this._isOnlyFilesDragging) {
// Check if the reactive instance can provide a files draggable data.
if (this.reactive.getFilesDraggableData !== undefined && typeof this.reactive.getFilesDraggableData === 'function') {
return this.reactive.getFilesDraggableData(event.dataTransfer);
}
return undefined;
}
return activeDropData.get(this.reactive);
@ -455,15 +489,21 @@ export default class extends BaseComponent {
* Files dragging does not generate drop data because they came from outside the page and the component
* must check it before validating the event.
*
* Some browsers like Firefox add extra types to file dragging. To discard the false positives
* a double check is necessary.
*
* @param {Event} event the original event.
* @returns {boolean} if the drag dataTransfers contains files.
*/
_containsOnlyFiles(event) {
if (event.dataTransfer.types && event.dataTransfer.types.length > 0) {
// Chrome drag page images as files. To differentiate a real file from a page
// image we need to check if all the dataTransfers types are files.
return event.dataTransfer.types.every(type => type === 'Files');
if (!event.dataTransfer.types.includes('Files')) {
return false;
}
return false;
return event.dataTransfer.types.every((type) => {
return (type.toLowerCase() != 'text/uri-list'
&& type.toLowerCase() != 'text/html'
&& type.toLowerCase() != 'text/plain'
);
});
}
}

View File

@ -0,0 +1,171 @@
// 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/>.
/**
* Element overlay methods.
*
* This module is used to create overlay information on components. For example
* to generate or destroy file drop-zones.
*
* @module core/local/reactive/overlay
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Prefetch from 'core/prefetch';
// Prefetch the overlay html.
const overlayTemplate = 'core/local/reactive/overlay';
Prefetch.prefetchTemplate(overlayTemplate);
/**
* @var {boolean} isInitialized if the module is capturing the proper page events.
*/
let isInitialized = false;
/**
* @var {Object} isInitialized if the module is capturing the proper page events.
*/
const selectors = {
OVERLAY: "[data-overlay]",
REPOSITION: "[data-overlay-dynamic]",
NAVBAR: "nav.navbar.fixed-top",
};
/**
* Adds an overlay to a specific page element.
*
* @param {Object} definition the overlay definition.
* @param {String|Promise} definition.content an optional overlay content.
* @param {String|Promise} definition.icon an optional icon content.
* @param {String} definition.classes an optional CSS classes
* @param {HTMLElement} parent the parent object
* @return {HTMLElement|undefined} the new page element.
*/
export const addOverlay = async(definition, parent) => {
// Validate non of the passed params is a promise.
if (definition.content && typeof definition.content !== 'string') {
definition.content = await definition.content;
}
if (definition.icon && typeof definition.icon !== 'string') {
definition.icon = await definition.icon;
}
const data = {
content: definition.content,
css: definition.classes ?? 'file-drop-zone',
};
let overlay;
try {
const {html, js} = await Templates.renderForPromise(overlayTemplate, data);
Templates.appendNodeContents(parent, html, js);
overlay = parent.querySelector(selectors.OVERLAY);
rePositionPreviewInfoElement(overlay);
init();
} catch (error) {
throw error;
}
return overlay;
};
/**
* Adds an overlay to a specific page element.
*
* @param {HTMLElement} overlay the parent object
*/
export const removeOverlay = (overlay) => {
if (!overlay || !overlay.parentNode) {
return;
}
// Remove any forced parentNode position.
if (overlay.dataset?.overlayPosition) {
delete overlay.parentNode.style.position;
}
overlay.parentNode.removeChild(overlay);
};
export const removeAllOverlays = () => {
document.querySelectorAll(selectors.OVERLAY).forEach(
(overlay) => {
removeOverlay(overlay);
}
);
};
/**
* Re-position the preview information element by calculating the section position.
*
* @param {Object} overlay the overlay element.
*/
const rePositionPreviewInfoElement = function(overlay) {
if (!overlay) {
throw new Error('Inexistent overlay element');
}
// Add relative position to the parent object.
if (!overlay.parentNode?.style?.position) {
overlay.parentNode.style.position = 'relative';
overlay.dataset.overlayPosition = "true";
}
// Get the element to reposition.
const target = overlay.querySelector(selectors.REPOSITION);
if (!target) {
return;
}
// Get the new bounds.
const rect = overlay.getBoundingClientRect();
const sectionHeight = parseInt(window.getComputedStyle(overlay).height, 10);
const sectionOffset = rect.top;
const previewHeight = parseInt(window.getComputedStyle(target).height, 10) +
(2 * parseInt(window.getComputedStyle(target).padding, 10));
// Calculate the new target position.
let top, bottom;
if (sectionOffset < 0) {
if (sectionHeight + sectionOffset >= previewHeight) {
// We have enough space here, just stick the preview to the top.
let offSetTop = 0 - sectionOffset;
const navBar = document.querySelector(selectors.NAVBAR);
if (navBar) {
offSetTop = offSetTop + navBar.offsetHeight;
}
top = offSetTop + 'px';
bottom = 'unset';
} else {
// We do not have enough space here, just stick the preview to the bottom.
top = 'unset';
bottom = 0;
}
} else {
top = 0;
bottom = 'unset';
}
target.style.top = top;
target.style.bottom = bottom;
};
// Update overlays when the page scrolls.
const init = () => {
if (isInitialized) {
return;
}
// Add scroll events.
document.addEventListener('scroll', () => {
document.querySelectorAll(selectors.OVERLAY).forEach(
(overlay) => {
rePositionPreviewInfoElement(overlay);
}
);
}, true);
};

View File

@ -54,3 +54,14 @@ export const getList = nodes => {
// Fallback to just having a go.
return Array.from(nodes);
};
/**
* Return the first element in a list of normalised Nodes.
*
* @param {Array|jQuery|NodeList|HTMLElement} nodes the unmormalised list of nodes
* @returns {HTMLElement|undefined} the first list element
*/
export const getFirst = nodes => {
const list = getList(nodes);
return list[0];
};

View File

@ -0,0 +1,109 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Process monitor includer.
*
* @module core/process_monitor
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import log from 'core/log';
import {manager} from 'core/local/process_monitor/manager';
import {LoadingProcess} from 'core/local/process_monitor/loadingprocess';
import {ProcessQueue} from 'core/local/process_monitor/processqueue';
import Templates from 'core/templates';
let initialized = false;
/**
* Get the parent container.
* @private
* @return {HTMLelement} the process monitor container.
*/
const getParentContainer = () => {
// The footer pop over depends on the theme.
return document.querySelector(`#page`) ?? document.body;
};
export const processMonitor = {
/**
* Adds a new process to the monitor.
* @param {Object} definition the process definition
* @param {String} definition.name the process name
* @param {Number} definition.percentage the current percentage (0 - 100)
* @param {String} definition.error the error message if any
* @param {String} definition.url possible link url if any
* @returns {LoadingProcess} the loading process
*/
addLoadingProcess: function(definition) {
this.initProcessMonitor();
const process = new LoadingProcess(manager, definition);
return process;
},
/**
* Remove all processes form the current monitor.
*/
removeAllProcesses: function() {
manager.getInitialStatePromise().then(() => {
manager.dispatch('removeAllProcesses');
return;
}).catch(() => {
log.error(`Cannot update process monitor.`);
});
},
/**
* Initialize the process monitor.
*/
initProcessMonitor: async function() {
if (initialized) {
return;
}
initialized = true;
const container = getParentContainer();
if (document.getElementById(`#processMonitor`)) {
return;
}
try {
const {html, js} = await Templates.renderForPromise('core/local/process_monitor/monitor', {});
Templates.appendNodeContents(container, html, js);
} catch (error) {
log.error(`Cannot load the process monitor`);
}
},
/**
* Return the process monitor initial state promise.
* @returns {Promise} Promise of the initial state fully loaded
*/
getInitialStatePromise: function() {
return manager.getInitialStatePromise();
},
/**
* Load the load queue monitor.
*
* @return {Promise<ProcessQueue>} when the file uploader is ready to be used.
*/
createProcessQueue: async function() {
processMonitor.initProcessMonitor();
const processQueue = new ProcessQueue(manager);
await processMonitor.getInitialStatePromise();
return processQueue;
}
};

View File

@ -515,6 +515,12 @@ $functions = array(
'type' => 'read',
'ajax' => true,
),
'core_courseformat_file_handlers' => [
'classname' => 'core_courseformat\external\file_handlers',
'description' => 'Get the current course file hanlders.',
'type' => 'read',
'ajax' => true,
],
'core_courseformat_get_state' => [
'classname' => 'core_courseformat\external\get_state',
'description' => 'Get the current course state.',

View File

@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/local/process_monitor/monitor
Template to render the global reactive debug panel.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
"title": "Some title"
}
}}
<div
id="process-monitor-{{uniqid}}"
class="popover-process-monitor d-none shadow"
>
<div class="modal-header " data-region="header">
<h5 class="modal-title" data-region="title">
{{#title}} {{title}} {{/title}}
{{^title}} {{#str}} progress, core {{/str}} {{/title}}
</h5>
<button
type="button"
class="close"
data-action="hide"
aria-label="{{#str}}closebuttontitle, core{{/str}}"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div data-for="process-list" class="process-list"></div>
</div>
{{#js}}
require(['core/local/process_monitor/monitor'], function(component) {
component.init('#process-monitor-{{uniqid}}');
});
{{/js}}

View File

@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/local/process_monitor/process
Template to render a process inside the process monitor.
Example context (json):
{
"id": 42,
"name": "Sample",
"percentage": 30,
"error": "Something goes wrong"
}
}}
<div
class="queue-process d-flex flex-column p-2"
data-for="queue-process"
data-id="{{id}}"
>
<div class="d-flex flex-row align-items-center">
<div class="p-2 uploadname text-truncate" data-for="name"> {{name}} </div>
<div class="ml-auto p-2 progressbar">
<progress value="{{percentage}}" max="100"></progress>
<button
type="button"
class="d-none close"
data-action="closeProcess"
aria-label="{{#str}}closebuttontitle, core{{/str}}"
>
<span aria-hidden="true">×</span>
</button>
</div>
</div>
<div class="d-none alert alert-danger" role="alert" data-for="error">
{{error}}
</div>
</div>
{{#js}}
require(['core/local/process_monitor/process'], function(component) {
component.init('[data-for="queue-process"][data-id="{{id}}"]');
});
{{/js}}

View File

@ -0,0 +1,36 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/local/reactive/overlay
Template to render the global reactive debug panel.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
"content": "Drop here!",
"icon": "<i class='icon fa fa-question-circle'></i>"
}
}}
<div class="overlay-preview" data-overlay="true">
{{#content}}
<div class="overlay-preview-wrapper" data-overlay-dynamic="true">
<div class="overlay-preview-content">
{{{icon}}}
{{content}}
</div>
</div>
{{/content}}
</div>

View File

@ -48,3 +48,4 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/primarynavigation";
@import "moodle/secondarynavigation";
@import "moodle/tertiarynavigation";
@import "moodle/process-monitor";

View File

@ -2913,6 +2913,41 @@ body.dragging {
// Generic classes reactive components can use.
.overlay-preview {
background-color: rgba($white, .8);
border: 2px dashed $primary;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.overlay-preview-wrapper {
position: absolute;
top: 0;
padding: 2rem;
width: 100%;
}
.overlay-preview-content {
position: relative;
top: 0;
padding: $modal-inner-padding;
margin: 0 auto;
width: 100%;
max-width: 600px;
background-color: $primary;
color: $white;
text-align: center;
font-size: $font-size-lg;
@include border-radius();
}
}
.overlay-preview-borders {
outline: 2px dashed $primary;
}
.waitstate {
display: none;
}

View File

@ -1333,6 +1333,7 @@ $activity-add-hover: theme-color-level('primary', -10) !default;
display: inline-block;
}
// Legacy dndupload classes. Can be removed in 4.4 as part of MDL-77124.
&.dndupload-dropzone {
border: 2px dashed $primary;
padding-left: 2px;

View File

@ -0,0 +1,30 @@
// The popover process monitor.
$popover-process-monitor-right: 2rem !default;
$popover-process-monitor-bottom: 5rem !default;
$popover-process-monitor-max-height: 30vh !default;
$popover-process-monitor-width: 350px !default;
$popover-process-monitor-scroll-bg: $gray-100 !default;
.popover-process-monitor {
position: fixed;
right: $popover-process-monitor-right;
bottom: $popover-process-monitor-bottom;
width: $popover-process-monitor-width;
background-color: $white;
@include border-radius();
border: $border-width solid $border-color;
.process-list {
max-height: $popover-process-monitor-max-height;
overflow: auto;
@include thin-scrolls($popover-process-monitor-scroll-bg);
}
.queue-process {
border-bottom: 1px solid $gray-200;
}
.queue-process:last-child {
border-bottom: 0;
}
}

View File

@ -12231,6 +12231,35 @@ body.dragging .dragging {
visibility: visible;
cursor: move; }
.overlay-preview {
background-color: rgba(255, 255, 255, 0.8);
border: 2px dashed #0f6cbf;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; }
.overlay-preview .overlay-preview-wrapper {
position: absolute;
top: 0;
padding: 2rem;
width: 100%; }
.overlay-preview .overlay-preview-content {
position: relative;
top: 0;
padding: 1rem;
margin: 0 auto;
width: 100%;
max-width: 600px;
background-color: #0f6cbf;
color: #fff;
text-align: center;
font-size: 1.171875rem;
border-radius: 0.5rem; }
.overlay-preview-borders {
outline: 2px dashed #0f6cbf; }
.waitstate {
display: none; }
@ -22045,6 +22074,34 @@ div.editor_atto_toolbar button .icon {
.tertiary-navigation {
display: none; } }
.popover-process-monitor {
position: fixed;
right: 2rem;
bottom: 5rem;
width: 350px;
background-color: #fff;
border-radius: 0.5rem;
border: 1px solid #dee2e6; }
.popover-process-monitor .process-list {
max-height: 30vh;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #6a737b #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar {
width: 12px; }
.popover-process-monitor .process-list::-webkit-scrollbar-track {
background: #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar-thumb {
background-color: #6a737b;
border-radius: 20px;
border: 3px solid #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar-thumb:hover {
background-color: #495057; }
.popover-process-monitor .queue-process {
border-bottom: 1px solid #e9ecef; }
.popover-process-monitor .queue-process:last-child {
border-bottom: 0; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }

View File

@ -12231,6 +12231,35 @@ body.dragging .dragging {
visibility: visible;
cursor: move; }
.overlay-preview {
background-color: rgba(255, 255, 255, 0.8);
border: 2px dashed #0f6cbf;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; }
.overlay-preview .overlay-preview-wrapper {
position: absolute;
top: 0;
padding: 2rem;
width: 100%; }
.overlay-preview .overlay-preview-content {
position: relative;
top: 0;
padding: 1rem;
margin: 0 auto;
width: 100%;
max-width: 600px;
background-color: #0f6cbf;
color: #fff;
text-align: center;
font-size: 1.171875rem;
border-radius: 0.25rem; }
.overlay-preview-borders {
outline: 2px dashed #0f6cbf; }
.waitstate {
display: none; }
@ -21991,6 +22020,34 @@ div.editor_atto_toolbar button .icon {
.tertiary-navigation {
display: none; } }
.popover-process-monitor {
position: fixed;
right: 2rem;
bottom: 5rem;
width: 350px;
background-color: #fff;
border-radius: 0.25rem;
border: 1px solid #dee2e6; }
.popover-process-monitor .process-list {
max-height: 30vh;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #6a737b #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar {
width: 12px; }
.popover-process-monitor .process-list::-webkit-scrollbar-track {
background: #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar-thumb {
background-color: #6a737b;
border-radius: 20px;
border: 3px solid #f8f9fa; }
.popover-process-monitor .process-list::-webkit-scrollbar-thumb:hover {
background-color: #495057; }
.popover-process-monitor .queue-process {
border-bottom: 1px solid #e9ecef; }
.popover-process-monitor .queue-process:last-child {
border-bottom: 0; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2023020300.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2023020300.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.2dev (Build: 20230203)'; // Human-friendly version name