���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/courseeditor.tar
���ѧ٧ѧ�
mutations.js 0000644 00000044726 15151261417 0007145 0 ustar 00 // 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 ajax from 'core/ajax'; /** * Default mutation manager * * @module core_courseformat/local/courseeditor/mutations * @class core_courseformat/local/courseeditor/mutations * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default class { // All course editor mutations for Moodle 4.0 will be located in this file. /** * Private method to call core_courseformat_update_course webservice. * * @method _callEditWebservice * @param {string} action * @param {number} courseId * @param {array} ids * @param {number} targetSectionId optional target section id (for moving actions) * @param {number} targetCmId optional target cm id (for moving actions) */ async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) { const args = { action, courseid: courseId, ids, }; if (targetSectionId) { args.targetsectionid = targetSectionId; } if (targetCmId) { args.targetcmid = targetCmId; } let ajaxresult = await ajax.call([{ methodname: 'core_courseformat_update_course', args, }])[0]; return JSON.parse(ajaxresult); } /** * Execute a basic section state action. * @param {StateManager} stateManager the current state manager * @param {string} action the action name * @param {array} sectionIds the section ids * @param {number} targetSectionId optional target section id (for moving actions) * @param {number} targetCmId optional target cm id (for moving actions) */ async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) { const course = stateManager.get('course'); this.sectionLock(stateManager, sectionIds, true); const updates = await this._callEditWebservice( action, course.id, sectionIds, targetSectionId, targetCmId ); stateManager.processUpdates(updates); this.sectionLock(stateManager, sectionIds, false); } /** * Execute a basic course module state action. * @param {StateManager} stateManager the current state manager * @param {string} action the action name * @param {array} cmIds the cm ids * @param {number} targetSectionId optional target section id (for moving actions) * @param {number} targetCmId optional target cm id (for moving actions) */ async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) { const course = stateManager.get('course'); this.cmLock(stateManager, cmIds, true); const updates = await this._callEditWebservice( action, course.id, cmIds, targetSectionId, targetCmId ); stateManager.processUpdates(updates); this.cmLock(stateManager, cmIds, false); } /** * Mutation module initialize. * * The reactive instance will execute this method when addMutations or setMutation is invoked. * * @param {StateManager} stateManager the state manager */ init(stateManager) { // Add a method to prepare the fields when some update is comming from the server. stateManager.addUpdateTypes({ prepareFields: this._prepareFields, }); } /** * Add default values to state elements. * * This method is called every time a webservice returns a update state message. * * @param {Object} stateManager the state manager * @param {String} updateName the state element to update * @param {Object} fields the new data * @returns {Object} final fields data */ _prepareFields(stateManager, updateName, fields) { // Any update should unlock the element. fields.locked = false; return fields; } /** * Hides sections. * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids */ async sectionHide(stateManager, sectionIds) { await this._sectionBasicAction(stateManager, 'section_hide', sectionIds); } /** * Show sections. * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids */ async sectionShow(stateManager, sectionIds) { await this._sectionBasicAction(stateManager, 'section_show', sectionIds); } /** * Show cms. * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of cm ids */ async cmShow(stateManager, cmIds) { await this._cmBasicAction(stateManager, 'cm_show', cmIds); } /** * Hide cms. * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of cm ids */ async cmHide(stateManager, cmIds) { await this._cmBasicAction(stateManager, 'cm_hide', cmIds); } /** * Stealth cms. * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of cm ids */ async cmStealth(stateManager, cmIds) { await this._cmBasicAction(stateManager, 'cm_stealth', cmIds); } /** * Move course modules to specific course location. * * Note that one of targetSectionId or targetCmId should be provided in order to identify the * new location: * - targetCmId: the activities will be located avobe the target cm. The targetSectionId * value will be ignored in this case. * - targetSectionId: the activities will be appended to the section. In this case * targetSectionId should not be present. * * @param {StateManager} stateManager the current state manager * @param {array} cmids the list of cm ids to move * @param {number} targetSectionId the target section id * @param {number} targetCmId the target course module id */ async cmMove(stateManager, cmids, targetSectionId, targetCmId) { if (!targetSectionId && !targetCmId) { throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`); } const course = stateManager.get('course'); this.cmLock(stateManager, cmids, true); const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId); stateManager.processUpdates(updates); this.cmLock(stateManager, cmids, false); } /** * Move course modules to specific course location. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids to move * @param {number} targetSectionId the target section id */ async sectionMove(stateManager, sectionIds, targetSectionId) { if (!targetSectionId) { throw new Error(`Mutation sectionMove requires targetSectionId`); } const course = stateManager.get('course'); this.sectionLock(stateManager, sectionIds, true); const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId); stateManager.processUpdates(updates); this.sectionLock(stateManager, sectionIds, false); } /** * Add a new section to a specific course location. * * @param {StateManager} stateManager the current state manager * @param {number} targetSectionId optional the target section id */ async addSection(stateManager, targetSectionId) { if (!targetSectionId) { targetSectionId = 0; } const course = stateManager.get('course'); const updates = await this._callEditWebservice('section_add', course.id, [], targetSectionId); stateManager.processUpdates(updates); } /** * Delete sections. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of course modules ids */ async sectionDelete(stateManager, sectionIds) { const course = stateManager.get('course'); const updates = await this._callEditWebservice('section_delete', course.id, sectionIds); stateManager.processUpdates(updates); } /** * Mark or unmark course modules as dragging. * * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of course modules ids * @param {bool} dragValue the new dragging value */ cmDrag(stateManager, cmIds, dragValue) { this.setPageItem(stateManager); this._setElementsValue(stateManager, 'cm', cmIds, 'dragging', dragValue); } /** * Mark or unmark course sections as dragging. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids * @param {bool} dragValue the new dragging value */ sectionDrag(stateManager, sectionIds, dragValue) { this.setPageItem(stateManager); this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue); } /** * Mark or unmark course modules as complete. * * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of course modules ids * @param {bool} complete the new completion value */ cmCompletion(stateManager, cmIds, complete) { const newValue = (complete) ? 1 : 0; this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue); } /** * Move cms to the right: indent = 1. * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of cm ids */ async cmMoveRight(stateManager, cmIds) { await this._cmBasicAction(stateManager, 'cm_moveright', cmIds); } /** * Move cms to the left: indent = 0. * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of cm ids */ async cmMoveLeft(stateManager, cmIds) { await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds); } /** * Lock or unlock course modules. * * @param {StateManager} stateManager the current state manager * @param {array} cmIds the list of course modules ids * @param {bool} lockValue the new locked value */ cmLock(stateManager, cmIds, lockValue) { this._setElementsValue(stateManager, 'cm', cmIds, 'locked', lockValue); } /** * Lock or unlock course sections. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the list of section ids * @param {bool} lockValue the new locked value */ sectionLock(stateManager, sectionIds, lockValue) { this._setElementsValue(stateManager, 'section', sectionIds, 'locked', lockValue); } _setElementsValue(stateManager, name, ids, fieldName, newValue) { stateManager.setReadOnly(false); ids.forEach((id) => { const element = stateManager.get(name, id); if (element) { element[fieldName] = newValue; } }); stateManager.setReadOnly(true); } /** * Set the page current item. * * Only one element of the course state can be the page item at a time. * * There are several actions that can alter the page current item. For example, when the user is in an activity * page, the page item is always the activity one. However, in a course page, when the user scrolls to an element, * this element get the page item. * * If the page item is static means that it is not meant to change. This is important because * static page items has some special logic. For example, if a cm is the static page item * and it is inside a collapsed section, the course index will expand the section to make it visible. * * @param {StateManager} stateManager the current state manager * @param {String|undefined} type the element type (section or cm). Undefined will remove the current page item. * @param {Number|undefined} id the element id * @param {boolean|undefined} isStatic if the page item is static */ setPageItem(stateManager, type, id, isStatic) { let newPageItem; if (type !== undefined) { newPageItem = stateManager.get(type, id); if (!newPageItem) { return; } } stateManager.setReadOnly(false); // Remove the current page item. const course = stateManager.get('course'); course.pageItem = null; // Save the new page item. if (newPageItem) { course.pageItem = { id, type, sectionId: (type == 'section') ? newPageItem.id : newPageItem.sectionid, isStatic, }; } stateManager.setReadOnly(true); } /** * Unlock all course elements. * * @param {StateManager} stateManager the current state manager */ unlockAll(stateManager) { const state = stateManager.state; stateManager.setReadOnly(false); state.section.forEach((section) => { section.locked = false; }); state.cm.forEach((cm) => { cm.locked = false; }); stateManager.setReadOnly(true); } /** * Update the course index collapsed attribute of some sections. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the affected section ids * @param {boolean} collapsed the new collapsed value */ async sectionIndexCollapsed(stateManager, sectionIds, collapsed) { const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed); if (!collapsedIds) { return; } const course = stateManager.get('course'); await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds); } /** * Update the course content collapsed attribute of some sections. * * @param {StateManager} stateManager the current state manager * @param {array} sectionIds the affected section ids * @param {boolean} collapsed the new collapsed value */ async sectionContentCollapsed(stateManager, sectionIds, collapsed) { const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed); if (!collapsedIds) { return; } const course = stateManager.get('course'); await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds); } /** * Private batch update for a section preference attribute. * * @param {StateManager} stateManager the current state manager * @param {string} preferenceName the preference name * @param {array} sectionIds the affected section ids * @param {boolean} preferenceValue the new preferenceValue value * @return {Number[]|null} sections ids with the preference value true or null if no update is required */ _updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) { stateManager.setReadOnly(false); const affectedSections = new Set(); // Check if we need to update preferences. sectionIds.forEach(sectionId => { const section = stateManager.get('section', sectionId); if (section === undefined) { return null; } const newValue = preferenceValue ?? section[preferenceName]; if (section[preferenceName] != newValue) { section[preferenceName] = newValue; affectedSections.add(section.id); } }); stateManager.setReadOnly(true); if (affectedSections.size == 0) { return null; } // Get all collapsed section ids. const collapsedSectionIds = []; const state = stateManager.state; state.section.forEach(section => { if (section[preferenceName]) { collapsedSectionIds.push(section.id); } }); return collapsedSectionIds; } /** * Get updated state data related to some cm ids. * * @method cmState * @param {StateManager} stateManager the current state * @param {array} cmids the list of cm ids to update */ async cmState(stateManager, cmids) { this.cmLock(stateManager, cmids, true); const course = stateManager.get('course'); const updates = await this._callEditWebservice('cm_state', course.id, cmids); stateManager.processUpdates(updates); this.cmLock(stateManager, cmids, false); } /** * Get updated state data related to some section ids. * * @method sectionState * @param {StateManager} stateManager the current state * @param {array} sectionIds the list of section ids to update */ async sectionState(stateManager, sectionIds) { this.sectionLock(stateManager, sectionIds, true); const course = stateManager.get('course'); const updates = await this._callEditWebservice('section_state', course.id, sectionIds); stateManager.processUpdates(updates); this.sectionLock(stateManager, sectionIds, false); } /** * Get the full updated state data of the course. * * @param {StateManager} stateManager the current state */ async courseState(stateManager) { const course = stateManager.get('course'); const updates = await this._callEditWebservice('course_state', course.id); stateManager.processUpdates(updates); } } contenttree.js 0000644 00000014630 15151261417 0007443 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course index keyboard navigation and aria-tree compatibility. * * Node tree and bootstrap collapsibles don't use the same HTML structure. However, * all keybindings and logic is compatible. This class translate the primitive opetations * to a bootstrap collapsible structure. * * @module core_courseformat/local/courseindex/keyboardnav * @class core_courseformat/local/courseindex/keyboardnav * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // The core/tree uses jQuery to expand all nodes. import jQuery from 'jquery'; import Tree from 'core/tree'; import {getList} from 'core/normalise'; export default class extends Tree { /** * Setup the core/tree keyboard navigation. * * @param {Element|undefined} mainElement an alternative main element in case it is not from the parent component * @param {Object|undefined} selectors alternative selectors * @param {boolean} preventcache if the elements cache must be disabled. */ constructor(mainElement, selectors, preventcache) { // Init this value with the parent DOM element. super(mainElement); // Get selectors from parent. this.selectors = { SECTION: selectors.SECTION, TOGGLER: selectors.TOGGLER, COLLAPSE: selectors.COLLAPSE, ENTER: selectors.ENTER ?? selectors.TOGGLER, }; // The core/tree library saves the visible elements cache inside the main tree node. // However, in edit mode content can change suddenly so we need to refresh caches when needed. if (preventcache) { this._getVisibleItems = this.getVisibleItems; this.getVisibleItems = () => { this.refreshVisibleItemsCache(); return this._getVisibleItems(); }; } // All jQuery events can be replaced when MDL-71979 is integrated. this.treeRoot.on('hidden.bs.collapse shown.bs.collapse', () => { this.refreshVisibleItemsCache(); }); // Register a custom callback for pressing enter key. this.registerEnterCallback(this.enterCallback.bind(this)); } /** * Return the current active node. * * @return {Element|undefined} the active item if any */ getActiveItem() { const activeItem = this.treeRoot.data('activeItem'); if (activeItem) { return getList(activeItem)[0]; } return undefined; } /** * Handle enter key on a collpasible node. * * @param {JQuery} jQueryItem the jQuery object */ enterCallback(jQueryItem) { const item = getList(jQueryItem)[0]; if (this.isGroupItem(jQueryItem)) { // Group elements is like clicking a topic but without loosing the focus. const enter = item.querySelector(this.selectors.ENTER); if (enter.getAttribute('href') !== '#') { window.location.href = enter.getAttribute('href'); } enter.click(); } else { // Activity links just follow the link href. const link = item.querySelector('a'); if (link.getAttribute('href') !== '#') { window.location.href = link.getAttribute('href'); } else { link.click(); } return; } } /** * Handle an item click. * * @param {Event} event the click event * @param {jQuery} jQueryItem the item clicked */ handleItemClick(event, jQueryItem) { const isChevron = event.target.closest(this.selectors.COLLAPSE); // Only chevron clicks toogle the sections always. if (isChevron) { super.handleItemClick(event, jQueryItem); return; } // This is a title or activity name click. jQueryItem.focus(); if (this.isGroupItem(jQueryItem)) { this.expandGroup(jQueryItem); } } /** * Check if a gorup item is collapsed. * * @param {JQuery} jQueryItem the jQuery object * @returns {boolean} if the element is collapsed */ isGroupCollapsed(jQueryItem) { const item = getList(jQueryItem)[0]; const toggler = item.querySelector(`[aria-expanded]`); return toggler.getAttribute('aria-expanded') === 'false'; } /** * Toggle a group item. * * @param {JQuery} item the jQuery object */ toggleGroup(item) { // All jQuery in this segment of code can be replaced when MDL-71979 is integrated. const toggler = item.find(this.selectors.COLLAPSE); let collapsibleId = toggler.data('target') ?? toggler.attr('href'); if (!collapsibleId) { return; } collapsibleId = collapsibleId.replace('#', ''); // Bootstrap 4 uses jQuery to interact with collapsibles. const collapsible = jQuery(`#${collapsibleId}`); if (collapsible.length) { jQuery(`#${collapsibleId}`).collapse('toggle'); } } /** * Expand a group item. * * @param {JQuery} item the jQuery object */ expandGroup(item) { if (this.isGroupCollapsed(item)) { this.toggleGroup(item); } } /** * Collpase a group item. * * @param {JQuery} item the jQuery object */ collapseGroup(item) { if (!this.isGroupCollapsed(item)) { this.toggleGroup(item); } } /** * Expand all groups. */ expandAllGroups() { const togglers = getList(this.treeRoot)[0].querySelectorAll(this.selectors.SECTION); togglers.forEach(item => { this.expandGroup(jQuery(item)); }); } } dndcmitem.js 0000644 00000007406 15151261417 0007060 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course index cm component. * * This component is used to control specific course modules interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndcmitem * @class core_courseformat/local/courseeditor/dndcmitem * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {BaseComponent, DragDrop} from 'core/reactive'; export default class extends BaseComponent { /** * Configure the component drag and drop. * * @param {number} cmid course module id */ configDragDrop(cmid) { this.id = cmid; // Drag and drop is only available for components compatible course formats. if (this.reactive.isEditing && this.reactive.supportComponents) { // Init element drag and drop. this.dragdrop = new DragDrop(this); // Save dropzone classes. this.classes = this.dragdrop.getClasses(); } } /** * Remove all subcomponents dependencies. */ destroy() { if (this.dragdrop !== undefined) { this.dragdrop.unregister(); } } // Drag and drop methods. /** * The element drop start hook. * * @param {Object} dropdata the dropdata */ dragStart(dropdata) { this.reactive.dispatch('cmDrag', [dropdata.id], true); } /** * The element drop end hook. * * @param {Object} dropdata the dropdata */ dragEnd(dropdata) { this.reactive.dispatch('cmDrag', [dropdata.id], false); } /** * Get the draggable data of this component. * * @returns {Object} exported course module drop data */ getDraggableData() { const exporter = this.reactive.getExporter(); return exporter.cmDraggableData(this.reactive.state, this.id); } /** * Validate if the drop data can be dropped over the component. * * @param {Object} dropdata the exported drop data. * @returns {boolean} */ validateDropData(dropdata) { return dropdata?.type === 'cm'; } /** * Display the component dropzone. * * @param {Object} dropdata the accepted drop data */ showDropZone(dropdata) { // If we are the next cmid of the dragged element we accept the drop because otherwise it // will get captured by the section. However, we won't trigger any mutation. if (dropdata.nextcmid != this.id && dropdata.id != this.id) { this.element.classList.add(this.classes.DROPUP); } } /** * Hide the component dropzone. */ hideDropZone() { this.element.classList.remove(this.classes.DROPUP); } /** * Drop event handler. * * @param {Object} dropdata the accepted drop data */ drop(dropdata) { // Call the move mutation if necessary. if (dropdata.id != this.id && dropdata.nextcmid != this.id) { this.reactive.dispatch('cmMove', [dropdata.id], null, this.id); } } } dndsectionitem.js 0000644 00000010341 15151261417 0010115 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course index section title draggable component. * * This component is used to control specific course section interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndsectionitem * @class core_courseformat/local/courseeditor/dndsectionitem * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {BaseComponent, DragDrop} from 'core/reactive'; export default class extends BaseComponent { /** * Initial state ready method. * * @param {number} sectionid the section id * @param {Object} state the initial state * @param {Element} fullregion the complete section region to mark as dragged */ configDragDrop(sectionid, state, fullregion) { this.id = sectionid; if (this.section === undefined) { this.section = state.section.get(this.id); } if (this.course === undefined) { this.course = state.course; } // Prevent topic zero from being draggable. if (this.section.number > 0) { this.getDraggableData = this._getDraggableData; } this.fullregion = fullregion; // Drag and drop is only available for components compatible course formats. if (this.reactive.isEditing && this.reactive.supportComponents) { // Init the dropzone. this.dragdrop = new DragDrop(this); // Save dropzone classes. this.classes = this.dragdrop.getClasses(); } } /** * Remove all subcomponents dependencies. */ destroy() { if (this.dragdrop !== undefined) { this.dragdrop.unregister(); } } // Drag and drop methods. /** * The element drop start hook. * * @param {Object} dropdata the dropdata */ dragStart(dropdata) { this.reactive.dispatch('sectionDrag', [dropdata.id], true); } /** * The element end start hook. * * @param {Object} dropdata the dropdata */ dragEnd(dropdata) { this.reactive.dispatch('sectionDrag', [dropdata.id], false); } /** * Get the draggable data of this component. * * @returns {Object} exported course module drop data */ _getDraggableData() { const exporter = this.reactive.getExporter(); return exporter.sectionDraggableData(this.reactive.state, this.id); } /** * Validate if the drop data can be dropped over the component. * * @param {Object} dropdata the exported drop data. * @returns {boolean} */ validateDropData(dropdata) { // Course module validation. if (dropdata?.type === 'cm') { // The first section element is already there so we can ignore it. const firstcmid = this.section?.cmlist[0]; return dropdata.id !== firstcmid; } return false; } /** * Display the component dropzone. */ showDropZone() { this.element.classList.add(this.classes.DROPZONE); } /** * Hide the component dropzone. */ hideDropZone() { this.element.classList.remove(this.classes.DROPZONE); } /** * Drop event handler. * * @param {Object} dropdata the accepted drop data */ drop(dropdata) { // Call the move mutation. if (dropdata.type == 'cm') { this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]); } } } dndsection.js 0000644 00000011762 15151261417 0007246 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course index section component. * * This component is used to control specific course section interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndsection * @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 */ import {BaseComponent, DragDrop} from 'core/reactive'; export default class extends BaseComponent { /** * Save some values form the state. * * @param {Object} state the current state */ configState(state) { this.id = this.element.dataset.id; this.section = state.section.get(this.id); this.course = state.course; } /** * Register state values and the drag and drop subcomponent. * * @param {BaseComponent} sectionitem section item component */ configDragDrop(sectionitem) { // Drag and drop is only available for components compatible course formats. if (this.reactive.isEditing && this.reactive.supportComponents) { // Init the inner dragable element. this.sectionitem = sectionitem; // Init the dropzone. this.dragdrop = new DragDrop(this); // Save dropzone classes. this.classes = this.dragdrop.getClasses(); } } /** * Remove all subcomponents dependencies. */ destroy() { if (this.sectionitem !== undefined) { this.sectionitem.unregister(); } if (this.dragdrop !== undefined) { this.dragdrop.unregister(); } } /** * Get the last CM element of that section. * * @returns {element|null} the las course module element of the section. */ getLastCm() { return null; } // Drag and drop methods. /** * The element drop start hook. * * @param {Object} dropdata the dropdata */ dragStart(dropdata) { this.reactive.dispatch('sectionDrag', [dropdata.id], true); } /** * The element drop end hook. * * @param {Object} dropdata the dropdata */ dragEnd(dropdata) { this.reactive.dispatch('sectionDrag', [dropdata.id], false); } /** * Validate if the drop data can be dropped over the component. * * @param {Object} dropdata the exported drop data. * @returns {boolean} */ validateDropData(dropdata) { // We accept any course module. if (dropdata?.type === 'cm') { return true; } // We accept any section but the section 0 or ourself if (dropdata?.type === 'section') { const sectionzeroid = this.course.sectionlist[0]; return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid; } return false; } /** * Display the component dropzone. * * @param {Object} dropdata the accepted drop data */ showDropZone(dropdata) { if (dropdata.type == 'cm') { this.getLastCm()?.classList.add(this.classes.DROPDOWN); } if (dropdata.type == 'section') { // The relative move of section depends on the section number. if (this.section.number > dropdata.number) { this.element.classList.remove(this.classes.DROPUP); this.element.classList.add(this.classes.DROPDOWN); } else { this.element.classList.add(this.classes.DROPUP); this.element.classList.remove(this.classes.DROPDOWN); } } } /** * Hide the component dropzone. */ hideDropZone() { this.getLastCm()?.classList.remove(this.classes.DROPDOWN); this.element.classList.remove(this.classes.DROPUP); this.element.classList.remove(this.classes.DROPDOWN); } /** * Drop event handler. * * @param {Object} dropdata the accepted drop data */ drop(dropdata) { // Call the move mutation. if (dropdata.type == 'cm') { this.reactive.dispatch('cmMove', [dropdata.id], this.id); } if (dropdata.type == 'section') { this.reactive.dispatch('sectionMove', [dropdata.id], this.id); } } } exporter.js 0000644 00000014562 15151261417 0006765 0 ustar 00 // 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/>. /** * Module to export parts of the state and transform them to be used in templates * and as draggable data. * * @module core_courseformat/local/courseeditor/exporter * @class core_courseformat/local/courseeditor/exporter * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default class { /** * Class constructor. * * @param {CourseEditor} reactive the course editor object */ constructor(reactive) { this.reactive = reactive; // Completions states are defined in lib/completionlib.php. There are 4 different completion // state values, however, the course index uses the same state for complete and complete_pass. // This is the reason why completed appears twice in the array. this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail']; } /** * Generate the course export data from the state. * * @param {Object} state the current state. * @returns {Object} */ course(state) { // Collect section information from the state. const data = { sections: [], editmode: this.reactive.isEditing, highlighted: state.course.highlighted ?? '', }; const sectionlist = state.course.sectionlist ?? []; sectionlist.forEach(sectionid => { const sectioninfo = state.section.get(sectionid) ?? {}; const section = this.section(state, sectioninfo); data.sections.push(section); }); data.hassections = (data.sections.length != 0); return data; } /** * Generate a section export data from the state. * * @param {Object} state the current state. * @param {Object} sectioninfo the section state data. * @returns {Object} */ section(state, sectioninfo) { const section = { ...sectioninfo, highlighted: state.course.highlighted ?? '', cms: [], }; const cmlist = sectioninfo.cmlist ?? []; cmlist.forEach(cmid => { const cminfo = state.cm.get(cmid); const cm = this.cm(state, cminfo); section.cms.push(cm); }); section.hascms = (section.cms.length != 0); return section; } /** * Generate a cm export data from the state. * * @param {Object} state the current state. * @param {Object} cminfo the course module state data. * @returns {Object} */ cm(state, cminfo) { const cm = { ...cminfo, isactive: false, }; return cm; } /** * Generate a dragable cm data structure. * * This method is used by any draggable course module element to generate drop data * for its reactive/dragdrop instance. * * @param {*} state the state object * @param {*} cmid the cours emodule id * @returns {Object|null} */ cmDraggableData(state, cmid) { const cminfo = state.cm.get(cmid); if (!cminfo) { return null; } // Drop an activity over the next activity is the same as doing anything. let nextcmid; const section = state.section.get(cminfo.sectionid); const currentindex = section?.cmlist.indexOf(cminfo.id); if (currentindex !== undefined) { nextcmid = section?.cmlist[currentindex + 1]; } return { type: 'cm', id: cminfo.id, name: cminfo.name, sectionid: cminfo.sectionid, nextcmid, }; } /** * Generate a dragable cm data structure. * * This method is used by any draggable section element to generate drop data * for its reactive/dragdrop instance. * * @param {*} state the state object * @param {*} sectionid the cours section id * @returns {Object|null} */ sectionDraggableData(state, sectionid) { const sectioninfo = state.section.get(sectionid); if (!sectioninfo) { return null; } return { type: 'section', id: sectioninfo.id, name: sectioninfo.name, number: sectioninfo.number, }; } /** * Generate a compoetion export data from the cm element. * * @param {Object} state the current state. * @param {Object} cminfo the course module state data. * @returns {Object} */ cmCompletion(state, cminfo) { const data = { statename: '', state: 'NaN', }; if (cminfo.completionstate !== undefined) { data.state = cminfo.completionstate; data.hasstate = true; const statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN'; data[`is${statename}`] = true; } return data; } /** * Return a sorted list of all sections and cms items in the state. * * @param {Object} state the current state. * @returns {Array} all sections and cms items in the state. */ allItemsArray(state) { const items = []; const sectionlist = state.course.sectionlist ?? []; // Add sections. sectionlist.forEach(sectionid => { const sectioninfo = state.section.get(sectionid); items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl}); // Add cms. const cmlist = sectioninfo.cmlist ?? []; cmlist.forEach(cmid => { const cminfo = state.cm.get(cmid); items.push({type: 'cm', id: cminfo.id, url: cminfo.url}); }); }); return items; } } courseeditor.js 0000644 00000020622 15151261417 0007616 0 ustar 00 // 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 {Reactive} from 'core/reactive'; import notification from 'core/notification'; import Exporter from 'core_courseformat/local/courseeditor/exporter'; import log from 'core/log'; import ajax from 'core/ajax'; import * as Storage from 'core/sessionstorage'; /** * Main course editor module. * * All formats can register new components on this object to create new reactive * UI components that watch the current course state. * * @module core_courseformat/local/courseeditor/courseeditor * @class core_courseformat/local/courseeditor/courseeditor * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default class extends Reactive { /** * The current state cache key * * The state cache is considered dirty if the state changes from the last page or * if the page has editing mode on. * * @attribute stateKey * @type number|null * @default 1 * @package */ stateKey = 1; /** * The current page section return * @attribute sectionReturn * @type number * @default 0 */ sectionReturn = 0; /** * Set up the course editor when the page is ready. * * The course can only be loaded once per instance. Otherwise an error is thrown. * * The backend can inform the module of the current state key. This key changes every time some * update in the course affect the current user state. Some examples are: * - The course content has been edited * - The user marks some activity as completed * - The user collapses or uncollapses a section (it is stored as a user preference) * * @param {number} courseId course id * @param {string} serverStateKey the current backend course cache reference */ async loadCourse(courseId, serverStateKey) { if (this.courseId) { throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`); } if (!serverStateKey) { // The server state key is not provided, we use a invalid statekey to force reloading. serverStateKey = `invalidStateKey_${Date.now()}`; } // Default view format setup. this._editing = false; this._supportscomponents = false; this.courseId = courseId; let stateData; const storeStateKey = Storage.get(`course/${courseId}/stateKey`); try { // Check if the backend state key is the same we have in our session storage. if (!this.isEditing && serverStateKey == storeStateKey) { stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`)); } if (!stateData) { stateData = await this.getServerCourseState(); } } catch (error) { log.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"); log.error(error); return; } this.setInitialState(stateData); // In editing mode, the session cache is considered dirty always. if (this.isEditing) { this.stateKey = null; } else { // Check if the last state is the same as the cached one. const newState = JSON.stringify(stateData); const previousState = Storage.get(`course/${courseId}/staticState`); if (previousState !== newState || storeStateKey !== serverStateKey) { Storage.set(`course/${courseId}/staticState`, newState); Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey); } this.stateKey = Storage.get(`course/${courseId}/stateKey`); } } /** * Setup the current view settings * * @param {Object} setup format, page and course settings * @param {boolean} setup.editing if the page is in edit mode * @param {boolean} setup.supportscomponents if the format supports components for content * @param {string} setup.cacherev the backend cached state revision */ setViewFormat(setup) { this._editing = setup.editing ?? false; this._supportscomponents = setup.supportscomponents ?? false; } /** * Load the current course state from the server. * * @returns {Object} the current course state */ async getServerCourseState() { const courseState = await ajax.call([{ methodname: 'core_courseformat_get_state', args: { courseid: this.courseId, } }])[0]; const stateData = JSON.parse(courseState); return { course: {}, section: [], cm: [], ...stateData, }; } /** * Return the current edit mode. * * Components should use this method to check if edit mode is active. * * @return {boolean} if edit is enabled */ get isEditing() { return this._editing ?? false; } /** * Return a data exporter to transform state part into mustache contexts. * * @return {Exporter} the exporter class */ getExporter() { return new Exporter(this); } /** * Return if the current course support components to refresh the content. * * @returns {boolean} if the current content support components */ get supportComponents() { return this._supportscomponents ?? false; } /** * Get a value from the course editor static storage if any. * * The course editor static storage uses the sessionStorage to store values from the * components. This is used to prevent unnecesary template loadings on every page. However, * the storage does not work if no sessionStorage can be used (in debug mode for example), * if the page is in editing mode or if the initial state change from the last page. * * @param {string} key the key to get * @return {boolean|string} the storage value or false if cannot be loaded */ getStorageValue(key) { if (this.isEditing || !this.stateKey) { return false; } const dataJson = Storage.get(`course/${this.courseId}/${key}`); if (!dataJson) { return false; } // Check the stateKey. try { const data = JSON.parse(dataJson); if (data?.stateKey !== this.stateKey) { return false; } return data.value; } catch (error) { return false; } } /** * Stores a value into the course editor static storage if available * * @param {String} key the key to store * @param {*} value the value to store (must be compatible with JSON,stringify) * @returns {boolean} true if the value is stored */ setStorageValue(key, value) { // Values cannot be stored on edit mode. if (this.isEditing) { return false; } const data = { stateKey: this.stateKey, value, }; return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data)); } /** * Dispatch a change in the state. * * Usually reactive modules throw an error directly to the components when something * goes wrong. However, course editor can directly display a notification. * * @method dispatch * @param {mixed} args any number of params the mutation needs. */ async dispatch(...args) { try { await super.dispatch(...args); } catch (error) { // Display error modal. notification.exception(error); // Force unlock all elements. super.dispatch('unlockAll'); } } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�