mirror of
https://github.com/moodle/moodle.git
synced 2025-04-24 09:55:33 +02:00
Merge branch 'MDL-71663-master' of git://github.com/ferranrecio/moodle
This commit is contained in:
commit
f7ccc1b590
course
amd
format
amd
classes/output/local/content
templates/local/content
tests/behat
lib/amd
theme
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
@ -25,6 +25,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
'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) {
|
||||
|
||||
// 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'];
|
||||
|
||||
// The course reactive instance.
|
||||
const courseeditor = editor.getCurrentCourseEditor();
|
||||
|
||||
var CSS = {
|
||||
@ -488,10 +494,17 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
* @param {Nunmber} sectionid
|
||||
* @param {JQuery} target the element (menu item) that was clicked
|
||||
* @param {String} courseformat
|
||||
* @return {boolean} true the action call is sent to the server or false if it is ignored.
|
||||
*/
|
||||
var editSection = function(sectionElement, sectionid, target, courseformat) {
|
||||
var action = target.attr('data-action'),
|
||||
sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0;
|
||||
|
||||
// Filter direct component handled actions.
|
||||
if (courseeditor.supportComponents && componentActions.includes(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var spinner = addSectionSpinner(sectionElement);
|
||||
var promises = ajax.call([{
|
||||
methodname: 'core_course_edit_section',
|
||||
@ -522,6 +535,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
notification.exception(ex);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Register a function to be executed after D&D of an activity.
|
||||
@ -735,14 +749,19 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
var actionItem = $(this),
|
||||
sectionElement = actionItem.closest(SELECTOR.SECTIONLI),
|
||||
sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENU).attr('data-sectionid');
|
||||
e.preventDefault();
|
||||
|
||||
let isExecuted = true;
|
||||
if (actionItem.attr('data-confirm')) {
|
||||
// Action requires confirmation.
|
||||
confirmEditSection(actionItem.attr('data-confirm'), function() {
|
||||
editSection(sectionElement, sectionId, actionItem, courseformat);
|
||||
isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);
|
||||
});
|
||||
} else {
|
||||
editSection(sectionElement, sectionId, actionItem, courseformat);
|
||||
isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);
|
||||
}
|
||||
// Prevent any other module from capturing the action if it is already in execution.
|
||||
if (isExecuted) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
|
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
2
course/format/amd/build/local/content/actions.min.js
vendored
Normal file
2
course/format/amd/build/local/content/actions.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
course/format/amd/build/local/content/actions.min.js.map
Normal file
1
course/format/amd/build/local/content/actions.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
define ("core_courseformat/local/courseeditor/exporter",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(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 c(a){for(var c=1,e;c<arguments.length;c++){e=null!=arguments[c]?arguments[c]:{};if(c%2){b(Object(e),!0).forEach(function(b){d(a,b,e[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(e))}else{b(Object(e)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(e,b))})}}return a}function d(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 e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(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 g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}var h=function(){function a(b){e(this,a);this.reactive=b}g(a,[{key:"course",value:function course(a){var b,c,d=this,e={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(b=a.course.highlighted)&&void 0!==b?b:""},f=null!==(c=a.course.sectionlist)&&void 0!==c?c:[];f.forEach(function(b){var c,f=null!==(c=a.section.get(b))&&void 0!==c?c:{},g=d.section(a,f);e.sections.push(g)});e.hassections=0!=e.sections.length;return e}},{key:"section",value:function(a,b){var d,e=this,f=c({},b,{cms:[]}),g=null!==(d=b.cmlist)&&void 0!==d?d:[];g.forEach(function(b){var c=a.cm.get(b),d=e.cm(a,c);f.cms.push(d)});f.hascms=0!=f.cms.length;return f}},{key:"cm",value:function(a,b){var d=c({},b,{isactive:!1});return d}},{key:"cmDraggableData",value:function cmDraggableData(a,b){var c=a.cm.get(b);if(!c){return null}var d,e=a.section.get(c.sectionid),f=null===e||void 0===e?void 0:e.cmlist.indexOf(c.id);if(f!==void 0){d=null===e||void 0===e?void 0:e.cmlist[f+1]}return{type:"cm",id:c.id,name:c.name,nextcmid:d}}},{key:"sectionDraggableData",value:function sectionDraggableData(a,b){var c=a.section.get(b);if(!c){return null}return{type:"section",id:c.id,name:c.name,number:c.number}}}]);return a}();a.default=h;return a.default});
|
||||
define ("core_courseformat/local/courseeditor/exporter",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(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 c(a){for(var c=1,e;c<arguments.length;c++){e=null!=arguments[c]?arguments[c]:{};if(c%2){b(Object(e),!0).forEach(function(b){d(a,b,e[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(e))}else{b(Object(e)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(e,b))})}}return a}function d(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 e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(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 g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}var h=function(){function a(b){e(this,a);this.reactive=b}g(a,[{key:"course",value:function course(a){var b,c,d=this,e={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(b=a.course.highlighted)&&void 0!==b?b:""},f=null!==(c=a.course.sectionlist)&&void 0!==c?c:[];f.forEach(function(b){var c,f=null!==(c=a.section.get(b))&&void 0!==c?c:{},g=d.section(a,f);e.sections.push(g)});e.hassections=0!=e.sections.length;return e}},{key:"section",value:function(a,b){var d,e=this,f=c({},b,{cms:[]}),g=null!==(d=b.cmlist)&&void 0!==d?d:[];g.forEach(function(b){var c=a.cm.get(b),d=e.cm(a,c);f.cms.push(d)});f.hascms=0!=f.cms.length;return f}},{key:"cm",value:function(a,b){var d=c({},b,{isactive:!1});return d}},{key:"cmDraggableData",value:function cmDraggableData(a,b){var c=a.cm.get(b);if(!c){return null}var d,e=a.section.get(c.sectionid),f=null===e||void 0===e?void 0:e.cmlist.indexOf(c.id);if(f!==void 0){d=null===e||void 0===e?void 0:e.cmlist[f+1]}return{type:"cm",id:c.id,name:c.name,sectionid:c.sectionid,nextcmid:d}}},{key:"sectionDraggableData",value:function sectionDraggableData(a,b){var c=a.section.get(b);if(!c){return null}return{type:"section",id:c.id,name:c.name,number:c.number}}}]);return a}();a.default=h;return a.default});
|
||||
//# sourceMappingURL=exporter.min.js.map
|
||||
|
File diff suppressed because one or more lines are too long
@ -29,6 +29,7 @@ import Section from 'core_courseformat/local/content/section';
|
||||
import CmItem from 'core_courseformat/local/content/section/cmitem';
|
||||
// Course actions is needed for actions that are not migrated to components.
|
||||
import courseActions from 'core_course/actions';
|
||||
import DispatchActions from 'core_courseformat/local/content/actions';
|
||||
|
||||
export default class Component extends BaseComponent {
|
||||
|
||||
@ -51,6 +52,12 @@ export default class Component extends BaseComponent {
|
||||
// Default classes to toggle on refresh.
|
||||
this.classes = {
|
||||
COLLAPSED: `collapsed`,
|
||||
// Formats can override the activity tag but a default one is needed to create new elements.
|
||||
ACTIVITYTAG: 'li',
|
||||
|
||||
// Course content classes.
|
||||
ACTIVITY: `activity`,
|
||||
STATEDREADY: `stateready`,
|
||||
};
|
||||
// Array to save dettached elements during element resorting.
|
||||
this.dettachedCms = {};
|
||||
@ -116,6 +123,30 @@ export default class Component extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Course content elements could not provide JS Components because the elements HTML is applied
|
||||
* directly from the course actions. To keep internal components updated this module keeps
|
||||
* a list of the active components and mark them as "indexed". This way when any action replace
|
||||
* the HTML this component will recreate the components an add any necessary event listener.
|
||||
*
|
||||
* Format plugins can override this method to provide extra logic to the course frontend.
|
||||
*
|
||||
*/
|
||||
stateReady() {
|
||||
this._indexContents();
|
||||
|
||||
if (this.reactive.supportComponents) {
|
||||
// Actions are only available in edit mode.
|
||||
if (this.reactive.isEditing) {
|
||||
new DispatchActions(this);
|
||||
}
|
||||
|
||||
// Mark content as state ready.
|
||||
this.element.classList.add(this.classes.STATEDREADY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the component watchers.
|
||||
*
|
||||
@ -209,7 +240,8 @@ export default class Component extends BaseComponent {
|
||||
// Find the element.
|
||||
const target = this.getElement(this.selectors.SECTION, element.id);
|
||||
if (!target) {
|
||||
throw new Error(`Unkown section with ID ${element.id}`);
|
||||
// Job done. Nothing to refresh.
|
||||
return;
|
||||
}
|
||||
// Update section numbers in all data, css and YUI attributes.
|
||||
target.id = `section-${element.number}`;
|
||||
@ -240,14 +272,17 @@ export default class Component extends BaseComponent {
|
||||
/**
|
||||
* Refresh a section cm list.
|
||||
*
|
||||
* @param {Object} details the update details.
|
||||
* @param {details} details the update details
|
||||
* @property {object} details.element the state object
|
||||
*/
|
||||
_refreshSectionCmlist({element}) {
|
||||
const cmlist = element.cmlist ?? [];
|
||||
const section = this.getElement(this.selectors.SECTION, element.id);
|
||||
const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
|
||||
// A method to create a fake element to be replaced when the item is ready.
|
||||
const createCm = this._createCmItem.bind(this);
|
||||
if (listparent) {
|
||||
this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms);
|
||||
this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,8 +294,10 @@ export default class Component extends BaseComponent {
|
||||
_refreshCourseSectionlist({element}) {
|
||||
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;
|
||||
if (listparent) {
|
||||
this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections);
|
||||
this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +358,9 @@ export default class Component extends BaseComponent {
|
||||
/**
|
||||
* Reload a course module contents.
|
||||
*
|
||||
* Most course module HTML is still strongly backend dependant.
|
||||
* Some changes require to get a new version of the module.
|
||||
*
|
||||
* @param {details} param0 the watcher details
|
||||
* @property {object} param0.element the state object
|
||||
*/
|
||||
@ -335,6 +375,30 @@ export default class Component extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new course module item in a section.
|
||||
*
|
||||
* Thos 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} cmid the course-module ID
|
||||
* @returns {Element} the created element
|
||||
*/
|
||||
_createCmItem(container, cmid) {
|
||||
const newItem = document.createElement(this.selectors.ACTIVITYTAG);
|
||||
newItem.dataset.for = 'cmitem';
|
||||
newItem.dataset.id = cmid;
|
||||
// The legacy actions.js requires a specific ID and class to refresh the CM.
|
||||
newItem.id = `module-${cmid}`;
|
||||
newItem.classList.add(this.classes.ACTIVITY);
|
||||
container.append(newItem);
|
||||
this._reloadCm({
|
||||
element: this.reactive.get('cm', cmid),
|
||||
});
|
||||
return newItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix/reorder the section or cms order.
|
||||
*
|
||||
@ -342,8 +406,12 @@ export default class Component extends BaseComponent {
|
||||
* @param {Array} neworder an array with the ids order
|
||||
* @param {string} selector the element selector
|
||||
* @param {Object} dettachedelements a list of dettached elements
|
||||
* @param {function} createMethod method to create missing elements
|
||||
*/
|
||||
_fixOrder(container, neworder, selector, dettachedelements) {
|
||||
async _fixOrder(container, neworder, selector, dettachedelements, createMethod) {
|
||||
if (container === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty lists should not be visible.
|
||||
if (!neworder.length) {
|
||||
@ -357,7 +425,11 @@ export default class Component extends BaseComponent {
|
||||
|
||||
// Move the elements in order at the beginning of the list.
|
||||
neworder.forEach((itemid, index) => {
|
||||
const item = this.getElement(selector, itemid) ?? dettachedelements[itemid];
|
||||
let item = this.getElement(selector, itemid) ?? dettachedelements[itemid] ?? createMethod(container, itemid);
|
||||
if (item === undefined) {
|
||||
// Missing elements cannot be sorted.
|
||||
return;
|
||||
}
|
||||
// Get the current elemnt at that position.
|
||||
const currentitem = container.children[index];
|
||||
if (currentitem === undefined) {
|
||||
@ -375,7 +447,7 @@ export default class Component extends BaseComponent {
|
||||
// Remove the remaining elements.
|
||||
while (container.children.length > neworder.length) {
|
||||
const lastchild = container.lastChild;
|
||||
if (lastchild.classList.contains('dndupload-preview')) {
|
||||
if (lastchild?.classList?.contains('dndupload-preview')) {
|
||||
dndFakeActivity = lastchild;
|
||||
} else {
|
||||
dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
|
||||
|
255
course/format/amd/src/local/content/actions.js
Normal file
255
course/format/amd/src/local/content/actions.js
Normal file
@ -0,0 +1,255 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Course state actions dispatcher.
|
||||
*
|
||||
* This module captures all data-dispatch links in the course content and dispatch the proper
|
||||
* state mutation, including any confirmation and modal required.
|
||||
*
|
||||
* @module core_courseformat/local/content/actions
|
||||
* @class core_courseformat/local/content/actions
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import ModalFactory from 'core/modal_factory';
|
||||
import ModalEvents from 'core/modal_events';
|
||||
import Templates from 'core/templates';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {getList} from 'core/normalise';
|
||||
|
||||
// Load global strings.
|
||||
prefetchStrings('core', ['movecoursesection', 'movecoursemodule']);
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
/**
|
||||
* Constructor hook.
|
||||
*/
|
||||
create() {
|
||||
// Optional component name for debugging.
|
||||
this.name = 'content_actions';
|
||||
// Default query selectors.
|
||||
this.selectors = {
|
||||
ACTIONLINK: `[data-action]`,
|
||||
SECTIONLINK: `[data-for='section']`,
|
||||
CMLINK: `[data-for='cm']`,
|
||||
SECTIONNODE: `[data-for='sectionnode']`,
|
||||
TOGGLER: `[data-toggle='collapse']`,
|
||||
};
|
||||
// Component css classes.
|
||||
this.classes = {
|
||||
DISABLED: `disabled`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state ready method.
|
||||
*
|
||||
*/
|
||||
stateReady() {
|
||||
// Delegate dispatch clicks.
|
||||
this.addEventListener(
|
||||
this.element,
|
||||
'click',
|
||||
this._dispatchClick
|
||||
);
|
||||
}
|
||||
|
||||
_dispatchClick(event) {
|
||||
const target = event.target.closest(this.selectors.ACTIONLINK);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke proper method.
|
||||
const methodName = this._actionMethodName(target.dataset.action);
|
||||
|
||||
if (this[methodName] !== undefined) {
|
||||
this[methodName](target, event);
|
||||
}
|
||||
}
|
||||
|
||||
_actionMethodName(name) {
|
||||
const requestName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `_request${requestName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a move section request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestMoveSection(target, event) {
|
||||
// Check we have an id.
|
||||
const sectionId = target.dataset.id;
|
||||
if (!sectionId) {
|
||||
return;
|
||||
}
|
||||
const sectionInfo = this.reactive.get('section', sectionId);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Collect section information from the state.
|
||||
const exporter = this.reactive.getExporter();
|
||||
const data = exporter.course(this.reactive.state);
|
||||
|
||||
// Add the target section id and title.
|
||||
data.sectionid = sectionInfo.id;
|
||||
data.sectiontitle = sectionInfo.title;
|
||||
|
||||
// Build the modal parameters from the event data.
|
||||
const modalParams = {
|
||||
title: getString('movecoursesection', 'core'),
|
||||
body: Templates.render('core_courseformat/local/content/movesection', data),
|
||||
};
|
||||
|
||||
// Create the modal.
|
||||
const modal = await this._modalBodyRenderedPromise(modalParams);
|
||||
|
||||
const modalBody = getList(modal.getBody())[0];
|
||||
|
||||
// Disable current element and section zero.
|
||||
const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);
|
||||
this._disableLink(currentElement);
|
||||
const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);
|
||||
this._disableLink(generalSection);
|
||||
|
||||
// Capture click.
|
||||
modalBody.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {
|
||||
return;
|
||||
}
|
||||
if (target.getAttribute('aria-disabled')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);
|
||||
modal.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a move cm request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestMoveCm(target, event) {
|
||||
// Check we have an id.
|
||||
const cmId = target.dataset.id;
|
||||
if (!cmId) {
|
||||
return;
|
||||
}
|
||||
const cmInfo = this.reactive.get('cm', cmId);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Collect section information from the state.
|
||||
const exporter = this.reactive.getExporter();
|
||||
const data = exporter.course(this.reactive.state);
|
||||
|
||||
// Add the target cm info.
|
||||
data.cmid = cmInfo.id;
|
||||
data.cmname = cmInfo.name;
|
||||
|
||||
// Build the modal parameters from the event data.
|
||||
const modalParams = {
|
||||
title: getString('movecoursemodule', 'core'),
|
||||
body: Templates.render('core_courseformat/local/content/movecm', data),
|
||||
};
|
||||
|
||||
// Create the modal.
|
||||
const modal = await this._modalBodyRenderedPromise(modalParams);
|
||||
|
||||
const modalBody = getList(modal.getBody())[0];
|
||||
|
||||
// Disable current element.
|
||||
let currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);
|
||||
this._disableLink(currentElement);
|
||||
|
||||
// Open the cm section node if possible.
|
||||
currentElement.closest(this.selectors.SECTIONNODE)?.querySelector(this.selectors.TOGGLER)?.click();
|
||||
|
||||
// Capture click.
|
||||
modalBody.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {
|
||||
return;
|
||||
}
|
||||
if (target.getAttribute('aria-disabled')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Get draggable data from cm or section to dispatch.
|
||||
let targetSectionId;
|
||||
let targetCmId;
|
||||
if (target.dataset.for == 'cm') {
|
||||
const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);
|
||||
targetSectionId = dropData.sectionid;
|
||||
targetCmId = dropData.nextcmid;
|
||||
} else {
|
||||
const section = this.reactive.get('section', target.dataset.id);
|
||||
targetSectionId = target.dataset.id;
|
||||
targetCmId = section?.cmlist[0];
|
||||
}
|
||||
|
||||
this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);
|
||||
modal.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an element with a copy with a different tag name.
|
||||
*
|
||||
* @param {Element} element the original element
|
||||
*/
|
||||
_disableLink(element) {
|
||||
if (element) {
|
||||
element.style.pointerEvents = 'none';
|
||||
element.style.userSelect = 'none';
|
||||
element.classList.add(this.classes.DISABLED);
|
||||
element.setAttribute('aria-disabled', true);
|
||||
element.addEventListener('click', event => event.preventDefault());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a modal and return a body ready promise.
|
||||
*
|
||||
* @param {object} modalParams the modal params
|
||||
* @return {Promise} the modal body ready promise
|
||||
*/
|
||||
_modalBodyRenderedPromise(modalParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ModalFactory.create(modalParams).then((modal) => {
|
||||
// Handle body loading event.
|
||||
modal.getRoot().on(ModalEvents.bodyRendered, () => {
|
||||
resolve(modal);
|
||||
});
|
||||
modal.show();
|
||||
return;
|
||||
}).catch(() => {
|
||||
reject(`Cannot load modal content`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -123,6 +123,7 @@ export default class {
|
||||
type: 'cm',
|
||||
id: cminfo.id,
|
||||
name: cminfo.name,
|
||||
sectionid: cminfo.sectionid,
|
||||
nextcmid,
|
||||
};
|
||||
}
|
||||
|
@ -192,7 +192,12 @@ class cm implements renderable, templatable {
|
||||
$data->controlmenu = $controlmenu->export_for_template($output);
|
||||
|
||||
// Move and select options.
|
||||
$data->moveicon = course_get_cm_move($mod, $returnsection);
|
||||
if ($format->supports_components()) {
|
||||
$data->moveicon = $output->pix_icon('i/dragdrop', '', 'moodle', ['class' => 'editing_move dragicon']);
|
||||
} else {
|
||||
// Add the legacy YUI move link.
|
||||
$data->moveicon = course_get_cm_move($mod, $returnsection);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
@ -85,11 +85,11 @@ class controlmenu implements renderable, templatable {
|
||||
$url = empty($value['url']) ? '' : $value['url'];
|
||||
$icon = empty($value['icon']) ? '' : $value['icon'];
|
||||
$name = empty($value['name']) ? '' : $value['name'];
|
||||
$attr = empty($value['attr']) ? array() : $value['attr'];
|
||||
$attr = empty($value['attr']) ? [] : $value['attr'];
|
||||
$class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class'];
|
||||
$al = new action_menu_link_secondary(
|
||||
new moodle_url($url),
|
||||
new pix_icon($icon, '', null, array('class' => "smallicon " . $class)),
|
||||
new pix_icon($icon, '', null, ['class' => "smallicon " . $class]),
|
||||
$name,
|
||||
$attr
|
||||
);
|
||||
@ -124,6 +124,7 @@ class controlmenu implements renderable, templatable {
|
||||
$sectionreturn = $format->get_section_number();
|
||||
$user = $USER;
|
||||
|
||||
$usecomponents = $format->supports_components();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$numsections = $format->get_last_section_number();
|
||||
$isstealth = $section->section > $numsections;
|
||||
@ -141,12 +142,13 @@ class controlmenu implements renderable, templatable {
|
||||
$streditsection = get_string('editsection');
|
||||
}
|
||||
|
||||
$controls['edit'] = array(
|
||||
'url' => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)),
|
||||
$controls['edit'] = [
|
||||
'url' => new moodle_url('/course/editsection.php', ['id' => $section->id, 'sr' => $sectionreturn]),
|
||||
'icon' => 'i/settings',
|
||||
'name' => $streditsection,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon edit'));
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => ['class' => 'icon edit'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($section->section) {
|
||||
@ -156,53 +158,79 @@ class controlmenu implements renderable, templatable {
|
||||
if ($section->visible) { // Show the hide/show eye.
|
||||
$strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
|
||||
$url->param('hide', $section->section);
|
||||
$controls['visiblity'] = array(
|
||||
$controls['visiblity'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/hide',
|
||||
'name' => $strhidefromothers,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon editing_showhide',
|
||||
'data-sectionreturn' => $sectionreturn, 'data-action' => 'hide'));
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => [
|
||||
'class' => 'icon editing_showhide',
|
||||
'data-sectionreturn' => $sectionreturn,
|
||||
'data-action' => 'hide',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
|
||||
$url->param('show', $section->section);
|
||||
$controls['visiblity'] = array(
|
||||
$controls['visiblity'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/show',
|
||||
'name' => $strshowfromothers,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon editing_showhide',
|
||||
'data-sectionreturn' => $sectionreturn, 'data-action' => 'show'));
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => [
|
||||
'class' => 'icon editing_showhide',
|
||||
'data-sectionreturn' => $sectionreturn,
|
||||
'data-action' => 'show',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sectionreturn) {
|
||||
if (has_capability('moodle/course:movesections', $coursecontext, $user)) {
|
||||
$url = clone($baseurl);
|
||||
if ($section->section > 1) { // Add a arrow to move section up.
|
||||
$url->param('section', $section->section);
|
||||
$url->param('move', -1);
|
||||
$strmoveup = get_string('moveup');
|
||||
$controls['moveup'] = array(
|
||||
'url' => $url,
|
||||
'icon' => 'i/up',
|
||||
'name' => $strmoveup,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon moveup'));
|
||||
}
|
||||
if (!$sectionreturn && has_capability('moodle/course:movesections', $coursecontext, $user)) {
|
||||
if ($usecomponents) {
|
||||
// This tool will appear only when the state is ready.
|
||||
$url = clone ($baseurl);
|
||||
$url->param('movesection', $section->section);
|
||||
$url->param('section', $section->section);
|
||||
$controls['movesection'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/dragdrop',
|
||||
'name' => get_string('move', 'moodle'),
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => [
|
||||
'class' => 'icon move waitstate',
|
||||
'data-action' => 'moveSection',
|
||||
'data-id' => $section->id,
|
||||
],
|
||||
];
|
||||
}
|
||||
// Legacy move up and down links for non component-based formats.
|
||||
$url = clone($baseurl);
|
||||
if ($section->section > 1) { // Add a arrow to move section up.
|
||||
$url->param('section', $section->section);
|
||||
$url->param('move', -1);
|
||||
$strmoveup = get_string('moveup');
|
||||
$controls['moveup'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/up',
|
||||
'name' => $strmoveup,
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => ['class' => 'icon moveup whilenostate'],
|
||||
];
|
||||
}
|
||||
|
||||
$url = clone($baseurl);
|
||||
if ($section->section < $numsections) { // Add a arrow to move section down.
|
||||
$url->param('section', $section->section);
|
||||
$url->param('move', 1);
|
||||
$strmovedown = get_string('movedown');
|
||||
$controls['movedown'] = array(
|
||||
'url' => $url,
|
||||
'icon' => 'i/down',
|
||||
'name' => $strmovedown,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon movedown'));
|
||||
}
|
||||
$url = clone($baseurl);
|
||||
if ($section->section < $numsections) { // Add a arrow to move section down.
|
||||
$url->param('section', $section->section);
|
||||
$url->param('move', 1);
|
||||
$strmovedown = get_string('movedown');
|
||||
$controls['movedown'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/down',
|
||||
'name' => $strmovedown,
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => ['class' => 'icon movedown whilenostate'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,17 +241,22 @@ class controlmenu implements renderable, templatable {
|
||||
} else {
|
||||
$strdelete = get_string('deletesection');
|
||||
}
|
||||
$url = new moodle_url('/course/editsection.php', array(
|
||||
'id' => $section->id,
|
||||
'sr' => $sectionreturn,
|
||||
'delete' => 1,
|
||||
'sesskey' => sesskey()));
|
||||
$controls['delete'] = array(
|
||||
$url = new moodle_url(
|
||||
'/course/editsection.php',
|
||||
[
|
||||
'id' => $section->id,
|
||||
'sr' => $sectionreturn,
|
||||
'delete' => 1,
|
||||
'sesskey' => sesskey(),
|
||||
]
|
||||
);
|
||||
$controls['delete'] = [
|
||||
'url' => $url,
|
||||
'icon' => 'i/delete',
|
||||
'name' => $strdelete,
|
||||
'pixattr' => array('class' => ''),
|
||||
'attr' => array('class' => 'icon editing_delete'));
|
||||
'pixattr' => ['class' => ''],
|
||||
'attr' => ['class' => 'icon editing_delete'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
133
course/format/templates/local/content/movecm.mustache
Normal file
133
course/format/templates/local/content/movecm.mustache
Normal file
@ -0,0 +1,133 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_courseformat/local/movemodal/movecm
|
||||
|
||||
Displays the course index.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"cmname": "Activity name",
|
||||
"cmid": 42,
|
||||
"sections": [
|
||||
{
|
||||
"title": "General",
|
||||
"id": 42,
|
||||
"number": 1,
|
||||
"sectionurl": "#",
|
||||
"hascms": true,
|
||||
"cms": [
|
||||
{
|
||||
"name": "Glossary of characters",
|
||||
"id": "10",
|
||||
"url": "#"
|
||||
},
|
||||
{
|
||||
"name": "World Cinema forum",
|
||||
"id": "11",
|
||||
"url": "#"
|
||||
},
|
||||
{
|
||||
"name": "Announcements",
|
||||
"id": "12",
|
||||
"url": "#"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "City of God or Cidade de Deus",
|
||||
"id": "43",
|
||||
"number": "2",
|
||||
"sectionurl": "#",
|
||||
"hascms": true,
|
||||
"cms": [
|
||||
{
|
||||
"name": "Resources",
|
||||
"id": "13",
|
||||
"url": "#"
|
||||
},
|
||||
{
|
||||
"name": "Studying City of God by Stephen Smith Bergman-Messerschmidt",
|
||||
"id": "14",
|
||||
"url": "#"
|
||||
},
|
||||
{
|
||||
"name": "Film education study guide",
|
||||
"id": "15",
|
||||
"url": "#"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}}
|
||||
<p data-for="sectionname">{{#str}} movefull, moodle, {{cmname}} {{/str}}:</p>
|
||||
<nav class="collapse-list" id="destination-selector">
|
||||
{{#sections}}
|
||||
<div data-for="sectionnode">
|
||||
<div class="collapse-list-item d-flex"
|
||||
id="movemodalsection{{number}}"
|
||||
data-for="section_item"
|
||||
>
|
||||
<a data-toggle="collapse"
|
||||
href="#movemodalcollapse{{number}}"
|
||||
aria-expanded="false"
|
||||
aria-controls="movemodalcollapse{{number}}"
|
||||
class="collapse-list-link icons-collapse-expand collapsed"
|
||||
>
|
||||
<span class="collapsed-icon icon-no-margin mr-1"
|
||||
data-toggle="tooltip" title="{{#str}} expand, core {{/str}}">
|
||||
{{#pix}} t/collapsed, core {{/pix}}
|
||||
<span class="sr-only">{{#str}} expand, core {{/str}}</span>
|
||||
</span>
|
||||
<span class="expanded-icon icon-no-margin mr-1"
|
||||
data-toggle="tooltip" title="{{#str}} collapse, core {{/str}}">
|
||||
{{#pix}} t/expanded, core {{/pix}}
|
||||
<span class="sr-only">{{#str}} collapse, core {{/str}}</span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="{{{sectionurl}}}"
|
||||
class="collapse-list-link text-truncate"
|
||||
data-for="section"
|
||||
data-id="{{id}}"
|
||||
data-number="{{number}}"
|
||||
>
|
||||
{{{title}}}
|
||||
</a>
|
||||
<span class="dragicon ml-auto">{{#pix}}i/dragdrop{{/pix}}</span>
|
||||
</div>
|
||||
<div id="movemodalcollapse{{number}}"
|
||||
class="collapse-list-item-content collapse"
|
||||
aria-labelledby="movemodalsection{{number}}">
|
||||
<ul class="unlist" data-for="cmlist" data-id="{{id}}">
|
||||
{{#cms}}
|
||||
<li class="collapse-list-item d-flex">
|
||||
<a class="collapse-list-link text-truncate"
|
||||
href="{{{url}}}"
|
||||
data-for="cm"
|
||||
data-id="{{id}}"
|
||||
>
|
||||
{{{name}}}
|
||||
</a>
|
||||
</li>
|
||||
{{/cms}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/sections}}
|
||||
</nav>
|
61
course/format/templates/local/content/movesection.mustache
Normal file
61
course/format/templates/local/content/movesection.mustache
Normal file
@ -0,0 +1,61 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_courseformat/local/movemodal/movesection
|
||||
|
||||
Displays the course index.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"sectionid": 23,
|
||||
"sectiontitle": "Section title",
|
||||
"sections": [
|
||||
{
|
||||
"title": "General",
|
||||
"id": 42,
|
||||
"number": 1,
|
||||
"sectionurl": "#",
|
||||
"isactive": 1
|
||||
},
|
||||
{
|
||||
"title": "City of God or Cidade de Deus",
|
||||
"id": "43",
|
||||
"number": "2",
|
||||
"sectionurl": "#",
|
||||
"isactive": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}}
|
||||
<p data-for="sectionname">{{#str}} movefull, moodle, {{sectiontitle}} {{/str}}:</p>
|
||||
<nav class="collapse-list" id="destination-selector">
|
||||
{{#sections}}
|
||||
<div
|
||||
class="collapse-list-item"
|
||||
>
|
||||
<a href="{{{sectionurl}}}"
|
||||
class="collapse-list-link text-truncate"
|
||||
data-for="section"
|
||||
data-id="{{id}}"
|
||||
data-number="{{number}}"
|
||||
>
|
||||
{{{title}}}
|
||||
</a>
|
||||
</div>
|
||||
{{/sections}}
|
||||
</nav>
|
@ -1820,6 +1820,23 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
|
||||
);
|
||||
}
|
||||
|
||||
// Move (only for component compatible formats).
|
||||
if ($courseformat->supports_components()) {
|
||||
$actions['move'] = new action_menu_link_secondary(
|
||||
new moodle_url($baseurl, [
|
||||
'sesskey' => sesskey(),
|
||||
'copy' => $mod->id,
|
||||
]),
|
||||
new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']),
|
||||
$str->move,
|
||||
[
|
||||
'class' => 'editing_movecm',
|
||||
'data-action' => 'moveCm',
|
||||
'data-id' => $mod->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Indent.
|
||||
if ($hasmanageactivities && $indent >= 0) {
|
||||
$indentlimits = new stdClass();
|
||||
@ -3217,17 +3234,17 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
|
||||
$config = new stdClass();
|
||||
}
|
||||
|
||||
// The URL to use for resource changes
|
||||
// The URL to use for resource changes.
|
||||
if (!isset($config->resourceurl)) {
|
||||
$config->resourceurl = '/course/rest.php';
|
||||
}
|
||||
|
||||
// The URL to use for section changes
|
||||
// The URL to use for section changes.
|
||||
if (!isset($config->sectionurl)) {
|
||||
$config->sectionurl = '/course/rest.php';
|
||||
}
|
||||
|
||||
// Any additional parameters which need to be included on page submission
|
||||
// Any additional parameters which need to be included on page submission.
|
||||
if (!isset($config->pageparams)) {
|
||||
$config->pageparams = array();
|
||||
}
|
||||
|
@ -762,38 +762,84 @@ class behat_course extends behat_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the specified activity to the first slot of a section. This step is experimental when using it in Javascript tests. Editing mode should be on.
|
||||
* Moves the specified activity to the first slot of a section.
|
||||
*
|
||||
* Editing mode should be on.
|
||||
*
|
||||
* @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
|
||||
* @param string $activityname The activity name
|
||||
* @param int $sectionnumber The number of section
|
||||
*/
|
||||
public function i_move_activity_to_section($activityname, $sectionnumber) {
|
||||
public function i_move_activity_to_section($activityname, $sectionnumber): void {
|
||||
// Ensure the destination is valid.
|
||||
$sectionxpath = $this->section_exists($sectionnumber);
|
||||
|
||||
// Not all formats are compatible with the move tool.
|
||||
$activitynode = $this->get_activity_node($activityname);
|
||||
if (!$activitynode->find('css', "[data-action='moveCm']", false, false, 0)) {
|
||||
// Execute the legacy YUI move option.
|
||||
$this->i_move_activity_to_section_yui($activityname, $sectionnumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// JS enabled.
|
||||
if ($this->running_javascript()) {
|
||||
$this->i_open_actions_menu($activityname);
|
||||
$this->execute(
|
||||
'behat_course::i_click_on_in_the_activity',
|
||||
[get_string('move'), "link", $this->escape($activityname)]
|
||||
);
|
||||
$this->execute("behat_general::i_click_on_in_the", [
|
||||
"[data-for='section'][data-number='$sectionnumber']",
|
||||
'css_element',
|
||||
"[data-region='modal-container']",
|
||||
'css_element'
|
||||
]);
|
||||
} else {
|
||||
$this->execute(
|
||||
'behat_course::i_click_on_in_the_activity',
|
||||
[get_string('move'), "link", $this->escape($activityname)]
|
||||
);
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
["li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the specified activity to the first slot of a section using the YUI course format.
|
||||
*
|
||||
* This step is experimental when using it in Javascript tests. Editing mode should be on.
|
||||
*
|
||||
* @param string $activityname The activity name
|
||||
* @param int $sectionnumber The number of section
|
||||
*/
|
||||
public function i_move_activity_to_section_yui($activityname, $sectionnumber): void {
|
||||
// Ensure the destination is valid.
|
||||
$sectionxpath = $this->section_exists($sectionnumber);
|
||||
|
||||
// JS enabled.
|
||||
if ($this->running_javascript()) {
|
||||
|
||||
$activitynode = $this->get_activity_element('Move', 'icon', $activityname);
|
||||
$destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
|
||||
|
||||
$this->execute("behat_general::i_drag_and_i_drop_it_in",
|
||||
array($this->escape($activitynode->getXpath()), "xpath_element",
|
||||
$this->escape($destinationxpath), "xpath_element")
|
||||
$this->execute(
|
||||
"behat_general::i_drag_and_i_drop_it_in",
|
||||
[
|
||||
$this->escape($activitynode->getXpath()), "xpath_element",
|
||||
$this->escape($destinationxpath), "xpath_element",
|
||||
]
|
||||
);
|
||||
|
||||
} else {
|
||||
// Following links with no-JS.
|
||||
|
||||
// Moving to the fist spot of the section (before all other section's activities).
|
||||
$this->execute('behat_course::i_click_on_in_the_activity',
|
||||
array("a.editing_move", "css_element", $this->escape($activityname))
|
||||
$this->execute(
|
||||
'behat_course::i_click_on_in_the_activity',
|
||||
["a.editing_move", "css_element", $this->escape($activityname)]
|
||||
);
|
||||
|
||||
$this->execute('behat_general::i_click_on_in_the',
|
||||
array("li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element")
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
["li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,6 @@ Feature: Activities can be moved between sections
|
||||
| section | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Recent activity" block
|
||||
And I follow "Delete Recent activity block"
|
||||
And I press "Yes"
|
||||
|
||||
Scenario: Move activities in a single page course with Javascript disabled
|
||||
When I move "Test forum name" activity to section "2"
|
||||
@ -56,3 +53,8 @@ Feature: Activities can be moved between sections
|
||||
And I follow "Topic 1"
|
||||
When I move "Second forum name" activity to section "1"
|
||||
Then "Second forum name" "link" should appear before "Test forum name" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Move activity with javascript
|
||||
When I move "Test forum name" activity to section "3"
|
||||
Then I should see "Test forum name" in the "Topic 3" "section"
|
||||
|
@ -6,32 +6,28 @@ Feature: Sections can be moved
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format | coursedisplay | numsections |
|
||||
| Course 1 | C1 | topics | 0 | 5 |
|
||||
| Course 1 | C1 | topics | 0 | 5 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| forum | Test forum name | Test forum name description | C1 | forum1 | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
Scenario: Move up and down a section with Javascript disabled in a single page course
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| forum | Test forum name | Test forum name description | C1 | forum1 | 1 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I move down section "1"
|
||||
Then I should see "Test forum name" in the "Topic 2" "section"
|
||||
And I move up section "2"
|
||||
And I should see "Test forum name" in the "Topic 1" "section"
|
||||
|
||||
Scenario: Move up and down a section with Javascript disabled in the course home of a course using paged mode
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| forum | Test forum name | Test forum name description | C1 | forum1 | 1 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
Given I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course layout | Show one section per page |
|
||||
And I press "Save and display"
|
||||
@ -41,11 +37,7 @@ Feature: Sections can be moved
|
||||
And I should see "Test forum name" in the "Topic 1" "section"
|
||||
|
||||
Scenario: Sections can not be moved with Javascript disabled in a section page of a course using paged mode
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| forum | Test forum name | Test forum name description | C1 | forum1 | 2 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
Given I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course layout | Show one section per page |
|
||||
And I press "Save and display"
|
||||
@ -54,3 +46,10 @@ Feature: Sections can be moved
|
||||
And "Topic 3" "section" should not exist
|
||||
And "Move down" "link" should not exist
|
||||
And "Move up" "link" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Move section with javascript
|
||||
When I open section "1" edit menu
|
||||
And I click on "Move" "link" in the "Topic 1" "section"
|
||||
And I click on "Topic 3" "link" in the ".modal-body" "css_element"
|
||||
Then I should see "Test forum name" in the "Topic 3" "section"
|
||||
|
2
lib/amd/build/local/reactive/reactive.min.js
vendored
2
lib/amd/build/local/reactive/reactive.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
@ -191,6 +191,20 @@ export default class {
|
||||
return this.stateManager.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state data.
|
||||
*
|
||||
* Components access the state frequently. This convenience method is a shortcut to
|
||||
* this.reactive.state.stateManager.get() method.
|
||||
*
|
||||
* @param {String} name the state object name
|
||||
* @param {*} id an optional object id for state maps.
|
||||
* @return {Object|undefined} the state object found
|
||||
*/
|
||||
get(name, id) {
|
||||
return this.stateManager.get(name, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the initial state promise.
|
||||
*
|
||||
|
@ -7,6 +7,11 @@ $bg-inverse-link-color: #fff !default;
|
||||
|
||||
$dropzone-border: $gray-900 !default;
|
||||
|
||||
$collapse-list-item-padding-y: 0.5rem !default;
|
||||
$collapse-list-item-padding-x: 1rem !default;
|
||||
$collapse-list-item-hover-bg: theme-color-level('info', -11) !default;
|
||||
$collapse-list-item-hover-border: theme-color-level('info', -9) !default;
|
||||
|
||||
$font-size-xs: ($font-size-base * .75) !default;
|
||||
|
||||
#region-main {
|
||||
@ -2829,3 +2834,35 @@ body.dragging {
|
||||
visibility: visible;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
// Generic classes reactive components can use.
|
||||
|
||||
.waitstate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stateready {
|
||||
.waitstate {
|
||||
display: inherit;
|
||||
}
|
||||
.whilenostate {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsible list.
|
||||
|
||||
.collapse-list {
|
||||
.collapse-list-item {
|
||||
padding: $collapse-list-item-padding-y $collapse-list-item-padding-x;
|
||||
@include hover-focus() {
|
||||
background-color: $collapse-list-item-hover-bg;
|
||||
border-color: $collapse-list-item-hover-border;
|
||||
}
|
||||
}
|
||||
.collapse-list-item-content {
|
||||
.collapse-list-item {
|
||||
padding-left: calc(#{$collapse-list-item-padding-x} * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12187,6 +12187,24 @@ body.dragging .dragging {
|
||||
visibility: visible;
|
||||
cursor: move; }
|
||||
|
||||
.waitstate {
|
||||
display: none; }
|
||||
|
||||
.stateready .waitstate {
|
||||
display: inherit; }
|
||||
|
||||
.stateready .whilenostate {
|
||||
display: none; }
|
||||
|
||||
.collapse-list .collapse-list-item {
|
||||
padding: 0.5rem 1rem; }
|
||||
.collapse-list .collapse-list-item:hover, .collapse-list .collapse-list-item:focus {
|
||||
background-color: #e0f0f2;
|
||||
border-color: #b8dce2; }
|
||||
|
||||
.collapse-list .collapse-list-item-content .collapse-list-item {
|
||||
padding-left: calc(1rem * 3); }
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
@ -12187,6 +12187,24 @@ body.dragging .dragging {
|
||||
visibility: visible;
|
||||
cursor: move; }
|
||||
|
||||
.waitstate {
|
||||
display: none; }
|
||||
|
||||
.stateready .waitstate {
|
||||
display: inherit; }
|
||||
|
||||
.stateready .whilenostate {
|
||||
display: none; }
|
||||
|
||||
.collapse-list .collapse-list-item {
|
||||
padding: 0.5rem 1rem; }
|
||||
.collapse-list .collapse-list-item:hover, .collapse-list .collapse-list-item:focus {
|
||||
background-color: #e0f0f2;
|
||||
border-color: #b8dce2; }
|
||||
|
||||
.collapse-list .collapse-list-item-content .collapse-list-item {
|
||||
padding-left: calc(1rem * 3); }
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user