first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,513 @@
YUI.add('moodle-course-categoryexpander', function (Y, NAME) {
/**
* Adds toggling of subcategory with automatic loading using AJAX.
*
* This also includes application of an animation to improve user experience.
*
* @module moodle-course-categoryexpander
*/
/**
* The course category expander.
*
* @constructor
* @class Y.Moodle.course.categoryexpander
*/
var CSS = {
CONTENTNODE: 'content',
COLLAPSEALL: 'collapse-all',
DISABLED: 'disabled',
LOADED: 'loaded',
NOTLOADED: 'notloaded',
SECTIONCOLLAPSED: 'collapsed',
HASCHILDREN: 'with_children'
},
SELECTORS = {
WITHCHILDRENTREES: '.with_children',
LOADEDTREES: '.with_children.loaded',
CONTENTNODE: '.content',
CATEGORYLISTENLINK: '.category .info .categoryname',
CATEGORYSPINNERLOCATION: '.categoryname',
CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed',
CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed',
CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)',
COLLAPSEEXPAND: '.collapseexpand',
COURSEBOX: '.coursebox',
COURSEBOXLISTENLINK: '.coursebox .moreinfo',
COURSEBOXSPINNERLOCATION: '.info .moreinfo',
COURSECATEGORYTREE: '.course_category_tree',
PARENTWITHCHILDREN: '.category'
},
NS = Y.namespace('Moodle.course.categoryexpander'),
TYPE_CATEGORY = 0,
TYPE_COURSE = 1,
URL = M.cfg.wwwroot + '/course/category.ajax.php';
/**
* Set up the category expander.
*
* No arguments are required.
*
* @method init
*/
NS.init = function() {
var doc = Y.one(Y.config.doc);
doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this);
doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this);
doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this);
// Only set up they keybaord listeners when tab is first pressed - it
// may never happen and modifying the DOM on a large number of nodes
// can be very expensive.
doc.once('key', this.setup_keyboard_listeners, 'tab', this);
};
/**
* Set up keyboard expansion for course content.
*
* This includes setting up the delegation but also adding the nodes to the
* tabflow.
*
* @method setup_keyboard_listeners
*/
NS.setup_keyboard_listeners = function() {
var doc = Y.one(Y.config.doc);
Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander');
doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0');
Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this);
};
/**
* Expand all categories.
*
* @method expand_category
* @private
* @param {Node} categorynode The node to expand
*/
NS.expand_category = function(categorynode) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the expand_category with the _expand_category function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.expand_category = NS._expand_category;
NS.expand_category(categorynode);
});
};
NS._expand_category = function(categorynode) {
var categoryid,
depth;
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Toggle the animation of the clicked category node.
*
* @method toggle_category_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_category_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_category_expansion = NS._toggle_category_expansion;
NS.toggle_category_expansion(e);
});
};
/**
* Toggle the animation of the clicked coursebox node.
*
* @method toggle_coursebox_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_coursebox_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion;
NS.toggle_coursebox_expansion(e);
});
e.preventDefault();
};
NS._toggle_coursebox_expansion = function(e) {
var courseboxnode;
// Grab the parent category container - this is where the new content will be added.
courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true);
e.preventDefault();
if (courseboxnode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(courseboxnode);
return;
}
this._toggle_generic_expansion({
parentnode: courseboxnode,
childnode: courseboxnode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION,
data: {
courseid: courseboxnode.getData('courseid'),
type: TYPE_COURSE
}
});
};
NS._toggle_category_expansion = function(e) {
var categorynode,
categoryid,
depth;
if (e.target.test('a') || e.target.test('img')) {
// Return early if either an anchor or an image were clicked.
return;
}
// Grab the parent category container - this is where the new content will be added.
categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true);
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Wrapper function to handle toggling of generic types.
*
* @method _toggle_generic_expansion
* @private
* @param {Object} config
*/
NS._toggle_generic_expansion = function(config) {
var spinner;
if (config.spinnerhandle) {
// Add a spinner to give some feedback to the user.
spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
}
// Fetch the data.
Y.io(URL, {
method: 'POST',
context: this,
on: {
complete: this.process_results
},
data: config.data,
"arguments": {
parentnode: config.parentnode,
childnode: config.childnode,
spinner: spinner
}
});
};
/**
* Apply the animation on the supplied node.
*
* @method run_expansion
* @private
* @param {Node} categorynode The node to apply the animation to
*/
NS.run_expansion = function(categorynode) {
var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
self = this,
ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
// Add our animation to the categorychildren.
this.add_animation(categorychildren);
// If we already have the class, remove it before showing otherwise we perform the
// animation whilst the node is hidden.
if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
// To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
categorychildren.setStyle('height', '0');
categorynode.removeClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'true');
categorychildren.fx.set('reverse', false);
} else {
categorychildren.fx.set('reverse', true);
categorychildren.fx.once('end', function(e, categorynode) {
categorynode.addClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'false');
}, this, categorynode);
}
categorychildren.fx.once('end', function(e, categorychildren) {
// Remove the styles that the animation has set.
categorychildren.setStyles({
height: '',
opacity: ''
});
// To avoid memory gobbling, remove the animation. It will be added back if called again.
this.destroy();
self.update_collapsible_actions(ancestor);
}, categorychildren.fx, categorychildren);
// Now that everything has been set up, run the animation.
categorychildren.fx.run();
};
/**
* Toggle collapsing of all nodes.
*
* @method collapse_expand_all
* @private
* @param {EventFacade} e
*/
NS.collapse_expand_all = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.collapse_expand_all = NS._collapse_expand_all;
NS.collapse_expand_all(e);
});
e.preventDefault();
};
NS._collapse_expand_all = function(e) {
// The collapse/expand button has no actual target but we need to prevent it's default
// action to ensure we don't make the page reload/jump.
e.preventDefault();
if (e.currentTarget.hasClass(CSS.DISABLED)) {
// The collapse/expand is currently disabled.
return;
}
var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
if (!ancestor) {
return;
}
var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (collapseall.hasClass(CSS.COLLAPSEALL)) {
this.collapse_all(ancestor);
} else {
this.expand_all(ancestor);
}
this.update_collapsible_actions(ancestor);
};
NS.expand_all = function(ancestor) {
var finalexpansions = [];
ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) {
// Expand the hidden children first without animation.
c.removeClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED);
} else {
finalexpansions.push(c);
}
}, this);
// Run the final expansion with animation on the visible items.
Y.all(finalexpansions).each(function(c) {
this.expand_category(c);
}, this);
};
NS.collapse_all = function(ancestor) {
var finalcollapses = [];
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
finalcollapses.push(c);
} else {
// Collapse the visible items first
this.run_expansion(c);
}
}, this);
// Run the final collapses now that the these are hidden hidden.
Y.all(finalcollapses).each(function(c) {
c.addClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
}, this);
};
NS.update_collapsible_actions = function(ancestor) {
var foundmaximisedchildren = false,
// Grab the anchor for the collapseexpand all link.
togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (!togglelink) {
// We should always have a togglelink but ensure.
return;
}
// Search for any visibly expanded children.
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
// If we can find any collapsed ancestors, skip.
if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
return false;
}
foundmaximisedchildren = true;
return true;
});
if (foundmaximisedchildren) {
// At least one maximised child found. Show the collapseall.
togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
.addClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
} else {
// No maximised children found but there are collapsed children. Show the expandall.
togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
.removeClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
}
};
/**
* Process the data returned by Y.io.
* This includes appending it to the relevant part of the DOM, and applying our animations.
*
* @method process_results
* @private
* @param {String} tid The Transaction ID
* @param {Object} response The Reponse returned by Y.IO
* @param {Object} ioargs The additional arguments provided by Y.IO
*/
NS.process_results = function(tid, response, args) {
var newnode,
data;
try {
data = Y.JSON.parse(response.responseText);
if (data.error) {
return new M.core.ajaxException(data);
}
} catch (e) {
return new M.core.exception(e);
}
// Insert the returned data into a new Node.
newnode = Y.Node.create(data);
// Append to the existing child location.
args.childnode.appendChild(newnode);
// Now that we have content, we can swap the classes on the toggled container.
args.parentnode
.addClass(CSS.LOADED)
.removeClass(CSS.NOTLOADED);
// Toggle the open/close status of the node now that it's content has been loaded.
this.run_expansion(args.parentnode);
// Remove the spinner now that we've started to show the content.
if (args.spinner) {
args.spinner.hide().destroy();
}
};
/**
* Add our animation to the Node.
*
* @method add_animation
* @private
* @param {Node} childnode
*/
NS.add_animation = function(childnode) {
if (typeof childnode.fx !== "undefined") {
// The animation has already been plugged to this node.
return childnode;
}
childnode.plug(Y.Plugin.NodeFX, {
from: {
height: 0,
opacity: 0
},
to: {
// This sets a dynamic height in case the node content changes.
height: function(node) {
// Get expanded height (offsetHeight may be zero).
return node.get('scrollHeight');
},
opacity: 1
},
duration: 0.2
});
return childnode;
};
}, '@VERSION@', {"requires": ["node", "event-key"]});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,512 @@
YUI.add('moodle-course-categoryexpander', function (Y, NAME) {
/**
* Adds toggling of subcategory with automatic loading using AJAX.
*
* This also includes application of an animation to improve user experience.
*
* @module moodle-course-categoryexpander
*/
/**
* The course category expander.
*
* @constructor
* @class Y.Moodle.course.categoryexpander
*/
var CSS = {
CONTENTNODE: 'content',
COLLAPSEALL: 'collapse-all',
DISABLED: 'disabled',
LOADED: 'loaded',
NOTLOADED: 'notloaded',
SECTIONCOLLAPSED: 'collapsed',
HASCHILDREN: 'with_children'
},
SELECTORS = {
WITHCHILDRENTREES: '.with_children',
LOADEDTREES: '.with_children.loaded',
CONTENTNODE: '.content',
CATEGORYLISTENLINK: '.category .info .categoryname',
CATEGORYSPINNERLOCATION: '.categoryname',
CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed',
CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed',
CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)',
COLLAPSEEXPAND: '.collapseexpand',
COURSEBOX: '.coursebox',
COURSEBOXLISTENLINK: '.coursebox .moreinfo',
COURSEBOXSPINNERLOCATION: '.info .moreinfo',
COURSECATEGORYTREE: '.course_category_tree',
PARENTWITHCHILDREN: '.category'
},
NS = Y.namespace('Moodle.course.categoryexpander'),
TYPE_CATEGORY = 0,
TYPE_COURSE = 1,
URL = M.cfg.wwwroot + '/course/category.ajax.php';
/**
* Set up the category expander.
*
* No arguments are required.
*
* @method init
*/
NS.init = function() {
var doc = Y.one(Y.config.doc);
doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this);
doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this);
doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this);
// Only set up they keybaord listeners when tab is first pressed - it
// may never happen and modifying the DOM on a large number of nodes
// can be very expensive.
doc.once('key', this.setup_keyboard_listeners, 'tab', this);
};
/**
* Set up keyboard expansion for course content.
*
* This includes setting up the delegation but also adding the nodes to the
* tabflow.
*
* @method setup_keyboard_listeners
*/
NS.setup_keyboard_listeners = function() {
var doc = Y.one(Y.config.doc);
doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0');
Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this);
};
/**
* Expand all categories.
*
* @method expand_category
* @private
* @param {Node} categorynode The node to expand
*/
NS.expand_category = function(categorynode) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the expand_category with the _expand_category function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.expand_category = NS._expand_category;
NS.expand_category(categorynode);
});
};
NS._expand_category = function(categorynode) {
var categoryid,
depth;
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Toggle the animation of the clicked category node.
*
* @method toggle_category_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_category_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_category_expansion = NS._toggle_category_expansion;
NS.toggle_category_expansion(e);
});
};
/**
* Toggle the animation of the clicked coursebox node.
*
* @method toggle_coursebox_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_coursebox_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion;
NS.toggle_coursebox_expansion(e);
});
e.preventDefault();
};
NS._toggle_coursebox_expansion = function(e) {
var courseboxnode;
// Grab the parent category container - this is where the new content will be added.
courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true);
e.preventDefault();
if (courseboxnode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(courseboxnode);
return;
}
this._toggle_generic_expansion({
parentnode: courseboxnode,
childnode: courseboxnode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION,
data: {
courseid: courseboxnode.getData('courseid'),
type: TYPE_COURSE
}
});
};
NS._toggle_category_expansion = function(e) {
var categorynode,
categoryid,
depth;
if (e.target.test('a') || e.target.test('img')) {
// Return early if either an anchor or an image were clicked.
return;
}
// Grab the parent category container - this is where the new content will be added.
categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true);
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Wrapper function to handle toggling of generic types.
*
* @method _toggle_generic_expansion
* @private
* @param {Object} config
*/
NS._toggle_generic_expansion = function(config) {
var spinner;
if (config.spinnerhandle) {
// Add a spinner to give some feedback to the user.
spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
}
// Fetch the data.
Y.io(URL, {
method: 'POST',
context: this,
on: {
complete: this.process_results
},
data: config.data,
"arguments": {
parentnode: config.parentnode,
childnode: config.childnode,
spinner: spinner
}
});
};
/**
* Apply the animation on the supplied node.
*
* @method run_expansion
* @private
* @param {Node} categorynode The node to apply the animation to
*/
NS.run_expansion = function(categorynode) {
var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
self = this,
ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
// Add our animation to the categorychildren.
this.add_animation(categorychildren);
// If we already have the class, remove it before showing otherwise we perform the
// animation whilst the node is hidden.
if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
// To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
categorychildren.setStyle('height', '0');
categorynode.removeClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'true');
categorychildren.fx.set('reverse', false);
} else {
categorychildren.fx.set('reverse', true);
categorychildren.fx.once('end', function(e, categorynode) {
categorynode.addClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'false');
}, this, categorynode);
}
categorychildren.fx.once('end', function(e, categorychildren) {
// Remove the styles that the animation has set.
categorychildren.setStyles({
height: '',
opacity: ''
});
// To avoid memory gobbling, remove the animation. It will be added back if called again.
this.destroy();
self.update_collapsible_actions(ancestor);
}, categorychildren.fx, categorychildren);
// Now that everything has been set up, run the animation.
categorychildren.fx.run();
};
/**
* Toggle collapsing of all nodes.
*
* @method collapse_expand_all
* @private
* @param {EventFacade} e
*/
NS.collapse_expand_all = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.collapse_expand_all = NS._collapse_expand_all;
NS.collapse_expand_all(e);
});
e.preventDefault();
};
NS._collapse_expand_all = function(e) {
// The collapse/expand button has no actual target but we need to prevent it's default
// action to ensure we don't make the page reload/jump.
e.preventDefault();
if (e.currentTarget.hasClass(CSS.DISABLED)) {
// The collapse/expand is currently disabled.
return;
}
var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
if (!ancestor) {
return;
}
var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (collapseall.hasClass(CSS.COLLAPSEALL)) {
this.collapse_all(ancestor);
} else {
this.expand_all(ancestor);
}
this.update_collapsible_actions(ancestor);
};
NS.expand_all = function(ancestor) {
var finalexpansions = [];
ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) {
// Expand the hidden children first without animation.
c.removeClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED);
} else {
finalexpansions.push(c);
}
}, this);
// Run the final expansion with animation on the visible items.
Y.all(finalexpansions).each(function(c) {
this.expand_category(c);
}, this);
};
NS.collapse_all = function(ancestor) {
var finalcollapses = [];
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
finalcollapses.push(c);
} else {
// Collapse the visible items first
this.run_expansion(c);
}
}, this);
// Run the final collapses now that the these are hidden hidden.
Y.all(finalcollapses).each(function(c) {
c.addClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
}, this);
};
NS.update_collapsible_actions = function(ancestor) {
var foundmaximisedchildren = false,
// Grab the anchor for the collapseexpand all link.
togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (!togglelink) {
// We should always have a togglelink but ensure.
return;
}
// Search for any visibly expanded children.
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
// If we can find any collapsed ancestors, skip.
if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
return false;
}
foundmaximisedchildren = true;
return true;
});
if (foundmaximisedchildren) {
// At least one maximised child found. Show the collapseall.
togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
.addClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
} else {
// No maximised children found but there are collapsed children. Show the expandall.
togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
.removeClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
}
};
/**
* Process the data returned by Y.io.
* This includes appending it to the relevant part of the DOM, and applying our animations.
*
* @method process_results
* @private
* @param {String} tid The Transaction ID
* @param {Object} response The Reponse returned by Y.IO
* @param {Object} ioargs The additional arguments provided by Y.IO
*/
NS.process_results = function(tid, response, args) {
var newnode,
data;
try {
data = Y.JSON.parse(response.responseText);
if (data.error) {
return new M.core.ajaxException(data);
}
} catch (e) {
return new M.core.exception(e);
}
// Insert the returned data into a new Node.
newnode = Y.Node.create(data);
// Append to the existing child location.
args.childnode.appendChild(newnode);
// Now that we have content, we can swap the classes on the toggled container.
args.parentnode
.addClass(CSS.LOADED)
.removeClass(CSS.NOTLOADED);
// Toggle the open/close status of the node now that it's content has been loaded.
this.run_expansion(args.parentnode);
// Remove the spinner now that we've started to show the content.
if (args.spinner) {
args.spinner.hide().destroy();
}
};
/**
* Add our animation to the Node.
*
* @method add_animation
* @private
* @param {Node} childnode
*/
NS.add_animation = function(childnode) {
if (typeof childnode.fx !== "undefined") {
// The animation has already been plugged to this node.
return childnode;
}
childnode.plug(Y.Plugin.NodeFX, {
from: {
height: 0,
opacity: 0
},
to: {
// This sets a dynamic height in case the node content changes.
height: function(node) {
// Get expanded height (offsetHeight may be zero).
return node.get('scrollHeight');
},
opacity: 1
},
duration: 0.2
});
return childnode;
};
}, '@VERSION@', {"requires": ["node", "event-key"]});
@@ -0,0 +1,240 @@
YUI.add('moodle-course-coursebase', function (Y, NAME) {
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @module moodle-course-coursebase
*/
var COURSEBASENAME = 'course-coursebase';
var COURSEBASE = function() {
COURSEBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(COURSEBASE, Y.Base, {
// Registered Modules
registermodules: [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module: function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function: function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME: COURSEBASENAME,
ATTRS: {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.course = M.course || {};
M.course.coursebase = M.course.coursebase || new COURSEBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.course.format = M.course.format || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @method M.course.format.swap_sections
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.course.format.swap_sections = M.course.format.swap_sections || function() {
return null;
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @method M.course.format.process_sections
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
*/
M.course.format.process_sections = M.course.format.process_sections || function() {
return null;
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @method M.course.format.get_config
* @return {object} section list configuration
*/
M.course.format.get_config = M.course.format.get_config || function() {
return {
container_node: null, // compulsory
container_class: null, // compulsory
section_wrapper_node: null, // optional
section_wrapper_class: null, // optional
section_node: null, // compulsory
section_class: null // compulsory
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @method M.course.format.get_section_selector
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.course.format.get_section_selector = M.course.format.get_section_selector || function() {
var config = M.course.format.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
Y.log('section_node and section_class are not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @method M.course.format.get_section_wrapper
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.course.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.course.format.get_section_wrapper = M.course.format.get_section_wrapper || function(Y) {
var config = M.course.format.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.course.format.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @method M.course.format.get_containernode
* @return {string} tag of container node.
*/
M.course.format.get_containernode = M.course.format.get_containernode || function() {
var config = M.course.format.get_config();
if (config.container_node) {
return config.container_node;
} else {
Y.log('container_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the class of container node
*
* @method M.course.format.get_containerclass
* @return {string} class of the container node.
*/
M.course.format.get_containerclass = M.course.format.get_containerclass || function() {
var config = M.course.format.get_config();
if (config.container_class) {
return config.container_class;
} else {
Y.log('container_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrappernode
* @return {string} tag of the draggable node.
*/
M.course.format.get_sectionwrappernode = M.course.format.get_sectionwrappernode || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrapperclass
* @return {string} class of the draggable node.
*/
M.course.format.get_sectionwrapperclass = M.course.format.get_sectionwrapperclass || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @method M.course.format.get_sectionnode
* @return {string} tag of section node.
*/
M.course.format.get_sectionnode = M.course.format.get_sectionnode || function() {
var config = M.course.format.get_config();
if (config.section_node) {
return config.section_node;
} else {
Y.log('section_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the class of section node
*
* @method M.course.format.get_sectionclass
* @return {string} class of the section node.
*/
M.course.format.get_sectionclass = M.course.format.get_sectionclass || function() {
var config = M.course.format.get_config();
if (config.section_class) {
return config.section_class;
} else {
Y.log('section_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
@@ -0,0 +1 @@
YUI.add("moodle-course-coursebase",function(e,o){var r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,e.Base,{registermodules:[],register_module:function(e){return this.registermodules.push(e),this},invoke_function:function(e,o){for(var r in this.registermodules)e in this.registermodules[r]&&this.registermodules[r][e](o);return this}},{NAME:"course-coursebase",ATTRS:{}}),M.course=M.course||{},M.course.coursebase=M.course.coursebase||new r,M.course.format=M.course.format||{},M.course.format.swap_sections=M.course.format.swap_sections||function(){return null},M.course.format.process_sections=M.course.format.process_sections||function(){return null},M.course.format.get_config=M.course.format.get_config||function(){return{container_node:null,container_class:null,section_wrapper_node:null,section_wrapper_class:null,section_node:null,section_class:null}},M.course.format.get_section_selector=M.course.format.get_section_selector||function(){var e=M.course.format.get_config();return e.section_node&&e.section_class?e.section_node+"."+e.section_class:null},M.course.format.get_section_wrapper=M.course.format.get_section_wrapper||function(e){var o=M.course.format.get_config();return o.section_wrapper_node&&o.section_wrapper_class?o.section_wrapper_node+"."+o.section_wrapper_class:M.course.format.get_section_selector(e)},M.course.format.get_containernode=M.course.format.get_containernode||function(){var e=M.course.format.get_config();if(e.container_node)return e.container_node},M.course.format.get_containerclass=M.course.format.get_containerclass||function(){var e=M.course.format.get_config();if(e.container_class)return e.container_class},M.course.format.get_sectionwrappernode=M.course.format.get_sectionwrappernode||function(){var e=M.course.format.get_config();return e.section_wrapper_node||e.section_node},M.course.format.get_sectionwrapperclass=M.course.format.get_sectionwrapperclass||function(){var e=M.course.format.get_config();return e.section_wrapper_class||e.section_class},M.course.format.get_sectionnode=M.course.format.get_sectionnode||function(){var e=M.course.format.get_config();if(e.section_node)return e.section_node},M.course.format.get_sectionclass=M.course.format.get_sectionclass||function(){var e=M.course.format.get_config();if(e.section_class)return e.section_class}},"@VERSION@",{requires:["base","node"]});
@@ -0,0 +1,235 @@
YUI.add('moodle-course-coursebase', function (Y, NAME) {
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @module moodle-course-coursebase
*/
var COURSEBASENAME = 'course-coursebase';
var COURSEBASE = function() {
COURSEBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(COURSEBASE, Y.Base, {
// Registered Modules
registermodules: [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module: function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function: function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME: COURSEBASENAME,
ATTRS: {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.course = M.course || {};
M.course.coursebase = M.course.coursebase || new COURSEBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.course.format = M.course.format || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @method M.course.format.swap_sections
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.course.format.swap_sections = M.course.format.swap_sections || function() {
return null;
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @method M.course.format.process_sections
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
*/
M.course.format.process_sections = M.course.format.process_sections || function() {
return null;
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @method M.course.format.get_config
* @return {object} section list configuration
*/
M.course.format.get_config = M.course.format.get_config || function() {
return {
container_node: null, // compulsory
container_class: null, // compulsory
section_wrapper_node: null, // optional
section_wrapper_class: null, // optional
section_node: null, // compulsory
section_class: null // compulsory
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @method M.course.format.get_section_selector
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.course.format.get_section_selector = M.course.format.get_section_selector || function() {
var config = M.course.format.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @method M.course.format.get_section_wrapper
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.course.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.course.format.get_section_wrapper = M.course.format.get_section_wrapper || function(Y) {
var config = M.course.format.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.course.format.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @method M.course.format.get_containernode
* @return {string} tag of container node.
*/
M.course.format.get_containernode = M.course.format.get_containernode || function() {
var config = M.course.format.get_config();
if (config.container_node) {
return config.container_node;
} else {
}
};
/**
* Get the class of container node
*
* @method M.course.format.get_containerclass
* @return {string} class of the container node.
*/
M.course.format.get_containerclass = M.course.format.get_containerclass || function() {
var config = M.course.format.get_config();
if (config.container_class) {
return config.container_class;
} else {
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrappernode
* @return {string} tag of the draggable node.
*/
M.course.format.get_sectionwrappernode = M.course.format.get_sectionwrappernode || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrapperclass
* @return {string} class of the draggable node.
*/
M.course.format.get_sectionwrapperclass = M.course.format.get_sectionwrapperclass || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @method M.course.format.get_sectionnode
* @return {string} tag of section node.
*/
M.course.format.get_sectionnode = M.course.format.get_sectionnode || function() {
var config = M.course.format.get_config();
if (config.section_node) {
return config.section_node;
} else {
}
};
/**
* Get the class of section node
*
* @method M.course.format.get_sectionclass
* @return {string} class of the section node.
*/
M.course.format.get_sectionclass = M.course.format.get_sectionclass || function() {
var config = M.course.format.get_config();
if (config.section_class) {
return config.section_class;
} else {
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
@@ -0,0 +1,575 @@
YUI.add('moodle-course-dragdrop', function (Y, NAME) {
/* eslint-disable no-unused-vars */
/**
* Drag and Drop for course sections and course modules.
*
* @module moodle-course-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'course-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
};
M.course = M.course || {};
/**
* Section drag and drop.
*
* @class M.course.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [CSS.SECTIONDRAGGABLE];
this.samenodeclass = M.course.format.get_sectionwrapperclass();
this.parentnodeclass = M.course.format.get_containerclass();
// Detect the direction of travel.
this.detectkeyboarddirection = true;
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.course.format.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
if (moveup.previous('br')) {
moveup.previous('br').remove();
} else if (moveup.next('br')) {
moveup.next('br').remove();
}
if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') {
moveup.ancestor().remove();
} else {
moveup.remove();
}
}
if (movedown) {
if (movedown.previous('br')) {
movedown.previous('br').remove();
} else if (movedown.next('br')) {
movedown.next('br').remove();
}
var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase();
if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') {
movedown.ancestor().remove();
} else {
movedown.remove();
}
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// This is the node that the user started to drag.
var node = drag.get('node');
// This is the container node that will follow the mouse around,
// or during a keyboard drag and drop the original node.
var dragnode = drag.get('dragNode');
if (node === dragnode) {
return;
}
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
'></' + M.course.format.get_containernode() + '>');
containernode.addClass(M.course.format.get_containerclass());
var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() +
'></' + M.course.format.get_sectionwrappernode() + '>');
sectionnode.addClass(M.course.format.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(node.get('innerHTML'));
containernode.appendChild(sectionnode);
dragnode.setContent(containernode);
dragnode.addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
return;
}
Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {
// Ignore.
}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
" with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.course.format.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
sectionlist.item(index).setAttribute('data-sectionid',
Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
this.samenodelabel = {
identifier: 'afterresource',
component: 'moodle'
};
this.parentnodelabel = {
identifier: 'totopofsection',
component: 'moodle'
};
// Go through all sections
var sectionlistselector = M.course.format.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.course.coursebase.register_module(this);
M.course.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
var draggroups = resourcesnode.getData('draggroups');
if (!draggroups) {
// This Drop Node has not been set up. Configure it now.
resourcesnode.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resourcesnode,
groups: this.groups,
padding: '20 0 20 0'
});
}
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
var sr = move.getData('sectionreturn');
move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'),
CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
if (drag.get('dragNode') === drag.get('node')) {
// We do not want to modify the contents of the real node.
// They will be the same during a keyboard drag and drop.
return;
}
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
// TODO: revert nodes location
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-course-coursebase",
"moodle-course-util"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,571 @@
YUI.add('moodle-course-dragdrop', function (Y, NAME) {
/* eslint-disable no-unused-vars */
/**
* Drag and Drop for course sections and course modules.
*
* @module moodle-course-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'course-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
};
M.course = M.course || {};
/**
* Section drag and drop.
*
* @class M.course.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [CSS.SECTIONDRAGGABLE];
this.samenodeclass = M.course.format.get_sectionwrapperclass();
this.parentnodeclass = M.course.format.get_containerclass();
// Detect the direction of travel.
this.detectkeyboarddirection = true;
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.course.format.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
if (moveup.previous('br')) {
moveup.previous('br').remove();
} else if (moveup.next('br')) {
moveup.next('br').remove();
}
if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') {
moveup.ancestor().remove();
} else {
moveup.remove();
}
}
if (movedown) {
if (movedown.previous('br')) {
movedown.previous('br').remove();
} else if (movedown.next('br')) {
movedown.next('br').remove();
}
var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase();
if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') {
movedown.ancestor().remove();
} else {
movedown.remove();
}
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// This is the node that the user started to drag.
var node = drag.get('node');
// This is the container node that will follow the mouse around,
// or during a keyboard drag and drop the original node.
var dragnode = drag.get('dragNode');
if (node === dragnode) {
return;
}
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
'></' + M.course.format.get_containernode() + '>');
containernode.addClass(M.course.format.get_containerclass());
var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() +
'></' + M.course.format.get_sectionwrappernode() + '>');
sectionnode.addClass(M.course.format.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(node.get('innerHTML'));
containernode.appendChild(sectionnode);
dragnode.setContent(containernode);
dragnode.addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
return;
}
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {
// Ignore.
}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.course.format.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
sectionlist.item(index).setAttribute('data-sectionid',
Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
this.samenodelabel = {
identifier: 'afterresource',
component: 'moodle'
};
this.parentnodelabel = {
identifier: 'totopofsection',
component: 'moodle'
};
// Go through all sections
var sectionlistselector = M.course.format.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.course.coursebase.register_module(this);
M.course.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
var draggroups = resourcesnode.getData('draggroups');
if (!draggroups) {
// This Drop Node has not been set up. Configure it now.
resourcesnode.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resourcesnode,
groups: this.groups,
padding: '20 0 20 0'
});
}
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
var sr = move.getData('sectionreturn');
move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'),
CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
if (drag.get('dragNode') === drag.get('node')) {
// We do not want to modify the contents of the real node.
// They will be the same during a keyboard drag and drop.
return;
}
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
// TODO: revert nodes location
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-course-coursebase",
"moodle-course-util"
]
});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
YUI.add('moodle-course-util-base', function (Y, NAME) {
/**
* The Moodle.core_course.util classes provide course-related utility functions.
*
* @module moodle-course-util
* @main
*/
Y.namespace('Moodle.core_course.util');
/**
* A collection of general utility functions for use in course.
*
* @class Moodle.core_course.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1 @@
YUI.add("moodle-course-util-base",function(e,o){e.namespace("Moodle.core_course.util")},"@VERSION@");
@@ -0,0 +1,21 @@
YUI.add('moodle-course-util-base', function (Y, NAME) {
/**
* The Moodle.core_course.util classes provide course-related utility functions.
*
* @module moodle-course-util
* @main
*/
Y.namespace('Moodle.core_course.util');
/**
* A collection of general utility functions for use in course.
*
* @class Moodle.core_course.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1,75 @@
YUI.add('moodle-course-util-cm', function (Y, NAME) {
/**
* A collection of utility classes for use with course modules.
*
* @module moodle-course-util
* @submodule moodle-course-util-cm
*/
Y.namespace('Moodle.core_course.util.cm');
/**
* A collection of utility classes for use with course modules.
*
* @class Moodle.core_course.util.cm
* @static
*/
Y.Moodle.core_course.util.cm = {
CONSTANTS: {
MODULEIDPREFIX: 'module-'
},
SELECTORS: {
COURSEMODULE: '.activity',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the course module item from one of it's child Nodes.
*
* @method getCourseModuleNodeFromComponent
* @param coursemodulecomponent {Node} The component Node.
* @return {Node|null} The Course Module Node.
*/
getCourseModuleFromComponent: function(coursemodulecomponent) {
return Y.one(coursemodulecomponent).ancestor(this.SELECTORS.COURSEMODULE, true);
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getId: function(coursemodule) {
// We perform a simple substitution operation to get the ID.
var id = coursemodule.get('id').replace(
this.CONSTANTS.MODULEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the section ID for the provided section.
*
* @method getName
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getName: function(coursemodule) {
var instance = coursemodule.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]});
@@ -0,0 +1 @@
YUI.add("moodle-course-util-cm",function(t,e){t.namespace("Moodle.core_course.util.cm"),t.Moodle.core_course.util.cm={CONSTANTS:{MODULEIDPREFIX:"module-"},SELECTORS:{COURSEMODULE:".activity",INSTANCENAME:".instancename"},getCourseModuleFromComponent:function(e){return t.one(e).ancestor(this.SELECTORS.COURSEMODULE,!0)},getId:function(e){e=e.get("id").replace(this.CONSTANTS.MODULEIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},getName:function(e){e=e.one(this.SELECTORS.INSTANCENAME);return e?e.get("firstChild").get("data"):null}}},"@VERSION@",{requires:["node","moodle-course-util-base"]});
@@ -0,0 +1,75 @@
YUI.add('moodle-course-util-cm', function (Y, NAME) {
/**
* A collection of utility classes for use with course modules.
*
* @module moodle-course-util
* @submodule moodle-course-util-cm
*/
Y.namespace('Moodle.core_course.util.cm');
/**
* A collection of utility classes for use with course modules.
*
* @class Moodle.core_course.util.cm
* @static
*/
Y.Moodle.core_course.util.cm = {
CONSTANTS: {
MODULEIDPREFIX: 'module-'
},
SELECTORS: {
COURSEMODULE: '.activity',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the course module item from one of it's child Nodes.
*
* @method getCourseModuleNodeFromComponent
* @param coursemodulecomponent {Node} The component Node.
* @return {Node|null} The Course Module Node.
*/
getCourseModuleFromComponent: function(coursemodulecomponent) {
return Y.one(coursemodulecomponent).ancestor(this.SELECTORS.COURSEMODULE, true);
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getId: function(coursemodule) {
// We perform a simple substitution operation to get the ID.
var id = coursemodule.get('id').replace(
this.CONSTANTS.MODULEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the section ID for the provided section.
*
* @method getName
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getName: function(coursemodule) {
var instance = coursemodule.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]});
@@ -0,0 +1,45 @@
YUI.add('moodle-course-util-section', function (Y, NAME) {
/**
* A collection of utility classes for use with course sections.
*
* @module moodle-course-util
* @submodule moodle-course-util-section
*/
Y.namespace('Moodle.core_course.util.section');
/**
* A collection of utility classes for use with course sections.
*
* @class Moodle.core_course.util.section
* @static
*/
Y.Moodle.core_course.util.section = {
CONSTANTS: {
SECTIONIDPREFIX: 'section-'
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param section {Node} The section to find an ID for.
* @return {Number|false} The ID of the section in question or false if no ID was found.
*/
getId: function(section) {
// We perform a simple substitution operation to get the ID.
var id = section.get('id').replace(
this.CONSTANTS.SECTIONIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]});
@@ -0,0 +1 @@
YUI.add("moodle-course-util-section",function(e,o){e.namespace("Moodle.core_course.util.section"),e.Moodle.core_course.util.section={CONSTANTS:{SECTIONIDPREFIX:"section-"},getId:function(e){e=e.get("id").replace(this.CONSTANTS.SECTIONIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e}}},"@VERSION@",{requires:["node","moodle-course-util-base"]});
@@ -0,0 +1,45 @@
YUI.add('moodle-course-util-section', function (Y, NAME) {
/**
* A collection of utility classes for use with course sections.
*
* @module moodle-course-util
* @submodule moodle-course-util-section
*/
Y.namespace('Moodle.core_course.util.section');
/**
* A collection of utility classes for use with course sections.
*
* @class Moodle.core_course.util.section
* @static
*/
Y.Moodle.core_course.util.section = {
CONSTANTS: {
SECTIONIDPREFIX: 'section-'
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param section {Node} The section to find an ID for.
* @return {Number|false} The ID of the section in question or false if no ID was found.
*/
getId: function(section) {
// We perform a simple substitution operation to get the ID.
var id = section.get('id').replace(
this.CONSTANTS.SECTIONIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]});
@@ -0,0 +1,10 @@
{
"name": "moodle-course-categoryexpander",
"builds": {
"moodle-course-categoryexpander": {
"jsfiles": [
"categoryexpander.js"
]
}
}
}
+508
View File
@@ -0,0 +1,508 @@
/**
* Adds toggling of subcategory with automatic loading using AJAX.
*
* This also includes application of an animation to improve user experience.
*
* @module moodle-course-categoryexpander
*/
/**
* The course category expander.
*
* @constructor
* @class Y.Moodle.course.categoryexpander
*/
var CSS = {
CONTENTNODE: 'content',
COLLAPSEALL: 'collapse-all',
DISABLED: 'disabled',
LOADED: 'loaded',
NOTLOADED: 'notloaded',
SECTIONCOLLAPSED: 'collapsed',
HASCHILDREN: 'with_children'
},
SELECTORS = {
WITHCHILDRENTREES: '.with_children',
LOADEDTREES: '.with_children.loaded',
CONTENTNODE: '.content',
CATEGORYLISTENLINK: '.category .info .categoryname',
CATEGORYSPINNERLOCATION: '.categoryname',
CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed',
CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed',
CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)',
COLLAPSEEXPAND: '.collapseexpand',
COURSEBOX: '.coursebox',
COURSEBOXLISTENLINK: '.coursebox .moreinfo',
COURSEBOXSPINNERLOCATION: '.info .moreinfo',
COURSECATEGORYTREE: '.course_category_tree',
PARENTWITHCHILDREN: '.category'
},
NS = Y.namespace('Moodle.course.categoryexpander'),
TYPE_CATEGORY = 0,
TYPE_COURSE = 1,
URL = M.cfg.wwwroot + '/course/category.ajax.php';
/**
* Set up the category expander.
*
* No arguments are required.
*
* @method init
*/
NS.init = function() {
var doc = Y.one(Y.config.doc);
doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this);
doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this);
doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this);
// Only set up they keybaord listeners when tab is first pressed - it
// may never happen and modifying the DOM on a large number of nodes
// can be very expensive.
doc.once('key', this.setup_keyboard_listeners, 'tab', this);
};
/**
* Set up keyboard expansion for course content.
*
* This includes setting up the delegation but also adding the nodes to the
* tabflow.
*
* @method setup_keyboard_listeners
*/
NS.setup_keyboard_listeners = function() {
var doc = Y.one(Y.config.doc);
Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander');
doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0');
Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this);
Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this);
};
/**
* Expand all categories.
*
* @method expand_category
* @private
* @param {Node} categorynode The node to expand
*/
NS.expand_category = function(categorynode) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the expand_category with the _expand_category function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.expand_category = NS._expand_category;
NS.expand_category(categorynode);
});
};
NS._expand_category = function(categorynode) {
var categoryid,
depth;
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Toggle the animation of the clicked category node.
*
* @method toggle_category_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_category_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_category_expansion = NS._toggle_category_expansion;
NS.toggle_category_expansion(e);
});
};
/**
* Toggle the animation of the clicked coursebox node.
*
* @method toggle_coursebox_expansion
* @private
* @param {EventFacade} e
*/
NS.toggle_coursebox_expansion = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion;
NS.toggle_coursebox_expansion(e);
});
e.preventDefault();
};
NS._toggle_coursebox_expansion = function(e) {
var courseboxnode;
// Grab the parent category container - this is where the new content will be added.
courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true);
e.preventDefault();
if (courseboxnode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(courseboxnode);
return;
}
this._toggle_generic_expansion({
parentnode: courseboxnode,
childnode: courseboxnode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION,
data: {
courseid: courseboxnode.getData('courseid'),
type: TYPE_COURSE
}
});
};
NS._toggle_category_expansion = function(e) {
var categorynode,
categoryid,
depth;
if (e.target.test('a') || e.target.test('img')) {
// Return early if either an anchor or an image were clicked.
return;
}
// Grab the parent category container - this is where the new content will be added.
categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true);
if (!categorynode.hasClass(CSS.HASCHILDREN)) {
// Nothing to do here - this category has no children.
return;
}
if (categorynode.hasClass(CSS.LOADED)) {
// We've already loaded this content so we just need to toggle the view of it.
this.run_expansion(categorynode);
return;
}
// We use Data attributes to store the category.
categoryid = categorynode.getData('categoryid');
depth = categorynode.getData('depth');
if (typeof categoryid === "undefined" || typeof depth === "undefined") {
return;
}
this._toggle_generic_expansion({
parentnode: categorynode,
childnode: categorynode.one(SELECTORS.CONTENTNODE),
spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
data: {
categoryid: categoryid,
depth: depth,
showcourses: categorynode.getData('showcourses'),
type: TYPE_CATEGORY
}
});
};
/**
* Wrapper function to handle toggling of generic types.
*
* @method _toggle_generic_expansion
* @private
* @param {Object} config
*/
NS._toggle_generic_expansion = function(config) {
var spinner;
if (config.spinnerhandle) {
// Add a spinner to give some feedback to the user.
spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
}
// Fetch the data.
Y.io(URL, {
method: 'POST',
context: this,
on: {
complete: this.process_results
},
data: config.data,
"arguments": {
parentnode: config.parentnode,
childnode: config.childnode,
spinner: spinner
}
});
};
/**
* Apply the animation on the supplied node.
*
* @method run_expansion
* @private
* @param {Node} categorynode The node to apply the animation to
*/
NS.run_expansion = function(categorynode) {
var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
self = this,
ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
// Add our animation to the categorychildren.
this.add_animation(categorychildren);
// If we already have the class, remove it before showing otherwise we perform the
// animation whilst the node is hidden.
if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
// To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
categorychildren.setStyle('height', '0');
categorynode.removeClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'true');
categorychildren.fx.set('reverse', false);
} else {
categorychildren.fx.set('reverse', true);
categorychildren.fx.once('end', function(e, categorynode) {
categorynode.addClass(CSS.SECTIONCOLLAPSED);
categorynode.setAttribute('aria-expanded', 'false');
}, this, categorynode);
}
categorychildren.fx.once('end', function(e, categorychildren) {
// Remove the styles that the animation has set.
categorychildren.setStyles({
height: '',
opacity: ''
});
// To avoid memory gobbling, remove the animation. It will be added back if called again.
this.destroy();
self.update_collapsible_actions(ancestor);
}, categorychildren.fx, categorychildren);
// Now that everything has been set up, run the animation.
categorychildren.fx.run();
};
/**
* Toggle collapsing of all nodes.
*
* @method collapse_expand_all
* @private
* @param {EventFacade} e
*/
NS.collapse_expand_all = function(e) {
// Load the actual dependencies now that we've been called.
Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
// Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
// this function isn't called in the future, and call it for the first time.
NS.collapse_expand_all = NS._collapse_expand_all;
NS.collapse_expand_all(e);
});
e.preventDefault();
};
NS._collapse_expand_all = function(e) {
// The collapse/expand button has no actual target but we need to prevent it's default
// action to ensure we don't make the page reload/jump.
e.preventDefault();
if (e.currentTarget.hasClass(CSS.DISABLED)) {
// The collapse/expand is currently disabled.
return;
}
var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
if (!ancestor) {
return;
}
var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (collapseall.hasClass(CSS.COLLAPSEALL)) {
this.collapse_all(ancestor);
} else {
this.expand_all(ancestor);
}
this.update_collapsible_actions(ancestor);
};
NS.expand_all = function(ancestor) {
var finalexpansions = [];
ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) {
// Expand the hidden children first without animation.
c.removeClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED);
} else {
finalexpansions.push(c);
}
}, this);
// Run the final expansion with animation on the visible items.
Y.all(finalexpansions).each(function(c) {
this.expand_category(c);
}, this);
};
NS.collapse_all = function(ancestor) {
var finalcollapses = [];
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
.each(function(c) {
if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
finalcollapses.push(c);
} else {
// Collapse the visible items first
this.run_expansion(c);
}
}, this);
// Run the final collapses now that the these are hidden hidden.
Y.all(finalcollapses).each(function(c) {
c.addClass(CSS.SECTIONCOLLAPSED);
c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
}, this);
};
NS.update_collapsible_actions = function(ancestor) {
var foundmaximisedchildren = false,
// Grab the anchor for the collapseexpand all link.
togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
if (!togglelink) {
// We should always have a togglelink but ensure.
return;
}
// Search for any visibly expanded children.
ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
// If we can find any collapsed ancestors, skip.
if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
return false;
}
foundmaximisedchildren = true;
return true;
});
if (foundmaximisedchildren) {
// At least one maximised child found. Show the collapseall.
togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
.addClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
} else {
// No maximised children found but there are collapsed children. Show the expandall.
togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
.removeClass(CSS.COLLAPSEALL)
.removeClass(CSS.DISABLED);
}
};
/**
* Process the data returned by Y.io.
* This includes appending it to the relevant part of the DOM, and applying our animations.
*
* @method process_results
* @private
* @param {String} tid The Transaction ID
* @param {Object} response The Reponse returned by Y.IO
* @param {Object} ioargs The additional arguments provided by Y.IO
*/
NS.process_results = function(tid, response, args) {
var newnode,
data;
try {
data = Y.JSON.parse(response.responseText);
if (data.error) {
return new M.core.ajaxException(data);
}
} catch (e) {
return new M.core.exception(e);
}
// Insert the returned data into a new Node.
newnode = Y.Node.create(data);
// Append to the existing child location.
args.childnode.appendChild(newnode);
// Now that we have content, we can swap the classes on the toggled container.
args.parentnode
.addClass(CSS.LOADED)
.removeClass(CSS.NOTLOADED);
// Toggle the open/close status of the node now that it's content has been loaded.
this.run_expansion(args.parentnode);
// Remove the spinner now that we've started to show the content.
if (args.spinner) {
args.spinner.hide().destroy();
}
};
/**
* Add our animation to the Node.
*
* @method add_animation
* @private
* @param {Node} childnode
*/
NS.add_animation = function(childnode) {
if (typeof childnode.fx !== "undefined") {
// The animation has already been plugged to this node.
return childnode;
}
childnode.plug(Y.Plugin.NodeFX, {
from: {
height: 0,
opacity: 0
},
to: {
// This sets a dynamic height in case the node content changes.
height: function(node) {
// Get expanded height (offsetHeight may be zero).
return node.get('scrollHeight');
},
opacity: 1
},
duration: 0.2
});
return childnode;
};
@@ -0,0 +1,8 @@
{
"moodle-course-categoryexpander": {
"requires": [
"node",
"event-key"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "moodle-course-coursebase",
"builds": {
"moodle-course-coursebase": {
"jsfiles": [
"coursebase.js"
]
}
}
}
+235
View File
@@ -0,0 +1,235 @@
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @module moodle-course-coursebase
*/
var COURSEBASENAME = 'course-coursebase';
var COURSEBASE = function() {
COURSEBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(COURSEBASE, Y.Base, {
// Registered Modules
registermodules: [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module: function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function: function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME: COURSEBASENAME,
ATTRS: {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.course = M.course || {};
M.course.coursebase = M.course.coursebase || new COURSEBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.course.format = M.course.format || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @method M.course.format.swap_sections
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.course.format.swap_sections = M.course.format.swap_sections || function() {
return null;
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @method M.course.format.process_sections
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
*/
M.course.format.process_sections = M.course.format.process_sections || function() {
return null;
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @method M.course.format.get_config
* @return {object} section list configuration
*/
M.course.format.get_config = M.course.format.get_config || function() {
return {
container_node: null, // compulsory
container_class: null, // compulsory
section_wrapper_node: null, // optional
section_wrapper_class: null, // optional
section_node: null, // compulsory
section_class: null // compulsory
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @method M.course.format.get_section_selector
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.course.format.get_section_selector = M.course.format.get_section_selector || function() {
var config = M.course.format.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
Y.log('section_node and section_class are not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @method M.course.format.get_section_wrapper
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.course.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.course.format.get_section_wrapper = M.course.format.get_section_wrapper || function(Y) {
var config = M.course.format.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.course.format.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @method M.course.format.get_containernode
* @return {string} tag of container node.
*/
M.course.format.get_containernode = M.course.format.get_containernode || function() {
var config = M.course.format.get_config();
if (config.container_node) {
return config.container_node;
} else {
Y.log('container_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the class of container node
*
* @method M.course.format.get_containerclass
* @return {string} class of the container node.
*/
M.course.format.get_containerclass = M.course.format.get_containerclass || function() {
var config = M.course.format.get_config();
if (config.container_class) {
return config.container_class;
} else {
Y.log('container_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrappernode
* @return {string} tag of the draggable node.
*/
M.course.format.get_sectionwrappernode = M.course.format.get_sectionwrappernode || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @method M.course.format.get_sectionwrapperclass
* @return {string} class of the draggable node.
*/
M.course.format.get_sectionwrapperclass = M.course.format.get_sectionwrapperclass || function() {
var config = M.course.format.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @method M.course.format.get_sectionnode
* @return {string} tag of section node.
*/
M.course.format.get_sectionnode = M.course.format.get_sectionnode || function() {
var config = M.course.format.get_config();
if (config.section_node) {
return config.section_node;
} else {
Y.log('section_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
/**
* Get the class of section node
*
* @method M.course.format.get_sectionclass
* @return {string} class of the section node.
*/
M.course.format.get_sectionclass = M.course.format.get_sectionclass || function() {
var config = M.course.format.get_config();
if (config.section_class) {
return config.section_class;
} else {
Y.log('section_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase');
}
};
@@ -0,0 +1,8 @@
{
"moodle-course-coursebase": {
"requires": [
"base",
"node"
]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "moodle-course-dragdrop",
"builds": {
"moodle-course-dragdrop": {
"jsfiles": [
"dragdrop.js",
"section.js",
"resource.js"
]
}
}
}
+30
View File
@@ -0,0 +1,30 @@
/* eslint-disable no-unused-vars */
/**
* Drag and Drop for course sections and course modules.
*
* @module moodle-course-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'course-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
};
M.course = M.course || {};
+229
View File
@@ -0,0 +1,229 @@
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
this.samenodelabel = {
identifier: 'afterresource',
component: 'moodle'
};
this.parentnodelabel = {
identifier: 'totopofsection',
component: 'moodle'
};
// Go through all sections
var sectionlistselector = M.course.format.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.course.coursebase.register_module(this);
M.course.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
var draggroups = resourcesnode.getData('draggroups');
if (!draggroups) {
// This Drop Node has not been set up. Configure it now.
resourcesnode.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resourcesnode,
groups: this.groups,
padding: '20 0 20 0'
});
}
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
var sr = move.getData('sectionreturn');
move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'),
CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
if (drag.get('dragNode') === drag.get('node')) {
// We do not want to modify the contents of the real node.
// They will be the same during a keyboard drag and drop.
return;
}
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
// TODO: revert nodes location
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
+298
View File
@@ -0,0 +1,298 @@
/**
* Section drag and drop.
*
* @class M.course.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [CSS.SECTIONDRAGGABLE];
this.samenodeclass = M.course.format.get_sectionwrapperclass();
this.parentnodeclass = M.course.format.get_containerclass();
// Detect the direction of travel.
this.detectkeyboarddirection = true;
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.course.format.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .course-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
if (moveup.previous('br')) {
moveup.previous('br').remove();
} else if (moveup.next('br')) {
moveup.next('br').remove();
}
if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') {
moveup.ancestor().remove();
} else {
moveup.remove();
}
}
if (movedown) {
if (movedown.previous('br')) {
movedown.previous('br').remove();
} else if (movedown.next('br')) {
movedown.next('br').remove();
}
var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase();
if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') {
movedown.ancestor().remove();
} else {
movedown.remove();
}
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// This is the node that the user started to drag.
var node = drag.get('node');
// This is the container node that will follow the mouse around,
// or during a keyboard drag and drop the original node.
var dragnode = drag.get('dragNode');
if (node === dragnode) {
return;
}
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
'></' + M.course.format.get_containernode() + '>');
containernode.addClass(M.course.format.get_containerclass());
var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() +
'></' + M.course.format.get_sectionwrappernode() + '>');
sectionnode.addClass(M.course.format.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(node.get('innerHTML'));
containernode.appendChild(sectionnode);
dragnode.setContent(containernode);
dragnode.addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
return;
}
Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {
// Ignore.
}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
" with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.course.format.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
sectionlist.item(index).setAttribute('data-sectionid',
Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context: this
});
}
}, {
NAME: 'course-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.course = M.course || {};
M.course.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
@@ -0,0 +1,16 @@
{
"moodle-course-dragdrop": {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-course-coursebase",
"moodle-course-util"
]
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "moodle-course-management",
"builds": {
"moodle-course-management": {
"jsfiles": [
"shared.js",
"console.js",
"dd.js",
"item.js",
"category.js",
"course.js"
]
}
}
}
+459
View File
@@ -0,0 +1,459 @@
/**
* A managed category.
*
* @namespace M.course.management
* @class Category
* @constructor
* @extends Item
*/
Category = function() {
Category.superclass.constructor.apply(this, arguments);
};
Category.NAME = 'moodle-course-management-category';
Category.CSS_PREFIX = 'management-category';
Category.ATTRS = {
/**
* The category ID relating to this category.
* @attribute categoryid
* @type Number
* @writeOnce
* @default null
*/
categoryid: {
getter: function(value, name) {
if (value === null) {
value = this.get('node').getData('id');
this.set(name, value);
}
return value;
},
value: null,
writeOnce: true
},
/**
* True if this category is the currently selected category.
* @attribute selected
* @type Boolean
* @default null
*/
selected: {
getter: function(value, name) {
if (value === null) {
value = this.get('node').getData(name);
if (value === null) {
value = false;
}
this.set(name, value);
}
return value;
},
value: null
},
/**
* An array of courses belonging to this category.
* @attribute courses
* @type Course[]
* @default Array
*/
courses: {
validator: function(val) {
return Y.Lang.isArray(val);
},
value: []
}
};
Category.prototype = {
/**
* Initialises an instance of a Category.
* @method initializer
*/
initializer: function() {
this.set('itemname', 'category');
},
/**
* Returns the name of the category.
* @method getName
* @return {String}
*/
getName: function() {
return this.get('node').one('a.categoryname').get('innerHTML');
},
/**
* Registers a course as belonging to this category.
* @method registerCourse
* @param {Course} course
*/
registerCourse: function(course) {
var courses = this.get('courses');
courses.push(course);
this.set('courses', courses);
},
/**
* Handles a category related event.
*
* @method handle
* @param {String} action
* @param {EventFacade} e
* @return {Boolean}
*/
handle: function(action, e) {
var catarg = {categoryid: this.get('categoryid')},
selected = this.get('console').get('activecategoryid');
if (selected && selected !== catarg.categoryid) {
catarg.selectedcategory = selected;
}
switch (action) {
case 'moveup':
e.preventDefault();
this.get('console').performAjaxAction('movecategoryup', catarg, this.moveup, this);
break;
case 'movedown':
e.preventDefault();
this.get('console').performAjaxAction('movecategorydown', catarg, this.movedown, this);
break;
case 'show':
e.preventDefault();
this.get('console').performAjaxAction('showcategory', catarg, this.show, this);
break;
case 'hide':
e.preventDefault();
this.get('console').performAjaxAction('hidecategory', catarg, this.hide, this);
break;
case 'expand':
e.preventDefault();
if (this.get('node').getData('expanded') === '0') {
this.get('node').setAttribute('data-expanded', '1').setData('expanded', 'true');
this.get('console').performAjaxAction('getsubcategorieshtml', catarg, this.loadSubcategories, this);
}
this.expand();
break;
case 'collapse':
e.preventDefault();
this.collapse();
break;
case 'select':
var c = this.get('console'),
movecategoryto = c.get('categorylisting').one('#menumovecategoriesto');
// If any category is selected and there are more then one categories.
if (movecategoryto) {
if (c.isCategorySelected(e.currentTarget) &&
c.get('categories').length > 1) {
movecategoryto.removeAttribute('disabled');
} else {
movecategoryto.setAttribute('disabled', true);
}
c.handleBulkSortByaction();
}
break;
default:
Y.log('Invalid AJAX action requested of managed category.', 'warn', 'moodle-course-management');
return false;
}
},
/**
* Expands the category making its sub categories visible.
* @method expand
*/
expand: function() {
var node = this.get('node'),
action = node.one('a[data-action=expand]'),
ul = node.one('ul[role=group]');
node.removeClass('collapsed').setAttribute('aria-expanded', 'true');
action.setAttribute('data-action', 'collapse').setAttrs({
title: M.util.get_string('collapsecategory', 'moodle', this.getName())
});
require(['core/str', 'core/templates', 'core/notification'], function(Str, Templates, Notification) {
Str.get_string('collapse', 'core')
.then(function(string) {
return Templates.renderPix('t/switch_minus', 'core', string);
})
.then(function(html) {
html = Y.Node.create(html).addClass('tree-icon').getDOMNode().outerHTML;
return action.set('innerHTML', html);
}).fail(Notification.exception);
});
if (ul) {
ul.setAttribute('aria-hidden', 'false');
}
this.get('console').performAjaxAction('expandcategory', {categoryid: this.get('categoryid')}, null, this);
},
/**
* Collapses the category making its sub categories hidden.
* @method collapse
*/
collapse: function() {
var node = this.get('node'),
action = node.one('a[data-action=collapse]'),
ul = node.one('ul[role=group]');
node.addClass('collapsed').setAttribute('aria-expanded', 'false');
action.setAttribute('data-action', 'expand').setAttrs({
title: M.util.get_string('expandcategory', 'moodle', this.getName())
});
require(['core/str', 'core/templates', 'core/notification'], function(Str, Templates, Notification) {
Str.get_string('expand', 'core')
.then(function(string) {
return Templates.renderPix('t/switch_plus', 'core', string);
})
.then(function(html) {
html = Y.Node.create(html).addClass('tree-icon').getDOMNode().outerHTML;
return action.set('innerHTML', html);
}).fail(Notification.exception);
});
if (ul) {
ul.setAttribute('aria-hidden', 'true');
}
this.get('console').performAjaxAction('collapsecategory', {categoryid: this.get('categoryid')}, null, this);
},
/**
* Loads sub categories provided by an AJAX request..
*
* @method loadSubcategories
* @protected
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean} Returns true on success - false otherwise.
*/
loadSubcategories: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
node = this.get('node'),
managementconsole = this.get('console'),
ul,
actionnode;
if (outcome === false) {
Y.log('AJAX failed to load sub categories for ' + this.get('itemname'), 'warn', 'moodle-course-management');
return false;
}
Y.log('AJAX loaded subcategories for ' + this.get('itemname'), 'info', 'moodle-course-management');
node.append(outcome.html);
managementconsole.initialiseCategories(node);
if (M.core && M.core.actionmenu && M.core.actionmenu.newDOMNode) {
M.core.actionmenu.newDOMNode(node);
}
ul = node.one('ul[role=group]');
actionnode = node.one('a[data-action=collapse]');
if (ul && actionnode) {
actionnode.setAttribute('aria-controls', ul.generateID());
}
return true;
},
/**
* Moves the course to this category.
*
* @method moveCourseTo
* @param {Course} course
*/
moveCourseTo: function(course) {
require(['core/notification'], function(Notification) {
Notification.saveCancelPromise(
M.util.get_string('confirmation', 'admin'),
M.util.get_string('confirmcoursemove', 'moodle',
{
course: course.getName(),
category: this.getName(),
}),
M.util.get_string('move', 'moodle')
).then(function() {
this.get('console').performAjaxAction('movecourseintocategory', {
courseid: course.get('courseid'),
categoryid: this.get('categoryid'),
}, this.completeMoveCourse, this);
return;
}.bind(this)).catch(function() {
// User cancelled.
});
}.bind(this));
},
/**
* Completes moving a course to this category.
* @method completeMoveCourse
* @protected
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
completeMoveCourse: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
managementconsole = this.get('console'),
category,
course,
totals;
if (outcome === false) {
Y.log('AJAX failed to move courses into this category: ' + this.get('itemname'), 'warn', 'moodle-course-management');
return false;
}
course = managementconsole.getCourseById(args.courseid);
if (!course) {
Y.log('Course was moved but the course listing could not be found to reflect this', 'warn', 'moodle-course-management');
return false;
}
Y.log('Moved the course (' + course.getName() + ') into this category (' + this.getName() + ')',
'debug', 'moodle-course-management');
this.highlight();
if (course) {
if (outcome.paginationtotals) {
totals = managementconsole.get('courselisting').one('.listing-pagination-totals');
if (totals) {
totals.set('innerHTML', outcome.paginationtotals);
}
}
if (outcome.totalcatcourses !== 'undefined') {
totals = this.get('node').one('.course-count span');
if (totals) {
totals.set('innerHTML', totals.get('innerHTML').replace(/^\d+/, outcome.totalcatcourses));
}
}
if (typeof outcome.fromcatcoursecount !== 'undefined') {
category = managementconsole.get('activecategoryid');
category = managementconsole.getCategoryById(category);
if (category) {
totals = category.get('node').one('.course-count span');
if (totals) {
totals.set('innerHTML', totals.get('innerHTML').replace(/^\d+/, outcome.fromcatcoursecount));
}
}
}
course.remove();
}
return true;
},
/**
* Makes an item visible.
*
* @method show
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
show: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
hidebtn;
if (outcome === false) {
Y.log('AJAX request to show ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management');
return false;
}
this.markVisible();
hidebtn = this.get('node').one('a[data-action=hide]');
if (hidebtn) {
hidebtn.focus();
}
if (outcome.categoryvisibility) {
this.updateChildVisibility(outcome.categoryvisibility);
}
if (outcome.coursevisibility) {
this.updateCourseVisiblity(outcome.coursevisibility);
}
this.updated();
Y.log('Success: category made visible by AJAX.', 'info', 'moodle-course-management');
},
/**
* Hides an item.
*
* @method hide
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
hide: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
showbtn;
if (outcome === false) {
Y.log('AJAX request to hide ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management');
return false;
}
this.markHidden();
showbtn = this.get('node').one('a[data-action=show]');
if (showbtn) {
showbtn.focus();
}
if (outcome.categoryvisibility) {
this.updateChildVisibility(outcome.categoryvisibility);
}
if (outcome.coursevisibility) {
this.updateCourseVisiblity(outcome.coursevisibility);
}
this.updated();
Y.log('Success: ' + this.get('itemname') + ' made hidden by AJAX.', 'info', 'moodle-course-management');
},
/**
* Updates the visibility of child courses if required.
* @method updateCourseVisiblity
* @chainable
* @param courses
*/
updateCourseVisiblity: function(courses) {
var managementconsole = this.get('console'),
key,
course;
Y.log('Changing categories course visibility', 'info', 'moodle-course-management');
try {
for (key in courses) {
if (typeof courses[key] === 'object') {
course = managementconsole.getCourseById(courses[key].id);
if (course) {
if (courses[key].visible === "1") {
course.markVisible();
} else {
course.markHidden();
}
}
}
}
} catch (err) {
Y.log('Error trying to update course visibility: ' + err.message, 'warn', 'moodle-course-management');
}
return this;
},
/**
* Updates the visibility of subcategories if required.
* @method updateChildVisibility
* @chainable
* @param categories
*/
updateChildVisibility: function(categories) {
var managementconsole = this.get('console'),
key,
category;
Y.log('Changing categories subcategory visibility', 'info', 'moodle-course-management');
try {
for (key in categories) {
if (typeof categories[key] === 'object') {
category = managementconsole.getCategoryById(categories[key].id);
if (category) {
if (categories[key].visible === "1") {
category.markVisible();
} else {
category.markHidden();
}
}
}
}
} catch (err) {
Y.log('Error trying to update category visibility: ' + err.message, 'warn', 'moodle-course-management');
}
return this;
}
};
Y.extend(Category, Item, Category.prototype);
+588
View File
@@ -0,0 +1,588 @@
/**
* Provides drop down menus for list of action links.
*
* @module moodle-course-management
*/
/**
* Management JS console.
*
* Provides the organisation for course and category management JS.
*
* @namespace M.course.management
* @class Console
* @constructor
* @extends Base
*/
Console = function() {
Console.superclass.constructor.apply(this, arguments);
};
Console.NAME = 'moodle-course-management';
Console.CSS_PREFIX = 'management';
Console.ATTRS = {
/**
* The HTML element containing the management interface.
* @attribute element
* @type Node
*/
element: {
setter: function(node) {
if (typeof node === 'string') {
node = Y.one('#' + node);
}
return node;
}
},
/**
* The category listing container node.
* @attribute categorylisting
* @type Node
* @default null
*/
categorylisting: {
value: null
},
/**
* The course listing container node.
* @attribute courselisting
* @type Node
* @default null
*/
courselisting: {
value: null
},
/**
* The course details container node.
* @attribute coursedetails
* @type Node|null
* @default null
*/
coursedetails: {
value: null
},
/**
* The id of the currently active category.
* @attribute activecategoryid
* @type Number
* @default null
*/
activecategoryid: {
value: null
},
/**
* The id of the currently active course.
* @attribute activecourseid
* @type Number
* @default Null
*/
activecourseid: {
value: null
},
/**
* The categories that are currently available through the management interface.
* @attribute categories
* @type Array
* @default []
*/
categories: {
setter: function(item, name) {
if (Y.Lang.isArray(item)) {
return item;
}
var items = this.get(name);
items.push(item);
return items;
},
value: []
},
/**
* The courses that are currently available through the management interface.
* @attribute courses
* @type Course[]
* @default Array
*/
courses: {
validator: function(val) {
return Y.Lang.isArray(val);
},
value: []
},
/**
* The currently displayed page of courses.
* @attribute page
* @type Number
* @default null
*/
page: {
getter: function(value, name) {
if (value === null) {
value = this.get('element').getData(name);
this.set(name, value);
}
return value;
},
value: null
},
/**
* The total pages of courses that can be shown for this category.
* @attribute totalpages
* @type Number
* @default null
*/
totalpages: {
getter: function(value, name) {
if (value === null) {
value = this.get('element').getData(name);
this.set(name, value);
}
return value;
},
value: null
},
/**
* The total number of courses belonging to this category.
* @attribute totalcourses
* @type Number
* @default null
*/
totalcourses: {
getter: function(value, name) {
if (value === null) {
value = this.get('element').getData(name);
this.set(name, value);
}
return value;
},
value: null
},
/**
* The URL to use for AJAX actions/requests.
* @attribute ajaxurl
* @type String
* @default /course/ajax/management.php
*/
ajaxurl: {
getter: function(value) {
if (value === null) {
value = M.cfg.wwwroot + '/course/ajax/management.php';
}
return value;
},
value: null
},
/**
* The drag drop handler
* @attribute dragdrop
* @type DragDrop
* @default null
*/
dragdrop: {
value: null
}
};
Console.prototype = {
/**
* Gets set to true once the first categories have been initialised.
* @property categoriesinit
* @private
* @type {boolean}
*/
categoriesinit: false,
/**
* Initialises a new instance of the Console.
* @method initializer
*/
initializer: function() {
Y.log('Initialising course category management console', 'info', 'moodle-course-management');
this.set('element', 'coursecat-management');
var element = this.get('element'),
categorylisting = element.one('#category-listing'),
courselisting = element.one('#course-listing'),
selectedcategory = null,
selectedcourse = null;
if (categorylisting) {
selectedcategory = categorylisting.one('.listitem[data-selected="1"]');
}
if (courselisting) {
selectedcourse = courselisting.one('.listitem[data-selected="1"]');
}
this.set('categorylisting', categorylisting);
this.set('courselisting', courselisting);
this.set('coursedetails', element.one('#course-detail'));
if (selectedcategory) {
this.set('activecategoryid', selectedcategory.getData('id'));
}
if (selectedcourse) {
this.set('activecourseid', selectedcourse.getData('id'));
}
this.initialiseCategories(categorylisting);
this.initialiseCourses();
if (courselisting) {
// No need for dragdrop if we don't have a course listing.
this.set('dragdrop', new DragDrop({console: this}));
}
},
/**
* Initialises all the categories being shown.
* @method initialiseCategories
* @private
* @return {boolean}
*/
initialiseCategories: function(listing) {
var count = 0;
if (!listing) {
return false;
}
// Disable category bulk actions as nothing will be selected on initialise.
var menumovecatto = listing.one('#menumovecategoriesto');
if (menumovecatto) {
menumovecatto.setAttribute('disabled', true);
}
var menuresortcategoriesby = listing.one('#menuresortcategoriesby');
if (menuresortcategoriesby) {
menuresortcategoriesby.setAttribute('disabled', true);
}
var menuresortcoursesby = listing.one('#menuresortcoursesby');
if (menuresortcoursesby) {
menuresortcoursesby.setAttribute('disabled', true);
}
listing.all('.listitem[data-id]').each(function(node) {
this.set('categories', new Category({
node: node,
console: this
}));
count++;
}, this);
if (!this.categoriesinit) {
this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'a[data-action]', this);
this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'input[name="bcat[]"]', this);
this.get('categorylisting').delegate('change', this.handleBulkSortByaction, '#menuselectsortby', this);
this.categoriesinit = true;
Y.log(count + ' categories being managed', 'info', 'moodle-course-management');
} else {
Y.log(count + ' new categories being managed', 'info', 'moodle-course-management');
}
},
/**
* Initialises all the categories being shown.
* @method initialiseCourses
* @private
* @return {boolean}
*/
initialiseCourses: function() {
var category = this.getCategoryById(this.get('activecategoryid')),
listing = this.get('courselisting'),
count = 0;
if (!listing) {
return false;
}
// Disable course move to bulk action as nothing will be selected on initialise.
var menumovecoursesto = listing.one('#menumovecoursesto');
if (menumovecoursesto) {
menumovecoursesto.setAttribute('disabled', true);
}
listing.all('.listitem[data-id]').each(function(node) {
this.registerCourse(new Course({
node: node,
console: this,
category: category
}));
count++;
}, this);
listing.delegate('click', this.handleCourseDelegation, 'a[data-action]', this);
listing.delegate('click', this.handleCourseDelegation, 'input[name="bc[]"]', this);
Y.log(count + ' courses being managed', 'info', 'moodle-course-management');
},
/**
* Registers a course within the management display.
* @method registerCourse
* @param {Course} course
*/
registerCourse: function(course) {
var courses = this.get('courses');
courses.push(course);
this.set('courses', courses);
},
/**
* Handles the event fired by a delegated course listener.
*
* @method handleCourseDelegation
* @protected
* @param {EventFacade} e
*/
handleCourseDelegation: function(e) {
var target = e.currentTarget,
action = target.getData('action'),
courseid = target.ancestor('.listitem').getData('id'),
course = this.getCourseById(courseid);
if (course) {
course.handle(action, e);
} else {
Y.log('Course with ID ' + courseid + ' could not be found for delegation', 'error', 'moodle-course-management');
}
},
/**
* Handles the event fired by a delegated course listener.
*
* @method handleCategoryDelegation
* @protected
* @param {EventFacade} e
*/
handleCategoryDelegation: function(e) {
var target = e.currentTarget,
action = target.getData('action'),
categoryid = target.ancestor('.listitem').getData('id'),
category = this.getCategoryById(categoryid);
if (category) {
category.handle(action, e);
} else {
Y.log('Could not find category to delegate to.', 'error', 'moodle-course-management');
}
},
/**
* Check if any course is selected.
*
* @method isCourseSelected
* @param {Node} checkboxnode Checkbox node on which action happened.
* @return bool
*/
isCourseSelected: function(checkboxnode) {
var selected = false;
// If any course selected then show move to category select box.
if (checkboxnode && checkboxnode.get('checked')) {
selected = true;
} else {
var i,
course,
courses = this.get('courses'),
length = courses.length;
for (i = 0; i < length; i++) {
if (courses.hasOwnProperty(i)) {
course = courses[i];
if (course.get('node').one('input[name="bc[]"]').get('checked')) {
selected = true;
break;
}
}
}
}
return selected;
},
/**
* Check if any category is selected.
*
* @method isCategorySelected
* @param {Node} checkboxnode Checkbox node on which action happened.
* @return bool
*/
isCategorySelected: function(checkboxnode) {
var selected = false;
// If any category selected then show move to category select box.
if (checkboxnode && checkboxnode.get('checked')) {
selected = true;
} else {
var i,
category,
categories = this.get('categories'),
length = categories.length;
for (i = 0; i < length; i++) {
if (categories.hasOwnProperty(i)) {
category = categories[i];
if (category.get('node').one('input[name="bcat[]"]').get('checked')) {
selected = true;
break;
}
}
}
}
return selected;
},
/**
* Handle bulk sort action.
*
* @method handleBulkSortByaction
* @protected
* @param {EventFacade} e
*/
handleBulkSortByaction: function(e) {
var sortcategoryby = this.get('categorylisting').one('#menuresortcategoriesby'),
sortcourseby = this.get('categorylisting').one('#menuresortcoursesby'),
sortbybutton = this.get('categorylisting').one('input[name="bulksort"]'),
sortby = e;
if (!sortby) {
sortby = this.get('categorylisting').one('#menuselectsortby');
} else {
if (e && e.currentTarget) {
sortby = e.currentTarget;
}
}
// If no sortby select found then return as we can't do anything.
if (!sortby) {
return;
}
if ((this.get('categories').length <= 1) || (!this.isCategorySelected() &&
(sortby.get("options").item(sortby.get('selectedIndex')).getAttribute('value') === 'selectedcategories'))) {
if (sortcategoryby) {
sortcategoryby.setAttribute('disabled', true);
}
if (sortcourseby) {
sortcourseby.setAttribute('disabled', true);
}
if (sortbybutton) {
sortbybutton.setAttribute('disabled', true);
}
} else {
if (sortcategoryby) {
sortcategoryby.removeAttribute('disabled');
}
if (sortcourseby) {
sortcourseby.removeAttribute('disabled');
}
if (sortbybutton) {
sortbybutton.removeAttribute('disabled');
}
}
},
/**
* Returns the category with the given ID.
* @method getCategoryById
* @param {Number} id
* @return {Category|Boolean} The category or false if it can't be found.
*/
getCategoryById: function(id) {
var i,
category,
categories = this.get('categories'),
length = categories.length;
for (i = 0; i < length; i++) {
if (categories.hasOwnProperty(i)) {
category = categories[i];
if (category.get('categoryid') === id) {
return category;
}
}
}
return false;
},
/**
* Returns the course with the given id.
* @method getCourseById
* @param {Number} id
* @return {Course|Boolean} The course or false if not found/
*/
getCourseById: function(id) {
var i,
course,
courses = this.get('courses'),
length = courses.length;
for (i = 0; i < length; i++) {
if (courses.hasOwnProperty(i)) {
course = courses[i];
if (course.get('courseid') === id) {
return course;
}
}
}
return false;
},
/**
* Removes the course with the given ID.
* @method removeCourseById
* @param {Number} id
*/
removeCourseById: function(id) {
var courses = this.get('courses'),
length = courses.length,
course,
i;
for (i = 0; i < length; i++) {
course = courses[i];
if (course.get('courseid') === id) {
courses.splice(i, 1);
break;
}
}
},
/**
* Performs an AJAX action.
*
* @method performAjaxAction
* @param {String} action The action to perform.
* @param {Object} args The arguments to pass through with teh request.
* @param {Function} callback The function to call when all is done.
* @param {Object} context The object to use as the context for the callback.
*/
performAjaxAction: function(action, args, callback, context) {
var io = new Y.IO();
args.action = action;
args.ajax = '1';
args.sesskey = M.cfg.sesskey;
if (callback === null) {
callback = function() {
Y.log("'Action '" + action + "' completed", 'debug', 'moodle-course-management');
};
}
io.send(this.get('ajaxurl'), {
method: 'POST',
on: {
complete: callback
},
context: context,
data: args,
'arguments': args
});
}
};
Y.extend(Console, Y.Base, Console.prototype);
M.course = M.course || {};
M.course.management = M.course.management || {};
M.course.management.console = null;
/**
* Initalises the course management console.
*
* @method M.course.management.init
* @static
* @param {Object} config
*/
M.course.management.init = function(config) {
M.course.management.console = new Console(config);
};
+186
View File
@@ -0,0 +1,186 @@
/**
* A managed course.
*
* @namespace M.course.management
* @class Course
* @constructor
* @extends Item
*/
Course = function() {
Course.superclass.constructor.apply(this, arguments);
};
Course.NAME = 'moodle-course-management-course';
Course.CSS_PREFIX = 'management-course';
Course.ATTRS = {
/**
* The course ID of this course.
* @attribute courseid
* @type Number
*/
courseid: {},
/**
* True if this is the selected course.
* @attribute selected
* @type Boolean
* @default null
*/
selected: {
getter: function(value, name) {
if (value === null) {
value = this.get('node').getData(name);
this.set(name, value);
}
return value;
},
value: null
},
node: {
},
/**
* The management console tracking this course.
* @attribute console
* @type Console
* @writeOnce
*/
console: {
writeOnce: 'initOnly'
},
/**
* The category this course belongs to.
* @attribute category
* @type Category
* @writeOnce
*/
category: {
writeOnce: 'initOnly'
}
};
Course.prototype = {
/**
* Initialises the new course instance.
* @method initializer
*/
initializer: function() {
var node = this.get('node'),
category = this.get('category');
this.set('courseid', node.getData('id'));
if (category && category.registerCourse) {
category.registerCourse(this);
}
this.set('itemname', 'course');
},
/**
* Returns the name of the course.
* @method getName
* @return {String}
*/
getName: function() {
return this.get('node').one('a.coursename').get('innerHTML');
},
/**
* Handles an event relating to this course.
* @method handle
* @param {String} action
* @param {EventFacade} e
* @return {Boolean}
*/
handle: function(action, e) {
var managementconsole = this.get('console'),
args = {courseid: this.get('courseid')};
switch (action) {
case 'moveup':
e.halt();
managementconsole.performAjaxAction('movecourseup', args, this.moveup, this);
break;
case 'movedown':
e.halt();
managementconsole.performAjaxAction('movecoursedown', args, this.movedown, this);
break;
case 'show':
e.halt();
managementconsole.performAjaxAction('showcourse', args, this.show, this);
break;
case 'hide':
e.halt();
managementconsole.performAjaxAction('hidecourse', args, this.hide, this);
break;
case 'select':
var c = this.get('console'),
movetonode = c.get('courselisting').one('#menumovecoursesto');
if (movetonode) {
if (c.isCourseSelected(e.currentTarget)) {
movetonode.removeAttribute('disabled');
} else {
movetonode.setAttribute('disabled', true);
}
}
break;
default:
Y.log('Invalid AJAX action requested of managed course.', 'warn', 'moodle-course-management');
return false;
}
},
/**
* Removes this course.
* @method remove
*/
remove: function() {
this.get('console').removeCourseById(this.get('courseid'));
this.get('node').remove();
},
/**
* Moves this course after another course.
*
* @method moveAfter
* @param {Number} moveaftercourse The course to move after or 0 to put it at the top.
* @param {Number} previousid the course it was previously after in case we need to revert.
*/
moveAfter: function(moveaftercourse, previousid) {
var managementconsole = this.get('console'),
args = {
courseid: this.get('courseid'),
moveafter: moveaftercourse,
previous: previousid
};
managementconsole.performAjaxAction('movecourseafter', args, this.moveAfterResponse, this);
},
/**
* Performs the actual move.
*
* @method moveAfterResponse
* @protected
* @param {Number} transactionid The transaction ID for the request.
* @param {Object} response The response to the request.
* @param {Objects} args The arguments that were given with the request.
* @return {Boolean}
*/
moveAfterResponse: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
node = this.get('node'),
previous;
if (outcome === false) {
previous = node.ancestor('ul').one('li[data-id=' + args.previous + ']');
Y.log('AJAX failed to move this course after the requested course', 'warn', 'moodle-course-management');
if (previous) {
// After the last previous.
previous.insertAfter(node, 'after');
} else {
// Start of the list.
node.ancestor('ul').one('li').insert(node, 'before');
}
return false;
}
Y.log('AJAX successfully moved course (' + this.getName() + ')', 'info', 'moodle-course-management');
this.highlight();
}
};
Y.extend(Course, Item, Course.prototype);
+273
View File
@@ -0,0 +1,273 @@
/**
* Drag and Drop handler
*
* @namespace M.course.management
* @class DragDrop
* @constructor
* @extends Base
*/
DragDrop = function(config) {
Console.superclass.constructor.apply(this, [config]);
};
DragDrop.NAME = 'moodle-course-management-dd';
DragDrop.CSS_PREFIX = 'management-dd';
DragDrop.ATTRS = {
/**
* The management console this drag and drop has been set up for.
* @attribute console
* @type Console
* @writeOnce
*/
console: {
writeOnce: 'initOnly'
}
};
DragDrop.prototype = {
/**
* True if the user is dragging a course upwards.
* @property goingup
* @protected
* @default false
*/
goingup: false,
/**
* The last Y position of the course being dragged
* @property lasty
* @protected
* @default null
*/
lasty: null,
/**
* The sibling above the course being dragged currently (tracking its original position).
*
* @property previoussibling
* @protected
* @default false
*/
previoussibling: null,
/**
* Initialises the DragDrop instance.
* @method initializer
*/
initializer: function() {
var managementconsole = this.get('console'),
container = managementconsole.get('element'),
categorylisting = container.one('#category-listing'),
courselisting = container.one('#course-listing > .course-listing'),
categoryul = (categorylisting) ? categorylisting.one('ul.ml') : null,
courseul = (courselisting) ? courselisting.one('ul.ml') : null,
canmoveoutof = (courselisting) ? courselisting.getData('canmoveoutof') : false,
contstraint = (canmoveoutof) ? container : courseul;
if (!courseul) {
// No course listings found.
return false;
}
while (contstraint.get('scrollHeight') === 0 && !contstraint.compareTo(window.document.body)) {
contstraint = contstraint.get('parentNode');
}
courseul.all('> li').each(function(li) {
this.initCourseListing(li, contstraint);
}, this);
courseul.setData('dd', new Y.DD.Drop({
node: courseul
}));
if (canmoveoutof && categoryul) {
// Category UL may not be there if viewmode is just courses.
categoryul.all('li > div').each(function(div) {
this.initCategoryListitem(div);
}, this);
}
Y.DD.DDM.on('drag:start', this.dragStart, this);
Y.DD.DDM.on('drag:end', this.dragEnd, this);
Y.DD.DDM.on('drag:drag', this.dragDrag, this);
Y.DD.DDM.on('drop:over', this.dropOver, this);
Y.DD.DDM.on('drop:enter', this.dropEnter, this);
Y.DD.DDM.on('drop:exit', this.dropExit, this);
Y.DD.DDM.on('drop:hit', this.dropHit, this);
},
/**
* Initialises a course listing.
* @method initCourseListing
* @param Node
*/
initCourseListing: function(node, contstraint) {
node.setData('dd', new Y.DD.Drag({
node: node,
target: {
padding: '0 0 0 20'
}
}).addHandle(
'.drag-handle'
).plug(Y.Plugin.DDProxy, {
moveOnEnd: false,
borderStyle: false
}).plug(Y.Plugin.DDConstrained, {
constrain2node: contstraint
}));
},
/**
* Initialises a category listing.
* @method initCategoryListitem
* @param Node
*/
initCategoryListitem: function(node) {
node.setData('dd', new Y.DD.Drop({
node: node
}));
},
/**
* Dragging has started.
* @method dragStart
* @private
* @param {EventFacade} e
*/
dragStart: function(e) {
var drag = e.target,
node = drag.get('node'),
dragnode = drag.get('dragNode');
node.addClass('course-being-dragged');
dragnode.addClass('course-being-dragged-proxy').set('innerHTML', node.one('a.coursename').get('innerHTML'));
this.previoussibling = node.get('previousSibling');
},
/**
* Dragging has ended.
* @method dragEnd
* @private
* @param {EventFacade} e
*/
dragEnd: function(e) {
var drag = e.target,
node = drag.get('node');
node.removeClass('course-being-dragged');
this.get('console').get('element').all('#category-listing li.highlight').removeClass('highlight');
},
/**
* Dragging in progress.
* @method dragDrag
* @private
* @param {EventFacade} e
*/
dragDrag: function(e) {
var y = e.target.lastXY[1];
if (y < this.lasty) {
this.goingup = true;
} else {
this.goingup = false;
}
this.lasty = y;
},
/**
* The course has been dragged over a drop target.
* @method dropOver
* @private
* @param {EventFacade} e
*/
dropOver: function(e) {
// Get a reference to our drag and drop nodes
var drag = e.drag.get('node'),
drop = e.drop.get('node'),
tag = drop.get('tagName').toLowerCase();
if (tag === 'li' && drop.hasClass('listitem-course')) {
if (!this.goingup) {
drop = drop.get('nextSibling');
if (!drop) {
drop = e.drop.get('node');
drop.get('parentNode').append(drag);
return false;
}
}
drop.get('parentNode').insertBefore(drag, drop);
e.drop.sizeShim();
}
},
/**
* The course has been dragged over a drop target.
* @method dropEnter
* @private
* @param {EventFacade} e
*/
dropEnter: function(e) {
var drop = e.drop.get('node'),
tag = drop.get('tagName').toLowerCase();
if (tag === 'div') {
drop.ancestor('li.listitem-category').addClass('highlight');
}
},
/**
* The course has been dragged off a drop target.
* @method dropExit
* @private
* @param {EventFacade} e
*/
dropExit: function(e) {
var drop = e.drop.get('node'),
tag = drop.get('tagName').toLowerCase();
if (tag === 'div') {
drop.ancestor('li.listitem-category').removeClass('highlight');
}
},
/**
* The course has been dropped on a target.
* @method dropHit
* @private
* @param {EventFacade} e
*/
dropHit: function(e) {
var drag = e.drag.get('node'),
drop = e.drop.get('node'),
iscategory = (drop.ancestor('.listitem-category') !== null),
iscourse = !iscategory && (drop.test('.listitem-course')),
managementconsole = this.get('console'),
categoryid,
category,
courseid,
course,
aftercourseid,
previoussibling,
previousid;
if (!drag.test('.listitem-course')) {
Y.log('It was not a course being dragged.', 'warn', 'moodle-course-management');
return false;
}
courseid = drag.getData('id');
if (iscategory) {
categoryid = drop.ancestor('.listitem-category').getData('id');
Y.log('Course ' + courseid + ' dragged into category ' + categoryid);
category = managementconsole.getCategoryById(categoryid);
if (category) {
course = managementconsole.getCourseById(courseid);
if (course) {
category.moveCourseTo(course);
}
}
} else if (iscourse || drop.ancestor('#course-listing')) {
course = managementconsole.getCourseById(courseid);
previoussibling = drag.get('previousSibling');
aftercourseid = (previoussibling) ? previoussibling.getData('id') || 0 : 0;
previousid = (this.previoussibling) ? this.previoussibling.getData('id') : 0;
if (aftercourseid !== previousid) {
course.moveAfter(aftercourseid, previousid);
}
} else {
Y.log('Course dropped over unhandled target.', 'info', 'moodle-course-management');
}
}
};
Y.extend(DragDrop, Y.Base, DragDrop.prototype);
+316
View File
@@ -0,0 +1,316 @@
/**
* A managed course.
*
* @namespace M.course.management
* @class Item
* @constructor
* @extends Base
*/
Item = function() {
Item.superclass.constructor.apply(this, arguments);
};
Item.NAME = 'moodle-course-management-item';
Item.CSS_PREFIX = 'management-item';
Item.ATTRS = {
/**
* The node for this item.
* @attribute node
* @type Node
*/
node: {},
/**
* The management console.
* @attribute console
* @type Console
*/
console: {},
/**
* Describes the type of this item. Should be set by the extending class.
* @attribute itemname
* @type {String}
* @default item
*/
itemname: {
value: 'item'
}
};
Item.prototype = {
/**
* The highlight timeout for this item if there is one.
* @property highlighttimeout
* @protected
* @type Timeout
* @default null
*/
highlighttimeout: null,
/**
* Checks and parses an AJAX response for an item.
*
* @method checkAjaxResponse
* @protected
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Object|Boolean}
*/
checkAjaxResponse: function(transactionid, response, args) {
if (response.status !== 200) {
Y.log('Error: AJAX response resulted in non 200 status.', 'error', 'Item.checkAjaxResponse');
return false;
}
if (transactionid === null || args === null) {
Y.log('Error: Invalid AJAX response details provided.', 'error', 'Item.checkAjaxResponse');
return false;
}
var outcome = Y.JSON.parse(response.responseText);
if (outcome.error !== false) {
new M.core.exception(outcome);
}
if (outcome.outcome === false) {
return false;
}
return outcome;
},
/**
* Moves an item up by one.
*
* @method moveup
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
moveup: function(transactionid, response, args) {
var node,
nodeup,
nodedown,
previous,
previousup,
previousdown,
tmpnode,
outcome = this.checkAjaxResponse(transactionid, response, args);
if (outcome === false) {
Y.log('AJAX request to move ' + this.get('itemname') + ' up failed by outcome.', 'warn', 'moodle-course-management');
return false;
}
node = this.get('node');
previous = node.previous('.listitem');
if (previous) {
previous.insert(node, 'before');
previousup = previous.one(' > div a.action-moveup');
nodedown = node.one(' > div a.action-movedown');
if (!previousup || !nodedown) {
// We can have two situations here:
// 1. previousup is not set and nodedown is not set. This happens when there are only two courses.
// 2. nodedown is not set. This happens when they are moving the bottom course up.
// node up and previous down should always be there. They would be required to trigger the action.
nodeup = node.one(' > div a.action-moveup');
previousdown = previous.one(' > div a.action-movedown');
if (!previousup && !nodedown) {
// Ok, must be two courses. We need to switch the up and down icons.
tmpnode = Y.Node.create('<a style="visibility:hidden;">&nbsp;</a>');
previousdown.replace(tmpnode);
nodeup.replace(previousdown);
tmpnode.replace(nodeup);
tmpnode.destroy();
} else if (!nodedown) {
// previous down needs to be given to node.
nodeup.insert(previousdown, 'after');
}
}
nodeup = node.one(' > div a.action-moveup');
if (nodeup) {
// Try to re-focus on up.
nodeup.focus();
} else {
// If we can't focus up we're at the bottom, try to focus on up.
nodedown = node.one(' > div a.action-movedown');
if (nodedown) {
nodedown.focus();
}
}
this.updated(true);
Y.log('Success: ' + this.get('itemname') + ' moved up by AJAX.', 'info', 'moodle-course-management');
} else {
// Aha it succeeded but this is the top item in the list. Pagination is in play!
// Refresh to update the state of things.
Y.log(this.get('itemname') + ' cannot be moved up as its the top item on this page.',
'info', 'moodle-course-management');
window.location.reload();
}
},
/**
* Moves an item down by one.
*
* @method movedown
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
movedown: function(transactionid, response, args) {
var node,
next,
nodeup,
nodedown,
nextup,
nextdown,
tmpnode,
outcome = this.checkAjaxResponse(transactionid, response, args);
if (outcome === false) {
Y.log('AJAX request to move ' + this.get('itemname') + ' down failed by outcome.', 'warn', 'moodle-course-management');
return false;
}
node = this.get('node');
next = node.next('.listitem');
if (next) {
node.insert(next, 'before');
nextdown = next.one(' > div a.action-movedown');
nodeup = node.one(' > div a.action-moveup');
if (!nextdown || !nodeup) {
// next up and node down should always be there. They would be required to trigger the action.
nextup = next.one(' > div a.action-moveup');
nodedown = node.one(' > div a.action-movedown');
if (!nextdown && !nodeup) {
// We can have two situations here:
// 1. nextdown is not set and nodeup is not set. This happens when there are only two courses.
// 2. nodeup is not set. This happens when we are moving the first course down.
// Ok, must be two courses. We need to switch the up and down icons.
tmpnode = Y.Node.create('<a style="visibility:hidden;">&nbsp;</a>');
nextup.replace(tmpnode);
nodedown.replace(nextup);
tmpnode.replace(nodedown);
tmpnode.destroy();
} else if (!nodeup) {
// next up needs to be given to node.
nodedown.insert(nextup, 'before');
}
}
nodedown = node.one(' > div a.action-movedown');
if (nodedown) {
// Try to ensure the up is focused again.
nodedown.focus();
} else {
// If we can't focus up we're at the top, try to focus on down.
nodeup = node.one(' > div a.action-moveup');
if (nodeup) {
nodeup.focus();
}
}
this.updated(true);
Y.log('Success: ' + this.get('itemname') + ' moved down by AJAX.', 'info', 'moodle-course-management');
} else {
// Aha it succeeded but this is the bottom item in the list. Pagination is in play!
// Refresh to update the state of things.
Y.log(this.get('itemname') + ' cannot be moved down as its the top item on this page.',
'info', 'moodle-course-management');
window.location.reload();
}
},
/**
* Makes an item visible.
*
* @method show
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
show: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
hidebtn;
if (outcome === false) {
Y.log('AJAX request to show ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management');
return false;
}
this.markVisible();
hidebtn = this.get('node').one('a[data-action=hide]');
if (hidebtn) {
hidebtn.focus();
}
this.updated();
Y.log('Success: ' + this.get('itemname') + ' made visible by AJAX.', 'info', 'moodle-course-management');
},
/**
* Marks the item as visible
* @method markVisible
*/
markVisible: function() {
this.get('node').setAttribute('data-visible', '1');
Y.log('Marked ' + this.get('itemname') + ' as visible', 'info', 'moodle-course-management');
return true;
},
/**
* Hides an item.
*
* @method hide
* @param {Number} transactionid The transaction ID of the AJAX request (unique)
* @param {Object} response The response from the AJAX request.
* @param {Object} args The arguments given to the request.
* @return {Boolean}
*/
hide: function(transactionid, response, args) {
var outcome = this.checkAjaxResponse(transactionid, response, args),
showbtn;
if (outcome === false) {
Y.log('AJAX request to hide ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management');
return false;
}
this.markHidden();
showbtn = this.get('node').one('a[data-action=show]');
if (showbtn) {
showbtn.focus();
}
this.updated();
Y.log('Success: ' + this.get('itemname') + ' made hidden by AJAX.', 'info', 'moodle-course-management');
},
/**
* Marks the item as hidden.
* @method makeHidden
*/
markHidden: function() {
this.get('node').setAttribute('data-visible', '0');
Y.log('Marked ' + this.get('itemname') + ' as hidden', 'info', 'moodle-course-management');
return true;
},
/**
* Called when ever a node is updated.
*
* @method updated
* @param {Boolean} moved True if this item was moved.
*/
updated: function(moved) {
if (moved) {
this.highlight();
}
},
/**
* Highlights this option for a breif time.
*
* @method highlight
*/
highlight: function() {
var node = this.get('node');
node.siblings('.highlight').removeClass('highlight');
node.addClass('highlight');
if (this.highlighttimeout) {
window.clearTimeout(this.highlighttimeout);
}
this.highlighttimeout = window.setTimeout(function() {
node.removeClass('highlight');
}, 2500);
}
};
Y.extend(Item, Y.Base, Item.prototype);
+5
View File
@@ -0,0 +1,5 @@
var Category;
var Console;
var Course;
var DragDrop;
var Item;
@@ -0,0 +1,16 @@
{
"moodle-course-management": {
"requires": [
"base",
"node",
"io-base",
"moodle-core-notification-exception",
"json-parse",
"dd-constrain",
"dd-proxy",
"dd-drop",
"dd-delegate",
"node-event-delegate"
]
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "moodle-course-util",
"builds": {
"moodle-course-util-base": {
"jsfiles": [
"base.js"
]
},
"moodle-course-util-section": {
"jsfiles": [
"section.js"
]
},
"moodle-course-util-cm": {
"jsfiles": [
"cm.js"
]
}
}
}
+16
View File
@@ -0,0 +1,16 @@
/**
* The Moodle.core_course.util classes provide course-related utility functions.
*
* @module moodle-course-util
* @main
*/
Y.namespace('Moodle.core_course.util');
/**
* A collection of general utility functions for use in course.
*
* @class Moodle.core_course.util
* @static
*/
+70
View File
@@ -0,0 +1,70 @@
/**
* A collection of utility classes for use with course modules.
*
* @module moodle-course-util
* @submodule moodle-course-util-cm
*/
Y.namespace('Moodle.core_course.util.cm');
/**
* A collection of utility classes for use with course modules.
*
* @class Moodle.core_course.util.cm
* @static
*/
Y.Moodle.core_course.util.cm = {
CONSTANTS: {
MODULEIDPREFIX: 'module-'
},
SELECTORS: {
COURSEMODULE: '.activity',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the course module item from one of it's child Nodes.
*
* @method getCourseModuleNodeFromComponent
* @param coursemodulecomponent {Node} The component Node.
* @return {Node|null} The Course Module Node.
*/
getCourseModuleFromComponent: function(coursemodulecomponent) {
return Y.one(coursemodulecomponent).ancestor(this.SELECTORS.COURSEMODULE, true);
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getId: function(coursemodule) {
// We perform a simple substitution operation to get the ID.
var id = coursemodule.get('id').replace(
this.CONSTANTS.MODULEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the section ID for the provided section.
*
* @method getName
* @param coursemodule {Node} The course module to find an ID for.
* @return {Number|false} The ID of the course module in question or false if no ID was found.
*/
getName: function(coursemodule) {
var instance = coursemodule.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
}
};
+40
View File
@@ -0,0 +1,40 @@
/**
* A collection of utility classes for use with course sections.
*
* @module moodle-course-util
* @submodule moodle-course-util-section
*/
Y.namespace('Moodle.core_course.util.section');
/**
* A collection of utility classes for use with course sections.
*
* @class Moodle.core_course.util.section
* @static
*/
Y.Moodle.core_course.util.section = {
CONSTANTS: {
SECTIONIDPREFIX: 'section-'
},
/**
* Determines the section ID for the provided section.
*
* @method getId
* @param section {Node} The section to find an ID for.
* @return {Number|false} The ID of the section in question or false if no ID was found.
*/
getId: function(section) {
// We perform a simple substitution operation to get the ID.
var id = section.get('id').replace(
this.CONSTANTS.SECTIONIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
}
};
+26
View File
@@ -0,0 +1,26 @@
{
"moodle-course-util": {
"requires": [
"node"
],
"use": [
"moodle-course-util-base"
],
"submodules": {
"moodle-course-util-base": {
},
"moodle-course-util-section": {
"requires": [
"node",
"moodle-course-util-base"
]
},
"moodle-course-util-cm": {
"requires": [
"node",
"moodle-course-util-base"
]
}
}
}
}