MDL-71779 core_courseformat: reactive add and delete sections

This commit is contained in:
Ferran Recio 2021-07-13 17:54:51 +02:00
parent a9d44b0f75
commit 3d2a6eacae
57 changed files with 883 additions and 140 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,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

View file

@ -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"}

View file

@ -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,
};
});

View file

@ -25,4 +25,5 @@ export default {
unfavorited: 'core_course:unfavorited',
manualCompletionToggled: 'core_course:manualcompletiontoggled',
stateChanged: 'core_course:stateChanged',
sectionRefreshed: 'core_course:sectionRefreshed',
};

View file

@ -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)

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

View file

@ -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

View file

@ -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

View file

@ -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.
*

View file

@ -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(() => {

View file

@ -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.
*

View file

@ -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.
*

View file

@ -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;

View file

@ -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.
*

View file

@ -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 = '&nbsp;';
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];
}
}

View file

@ -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},
];
}

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -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),
];

View file

@ -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,
],
];
}
}

View file

@ -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();

View file

@ -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),

View file

@ -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.
*

View file

@ -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.
*

View file

@ -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]);
}
/**

View file

@ -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}}

View file

@ -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}}

View file

@ -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>

View file

@ -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"

View file

@ -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"

View 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;
}

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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"

View file

@ -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';

View file

@ -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

View file

@ -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;
}
}

View file

@ -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.
*