YUI.add('moodle-core-dragdrop', function (Y, NAME) { /* eslint-disable no-empty-function */ /** * The core drag and drop module for Moodle which extends the YUI drag and * drop functionality with additional features. * * @module moodle-core-dragdrop */ var MOVEICON = { pix: "i/move_2d", largepix: "i/dragdrop", component: 'moodle', cssclass: 'moodle-core-dragdrop-draghandle' }; /** * General DRAGDROP class, this should not be used directly, * it is supposed to be extended by your class * * @class M.core.dragdrop * @constructor * @extends Base */ var DRAGDROP = function() { DRAGDROP.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGDROP, Y.Base, { /** * Whether the item is being moved upwards compared with the last * location. * * @property goingup * @type Boolean * @default null */ goingup: null, /** * Whether the item is being moved upwards compared with the start * point. * * @property absgoingup * @type Boolean * @default null */ absgoingup: null, /** * The class for the object. * * @property samenodeclass * @type String * @default null */ samenodeclass: null, /** * The class on the parent of the item being moved. * * @property parentnodeclass * @type String * @default */ parentnodeclass: null, /** * The label to use with keyboard drag/drop to describe items of the same Node. * * @property samenodelabel * @type Object * @default null */ samenodelabel: null, /** * The label to use with keyboard drag/drop to describe items of the parent Node. * * @property samenodelabel * @type Object * @default null */ parentnodelabel: null, /** * The groups for this instance. * * @property groups * @type Array * @default [] */ groups: [], /** * The previous drop location. * * @property lastdroptarget * @type Node * @default null */ lastdroptarget: null, /** * Listeners. * * @property listeners * @type Array * @default null */ listeners: null, /** * The initializer which sets up the move action. * * @method initializer * @protected */ initializer: function() { this.listeners = []; // Listen for all drag:start events. this.listeners.push(Y.DD.DDM.on('drag:start', this.global_drag_start, this)); // Listen for all drag:end events. this.listeners.push(Y.DD.DDM.on('drag:end', this.global_drag_end, this)); // Listen for all drag:drag events. this.listeners.push(Y.DD.DDM.on('drag:drag', this.global_drag_drag, this)); // Listen for all drop:over events. this.listeners.push(Y.DD.DDM.on('drop:over', this.global_drop_over, this)); // Listen for all drop:hit events. this.listeners.push(Y.DD.DDM.on('drop:hit', this.global_drop_hit, this)); // Listen for all drop:miss events. this.listeners.push(Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this)); // Add keybaord listeners for accessible drag/drop this.listeners.push(Y.one(Y.config.doc.body).delegate('key', this.global_keydown, 'down:32, enter, esc', '.' + MOVEICON.cssclass, this)); // Make the accessible drag/drop respond to a single click. this.listeners.push(Y.one(Y.config.doc.body).delegate('click', this.global_keydown, '.' + MOVEICON.cssclass, this)); }, /** * The destructor to shut down the instance of the dragdrop system. * * @method destructor * @protected */ destructor: function() { new Y.EventHandle(this.listeners).detach(); }, /** * Build a new drag handle Node. * * @method get_drag_handle * @param {String} title The title on the drag handle * @param {String} classname The name of the class to add to the node * wrapping the drag icon * @param {String} iconclass Additional class to add to the icon. * @return Node The built drag handle. */ get_drag_handle: function(title, classname, iconclass) { var dragelement = Y.Node.create('') .addClass(classname) .setAttribute('title', title) .setAttribute('tabIndex', 0) .setAttribute('data-draggroups', this.groups) .setAttribute('role', 'button'); dragelement.addClass(MOVEICON.cssclass); window.require(['core/templates'], function(Templates) { Templates.renderPix('i/move_2d', 'core').then(function(html) { var dragicon = Y.Node.create(html); dragicon.setStyle('cursor', 'move'); if (typeof iconclass != 'undefined') { dragicon.addClass(iconclass); } dragelement.appendChild(dragicon); }); }); return dragelement; }, lock_drag_handle: function(drag, classname) { drag.removeHandle('.' + classname); }, unlock_drag_handle: function(drag, classname) { drag.addHandle('.' + classname); drag.get('activeHandle').focus(); }, ajax_failure: function(response) { var e = { name: response.status + ' ' + response.statusText, message: response.responseText }; return new M.core.exception(e); }, in_group: function(target) { var ret = false; Y.each(this.groups, function(v) { if (target._groups[v]) { ret = true; } }, this); return ret; }, /* * Drag-dropping related functions */ global_drag_start: function(e) { // Get our drag object var drag = e.target; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Store the nodes current style, so we can restore it later. this.originalstyle = drag.get('node').getAttribute('style'); // Set some general styles here drag.get('node').setStyle('opacity', '.25'); drag.get('dragNode').setStyles({ opacity: '.75', borderColor: drag.get('node').getStyle('borderColor'), backgroundColor: drag.get('node').getStyle('backgroundColor') }); drag.get('dragNode').empty(); this.drag_start(e); }, global_drag_end: function(e) { var drag = e.target; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Put our general styles back drag.get('node').setAttribute('style', this.originalstyle); this.drag_end(e); }, global_drag_drag: function(e) { var drag = e.target, info = e.info; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Note, we test both < and > situations here. We don't want to // effect a change in direction if the user is only moving side // to side with no Y position change. // Detect changes in the position relative to the start point. if (info.start[1] < info.xy[1]) { // We are going up if our final position is higher than our start position. this.absgoingup = true; } else if (info.start[1] > info.xy[1]) { // Otherwise we're going down. this.absgoingup = false; } // Detect changes in the position relative to the last movement. if (info.delta[1] < 0) { // We are going up if our final position is higher than our start position. this.goingup = true; } else if (info.delta[1] > 0) { // Otherwise we're going down. this.goingup = false; } this.drag_drag(e); }, global_drop_over: function(e) { // Check that drop object belong to correct group. if (!e.drop || !e.drop.inGroup(this.groups)) { return; } // Get a reference to our drag and drop nodes. var drag = e.drag.get('node'), drop = e.drop.get('node'); // Save last drop target for the case of missed target processing. this.lastdroptarget = e.drop; // Are we dropping within the same parent node? if (drop.hasClass(this.samenodeclass)) { var where; if (this.goingup) { where = "before"; } else { where = "after"; } // Add the node contents so that it's moved, otherwise only the drag handle is moved. drop.insert(drag, where); } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) { // We are dropping on parent node and it is empty if (this.goingup) { drop.append(drag); } else { drop.prepend(drag); } } this.drop_over(e); }, global_drag_dropmiss: function(e) { // drag:dropmiss does not have e.drag and e.drop properties // we substitute them for the ease of use. For e.drop we use, // this.lastdroptarget (ghost node we use for indicating where to drop) e.drag = e.target; e.drop = this.lastdroptarget; // Check that drag object belongs to correct group if (!this.in_group(e.drag)) { return; } // Check that drop object belong to correct group if (!e.drop || !e.drop.inGroup(this.groups)) { return; } this.drag_dropmiss(e); }, global_drop_hit: function(e) { // Check that drop object belong to correct group if (!e.drop || !e.drop.inGroup(this.groups)) { return; } this.drop_hit(e); }, /** * This is used to build the text for the heading of the keyboard * drag drop menu and the text for the nodes in the list. * @method find_element_text * @param {Node} n The node to start searching for a valid text node. * @return {string} The text of the first text-like child node of n. */ find_element_text: function(n) { var text = ''; // Try to resolve using aria-label first. text = n.get('aria-label') || ''; if (text.length > 0) { return text; } // Now try to resolve using aria-labelledby. var labelledByNode = n.get('aria-labelledby'); if (labelledByNode) { var labelNode = Y.one('#' + labelledByNode); if (labelNode && labelNode.get('text').length > 0) { return labelNode.get('text'); } } // The valid node types to get text from. var nodes = n.all('h2, h3, h4, h5, span:not(.actions):not(.menu-action-text), p, div.no-overflow, div.dimmed_text'); nodes.each(function() { if (text === '') { if (Y.Lang.trim(this.get('text')) !== '') { text = this.get('text'); } } }); if (text !== '') { return text; } return M.util.get_string('emptydragdropregion', 'moodle'); }, /** * This is used to initiate a keyboard version of a drag and drop. * A dialog will open listing all the valid drop targets that can be selected * using tab, tab, tab, enter. * @method global_start_keyboard_drag * @param {Event} e The keydown / click event on the grab handle. * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle). * @param {Node} draghandle The node that triggered this action. */ global_start_keyboard_drag: function(e, draghandle, dragcontainer) { M.core.dragdrop.keydragcontainer = dragcontainer; M.core.dragdrop.keydraghandle = draghandle; // Get the name of the thing to move. var nodetitle = this.find_element_text(dragcontainer); var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle); // Build the list of drop targets. var droplist = Y.Node.create('