MDL-76432 core_courseformat: migrate file drag and drop
Replace the old course/dragdrop.js file (which was not even minimised) to AMD modules and integrate them to the new reactive course editor. From now on, a file can be drop over any course section, no matter if it is in the course content or in the course index. It will also start using the new process monitor to show the uploading state to the user.
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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(/&/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(/&/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
@@ -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;
|
||||
|
||||
@@ -118,6 +120,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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,6 +230,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.
|
||||
*
|
||||
@@ -235,6 +302,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.
|
||||
*
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
+17
-17
@@ -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));
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user