mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 16:36:37 +02:00
MDL-71779 core_courseformat: reactive add and delete sections
This commit is contained in:
parent
a9d44b0f75
commit
3d2a6eacae
57 changed files with 883 additions and 140 deletions
2
course/amd/build/actions.min.js
vendored
2
course/amd/build/actions.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
course/amd/build/events.min.js
vendored
2
course/amd/build/events.min.js
vendored
|
@ -1,2 +1,2 @@
|
|||
define ("core_course/events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled",stateChanged:"core_course:stateChanged"};return a.default});
|
||||
define ("core_course/events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled",stateChanged:"core_course:stateChanged",sectionRefreshed:"core_course:sectionRefreshed"};return a.default});
|
||||
//# sourceMappingURL=events.min.js.map
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"version":3,"sources":["../src/events.js"],"names":["favourited","unfavorited","manualCompletionToggled","stateChanged"],"mappings":"8IAsBe,CACXA,UAAU,CAAE,wBADD,CAEXC,WAAW,CAAE,yBAFF,CAGXC,uBAAuB,CAAE,qCAHd,CAIXC,YAAY,CAAE,0BAJH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @copyright 2018 Simey Lameze <simey@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n stateChanged: 'core_course:stateChanged',\n};\n"],"file":"events.min.js"}
|
||||
{"version":3,"sources":["../src/events.js"],"names":["favourited","unfavorited","manualCompletionToggled","stateChanged","sectionRefreshed"],"mappings":"8IAsBe,CACXA,UAAU,CAAE,wBADD,CAEXC,WAAW,CAAE,yBAFF,CAGXC,uBAAuB,CAAE,qCAHd,CAIXC,YAAY,CAAE,0BAJH,CAKXC,gBAAgB,CAAE,8BALP,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @copyright 2018 Simey Lameze <simey@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n stateChanged: 'core_course:stateChanged',\n sectionRefreshed: 'core_course:sectionRefreshed',\n};\n"],"file":"events.min.js"}
|
|
@ -21,18 +21,51 @@
|
|||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.3
|
||||
*/
|
||||
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui',
|
||||
'core/modal_factory', 'core/modal_events', 'core/key_codes', 'core/log', 'core_courseformat/courseeditor'],
|
||||
function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes, log, editor) {
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/ajax',
|
||||
'core/templates',
|
||||
'core/notification',
|
||||
'core/str',
|
||||
'core/url',
|
||||
'core/yui',
|
||||
'core/modal_factory',
|
||||
'core/modal_events',
|
||||
'core/key_codes',
|
||||
'core/log',
|
||||
'core_courseformat/courseeditor',
|
||||
'core/event_dispatcher',
|
||||
'core_course/events'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
ajax,
|
||||
templates,
|
||||
notification,
|
||||
str,
|
||||
url,
|
||||
Y,
|
||||
ModalFactory,
|
||||
ModalEvents,
|
||||
KeyCodes,
|
||||
log,
|
||||
editor,
|
||||
EventDispatcher,
|
||||
CourseEvents
|
||||
) {
|
||||
|
||||
// Eventually, core_courseformat/local/content/actions will handle all actions for
|
||||
// component compatible formats and the default actions.js won't be necessary anymore.
|
||||
// Meanwhile, we filter the migrated actions.
|
||||
const componentActions = ['moveSection', 'moveCm'];
|
||||
const componentActions = ['moveSection', 'moveCm', 'addSection', 'deleteSection'];
|
||||
|
||||
// The course reactive instance.
|
||||
const courseeditor = editor.getCurrentCourseEditor();
|
||||
|
||||
// The current course format name (loaded on init).
|
||||
let formatname;
|
||||
|
||||
var CSS = {
|
||||
EDITINPROGRESS: 'editinprogress',
|
||||
SECTIONDRAGGABLE: 'sectiondraggable',
|
||||
|
@ -46,7 +79,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
TOGGLE: '.toggle-display,.dropdown-toggle',
|
||||
SECTIONLI: 'li.section',
|
||||
SECTIONACTIONMENU: '.section_action_menu',
|
||||
ADDSECTIONS: '#changenumsections [data-add-sections]'
|
||||
ADDSECTIONS: '.changenumsections [data-add-sections]'
|
||||
};
|
||||
|
||||
Y.use('moodle-course-coursebase', function() {
|
||||
|
@ -56,6 +89,29 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dispatch event wrapper.
|
||||
*
|
||||
* Old jQuery events will be replaced by native events gradually.
|
||||
*
|
||||
* @method dispatchEvent
|
||||
* @param {String} eventName The name of the event
|
||||
* @param {Object} detail Any additional details to pass into the eveent
|
||||
* @param {Node|HTMLElement} container The point at which to dispatch the event
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.bubbles Whether to bubble up the DOM
|
||||
* @param {Boolean} options.cancelable Whether preventDefault() can be called
|
||||
* @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM boundary
|
||||
* @returns {CustomEvent}
|
||||
*/
|
||||
const dispatchEvent = function(eventName, detail, container, options) {
|
||||
// Most actions still uses jQuery node instead of regular HTMLElement.
|
||||
if (!(container instanceof Element) && container.get !== undefined) {
|
||||
container = container.get(0);
|
||||
}
|
||||
return EventDispatcher.dispatchEvent(eventName, detail, container, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for Y.Moodle.core_course.util.cm.getId
|
||||
*
|
||||
|
@ -238,6 +294,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
foundElement = this;
|
||||
return false; // Returning false in .each() is equivalent to "break;" inside the loop in php.
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return foundElement;
|
||||
};
|
||||
|
@ -316,6 +373,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
* @return {Promise} the refresh promise
|
||||
*/
|
||||
var refreshModule = function(element, cmid, sectionreturn) {
|
||||
|
||||
if (sectionreturn === undefined) {
|
||||
sectionreturn = courseeditor.sectionReturn;
|
||||
}
|
||||
|
||||
const activityElement = $(element);
|
||||
var spinner = addActivitySpinner(activityElement);
|
||||
var promises = ajax.call([{
|
||||
|
@ -336,6 +398,80 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests html for the section via WS core_course_edit_section and updates the section on the course page
|
||||
*
|
||||
* @param {JQuery|Element} element
|
||||
* @param {Number} sectionid
|
||||
* @param {Number} sectionreturn
|
||||
* @return {Promise} the refresh promise
|
||||
*/
|
||||
var refreshSection = function(element, sectionid, sectionreturn) {
|
||||
|
||||
if (sectionreturn === undefined) {
|
||||
sectionreturn = courseeditor.sectionReturn;
|
||||
}
|
||||
|
||||
const sectionElement = $(element);
|
||||
const action = 'refresh';
|
||||
const promises = ajax.call([{
|
||||
methodname: 'core_course_edit_section',
|
||||
args: {id: sectionid, action, sectionreturn},
|
||||
}], true);
|
||||
|
||||
var spinner = addSectionSpinner(sectionElement);
|
||||
return new Promise((resolve, reject) => {
|
||||
$.when.apply($, promises)
|
||||
.done(dataencoded => {
|
||||
|
||||
removeSpinner(sectionElement, spinner);
|
||||
const data = $.parseJSON(dataencoded);
|
||||
|
||||
const newSectionElement = $(data.content);
|
||||
sectionElement.replaceWith(newSectionElement);
|
||||
|
||||
// Init modules menus.
|
||||
$(`${SELECTOR.SECTIONLI}#${sectionid} ${SELECTOR.ACTIVITYLI}`).each(
|
||||
(index, activity) => {
|
||||
initActionMenu(activity.data('id'));
|
||||
}
|
||||
);
|
||||
|
||||
// Trigger event that can be observed by course formats.
|
||||
const event = dispatchEvent(
|
||||
CourseEvents.sectionRefreshed,
|
||||
{
|
||||
ajaxreturn: data,
|
||||
action: action,
|
||||
newSectionElement: newSectionElement.get(0),
|
||||
},
|
||||
newSectionElement
|
||||
);
|
||||
|
||||
if (!event.defaultPrevented) {
|
||||
defaultEditSectionHandler(
|
||||
newSectionElement, $(SELECTOR.SECTIONLI + '#' + sectionid),
|
||||
data,
|
||||
formatname,
|
||||
sectionid
|
||||
);
|
||||
}
|
||||
resolve(data);
|
||||
}).fail(ex => {
|
||||
// Trigger event that can be observed by course formats.
|
||||
const event = dispatchEvent(
|
||||
'coursesectionrefreshfailed',
|
||||
{exception: ex, action: action},
|
||||
sectionElement
|
||||
);
|
||||
if (!event.defaultPrevented) {
|
||||
notification.exception(ex);
|
||||
}
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the delete confirmation to delete a module
|
||||
*
|
||||
|
@ -352,7 +488,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
name: modulename
|
||||
};
|
||||
str.get_strings([
|
||||
{key: 'confirm'},
|
||||
{key: 'confirm', component: 'core'},
|
||||
{key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},
|
||||
{key: 'yes'},
|
||||
{key: 'no'}
|
||||
|
@ -699,6 +835,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
*/
|
||||
initCoursePage: function(courseformat) {
|
||||
|
||||
formatname = courseformat;
|
||||
|
||||
// Add a handler for course module actions.
|
||||
$('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +
|
||||
SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {
|
||||
|
@ -782,6 +920,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
}
|
||||
});
|
||||
|
||||
// Component-based formats don't use modals to create sections.
|
||||
if (courseeditor.supportComponents && componentActions.includes('addSection')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a handler for "Add sections" link to ask for a number of sections to add.
|
||||
str.get_string('numberweeks').done(function(strNumberSections) {
|
||||
var trigger = $(SELECTOR.ADDSECTIONS),
|
||||
|
@ -843,5 +986,6 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
|||
},
|
||||
// Method to refresh a module.
|
||||
refreshModule,
|
||||
refreshSection,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -25,4 +25,5 @@ export default {
|
|||
unfavorited: 'core_course:unfavorited',
|
||||
manualCompletionToggled: 'core_course:manualcompletiontoggled',
|
||||
stateChanged: 'core_course:stateChanged',
|
||||
sectionRefreshed: 'core_course:sectionRefreshed',
|
||||
};
|
||||
|
|
|
@ -96,13 +96,38 @@ M.course_dndupload = {
|
|||
this.add_status_div();
|
||||
}
|
||||
|
||||
// Any change to the course must be applied also to the course state via the courseeditor module.
|
||||
var self = this;
|
||||
require(['core_courseformat/courseeditor'], function(editor) {
|
||||
self.courseeditor = editor.getCurrentCourseEditor();
|
||||
require([
|
||||
'core_courseformat/courseeditor',
|
||||
'core_course/events'
|
||||
], function(
|
||||
Editor,
|
||||
CourseEvents
|
||||
) {
|
||||
// Any change to the course must be applied also to the course state via the courseeditor module.
|
||||
self.courseeditor = Editor.getCurrentCourseEditor();
|
||||
|
||||
// Some formats can add sections without reloading the page.
|
||||
document.querySelector('#' + self.pagecontentid).addEventListener(
|
||||
CourseEvents.sectionRefreshed,
|
||||
self.sectionRefreshed.bind(self)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup Drag and Drop in a section.
|
||||
* @param {CustomEvent} event The custom event
|
||||
*/
|
||||
sectionRefreshed: function(event) {
|
||||
if (event.detail.newSectionElement === undefined) {
|
||||
return;
|
||||
}
|
||||
var element = this.Y.one(event.detail.newSectionElement);
|
||||
this.add_preview_element(element);
|
||||
this.init_events(element);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a div element to tell the user that drag and drop upload
|
||||
* is available (or to explain why it is not available)
|
||||
|
|
2
course/format/amd/build/local/content.min.js
vendored
2
course/format/amd/build/local/content.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
|||
define ("core_courseformat/local/content/section",["exports","core_courseformat/local/content/section/header","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b<arguments.length;b++){c=null!=arguments[b]?arguments[b]:{};if(b%2){f(Object(c),!0).forEach(function(b){h(a,b,c[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(c))}else{f(Object(c)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(c,b))})}}return a}function h(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return a}function i(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function j(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function k(a,b,c){if(b)j(a.prototype,b);if(c)j(a,c);return a}function l(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)m(a,b)}function m(a,b){m=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return m(a,b)}function n(a){return function(){var b=r(a),c;if(q()){var d=r(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return o(this,c)}}function o(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return p(a)}function p(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function q(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function r(a){r=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return r(a)}var s=function(a){l(c,a);var d=n(c);function c(){i(this,c);return d.apply(this,arguments)}k(c,[{key:"create",value:function create(){this.name="content_section";this.selectors={SECTION_ITEM:"[data-for='section_title']",CM:"[data-for=\"cmitem\"]"};this.classes={LOCKED:"editinprogress"};this.id=this.element.dataset.id}},{key:"stateReady",value:function stateReady(a){this.configState(a);if(this.reactive.isEditing&&this.reactive.supportComponents){var c=this.getElement(this.selectors.SECTION_ITEM);if(c){var d=new b.default(g({},this,{element:c,fullregion:this.element}));this.configDragDrop(d)}}}},{key:"getWatchers",value:function getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}},{key:"getLastCm",value:function getLastCm(){var a=this.getElements(this.selectors.CM);if(!a||0===a.length){return null}return a[a.length-1]}},{key:"_refreshSection",value:function _refreshSection(a){var b,c,d=a.element;this.element.classList.toggle(this.classes.DRAGGING,null!==(b=d.dragging)&&void 0!==b?b:!1);this.element.classList.toggle(this.classes.LOCKED,null!==(c=d.locked)&&void 0!==c?c:!1);this.locked=d.locked}}]);return c}(c.default);a.default=s;return a.default});
|
||||
define ("core_courseformat/local/content/section",["exports","core_courseformat/local/content/section/header","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b<arguments.length;b++){c=null!=arguments[b]?arguments[b]:{};if(b%2){f(Object(c),!0).forEach(function(b){h(a,b,c[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(c))}else{f(Object(c)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(c,b))})}}return a}function h(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return a}function i(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function j(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function k(a,b,c){if(b)j(a.prototype,b);if(c)j(a,c);return a}function l(a,b,c){if("undefined"!=typeof Reflect&&Reflect.get){l=Reflect.get}else{l=function(a,b,c){var d=m(a,b);if(!d)return;var e=Object.getOwnPropertyDescriptor(d,b);if(e.get){return e.get.call(c)}return e.value}}return l(a,b,c||a)}function m(a,b){while(!Object.prototype.hasOwnProperty.call(a,b)){a=v(a);if(null===a)break}return a}function n(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)q(a,b)}function q(a,b){q=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return q(a,b)}function r(a){return function(){var b=v(a),c;if(u()){var d=v(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return s(this,c)}}function s(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return t(a)}function t(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function u(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function v(a){v=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return v(a)}var w=function(a){n(c,a);var d=r(c);function c(){i(this,c);return d.apply(this,arguments)}k(c,[{key:"create",value:function create(){this.name="content_section";this.selectors={SECTION_ITEM:"[data-for='section_title']",CM:"[data-for=\"cmitem\"]"};this.classes={LOCKED:"editinprogress"};this.id=this.element.dataset.id}},{key:"stateReady",value:function stateReady(a){this.configState(a);if(this.reactive.isEditing&&this.reactive.supportComponents){var c=this.getElement(this.selectors.SECTION_ITEM);if(c){var d=new b.default(g({},this,{element:c,fullregion:this.element}));this.configDragDrop(d)}}}},{key:"getWatchers",value:function getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}},{key:"validateDropData",value:function validateDropData(a){if("section"===(null===a||void 0===a?void 0:a.type)&&0!=this.reactive.sectionReturn){return!1}return l(v(c.prototype),"validateDropData",this).call(this,a)}},{key:"getLastCm",value:function getLastCm(){var a=this.getElements(this.selectors.CM);if(!a||0===a.length){return null}return a[a.length-1]}},{key:"_refreshSection",value:function _refreshSection(a){var b,c,d=a.element;this.element.classList.toggle(this.classes.DRAGGING,null!==(b=d.dragging)&&void 0!==b?b:!1);this.element.classList.toggle(this.classes.LOCKED,null!==(c=d.locked)&&void 0!==c?c:!1);this.locked=d.locked}}]);return c}(c.default);a.default=w;return a.default});
|
||||
//# sourceMappingURL=section.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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
|||
define ("core_courseformat/local/courseindex/section",["exports","core_courseformat/local/courseindex/sectiontitle","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b<arguments.length;b++){c=null!=arguments[b]?arguments[b]:{};if(b%2){f(Object(c),!0).forEach(function(b){h(a,b,c[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(c))}else{f(Object(c)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(c,b))})}}return a}function h(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return a}function i(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function j(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function k(a,b,c){if(b)j(a.prototype,b);if(c)j(a,c);return a}function l(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)m(a,b)}function m(a,b){m=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return m(a,b)}function n(a){return function(){var b=r(a),c;if(q()){var d=r(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return o(this,c)}}function o(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return p(a)}function p(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function q(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function r(a){r=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return r(a)}var s=function(a){l(c,a);var d=n(c);function c(){i(this,c);return d.apply(this,arguments)}k(c,[{key:"create",value:function 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"};this.id=this.element.dataset.id}},{key:"stateReady",value:function stateReady(a){this.configState(a);if(this.reactive.isEditing&&this.reactive.supportComponents){var c=new b.default(g({},this,{element:this.getElement(this.selectors.SECTION_ITEM),fullregion:this.element}));this.configDragDrop(c)}}},{key:"getWatchers",value:function getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}},{key:"getLastCm",value:function getLastCm(){return this.getElement(this.selectors.CM_LAST)}},{key:"_refreshSection",value:function _refreshSection(a){var b,c,d=a.element,e=this.getElement(this.selectors.SECTION_ITEM);e.classList.toggle(this.classes.SECTIONHIDDEN,!d.visible);this.element.classList.toggle(this.classes.SECTIONCURRENT,d.current);this.element.classList.toggle(this.classes.DRAGGING,null!==(b=d.dragging)&&void 0!==b?b:!1);this.element.classList.toggle(this.classes.LOCKED,null!==(c=d.locked)&&void 0!==c?c:!1);this.locked=d.locked;this.getElement(this.selectors.SECTION_TITLE).innerHTML=d.title}}],[{key:"init",value:function init(a,b){return new c({element:document.getElementById(a),selectors:b})}}]);return c}(c.default);a.default=s;return a.default});
|
||||
define ("core_courseformat/local/courseindex/section",["exports","core_courseformat/local/courseindex/sectiontitle","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b<arguments.length;b++){c=null!=arguments[b]?arguments[b]:{};if(b%2){f(Object(c),!0).forEach(function(b){h(a,b,c[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(c))}else{f(Object(c)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(c,b))})}}return a}function h(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return a}function i(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function j(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function k(a,b,c){if(b)j(a.prototype,b);if(c)j(a,c);return a}function l(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)m(a,b)}function m(a,b){m=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return m(a,b)}function n(a){return function(){var b=r(a),c;if(q()){var d=r(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return o(this,c)}}function o(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return p(a)}function p(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function q(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function r(a){r=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return r(a)}var s=function(a){l(c,a);var d=n(c);function c(){i(this,c);return d.apply(this,arguments)}k(c,[{key:"create",value:function 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"};this.id=this.element.dataset.id}},{key:"stateReady",value:function stateReady(a){this.configState(a);if(this.reactive.isEditing&&this.reactive.supportComponents){var c=new b.default(g({},this,{element:this.getElement(this.selectors.SECTION_ITEM),fullregion:this.element}));this.configDragDrop(c)}}},{key:"getWatchers",value:function getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}},{key:"getLastCm",value:function getLastCm(){return this.getElement(this.selectors.CM_LAST)}},{key:"_refreshSection",value:function _refreshSection(a){var b,c,d=a.element,e=this.getElement(this.selectors.SECTION_ITEM);e.classList.toggle(this.classes.SECTIONHIDDEN,!d.visible);this.element.classList.toggle(this.classes.SECTIONCURRENT,d.current);this.element.classList.toggle(this.classes.DRAGGING,null!==(b=d.dragging)&&void 0!==b?b:!1);this.element.classList.toggle(this.classes.LOCKED,null!==(c=d.locked)&&void 0!==c?c:!1);this.locked=d.locked;this.getElement(this.selectors.SECTION_TITLE).innerHTML=d.title}}],[{key:"init",value:function init(a,b){return new c({element:document.getElementById(a),selectors:b})}}]);return c}(c.default);a.default=s;return a.default});
|
||||
//# sourceMappingURL=section.min.js.map
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -35,8 +35,10 @@ export default class Component extends BaseComponent {
|
|||
|
||||
/**
|
||||
* Constructor hook.
|
||||
*
|
||||
* @param {Object} descriptor the component descriptor
|
||||
*/
|
||||
create() {
|
||||
create(descriptor) {
|
||||
// Optional component name for debugging.
|
||||
this.name = 'course_format';
|
||||
// Default query selectors.
|
||||
|
@ -50,6 +52,7 @@ export default class Component extends BaseComponent {
|
|||
COLLAPSE: `[data-toggle="collapse"]`,
|
||||
// Formats can override the activity tag but a default one is needed to create new elements.
|
||||
ACTIVITYTAG: 'li',
|
||||
SECTIONTAG: 'li',
|
||||
};
|
||||
// Default classes to toggle on refresh.
|
||||
this.classes = {
|
||||
|
@ -57,6 +60,7 @@ export default class Component extends BaseComponent {
|
|||
// Course content classes.
|
||||
ACTIVITY: `activity`,
|
||||
STATEDREADY: `stateready`,
|
||||
SECTION: `section`,
|
||||
};
|
||||
// Array to save dettached elements during element resorting.
|
||||
this.dettachedCms = {};
|
||||
|
@ -64,6 +68,8 @@ export default class Component extends BaseComponent {
|
|||
// Index of sections and cms components.
|
||||
this.sections = {};
|
||||
this.cms = {};
|
||||
// The page section return.
|
||||
this.sectionReturn = descriptor.sectionReturn ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,13 +77,15 @@ export default class Component extends BaseComponent {
|
|||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @param {number} sectionReturn the content section return
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
static init(target, selectors, sectionReturn) {
|
||||
return new Component({
|
||||
element: document.getElementById(target),
|
||||
reactive: getCurrentCourseEditor(),
|
||||
selectors,
|
||||
sectionReturn,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -138,6 +146,10 @@ export default class Component extends BaseComponent {
|
|||
* @returns {Array} of watchers
|
||||
*/
|
||||
getWatchers() {
|
||||
// Section return is a global page variable but most formats define it just before start printing
|
||||
// the course content. This is the reason why we define this page setting here.
|
||||
this.reactive.sectionReturn = this.sectionReturn;
|
||||
|
||||
// Check if the course format is compatible with reactive components.
|
||||
if (!this.reactive.supportComponents) {
|
||||
return [];
|
||||
|
@ -161,22 +173,6 @@ export default class Component extends BaseComponent {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload a course module.
|
||||
*
|
||||
* Most course module HTML is still strongly backend dependant.
|
||||
* Some changes require to get a new version af the module.
|
||||
*
|
||||
* @param {Object} param
|
||||
* @param {Object} param.element update the state update data
|
||||
*/
|
||||
_reloadCm({element}) {
|
||||
const cmitem = this.getElement(this.selectors.CM, element.id);
|
||||
if (cmitem) {
|
||||
courseActions.refreshModule(cmitem, element.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update section collapsed.
|
||||
*
|
||||
|
@ -281,10 +277,14 @@ export default class Component extends BaseComponent {
|
|||
* @param {Object} param.element details the update details.
|
||||
*/
|
||||
_refreshCourseSectionlist({element}) {
|
||||
// If we have a section return means we only show a single section so no need to fix order.
|
||||
if (this.reactive.sectionReturn != 0) {
|
||||
return;
|
||||
}
|
||||
const sectionlist = element.sectionlist ?? [];
|
||||
const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
|
||||
// For now section cannot be created at a frontend level.
|
||||
const createSection = () => undefined;
|
||||
const createSection = this._createSectionItem.bind(this);
|
||||
if (listparent) {
|
||||
this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);
|
||||
}
|
||||
|
@ -364,6 +364,26 @@ export default class Component extends BaseComponent {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload a course section contents.
|
||||
*
|
||||
* Section HTML is still strongly backend dependant.
|
||||
* Some changes require to get a new version of the section.
|
||||
*
|
||||
* @param {details} param0 the watcher details
|
||||
* @param {object} param0.element the state object
|
||||
*/
|
||||
_reloadSection({element}) {
|
||||
const sectionitem = this.getElement(this.selectors.SECTION, element.id);
|
||||
if (sectionitem) {
|
||||
const promise = courseActions.refreshSection(sectionitem, element.id);
|
||||
promise.then(() => {
|
||||
this._indexContents();
|
||||
return;
|
||||
}).catch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new course module item in a section.
|
||||
*
|
||||
|
@ -388,6 +408,32 @@ export default class Component extends BaseComponent {
|
|||
return newItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new section item.
|
||||
*
|
||||
* This method will append a fake item in the container and trigger an ajax request to
|
||||
* replace the fake element by the real content.
|
||||
*
|
||||
* @param {Element} container the container element (section)
|
||||
* @param {Number} sectionid the course-module ID
|
||||
* @returns {Element} the created element
|
||||
*/
|
||||
_createSectionItem(container, sectionid) {
|
||||
const section = this.reactive.get('section', sectionid);
|
||||
const newItem = document.createElement(this.selectors.SECTIONTAG);
|
||||
newItem.dataset.for = 'section';
|
||||
newItem.dataset.id = sectionid;
|
||||
newItem.dataset.number = section.number;
|
||||
// The legacy actions.js requires a specific ID and class to refresh the section.
|
||||
newItem.id = `section-${sectionid}`;
|
||||
newItem.classList.add(this.classes.SECTION);
|
||||
container.append(newItem);
|
||||
this._reloadSection({
|
||||
element: section,
|
||||
});
|
||||
return newItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix/reorder the section or cms order.
|
||||
*
|
||||
|
|
|
@ -32,9 +32,10 @@ import Templates from 'core/templates';
|
|||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {getList} from 'core/normalise';
|
||||
import * as CourseEvents from 'core_course/events';
|
||||
|
||||
// Load global strings.
|
||||
prefetchStrings('core', ['movecoursesection', 'movecoursemodule']);
|
||||
prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
|
@ -51,6 +52,7 @@ export default class extends BaseComponent {
|
|||
CMLINK: `[data-for='cm']`,
|
||||
SECTIONNODE: `[data-for='sectionnode']`,
|
||||
TOGGLER: `[data-toggle='collapse']`,
|
||||
ADDSECTION: `[data-action='addSection']`,
|
||||
};
|
||||
// Component css classes.
|
||||
this.classes = {
|
||||
|
@ -61,14 +63,36 @@ export default class extends BaseComponent {
|
|||
/**
|
||||
* Initial state ready method.
|
||||
*
|
||||
* @param {Object} state the state data.
|
||||
*
|
||||
*/
|
||||
stateReady() {
|
||||
stateReady(state) {
|
||||
// Delegate dispatch clicks.
|
||||
this.addEventListener(
|
||||
this.element,
|
||||
'click',
|
||||
this._dispatchClick
|
||||
);
|
||||
// Check section limit.
|
||||
this._checkSectionlist({state});
|
||||
// Add an Event listener to recalculate limits it if a section HTML is altered.
|
||||
this.addEventListener(
|
||||
this.element,
|
||||
CourseEvents.sectionRefreshed,
|
||||
() => this._checkSectionlist({state})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the component watchers.
|
||||
*
|
||||
* @returns {Array} of watchers
|
||||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
// Check section limit.
|
||||
{watch: `course.sectionlist:updated`, handler: this._checkSectionlist},
|
||||
];
|
||||
}
|
||||
|
||||
_dispatchClick(event) {
|
||||
|
@ -76,6 +100,10 @@ export default class extends BaseComponent {
|
|||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.classList.contains(this.classes.DISABLED)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke proper method.
|
||||
const methodName = this._actionMethodName(target.dataset.action);
|
||||
|
@ -90,6 +118,17 @@ export default class extends BaseComponent {
|
|||
return `_request${requestName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the section list and disable some options if needed.
|
||||
*
|
||||
* @param {Object} detail the update details.
|
||||
* @param {Object} detail.state the state object.
|
||||
*/
|
||||
_checkSectionlist({state}) {
|
||||
// Disable "add section" actions if the course max sections has been exceeded.
|
||||
this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a move section request.
|
||||
*
|
||||
|
@ -217,6 +256,75 @@ export default class extends BaseComponent {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a create section request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestAddSection(target, event) {
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('addSection', target.dataset.id ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a delete section request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestDeleteSection(target, event) {
|
||||
// Check we have an id.
|
||||
const sectionId = target.dataset.id;
|
||||
|
||||
if (!sectionId) {
|
||||
return;
|
||||
}
|
||||
const sectionInfo = this.reactive.get('section', sectionId);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const cmList = sectionInfo.cmlist ?? [];
|
||||
if (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle) {
|
||||
// We need confirmation if the section has something.
|
||||
const modalParams = {
|
||||
title: getString('confirm', 'core'),
|
||||
body: getString('confirmdeletesection', 'moodle', sectionInfo.title),
|
||||
saveButtonText: getString('delete', 'core'),
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
};
|
||||
|
||||
const modal = await this._modalBodyRenderedPromise(modalParams);
|
||||
|
||||
modal.getRoot().on(
|
||||
ModalEvents.save,
|
||||
e => {
|
||||
// Stop the default save button behaviour which is to close the modal.
|
||||
e.preventDefault();
|
||||
modal.destroy();
|
||||
this.reactive.dispatch('sectionDelete', [sectionId]);
|
||||
}
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// We don't need confirmation to delete empty sections.
|
||||
this.reactive.dispatch('sectionDelete', [sectionId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all add sections actions.
|
||||
*
|
||||
* @param {boolean} locked the new locked value.
|
||||
*/
|
||||
_setAddSectionLocked(locked) {
|
||||
const targets = this.getElements(this.selectors.ADDSECTION);
|
||||
targets.forEach(element => {
|
||||
element.classList.toggle(this.classes.DISABLED, locked);
|
||||
this.setElementLocked(element, locked);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an element with a copy with a different tag name.
|
||||
*
|
||||
|
@ -245,6 +353,10 @@ export default class extends BaseComponent {
|
|||
modal.getRoot().on(ModalEvents.bodyRendered, () => {
|
||||
resolve(modal);
|
||||
});
|
||||
// Configure some extra modal params.
|
||||
if (modalParams.saveButtonText !== undefined) {
|
||||
modal.setSaveButtonText(modalParams.saveButtonText);
|
||||
}
|
||||
modal.show();
|
||||
return;
|
||||
}).catch(() => {
|
||||
|
|
|
@ -81,6 +81,20 @@ export default class extends DndSection {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the drop data can be dropped over the component.
|
||||
*
|
||||
* @param {Object} dropdata the exported drop data.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateDropData(dropdata) {
|
||||
// If the format uses one section per page sections dropping in the content is ignored.
|
||||
if (dropdata?.type === 'section' && this.reactive.sectionReturn != 0) {
|
||||
return false;
|
||||
}
|
||||
return super.validateDropData(dropdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last CM element of that section.
|
||||
*
|
||||
|
|
|
@ -46,6 +46,14 @@ export default class extends Reactive {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -109,7 +109,7 @@ export default class extends BaseComponent {
|
|||
if (dropdata?.type === 'cm') {
|
||||
return true;
|
||||
}
|
||||
// We accept any section bu the section 0 or ourself
|
||||
// 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;
|
||||
|
|
|
@ -131,6 +131,33 @@ export default class {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -98,6 +98,8 @@ export default class Component extends BaseComponent {
|
|||
{watch: `section.isactive:updated`, handler: this._refreshSectionCollapsed},
|
||||
{watch: `cm:created`, handler: this._createCm},
|
||||
{watch: `cm:deleted`, handler: this._deleteCm},
|
||||
{watch: `section:created`, handler: this._createSection},
|
||||
{watch: `section:deleted`, handler: this._deleteSection},
|
||||
// Sections and cm sorting.
|
||||
{watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
|
||||
{watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
|
||||
|
@ -219,6 +221,35 @@ export default class Component extends BaseComponent {
|
|||
fakeelement.parentNode.replaceChild(newelement, fakeelement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new section instance.
|
||||
*
|
||||
* @param {Object} details the update details.
|
||||
* @param {Object} details.state the state data.
|
||||
* @param {Object} details.element the element data.
|
||||
*/
|
||||
async _createSection({state, element}) {
|
||||
// Create a fake node while the component is loading.
|
||||
const fakeelement = document.createElement('div');
|
||||
fakeelement.classList.add('bg-pulse-grey', 'w-100');
|
||||
fakeelement.innerHTML = ' ';
|
||||
this.sections[element.id] = fakeelement;
|
||||
// Place the fake node on the correct position.
|
||||
this._refreshCourseSectionlist({
|
||||
state,
|
||||
element: state.course,
|
||||
});
|
||||
// Collect render data.
|
||||
const exporter = this.reactive.getExporter();
|
||||
const data = exporter.section(state, element);
|
||||
// Create the new content.
|
||||
const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);
|
||||
// Replace the fake node with the real content.
|
||||
const newelement = newcomponent.getElement();
|
||||
this.sections[element.id] = newelement;
|
||||
fakeelement.parentNode.replaceChild(newelement, fakeelement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a section cm list.
|
||||
*
|
||||
|
@ -291,4 +322,16 @@ export default class Component extends BaseComponent {
|
|||
_deleteCm({element}) {
|
||||
delete this.cms[element.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a section from the list.
|
||||
*
|
||||
* The actual DOM element removal is delegated to the section component.
|
||||
*
|
||||
* @param {Object} details the update details.
|
||||
* @param {Object} details.element the element data.
|
||||
*/
|
||||
_deleteSection({element}) {
|
||||
delete this.sections[element.id];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ export default class Component extends DndSection {
|
|||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
{watch: `section[${this.id}]:deleted`, handler: this.remove},
|
||||
{watch: `section[${this.id}]:updated`, handler: this._refreshSection},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1504,20 +1504,8 @@ abstract class base {
|
|||
|
||||
$course = $this->get_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
switch($action) {
|
||||
case 'hide':
|
||||
case 'show':
|
||||
require_capability('moodle/course:sectionvisibility', $coursecontext);
|
||||
$visible = ($action === 'hide') ? 0 : 1;
|
||||
course_update_section($course, $section, array('visible' => $visible));
|
||||
break;
|
||||
default:
|
||||
throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
|
||||
}
|
||||
|
||||
$modules = [];
|
||||
|
||||
$modinfo = $this->get_modinfo();
|
||||
$renderer = $this->get_renderer($PAGE);
|
||||
|
||||
if (!($section instanceof section_info)) {
|
||||
$section = $modinfo->get_section_info($section->section);
|
||||
|
@ -1527,9 +1515,24 @@ abstract class base {
|
|||
$this->set_section_number($sr);
|
||||
}
|
||||
|
||||
// Load the cmlist output.
|
||||
$renderer = $this->get_renderer($PAGE);
|
||||
switch($action) {
|
||||
case 'hide':
|
||||
case 'show':
|
||||
require_capability('moodle/course:sectionvisibility', $coursecontext);
|
||||
$visible = ($action === 'hide') ? 0 : 1;
|
||||
course_update_section($course, $section, array('visible' => $visible));
|
||||
break;
|
||||
case 'refresh':
|
||||
return [
|
||||
'content' => $renderer->course_section_updated($this, $section),
|
||||
];
|
||||
default:
|
||||
throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
|
||||
}
|
||||
|
||||
$modules = [];
|
||||
|
||||
// Load the cmlist output.
|
||||
$coursesections = $modinfo->sections;
|
||||
if (array_key_exists($section->section, $coursesections)) {
|
||||
foreach ($coursesections[$section->section] as $cmid) {
|
||||
|
|
|
@ -54,6 +54,9 @@ class content implements renderable, templatable {
|
|||
/** @var string section selector class name */
|
||||
protected $sectionselectorclass;
|
||||
|
||||
/** @var bool if uses add section */
|
||||
protected $hasaddsection = true;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
@ -78,8 +81,6 @@ class content implements renderable, templatable {
|
|||
public function export_for_template(\renderer_base $output) {
|
||||
$format = $this->format;
|
||||
|
||||
$addsection = new $this->addsectionclass($format);
|
||||
|
||||
// Most formats uses section 0 as a separate section so we remove from the list.
|
||||
$sections = $this->export_sections($output);
|
||||
$initialsection = '';
|
||||
|
@ -91,8 +92,8 @@ class content implements renderable, templatable {
|
|||
'title' => $format->page_title(), // This method should be in the course_format class.
|
||||
'initialsection' => $initialsection,
|
||||
'sections' => $sections,
|
||||
'numsections' => $addsection->export_for_template($output),
|
||||
'format' => $format->get_format(),
|
||||
'sectionreturn' => 0,
|
||||
];
|
||||
|
||||
// The single section format has extra navigation.
|
||||
|
@ -106,6 +107,12 @@ class content implements renderable, templatable {
|
|||
|
||||
$data->hasnavigation = true;
|
||||
$data->singlesection = array_shift($data->sections);
|
||||
$data->sectionreturn = $singlesection;
|
||||
}
|
||||
|
||||
if ($this->hasaddsection) {
|
||||
$addsection = new $this->addsectionclass($format);
|
||||
$data->numsections = $addsection->export_for_template($output);
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
|
|
@ -73,6 +73,9 @@ class addsection implements renderable, templatable {
|
|||
return $data;
|
||||
}
|
||||
|
||||
// Component based formats handle add section button in the frontend.
|
||||
$show = ($lastsection < $maxsections) || course_get_format($course)->supports_components();
|
||||
|
||||
$supportsnumsections = array_key_exists('numsections', $options);
|
||||
if ($supportsnumsections) {
|
||||
// Current course format has 'numsections' option, which is very confusing and we suggest course format
|
||||
|
@ -96,7 +99,7 @@ class addsection implements renderable, templatable {
|
|||
];
|
||||
}
|
||||
|
||||
} else if (course_get_format($course)->uses_sections() && $lastsection < $maxsections) {
|
||||
} else if (course_get_format($course)->uses_sections() && $show) {
|
||||
// Current course format does not have 'numsections' option but it has multiple sections suppport.
|
||||
// Display the "Add section" link that will insert a section in the end.
|
||||
// Note to course format developers: inserting sections in the other positions should check both
|
||||
|
|
|
@ -132,6 +132,7 @@ class section implements renderable, templatable {
|
|||
'num' => $thissection->section ?? '0',
|
||||
'id' => $thissection->id,
|
||||
'sectionreturnid' => $singlesection,
|
||||
'insertafter' => false,
|
||||
'summary' => $summary->export_for_template($output),
|
||||
'availability' => $availability->export_for_template($output),
|
||||
];
|
||||
|
|
|
@ -255,7 +255,11 @@ class controlmenu implements renderable, templatable {
|
|||
'icon' => 'i/delete',
|
||||
'name' => $strdelete,
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => ['class' => 'icon editing_delete'],
|
||||
'attr' => [
|
||||
'class' => 'icon editing_delete',
|
||||
'data-action' => 'deleteSection',
|
||||
'data-id' => $section->id,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
namespace core_courseformat\output\local\state;
|
||||
|
||||
use core_courseformat\base as course_format;
|
||||
use course_modinfo;
|
||||
use renderable;
|
||||
use stdClass;
|
||||
|
||||
|
@ -50,7 +51,8 @@ class course implements renderable {
|
|||
public function export_for_template(\renderer_base $output): stdClass {
|
||||
$format = $this->format;
|
||||
$course = $format->get_course();
|
||||
$modinfo = $this->format->get_modinfo();
|
||||
// State must represent always the most updated version of the course.
|
||||
$modinfo = course_modinfo::instance($course);
|
||||
|
||||
$data = (object)[
|
||||
'id' => $course->id,
|
||||
|
@ -58,6 +60,7 @@ class course implements renderable {
|
|||
'sectionlist' => [],
|
||||
'editmode' => $format->show_editor(),
|
||||
'highlighted' => $format->get_section_highlighted_name(),
|
||||
'maxsections' => $format->get_max_sections(),
|
||||
];
|
||||
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
|
|
|
@ -78,6 +78,7 @@ class section implements renderable {
|
|||
'section' => $section->section,
|
||||
'number' => $section->section,
|
||||
'title' => $format->get_section_name($section),
|
||||
'hassummary' => !empty($section->summary),
|
||||
'rawtitle' => $section->name,
|
||||
'cmlist' => [],
|
||||
'visible' => !empty($section->visible),
|
||||
|
|
|
@ -166,6 +166,7 @@ abstract class section_renderer extends core_course_renderer {
|
|||
* @param section_info $section the section info
|
||||
* @param cm_info $cm the course module ionfo
|
||||
* @param array $displayoptions optional extra display options
|
||||
* @return string the rendered element
|
||||
*/
|
||||
public function course_section_updated_cm_item(
|
||||
course_format $format,
|
||||
|
@ -179,6 +180,28 @@ abstract class section_renderer extends core_course_renderer {
|
|||
return $this->render($cmitem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated rendered version of a section.
|
||||
*
|
||||
* This method will only be used when the course editor requires to get an updated cm item HTML
|
||||
* to perform partial page refresh. It will be used for supporting the course editor webservices.
|
||||
*
|
||||
* By default, the template used for update a section is the same as when it renders initially,
|
||||
* but format plugins are free to override this method to provide extra effects or so.
|
||||
*
|
||||
* @param course_format $format the course format
|
||||
* @param section_info $section the section info
|
||||
* @return string the rendered element
|
||||
*/
|
||||
public function course_section_updated(
|
||||
course_format $format,
|
||||
section_info $section
|
||||
): string {
|
||||
$sectionclass = $format->get_output_classname('content\\section');
|
||||
$output = new $sectionclass($format, $section);
|
||||
return $this->render($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course index drawer with placeholder.
|
||||
*
|
||||
|
|
|
@ -163,6 +163,99 @@ class stateactions {
|
|||
$updates->add_course_put();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a course section.
|
||||
*
|
||||
* This method follows the same logic as changenumsections.php.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids not used
|
||||
* @param int $targetsectionid optional target section id (if not passed section will be appended)
|
||||
* @param int $targetcmid not used
|
||||
*/
|
||||
public function section_add(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
require_capability('moodle/course:update', $coursecontext);
|
||||
|
||||
// Get course format settings.
|
||||
$format = course_get_format($course->id);
|
||||
$lastsectionnumber = $format->get_last_section_number();
|
||||
$maxsections = $format->get_max_sections();
|
||||
|
||||
if ($lastsectionnumber >= $maxsections) {
|
||||
throw new moodle_exception('maxsectionslimit', 'moodle', $maxsections);
|
||||
}
|
||||
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
|
||||
// Get target section.
|
||||
if ($targetsectionid) {
|
||||
$this->validate_sections($course, [$targetsectionid], __FUNCTION__);
|
||||
$targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
|
||||
// Inserting sections at any position except in the very end requires capability to move sections.
|
||||
require_capability('moodle/course:movesections', $coursecontext);
|
||||
$insertposition = $targetsection->section + 1;
|
||||
} else {
|
||||
// Get last section.
|
||||
$insertposition = 0;
|
||||
}
|
||||
|
||||
course_create_section($course, $insertposition);
|
||||
|
||||
// Adding a section affects the full course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete course sections.
|
||||
*
|
||||
* This method follows the same logic as editsection.php.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids section ids
|
||||
* @param int $targetsectionid not used
|
||||
* @param int $targetcmid not used
|
||||
*/
|
||||
public function section_delete(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
require_capability('moodle/course:update', $coursecontext);
|
||||
require_capability('moodle/course:movesections', $coursecontext);
|
||||
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
|
||||
foreach ($ids as $sectionid) {
|
||||
$section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
|
||||
// Send all activity deletions.
|
||||
if (!empty($modinfo->sections[$section->section])) {
|
||||
foreach ($modinfo->sections[$section->section] as $modnumber) {
|
||||
$cm = $modinfo->cms[$modnumber];
|
||||
$updates->add_cm_delete($cm->id);
|
||||
}
|
||||
}
|
||||
course_delete_section($course, $section, true, true);
|
||||
$updates->add_section_delete($sectionid);
|
||||
}
|
||||
|
||||
// Removing a section affects the full course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract several cm_info from the course_modinfo.
|
||||
*
|
||||
|
|
|
@ -130,7 +130,7 @@ class stateupdates implements JsonSerializable {
|
|||
* @param int $sectionid The affected section id.
|
||||
*/
|
||||
public function add_section_delete(int $sectionid): void {
|
||||
$this->add_update('section', 'delete', (object)['id' => $sectionid]);
|
||||
$this->add_update('section', 'remove', (object)['id' => $sectionid]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,7 +179,7 @@ class stateupdates implements JsonSerializable {
|
|||
* @param int $cmid the affected course module id
|
||||
*/
|
||||
public function add_cm_delete(int $cmid): void {
|
||||
$this->add_update('cm', 'delete', (object)['id' => $cmid]);
|
||||
$this->add_update('cm', 'remove', (object)['id' => $cmid]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
"nextname": "Section 5",
|
||||
"selector": "<select><option>Section 4</option></select>"
|
||||
},
|
||||
"sectionreturn": 1,
|
||||
"singlesection": {
|
||||
"num": 1,
|
||||
"id": 35,
|
||||
|
@ -183,6 +184,6 @@
|
|||
</div>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/content'], function(component) {
|
||||
component.init('{{uniqid}}-course-format');
|
||||
component.init('{{uniqid}}-course-format', {}, {{sectionreturn}});
|
||||
});
|
||||
{{/js}}
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
Example context (json):
|
||||
{
|
||||
"showaddsection": true,
|
||||
"id": 42,
|
||||
"insertafter": true,
|
||||
"num": 0,
|
||||
"increase": {
|
||||
"url": "#"
|
||||
},
|
||||
|
@ -48,7 +51,14 @@
|
|||
</a>
|
||||
{{/decrease}}
|
||||
{{#addsections}}
|
||||
<a href="{{{url}}}" class="add-sections" data-add-sections="{{title}}" data-new-sections="{{newsection}}">
|
||||
<a
|
||||
href="{{{url}}}"
|
||||
class="add-sections"
|
||||
data-add-sections="{{title}}"
|
||||
data-new-sections="{{newsection}}"
|
||||
data-action="addSection"
|
||||
{{#insertafter}} data-id="{{id}}" {{/insertafter}}
|
||||
>
|
||||
{{#pix}} t/add, moodle {{/pix}} {{title}}
|
||||
</a>
|
||||
{{/addsections}}
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
"iscoursedisplaymultipage": true,
|
||||
"sectionreturnid": 0,
|
||||
"contentexpanded": true,
|
||||
"insertafter": true,
|
||||
"numsections": 42,
|
||||
"sitehome": false
|
||||
}
|
||||
}}
|
||||
|
@ -110,5 +112,10 @@
|
|||
{{#cmsummary}} {{> core_courseformat/local/content/section/cmsummary }} {{/cmsummary}}
|
||||
{{#cmlist}} {{> core_courseformat/local/content/section/cmlist }} {{/cmlist}}
|
||||
{{{cmcontrols}}}
|
||||
{{#insertafter}}
|
||||
{{#numsections}}
|
||||
{{> core_courseformat/local/content/addsection}}
|
||||
{{/numsections}}
|
||||
{{/insertafter}}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -68,7 +68,7 @@ Feature: Course content collapsed user preferences
|
|||
And I click on "#collapssesection4" "css_element"
|
||||
And I turn editing mode on
|
||||
And I delete section "1"
|
||||
And I press "Delete"
|
||||
And I click on "Delete" "button" in the ".modal" "css_element"
|
||||
And I should not see "Activity sample 1" in the "region-main" "region"
|
||||
And I should see "Activity sample 2" in the "region-main" "region"
|
||||
And I should see "Activity sample 3" in the "region-main" "region"
|
||||
|
|
|
@ -203,10 +203,39 @@ Feature: Course index depending on role
|
|||
# Delete section 1
|
||||
And I turn editing mode on
|
||||
And I delete section "1"
|
||||
And I click on "Delete" "button"
|
||||
And I click on "Delete" "button" in the ".modal" "css_element"
|
||||
And I reload the page
|
||||
And I should not see "Activity sample 1" in the "courseindex-content" "region"
|
||||
And I should see "Topic 1" in the "courseindex-content" "region"
|
||||
And I should see "Activity sample 2" in the "courseindex-content" "region"
|
||||
And I should see "Topic 2" in the "courseindex-content" "region"
|
||||
And I should not see "Activity sample 3" in the "courseindex-content" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Adding section should alter the course index
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I click on "Side panel" "button"
|
||||
And I click on "Open course index drawer" "button"
|
||||
When I click on "Add topic after" "link" in the "Topic 4" "section"
|
||||
Then I should see "Topic 5" in the "courseindex-content" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Remove a section should alter the course index
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I click on "Side panel" "button"
|
||||
And I click on "Open course index drawer" "button"
|
||||
When I delete section "4"
|
||||
Then I should not see "Topic 4" in the "courseindex-content" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Delete a previous section should alter the course index unnamed sections
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I click on "Side panel" "button"
|
||||
And I click on "Open course index drawer" "button"
|
||||
When I delete section "1"
|
||||
And I click on "Delete" "button" in the ".modal" "css_element"
|
||||
Then I should not see "Topic 4" in the "courseindex-content" "region"
|
||||
And I should not see "Activity sample 1" in the "courseindex-content" "region"
|
||||
|
|
45
course/format/topics/classes/output/courseformat/content.php
Normal file
45
course/format/topics/classes/output/courseformat/content.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contains the default content output class.
|
||||
*
|
||||
* @package format_topics
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace format_topics\output\courseformat;
|
||||
|
||||
use core_courseformat\output\local\content as content_base;
|
||||
|
||||
/**
|
||||
* Base class to render a course content.
|
||||
*
|
||||
* @package format_topics
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class content extends content_base {
|
||||
|
||||
/**
|
||||
* @var bool Topic format has add section after each topic.
|
||||
*
|
||||
* The responsible for the buttons is core_courseformat\output\local\content\section.
|
||||
*/
|
||||
protected $hasaddsection = false;
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contains the default section controls output class.
|
||||
*
|
||||
* @package format_topics
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace format_topics\output\courseformat\content;
|
||||
|
||||
use core_courseformat\base as course_format;
|
||||
use core_courseformat\output\local\content\section as section_base;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Base class to render a course section.
|
||||
*
|
||||
* @package format_topics
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class section extends section_base {
|
||||
|
||||
/** @var course_format the course format */
|
||||
protected $format;
|
||||
|
||||
public function export_for_template(\renderer_base $output): stdClass {
|
||||
$format = $this->format;
|
||||
|
||||
$data = parent::export_for_template($output);
|
||||
|
||||
if (!$this->format->get_section_number()) {
|
||||
$addsectionclass = $format->get_output_classname('content\\addsection');
|
||||
$addsection = new $addsectionclass($format);
|
||||
$data->numsections = $addsection->export_for_template($output);
|
||||
$data->insertafter = true;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@
|
|||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addsections'] = 'Add topics';
|
||||
$string['addsections'] = 'Add topic after';
|
||||
$string['currentsection'] = 'This topic';
|
||||
$string['editsection'] = 'Edit topic';
|
||||
$string['editsectionname'] = 'Edit topic name';
|
||||
|
|
|
@ -29,10 +29,11 @@ Feature: Sections can be edited and deleted in topics format
|
|||
And the field "New value for Section name" matches value "General"
|
||||
|
||||
Scenario: Edit the default name of the general section in topics format
|
||||
Given I should see "General" in the "General" "section"
|
||||
When I edit the section "0" and I fill the form with:
|
||||
| Custom | 1 |
|
||||
| New value for Section name | This is the general section |
|
||||
Then I should see "This is the general section" in the "li#section-0" "css_element"
|
||||
Then I should see "This is the general section" in the "This is the general section" "section"
|
||||
|
||||
Scenario: View the default name of the second section in topics format
|
||||
When I edit the section "2"
|
||||
|
@ -42,24 +43,24 @@ Feature: Sections can be edited and deleted in topics format
|
|||
Scenario: Edit section summary in topics format
|
||||
When I edit the section "2" and I fill the form with:
|
||||
| Summary | Welcome to section 2 |
|
||||
Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
|
||||
Then I should see "Welcome to section 2" in the "Topic 2" "section"
|
||||
|
||||
Scenario: Edit section default name in topics format
|
||||
When I edit the section "2" and I fill the form with:
|
||||
| Custom | 1 |
|
||||
| New value for Section name | This is the second topic |
|
||||
Then I should see "This is the second topic" in the "li#section-2" "css_element"
|
||||
And I should not see "Topic 2" in the "li#section-2" "css_element"
|
||||
Then I should see "This is the second topic" in the "This is the second topic" "section"
|
||||
And I should not see "Topic 2" in the "region-main" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Inline edit section name in topics format
|
||||
When I set the field "Edit topic name" in the "li#section-1" "css_element" to "Midterm evaluation"
|
||||
When I set the field "Edit topic name" in the "Topic 1" "section" to "Midterm evaluation"
|
||||
Then I should not see "Topic 1" in the "region-main" "region"
|
||||
And "New name for topic" "field" should not exist
|
||||
And I should see "Midterm evaluation" in the "li#section-1" "css_element"
|
||||
And I should see "Midterm evaluation" in the "Midterm evaluation" "section"
|
||||
And I am on "Course 1" course homepage
|
||||
And I should not see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Midterm evaluation" in the "li#section-1" "css_element"
|
||||
And I should see "Midterm evaluation" in the "Midterm evaluation" "section"
|
||||
|
||||
Scenario: Deleting the last section in topics format
|
||||
When I delete section "5"
|
||||
|
@ -73,20 +74,18 @@ Feature: Sections can be edited and deleted in topics format
|
|||
And I press "Delete"
|
||||
Then I should not see "Topic 5"
|
||||
And I should not see "Test chat name"
|
||||
And I should see "Test choice name" in the "li#section-4" "css_element"
|
||||
And I should see "Test choice name" in the "Topic 4" "section"
|
||||
And I should see "Topic 4"
|
||||
|
||||
@javascript
|
||||
Scenario: Adding sections in topics format
|
||||
When I follow "Add topics"
|
||||
Then the field "Number of sections" matches value "1"
|
||||
And I press "Add topics"
|
||||
And I should see "Topic 6" in the "li#section-6" "css_element"
|
||||
And "li#section-7" "css_element" should not exist
|
||||
And I follow "Add topics"
|
||||
And I set the field "Number of sections" to "3"
|
||||
And I press "Add topics"
|
||||
And I should see "Topic 7" in the "li#section-7" "css_element"
|
||||
And I should see "Topic 8" in the "li#section-8" "css_element"
|
||||
And I should see "Topic 9" in the "li#section-9" "css_element"
|
||||
And "li#section-10" "css_element" should not exist
|
||||
Scenario: Adding sections at the end of a topics format
|
||||
When I click on "Add topic after" "link" in the "Topic 5" "section"
|
||||
Then I should see "Topic 6" in the "Topic 6" "section"
|
||||
And I should see "Test choice name" in the "Topic 5" "section"
|
||||
|
||||
@javascript
|
||||
Scenario: Adding sections between topics in topics format
|
||||
When I click on "Add topic after" "link" in the "Topic 4" "section"
|
||||
Then I should see "Topic 6" in the "Topic 6" "section"
|
||||
And I should not see "Test choice name" in the "Topic 5" "section"
|
||||
And I should see "Test choice name" in the "Topic 6" "section"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addsections'] = 'Add weeks';
|
||||
$string['addsections'] = 'Add week';
|
||||
$string['currentsection'] = 'This week';
|
||||
$string['editsection'] = 'Edit week';
|
||||
$string['editsectionname'] = 'Edit week name';
|
||||
|
|
|
@ -32,7 +32,7 @@ Feature: Sections can be edited and deleted in weeks format
|
|||
When I edit the section "0" and I fill the form with:
|
||||
| Custom | 1 |
|
||||
| New value for Section name | This is the general section |
|
||||
Then I should see "This is the general section" in the "li#section-0" "css_element"
|
||||
Then I should see "This is the general section" in the "This is the general section" "section"
|
||||
|
||||
Scenario: View the default name of the second section in weeks format
|
||||
When I edit the section "2"
|
||||
|
@ -42,28 +42,28 @@ Feature: Sections can be edited and deleted in weeks format
|
|||
Scenario: Edit section summary in weeks format
|
||||
When I edit the section "2" and I fill the form with:
|
||||
| Summary | Welcome to section 2 |
|
||||
Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
|
||||
Then I should see "Welcome to section 2" in the "8 May - 14 May" "section"
|
||||
|
||||
Scenario: Edit section default name in weeks format
|
||||
Given I should see "8 May - 14 May" in the "li#section-2" "css_element"
|
||||
Given I should see "8 May - 14 May" in the "8 May - 14 May" "section"
|
||||
When I edit the section "2" and I fill the form with:
|
||||
| Custom | 1 |
|
||||
| New value for Section name | This is the second week |
|
||||
Then I should see "This is the second week" in the "li#section-2" "css_element"
|
||||
And I should not see "8 May - 14 May" in the "li#section-2" "css_element"
|
||||
Then I should see "This is the second week" in the "This is the second week" "section"
|
||||
And I should not see "8 May - 14 May"
|
||||
|
||||
@javascript
|
||||
Scenario: Inline edit section name in weeks format
|
||||
When I set the field "Edit week name" in the "li#section-1" "css_element" to "Midterm evaluation"
|
||||
When I set the field "Edit week name" in the "1 May - 7 May" "section" to "Midterm evaluation"
|
||||
Then I should not see "1 May - 7 May" in the "region-main" "region"
|
||||
And "New name for week" "field" should not exist
|
||||
And I should see "Midterm evaluation" in the "li#section-1" "css_element"
|
||||
And I should see "Midterm evaluation" in the "Midterm evaluation" "section"
|
||||
And I am on "Course 1" course homepage
|
||||
And I should not see "1 May - 7 May" in the "region-main" "region"
|
||||
And I should see "Midterm evaluation" in the "li#section-1" "css_element"
|
||||
And I should see "Midterm evaluation" in the "Midterm evaluation" "section"
|
||||
|
||||
Scenario: Deleting the last section in weeks format
|
||||
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
|
||||
Given I should see "29 May - 4 June" in the "29 May - 4 June" "section"
|
||||
When I delete section "5"
|
||||
Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
|
||||
And I press "Delete"
|
||||
|
@ -71,25 +71,15 @@ Feature: Sections can be edited and deleted in weeks format
|
|||
And I should see "22 May - 28 May"
|
||||
|
||||
Scenario: Deleting the middle section in weeks format
|
||||
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
|
||||
Given I should see "29 May - 4 June" in the "29 May - 4 June" "section"
|
||||
When I delete section "4"
|
||||
And I press "Delete"
|
||||
Then I should not see "29 May - 4 June"
|
||||
And I should not see "Test chat name"
|
||||
And I should see "Test choice name" in the "li#section-4" "css_element"
|
||||
And I should see "Test choice name" in the "22 May - 28 May" "section"
|
||||
And I should see "22 May - 28 May"
|
||||
|
||||
@javascript
|
||||
Scenario: Adding sections in weeks format
|
||||
When I follow "Add weeks"
|
||||
Then the field "Number of sections" matches value "1"
|
||||
And I press "Add weeks"
|
||||
And I should see "5 June - 11 June" in the "li#section-6" "css_element"
|
||||
And "li#section-7" "css_element" should not exist
|
||||
And I follow "Add weeks"
|
||||
And I set the field "Number of sections" to "3"
|
||||
And I press "Add weeks"
|
||||
And I should see "12 June - 18 June" in the "li#section-7" "css_element"
|
||||
And I should see "19 June - 25 June" in the "li#section-8" "css_element"
|
||||
And I should see "26 June - 2 July" in the "li#section-9" "css_element"
|
||||
And "li#section-10" "css_element" should not exist
|
||||
When I follow "Add week"
|
||||
Then I should see "5 June - 11 June" in the "5 June - 11 June" "section"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -438,32 +438,52 @@ export default class {
|
|||
* @param {boolean} locked the new locked value
|
||||
*/
|
||||
set locked(locked) {
|
||||
this.element.dataset.locked = locked ?? false;
|
||||
this.setElementLocked(this.element, locked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current locked value from the element.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
get locked() {
|
||||
return this.getElementLocked(this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock/unlock an element.
|
||||
*
|
||||
* @param {Element} target the event target
|
||||
* @param {boolean} locked the new locked value
|
||||
*/
|
||||
setElementLocked(target, locked) {
|
||||
target.dataset.locked = locked ?? false;
|
||||
if (locked) {
|
||||
// Disable interactions.
|
||||
this.element.style.pointerEvents = 'none';
|
||||
this.element.style.userSelect = 'none';
|
||||
target.style.pointerEvents = 'none';
|
||||
target.style.userSelect = 'none';
|
||||
// Check if it is draggable.
|
||||
if (this.element.hasAttribute('draggable')) {
|
||||
this.element.setAttribute('draggable', false);
|
||||
if (target.hasAttribute('draggable')) {
|
||||
target.setAttribute('draggable', false);
|
||||
}
|
||||
} else {
|
||||
// Reanable interactions.
|
||||
this.element.style.pointerEvents = null;
|
||||
this.element.style.userSelect = null;
|
||||
// Enable interactions.
|
||||
target.style.pointerEvents = null;
|
||||
target.style.userSelect = null;
|
||||
// Check if it was draggable.
|
||||
if (this.element.hasAttribute('draggable')) {
|
||||
this.element.setAttribute('draggable', true);
|
||||
if (target.hasAttribute('draggable')) {
|
||||
target.setAttribute('draggable', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current locket value from the element.
|
||||
* Get the current locked value from the element.
|
||||
*
|
||||
* @param {Element} target the event target
|
||||
* @return {boolean}
|
||||
*/
|
||||
get locked() {
|
||||
return this.element.dataset.locked ?? false;
|
||||
getElementLocked(target) {
|
||||
return target.dataset.locked ?? false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ export default class StateManager {
|
|||
"delete": this.defaultDelete.bind(this),
|
||||
"put": this.defaultPut.bind(this),
|
||||
"override": this.defaultOverride.bind(this),
|
||||
"remove": this.defaultRemove.bind(this),
|
||||
"prepareFields": this.defaultPrepareFields.bind(this),
|
||||
};
|
||||
|
||||
|
@ -336,6 +337,31 @@ export default class StateManager {
|
|||
delete state[updateName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a remove state message.
|
||||
*
|
||||
* @param {Object} stateManager the state manager
|
||||
* @param {String} updateName the state element to update
|
||||
* @param {Object} fields the new data
|
||||
*/
|
||||
defaultRemove(stateManager, updateName, fields) {
|
||||
|
||||
// Get the current value.
|
||||
let current = stateManager.get(updateName, fields.id);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process deletion.
|
||||
let state = stateManager.state;
|
||||
|
||||
if (state[updateName] instanceof StateMap) {
|
||||
state[updateName].delete(fields.id);
|
||||
return;
|
||||
}
|
||||
delete state[updateName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a update state message.
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue