From 83cdecec1eabec183f8824aacb05a7c4ac04e60b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 4 Dec 2013 14:13:41 +0800 Subject: [PATCH] MDL-38774 JavaScript: Migrate moodle-core-dragdrop to Shifter --- .../moodle-core-dragdrop-debug.js | 645 ++++++++++++++++++ .../moodle-core-dragdrop-min.js | 2 + .../moodle-core-dragdrop.js | 645 ++++++++++++++++++ lib/yui/dragdrop/dragdrop.js | 513 -------------- lib/yui/src/dragdrop/build.json | 10 + lib/yui/src/dragdrop/js/dragdrop.js | 640 +++++++++++++++++ lib/yui/src/dragdrop/meta/dragdrop.json | 14 + 7 files changed, 1956 insertions(+), 513 deletions(-) create mode 100644 lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js create mode 100644 lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js create mode 100644 lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js delete mode 100644 lib/yui/dragdrop/dragdrop.js create mode 100644 lib/yui/src/dragdrop/build.json create mode 100644 lib/yui/src/dragdrop/js/dragdrop.js create mode 100644 lib/yui/src/dragdrop/meta/dragdrop.json diff --git a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js new file mode 100644 index 00000000000..50142859d76 --- /dev/null +++ b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js @@ -0,0 +1,645 @@ +YUI.add('moodle-core-dragdrop', function (Y, NAME) { + +/** + * 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 groups for this instance. + * + * @property groups + * @type Array + * @default [] + */ + groups: [], + + /** + * The previous drop location. + * + * @property lastdroptarget + * @type Node + * @default null + */ + lastdroptarget: null, + + /** + * The initializer which sets up the move action. + * + * @method initializer + * @protected + */ + initializer: function() { + // Listen for all drag:start events. + Y.DD.DDM.on('drag:start', this.global_drag_start, this); + + // Listen for all drag:end events. + Y.DD.DDM.on('drag:end', this.global_drag_end, this); + + // Listen for all drag:drag events. + Y.DD.DDM.on('drag:drag', this.global_drag_drag, this); + + // Listen for all drop:over events. + Y.DD.DDM.on('drop:over', this.global_drop_over, this); + + // Listen for all drop:hit events. + Y.DD.DDM.on('drop:hit', this.global_drop_hit, this); + + // Listen for all drop:miss events. + Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this); + + // Add keybaord listeners for accessible drag/drop + 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. + Y.one(Y.config.doc.body).delegate('click', this.global_keydown, + '.' + MOVEICON.cssclass , this); + }, + + /** + * 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] The class to add to the icon + * @param {Boolean} [large=false] whether to use the larger version of + * the drag icon + * @return Node The built drag handle. + */ + get_drag_handle: function(title, classname, iconclass, large) { + var iconname = MOVEICON.pix; + if (large) { + iconname = MOVEICON.largepix; + } + var dragicon = Y.Node.create('') + .setStyle('cursor', 'move') + .setAttrs({ + 'src': M.util.image_url(iconname, MOVEICON.component), + 'alt': title + }); + if (iconclass) { + dragicon.addClass(iconclass); + } + + var dragelement = Y.Node.create('') + .addClass(classname) + .setAttribute('title', title) + .setAttribute('tabIndex', 0) + .setAttribute('data-draggroups', this.groups) + .setAttribute('role', 'button') + .setAttribute('aria-grabbed', 'false'); + dragelement.appendChild(dragicon); + dragelement.addClass(MOVEICON.cssclass); + + return dragelement; + }, + + lock_drag_handle: function(drag, classname) { + drag.removeHandle('.'+classname); + }, + + unlock_drag_handle: function(drag, classname) { + drag.addHandle('.'+classname); + }, + + 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; + } + // 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').setStyles({ + visibility: '', + opacity: '1' + }); + 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) { + // The valid node types to get text from. + var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text'); + var 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; + + // Indicate to a screenreader the node that is selected for drag and drop. + dragcontainer.setAttribute('aria-grabbed', 'true'); + // 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(''); + droplist.addClass('dragdrop-keyboard-drag'); + var listitem; + var listitemtext; + + // Search for possible drop targets. + var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass); + + droptargets.each(function (node) { + var validdrop = false, labelroot = node; + if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) { + // This is a drag and drop target with the same class as the grabbed node. + validdrop = true; + } else { + var elementgroups = node.getAttribute('data-draggroups').split(' '); + var i, j; + for (i = 0; i < elementgroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (elementgroups[i] === this.groups[j]) { + // This is a parent node of the grabbed node (used for dropping in empty sections). + validdrop = true; + // This node will have no text - so we get the first valid text from the parent. + labelroot = node.get('parentNode'); + break; + } + } + if (validdrop) { + break; + } + } + } + + if (validdrop) { + // It is a valid drop target - create a list item for it. + listitem = Y.Node.create('
  • '); + listlink = Y.Node.create(''); + nodetitle = this.find_element_text(labelroot); + + listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle); + listlink.setContent(listitemtext); + + // Add a data attribute so we can get the real drop target. + listlink.setAttribute('data-drop-target', node.get('id')); + // Notify the screen reader this is a valid drop target. + listlink.setAttribute('aria-dropeffect', 'move'); + // Allow tabbing to the link. + listlink.setAttribute('tabindex', '0'); + + // Set the event listeners for enter, space or click. + listlink.on('click', this.global_keyboard_drop, this); + listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this); + + // Add to the list or drop targets. + listitem.append(listlink); + droplist.append(listitem); + } + }, this); + + // Create the dialog for the interaction. + M.core.dragdrop.dropui = new M.core.dialogue({ + headerContent: dialogtitle, + bodyContent: droplist, + draggable: true, + visible: true, + centered: true + }); + + // Focus the first drop target. + if (droplist.one('a')) { + droplist.one('a').focus(); + } + }, + + /** + * This is used as a simulated drag/drop event in order to prevent any + * subtle bugs from creating a real instance of a drag drop event. This means + * there are no state changes in the Y.DD.DDM and any undefined functions + * will trigger an obvious and fatal error. + * The end result is that we call all our drag/drop handlers but do not bubble the + * event to anyone else. + * + * The functions/properties implemented in the wrapper are: + * e.target + * e.drag + * e.drop + * e.drag.get('node') + * e.drop.get('node') + * e.drag.addHandle() + * e.drag.removeHandle() + * + * @method simulated_drag_drop_event + * @param {Node} dragnode The drag container node + * @param {Node} dropnode The node to initiate the drop on + */ + simulated_drag_drop_event: function(dragnode, dropnode) { + + // Subclass for wrapping both drag and drop. + var DragDropWrapper = function(node) { + this.node = node; + }; + + // Method e.drag.get() - get the node. + DragDropWrapper.prototype.get = function(param) { + if (param === 'node' || param === 'dragNode' || param === 'dropNode') { + return this.node; + } + return null; + }; + + // Method e.drag.inGroup() - we have already run the group checks before triggering the event. + DragDropWrapper.prototype.inGroup = function() { + return true; + }; + + // Method e.drag.addHandle() - we don't want to run this. + DragDropWrapper.prototype.addHandle = function() {}; + // Method e.drag.removeHandle() - we don't want to run this. + DragDropWrapper.prototype.removeHandle = function() {}; + + // Create instances of the DragDropWrapper. + this.drop = new DragDropWrapper(dropnode); + this.drag = new DragDropWrapper(dragnode); + this.target = this.drop; + }, + + /** + * This is used to complete a keyboard version of a drag and drop. + * A drop event will be simulated based on the drag and drop nodes. + * @method global_keyboard_drop + * @param {Event} e The keydown / click event on the proxy drop node. + */ + global_keyboard_drop: function(e) { + // The drag node was saved. + var dragcontainer = M.core.dragdrop.keydragcontainer; + dragcontainer.setAttribute('aria-grabbed', 'false'); + // The real drop node is stored in an attribute of the proxy. + var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target')); + + // Close the dialog. + M.core.dragdrop.dropui.hide(); + // Cancel the event. + e.preventDefault(); + // Convert to drag drop events. + var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer); + var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget); + // Simulate the full sequence. + this.drag_start(dragevent); + this.global_drop_over(dropevent); + this.global_drop_hit(dropevent); + M.core.dragdrop.keydraghandle.focus(); + }, + + /** + * This is used to cancel a keyboard version of a drag and drop. + * + * @method global_cancel_keyboard_drag + */ + global_cancel_keyboard_drag: function() { + if (M.core.dragdrop.keydragcontainer) { + M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false'); + M.core.dragdrop.keydraghandle.focus(); + M.core.dragdrop.keydragcontainer = null; + } + }, + + /** + * Process key events on the drag handles. + * + * @method global_keydown + * @param {EventFacade} e The keydown / click event on the drag handle. + */ + global_keydown: function(e) { + var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true), + dragcontainer, + draggroups; + + if (draghandle === null) { + // The element clicked did not have a a draghandle in it's lineage. + return; + } + + if (e.keyCode === 27 ) { + // Escape to cancel from anywhere. + this.global_cancel_keyboard_drag(); + e.preventDefault(); + return; + } + + // Only process events on a drag handle. + if (!draghandle.hasClass(MOVEICON.cssclass)) { + return; + } + + // Do nothing if not space or enter. + if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') { + return; + } + + // Check the drag groups to see if we are the handler for this node. + draggroups = draghandle.getAttribute('data-draggroups').split(' '); + var i, j, validgroup = false; + + for (i = 0; i < draggroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (draggroups[i] === this.groups[j]) { + validgroup = true; + break; + } + } + if (validgroup) { + break; + } + } + if (!validgroup) { + return; + } + + // Valid event - start the keyboard drag. + dragcontainer = draghandle.ancestor('.yui3-dd-drop'); + this.global_start_keyboard_drag(e, draghandle, dragcontainer); + + e.preventDefault(); + }, + + + // Abstract functions definitions. + + /** + * Callback to use when dragging starts. + * + * @method drag_start + * @param {EventFacade} e + */ + drag_start: function() {}, + + /** + * Callback to use when dragging ends. + * + * @method drag_end + * @param {EventFacade} e + */ + drag_end: function() {}, + + /** + * Callback to use during dragging. + * + * @method drag_drag + * @param {EventFacade} e + */ + drag_drag: function() {}, + + /** + * Callback to use when dragging ends and is not over a drop target. + * + * @method drag_dropmiss + * @param {EventFacade} e + */ + drag_dropmiss: function() {}, + + /** + * Callback to use when a drop over event occurs. + * + * @method drop_over + * @param {EventFacade} e + */ + drop_over: function() {}, + + /** + * Callback to use on drop:hit. + * + * @method drop_hit + * @param {EventFacade} e + */ + drop_hit: function() {} +}, { + NAME: 'dragdrop', + ATTRS: {} +}); + +M.core = M.core || {}; +M.core.dragdrop = DRAGDROP; + + +}, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]}); diff --git a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js new file mode 100644 index 00000000000..7cd70d5456e --- /dev/null +++ b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js @@ -0,0 +1,2 @@ +YUI.add("moodle-core-dragdrop",function(e,t){var n={pix:"i/move_2d",largepix:"i/dragdrop",component:"moodle",cssclass:"moodle-core-dragdrop-draghandle"},r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,e.Base,{goingup:null,absgoingup:null,samenodeclass:null,parentnodeclass:null,groups:[],lastdroptarget:null,initializer:function(){e.DD.DDM.on("drag:start",this.global_drag_start,this),e.DD.DDM.on("drag:end",this.global_drag_end,this),e.DD.DDM.on("drag:drag",this.global_drag_drag,this),e.DD.DDM.on("drop:over",this.global_drop_over,this),e.DD.DDM.on("drop:hit",this.global_drop_hit,this),e.DD.DDM.on("drag:dropmiss",this.global_drag_dropmiss,this),e.one(e.config.doc.body).delegate("key",this.global_keydown,"down:32, enter, esc","."+n.cssclass,this),e.one(e.config.doc.body).delegate("click",this.global_keydown,"."+n.cssclass,this)},get_drag_handle:function(t,r,i,s){var o=n.pix;s&&(o=n.largepix);var u=e.Node.create("").setStyle("cursor","move").setAttrs({src:M.util.image_url(o,n.component),alt:t});i&&u.addClass(i);var a=e.Node.create("").addClass(r).setAttribute("title",t).setAttribute("tabIndex",0).setAttribute("data-draggroups",this.groups).setAttribute("role","button").setAttribute("aria-grabbed","false");return a.appendChild(u),a.addClass(n.cssclass),a},lock_drag_handle:function(e,t){e.removeHandle("."+t)},unlock_drag_handle:function(e,t){e.addHandle("."+t)},ajax_failure:function(e){var t={name:e.status+" "+e.statusText,message:e.responseText};return new M.core.exception(t)},in_group:function(t){var n=!1;return e.each(this.groups,function(e){t._groups[e]&&(n=!0)},this),n},global_drag_start:function(e){var t=e.target;if(!this.in_group(t))return;t.get("node").setStyle("opacity",".25"),t.get("dragNode").setStyles({opacity:".75",borderColor:t.get("node").getStyle("borderColor"),backgroundColor:t.get("node").getStyle("backgroundColor")}),t.get("dragNode").empty(),this.drag_start(e)},global_drag_end:function(e){var t=e.target;if(!this.in_group(t))return;t.get("node").setStyles({visibility:"",opacity:"1"}),this.drag_end(e)},global_drag_drag:function(e){var t=e.target,n=e.info;if(!this.in_group(t))return;n.start[1]n.xy[1]&&(this.absgoingup=!1),n.delta[1]<0?this.goingup=!0:n.delta[1]>0&&(this.goingup=!1),this.drag_drag(e)},global_drop_over:function(e){if(!e.drop||!e.drop.inGroup(this.groups))return;var t=e.drag.get("node"),n=e.drop.get("node");this.lastdroptarget=e.drop;if(n.hasClass(this.samenodeclass)){var r;this.goingup?r="before":r="after",n.insert(t,r)}else(n.hasClass(this.parentnodeclass)||n.test('[data-droptarget="1"]'))&&!n.contains(t)&&(this.goingup?n.append(t):n.prepend(t));this.drop_over(e)},global_drag_dropmiss:function(e){e.drag=e.target,e.drop=this.lastdroptarget;if(!this.in_group(e.drag))return;if(!e.drop||!e.drop.inGroup(this.groups))return;this.drag_dropmiss(e)},global_drop_hit:function(e){if(!e.drop||!e.drop.inGroup(this.groups))return;this.drop_hit(e)},find_element_text:function(t){var n=t.all("h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text"),r="";return n.each(function(){r===""&&e.Lang.trim(this.get("text"))!==""&&(r=this.get("text"))}),r!==""?r:M.util.get_string("emptydragdropregion","moodle")},global_start_keyboard_drag:function(t,n,r){M.core.dragdrop.keydragcontainer=r,M.core.dragdrop.keydraghandle=n,r.setAttribute("aria-grabbed","true");var i=this.find_element_text(r),s=M.util.get_string("movecontent","moodle",i),o=e.Node.create("");o.addClass("dragdrop-keyboard-drag");var u,a,f=e.all("."+this.samenodeclass+", ."+this.parentnodeclass);f.each(function(t){var n=!1,s=t;if(t.drop&&t.drop.inGroup(this.groups)&&t.drop.get("node")!==r)n=!0;else{var f=t.getAttribute("data-draggroups").split(" "),l,c;for(l=0;l"),listlink=e.Node.create(""),i=this.find_element_text(s),a=M.util.get_string("tocontent","moodle",i),listlink.setContent(a),listlink.setAttribute("data-drop-target",t.get("id")),listlink.setAttribute("aria-dropeffect","move"),listlink.setAttribute("tabindex","0"),listlink.on("click",this.global_keyboard_drop,this),listlink.on("key",this.global_keyboard_drop,"down:enter,32",this),u.append(listlink),o.append(u))},this),M.core.dragdrop.dropui=new M.core.dialogue({headerContent:s,bodyContent:o,draggable:!0,visible:!0,centered:!0}),o.one("a")&&o.one("a").focus()},simulated_drag_drop_event:function(e,t){var n=function(e){this.node=e};n.prototype.get=function(e){return e==="node"||e==="dragNode"||e==="dropNode"?this.node:null},n.prototype.inGroup=function(){return!0},n.prototype.addHandle=function(){},n.prototype.removeHandle=function(){},this.drop=new n(t),this.drag=new n(e),this.target=this.drop},global_keyboard_drop:function(t){var n=M.core.dragdrop.keydragcontainer;n.setAttribute("aria-grabbed","false");var r=e.one("#"+t.target.getAttribute("data-drop-target"));M.core.dragdrop.dropui.hide(),t.preventDefault();var i=new this.simulated_drag_drop_event(n,n),s=new this.simulated_drag_drop_event(n,r);this.drag_start(i),this.global_drop_over(s),this.global_drop_hit(s),M.core.dragdrop.keydraghandle.focus()},global_cancel_keyboard_drag:function(){M.core.dragdrop.keydragcontainer&&(M.core.dragdrop.keydragcontainer.setAttribute("aria-grabbed","false"),M.core.dragdrop.keydraghandle.focus(),M.core.dragdrop.keydragcontainer=null)},global_keydown:function(e){var t=e.target.ancestor("."+n.cssclass,!0),r,i;if(t===null)return;if(e.keyCode===27){this.global_cancel_keyboard_drag(),e.preventDefault();return}if(!t.hasClass(n.cssclass))return;if(e.keyCode!==13&&e.keyCode!==32&&e.type!=="click")return;i=t.getAttribute("data-draggroups").split(" ");var s,o,u=!1;for(s=0;s') + .setStyle('cursor', 'move') + .setAttrs({ + 'src': M.util.image_url(iconname, MOVEICON.component), + 'alt': title + }); + if (iconclass) { + dragicon.addClass(iconclass); + } + + var dragelement = Y.Node.create('') + .addClass(classname) + .setAttribute('title', title) + .setAttribute('tabIndex', 0) + .setAttribute('data-draggroups', this.groups) + .setAttribute('role', 'button') + .setAttribute('aria-grabbed', 'false'); + dragelement.appendChild(dragicon); + dragelement.addClass(MOVEICON.cssclass); + + return dragelement; + }, + + lock_drag_handle: function(drag, classname) { + drag.removeHandle('.'+classname); + }, + + unlock_drag_handle: function(drag, classname) { + drag.addHandle('.'+classname); + }, + + 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; + } + // 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').setStyles({ + visibility: '', + opacity: '1' + }); + 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) { + // The valid node types to get text from. + var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text'); + var 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; + + // Indicate to a screenreader the node that is selected for drag and drop. + dragcontainer.setAttribute('aria-grabbed', 'true'); + // 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('
      '); + droplist.addClass('dragdrop-keyboard-drag'); + var listitem; + var listitemtext; + + // Search for possible drop targets. + var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass); + + droptargets.each(function (node) { + var validdrop = false, labelroot = node; + if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) { + // This is a drag and drop target with the same class as the grabbed node. + validdrop = true; + } else { + var elementgroups = node.getAttribute('data-draggroups').split(' '); + var i, j; + for (i = 0; i < elementgroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (elementgroups[i] === this.groups[j]) { + // This is a parent node of the grabbed node (used for dropping in empty sections). + validdrop = true; + // This node will have no text - so we get the first valid text from the parent. + labelroot = node.get('parentNode'); + break; + } + } + if (validdrop) { + break; + } + } + } + + if (validdrop) { + // It is a valid drop target - create a list item for it. + listitem = Y.Node.create('
    • '); + listlink = Y.Node.create(''); + nodetitle = this.find_element_text(labelroot); + + listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle); + listlink.setContent(listitemtext); + + // Add a data attribute so we can get the real drop target. + listlink.setAttribute('data-drop-target', node.get('id')); + // Notify the screen reader this is a valid drop target. + listlink.setAttribute('aria-dropeffect', 'move'); + // Allow tabbing to the link. + listlink.setAttribute('tabindex', '0'); + + // Set the event listeners for enter, space or click. + listlink.on('click', this.global_keyboard_drop, this); + listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this); + + // Add to the list or drop targets. + listitem.append(listlink); + droplist.append(listitem); + } + }, this); + + // Create the dialog for the interaction. + M.core.dragdrop.dropui = new M.core.dialogue({ + headerContent: dialogtitle, + bodyContent: droplist, + draggable: true, + visible: true, + centered: true + }); + + // Focus the first drop target. + if (droplist.one('a')) { + droplist.one('a').focus(); + } + }, + + /** + * This is used as a simulated drag/drop event in order to prevent any + * subtle bugs from creating a real instance of a drag drop event. This means + * there are no state changes in the Y.DD.DDM and any undefined functions + * will trigger an obvious and fatal error. + * The end result is that we call all our drag/drop handlers but do not bubble the + * event to anyone else. + * + * The functions/properties implemented in the wrapper are: + * e.target + * e.drag + * e.drop + * e.drag.get('node') + * e.drop.get('node') + * e.drag.addHandle() + * e.drag.removeHandle() + * + * @method simulated_drag_drop_event + * @param {Node} dragnode The drag container node + * @param {Node} dropnode The node to initiate the drop on + */ + simulated_drag_drop_event: function(dragnode, dropnode) { + + // Subclass for wrapping both drag and drop. + var DragDropWrapper = function(node) { + this.node = node; + }; + + // Method e.drag.get() - get the node. + DragDropWrapper.prototype.get = function(param) { + if (param === 'node' || param === 'dragNode' || param === 'dropNode') { + return this.node; + } + return null; + }; + + // Method e.drag.inGroup() - we have already run the group checks before triggering the event. + DragDropWrapper.prototype.inGroup = function() { + return true; + }; + + // Method e.drag.addHandle() - we don't want to run this. + DragDropWrapper.prototype.addHandle = function() {}; + // Method e.drag.removeHandle() - we don't want to run this. + DragDropWrapper.prototype.removeHandle = function() {}; + + // Create instances of the DragDropWrapper. + this.drop = new DragDropWrapper(dropnode); + this.drag = new DragDropWrapper(dragnode); + this.target = this.drop; + }, + + /** + * This is used to complete a keyboard version of a drag and drop. + * A drop event will be simulated based on the drag and drop nodes. + * @method global_keyboard_drop + * @param {Event} e The keydown / click event on the proxy drop node. + */ + global_keyboard_drop: function(e) { + // The drag node was saved. + var dragcontainer = M.core.dragdrop.keydragcontainer; + dragcontainer.setAttribute('aria-grabbed', 'false'); + // The real drop node is stored in an attribute of the proxy. + var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target')); + + // Close the dialog. + M.core.dragdrop.dropui.hide(); + // Cancel the event. + e.preventDefault(); + // Convert to drag drop events. + var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer); + var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget); + // Simulate the full sequence. + this.drag_start(dragevent); + this.global_drop_over(dropevent); + this.global_drop_hit(dropevent); + M.core.dragdrop.keydraghandle.focus(); + }, + + /** + * This is used to cancel a keyboard version of a drag and drop. + * + * @method global_cancel_keyboard_drag + */ + global_cancel_keyboard_drag: function() { + if (M.core.dragdrop.keydragcontainer) { + M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false'); + M.core.dragdrop.keydraghandle.focus(); + M.core.dragdrop.keydragcontainer = null; + } + }, + + /** + * Process key events on the drag handles. + * + * @method global_keydown + * @param {EventFacade} e The keydown / click event on the drag handle. + */ + global_keydown: function(e) { + var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true), + dragcontainer, + draggroups; + + if (draghandle === null) { + // The element clicked did not have a a draghandle in it's lineage. + return; + } + + if (e.keyCode === 27 ) { + // Escape to cancel from anywhere. + this.global_cancel_keyboard_drag(); + e.preventDefault(); + return; + } + + // Only process events on a drag handle. + if (!draghandle.hasClass(MOVEICON.cssclass)) { + return; + } + + // Do nothing if not space or enter. + if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') { + return; + } + + // Check the drag groups to see if we are the handler for this node. + draggroups = draghandle.getAttribute('data-draggroups').split(' '); + var i, j, validgroup = false; + + for (i = 0; i < draggroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (draggroups[i] === this.groups[j]) { + validgroup = true; + break; + } + } + if (validgroup) { + break; + } + } + if (!validgroup) { + return; + } + + // Valid event - start the keyboard drag. + dragcontainer = draghandle.ancestor('.yui3-dd-drop'); + this.global_start_keyboard_drag(e, draghandle, dragcontainer); + + e.preventDefault(); + }, + + + // Abstract functions definitions. + + /** + * Callback to use when dragging starts. + * + * @method drag_start + * @param {EventFacade} e + */ + drag_start: function() {}, + + /** + * Callback to use when dragging ends. + * + * @method drag_end + * @param {EventFacade} e + */ + drag_end: function() {}, + + /** + * Callback to use during dragging. + * + * @method drag_drag + * @param {EventFacade} e + */ + drag_drag: function() {}, + + /** + * Callback to use when dragging ends and is not over a drop target. + * + * @method drag_dropmiss + * @param {EventFacade} e + */ + drag_dropmiss: function() {}, + + /** + * Callback to use when a drop over event occurs. + * + * @method drop_over + * @param {EventFacade} e + */ + drop_over: function() {}, + + /** + * Callback to use on drop:hit. + * + * @method drop_hit + * @param {EventFacade} e + */ + drop_hit: function() {} +}, { + NAME: 'dragdrop', + ATTRS: {} +}); + +M.core = M.core || {}; +M.core.dragdrop = DRAGDROP; + + +}, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]}); diff --git a/lib/yui/dragdrop/dragdrop.js b/lib/yui/dragdrop/dragdrop.js deleted file mode 100644 index 773373c2fc6..00000000000 --- a/lib/yui/dragdrop/dragdrop.js +++ /dev/null @@ -1,513 +0,0 @@ -YUI.add('moodle-core-dragdrop', function(Y) { - 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 - */ - var DRAGDROP = function() { - DRAGDROP.superclass.constructor.apply(this, arguments); - }; - - Y.extend(DRAGDROP, Y.Base, { - goingup : null, - absgoingup : null, - samenodeclass : null, - parentnodeclass : null, - groups : [], - lastdroptarget : null, - initializer : function() { - // Listen for all drag:start events - Y.DD.DDM.on('drag:start', this.global_drag_start, this); - // Listen for all drag:end events - Y.DD.DDM.on('drag:end', this.global_drag_end, this); - // Listen for all drag:drag events - Y.DD.DDM.on('drag:drag', this.global_drag_drag, this); - // Listen for all drop:over events - Y.DD.DDM.on('drop:over', this.global_drop_over, this); - // Listen for all drop:hit events - Y.DD.DDM.on('drop:hit', this.global_drop_hit, this); - // Listen for all drop:miss events - Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this); - - Y.one(Y.config.doc.body).delegate('key', this.global_keydown, 'down:32, enter, esc', '.' + MOVEICON.cssclass, this); - Y.one(Y.config.doc.body).delegate('click', this.global_keydown, '.' + MOVEICON.cssclass , this); - }, - - get_drag_handle: function(title, classname, iconclass, large) { - var iconname = MOVEICON.pix; - if (large) { - iconname = MOVEICON.largepix; - } - var dragicon = Y.Node.create('') - .setStyle('cursor', 'move') - .setAttrs({ - 'src' : M.util.image_url(iconname, MOVEICON.component), - 'alt' : title - }); - if (iconclass) { - dragicon.addClass(iconclass); - } - - var dragelement = Y.Node.create('') - .addClass(classname) - .setAttribute('title', title) - .setAttribute('tabIndex', 0) - .setAttribute('data-draggroups', this.groups) - .setAttribute('aria-grabbed', 'false') - .setAttribute('role', 'button'); - dragelement.appendChild(dragicon); - dragelement.addClass(MOVEICON.cssclass); - - return dragelement; - }, - - lock_drag_handle: function(drag, classname) { - drag.removeHandle('.'+classname); - }, - - unlock_drag_handle: function(drag, classname) { - drag.addHandle('.'+classname); - }, - - 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, k) { - 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; - } - // 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').setStyles({ - visibility: '', - opacity: '1' - }); - 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. - * @returns {string} The text of the first text-like child node of n. - */ - find_element_text : function(n) { - // The valid node types to get text from. - var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text'); - var 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; - - // Indicate to a screenreader the node that is selected for drag and drop. - dragcontainer.setAttribute('aria-grabbed', 'true'); - // 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('
        '); - droplist.addClass('dragdrop-keyboard-drag'); - var listitem; - var listitemtext; - - // Search for possible drop targets. - var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass); - - droptargets.each(function (node) { - var validdrop = false, labelroot = node; - if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') != dragcontainer) { - // This is a drag and drop target with the same class as the grabbed node. - validdrop = true; - } else { - var elementgroups = node.getAttribute('data-draggroups').split(' '); - var i, j; - for (i = 0; i < elementgroups.length; i++) { - for (j = 0; j < this.groups.length; j++) { - if (elementgroups[i] == this.groups[j]) { - // This is a parent node of the grabbed node (used for dropping in empty sections). - validdrop = true; - // This node will have no text - so we get the first valid text from the parent. - labelroot = node.get('parentNode'); - break; - } - } - if (validdrop) { - break; - } - } - } - - if (validdrop) { - // It is a valid drop target - create a list item for it. - listitem = Y.Node.create('
      • '); - listlink = Y.Node.create(''); - nodetitle = this.find_element_text(labelroot); - - listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle); - listlink.setContent(listitemtext); - - // Add a data attribute so we can get the real drop target. - listlink.setAttribute('data-drop-target', node.get('id')); - // Notify the screen reader this is a valid drop target. - listlink.setAttribute('aria-dropeffect', 'move'); - // Allow tabbing to the link. - listlink.setAttribute('tabindex', '0'); - - // Set the event listeners for enter, space or click. - listlink.on('click', this.global_keyboard_drop, this); - listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this); - - // Add to the list or drop targets. - listitem.append(listlink); - droplist.append(listitem); - } - }, this); - - // Create the dialog for the interaction. - M.core.dragdrop.dropui = new M.core.dialogue({ - headerContent : dialogtitle, - bodyContent : droplist, - draggable : true, - visible : true, - centered : true - }); - - // Focus the first drop target. - if (droplist.one('a')) { - droplist.one('a').focus(); - } - }, - - /** - * This is used as a simulated drag/drop event in order to prevent any - * subtle bugs from creating a real instance of a drag drop event. This means - * there are no state changes in the Y.DD.DDM and any undefined functions - * will trigger an obvious and fatal error. - * The end result is that we call all our drag/drop handlers but do not bubble the - * event to anyone else. - * - * The functions/properties implemented in the wrapper are: - * e.target - * e.drag - * e.drop - * e.drag.get('node') - * e.drop.get('node') - * e.drag.addHandle() - * e.drag.removeHandle() - * - * @class simulated_drag_drop_event - * @param {Node} dragnode The drag container node - * @param {Node} dropnode The node to initiate the drop on - */ - simulated_drag_drop_event : function(dragnode, dropnode) { - - // Subclass for wrapping both drag and drop. - var dragdropwrapper = function(node) { - this.node = node; - } - - // Method e.drag.get() - get the node. - dragdropwrapper.prototype.get = function(param) { - if (param == 'node' || param == 'dragNode' || param == 'dropNode') { - return this.node; - } - return null; - }; - - // Method e.drag.inGroup() - we have already run the group checks before triggering the event. - dragdropwrapper.prototype.inGroup = function() { - return true; - }; - - // Method e.drag.addHandle() - we don't want to run this. - dragdropwrapper.prototype.addHandle = function() {}; - // Method e.drag.removeHandle() - we don't want to run this. - dragdropwrapper.prototype.removeHandle = function() {}; - - // Create instances of the dragdropwrapper. - this.drop = new dragdropwrapper(dropnode); - this.drag = new dragdropwrapper(dragnode); - this.target = this.drop; - }, - - /** - * This is used to complete a keyboard version of a drag and drop. - * A drop event will be simulated based on the drag and drop nodes. - * @method global_keyboard_drop - * @param {Event} e The keydown / click event on the proxy drop node. - */ - global_keyboard_drop : function(e) { - // The drag node was saved. - var dragcontainer = M.core.dragdrop.keydragcontainer; - dragcontainer.setAttribute('aria-grabbed', 'false'); - // The real drop node is stored in an attribute of the proxy. - var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target')); - - // Close the dialog. - M.core.dragdrop.dropui.hide(); - // Cancel the event. - e.preventDefault(); - // Convert to drag drop events. - var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer); - var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget); - // Simulate the full sequence. - this.drag_start(dragevent); - this.global_drop_over(dropevent); - this.global_drop_hit(dropevent); - M.core.dragdrop.keydraghandle.focus(); - }, - - /** - * This is used to cancel a keyboard version of a drag and drop. - * - * @method global_cancel_keyboard_drag - */ - global_cancel_keyboard_drag : function() { - if (M.core.dragdrop.keydragcontainer) { - M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false'); - M.core.dragdrop.keydraghandle.focus(); - M.core.dragdrop.keydragcontainer = null; - } - }, - - /** - * Process key events on the drag handles. - * @method global_keydown - * @param {Event} e The keydown / click event on the drag handle. - */ - global_keydown : function(e) { - var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true), - dragcontainer, - draggroups; - - if (draghandle === null) { - // The element clicked did not have a a draghandle in it's lineage. - return; - } - - if (e.keyCode === 27 ) { - // Escape to cancel from anywhere. - this.global_cancel_keyboard_drag(); - e.preventDefault(); - return; - } - - // Only process events on a drag handle. - if (!draghandle.hasClass(MOVEICON.cssclass)) { - return; - } - - // Do nothing if not space or enter. - if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') { - return; - } - - // Check the drag groups to see if we are the handler for this node. - draggroups = draghandle.getAttribute('data-draggroups').split(' '); - var i, j, validgroup = false; - - for (i = 0; i < draggroups.length; i++) { - for (j = 0; j < this.groups.length; j++) { - if (draggroups[i] === this.groups[j]) { - validgroup = true; - break; - } - } - if (validgroup) { - break; - } - } - if (!validgroup) { - return; - } - - // Valid event - start the keyboard drag. - dragcontainer = draghandle.ancestor('.yui3-dd-drop'); - this.global_start_keyboard_drag(e, draghandle, dragcontainer); - - e.preventDefault(); - }, - - /* - * Abstract functions definitions - */ - drag_start : function(e) {}, - drag_end : function(e) {}, - drag_drag : function(e) {}, - drag_dropmiss : function(e) {}, - drop_over : function(e) {}, - drop_hit : function(e) {} - }, { - NAME : 'dragdrop', - ATTRS : {} - }); - -M.core = M.core || {}; -M.core.dragdrop = DRAGDROP; - -}, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'event-key', 'event-focus', 'moodle-core-notification']}); diff --git a/lib/yui/src/dragdrop/build.json b/lib/yui/src/dragdrop/build.json new file mode 100644 index 00000000000..e21619a7333 --- /dev/null +++ b/lib/yui/src/dragdrop/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-core-dragdrop", + "builds": { + "moodle-core-dragdrop": { + "jsfiles": [ + "dragdrop.js" + ] + } + } +} diff --git a/lib/yui/src/dragdrop/js/dragdrop.js b/lib/yui/src/dragdrop/js/dragdrop.js new file mode 100644 index 00000000000..87bfd03b573 --- /dev/null +++ b/lib/yui/src/dragdrop/js/dragdrop.js @@ -0,0 +1,640 @@ +/** + * 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 groups for this instance. + * + * @property groups + * @type Array + * @default [] + */ + groups: [], + + /** + * The previous drop location. + * + * @property lastdroptarget + * @type Node + * @default null + */ + lastdroptarget: null, + + /** + * The initializer which sets up the move action. + * + * @method initializer + * @protected + */ + initializer: function() { + // Listen for all drag:start events. + Y.DD.DDM.on('drag:start', this.global_drag_start, this); + + // Listen for all drag:end events. + Y.DD.DDM.on('drag:end', this.global_drag_end, this); + + // Listen for all drag:drag events. + Y.DD.DDM.on('drag:drag', this.global_drag_drag, this); + + // Listen for all drop:over events. + Y.DD.DDM.on('drop:over', this.global_drop_over, this); + + // Listen for all drop:hit events. + Y.DD.DDM.on('drop:hit', this.global_drop_hit, this); + + // Listen for all drop:miss events. + Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this); + + // Add keybaord listeners for accessible drag/drop + 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. + Y.one(Y.config.doc.body).delegate('click', this.global_keydown, + '.' + MOVEICON.cssclass , this); + }, + + /** + * 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] The class to add to the icon + * @param {Boolean} [large=false] whether to use the larger version of + * the drag icon + * @return Node The built drag handle. + */ + get_drag_handle: function(title, classname, iconclass, large) { + var iconname = MOVEICON.pix; + if (large) { + iconname = MOVEICON.largepix; + } + var dragicon = Y.Node.create('') + .setStyle('cursor', 'move') + .setAttrs({ + 'src': M.util.image_url(iconname, MOVEICON.component), + 'alt': title + }); + if (iconclass) { + dragicon.addClass(iconclass); + } + + var dragelement = Y.Node.create('') + .addClass(classname) + .setAttribute('title', title) + .setAttribute('tabIndex', 0) + .setAttribute('data-draggroups', this.groups) + .setAttribute('role', 'button') + .setAttribute('aria-grabbed', 'false'); + dragelement.appendChild(dragicon); + dragelement.addClass(MOVEICON.cssclass); + + return dragelement; + }, + + lock_drag_handle: function(drag, classname) { + drag.removeHandle('.'+classname); + }, + + unlock_drag_handle: function(drag, classname) { + drag.addHandle('.'+classname); + }, + + 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; + } + // 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').setStyles({ + visibility: '', + opacity: '1' + }); + 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) { + // The valid node types to get text from. + var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text'); + var 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; + + // Indicate to a screenreader the node that is selected for drag and drop. + dragcontainer.setAttribute('aria-grabbed', 'true'); + // 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('
          '); + droplist.addClass('dragdrop-keyboard-drag'); + var listitem; + var listitemtext; + + // Search for possible drop targets. + var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass); + + droptargets.each(function (node) { + var validdrop = false, labelroot = node; + if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) { + // This is a drag and drop target with the same class as the grabbed node. + validdrop = true; + } else { + var elementgroups = node.getAttribute('data-draggroups').split(' '); + var i, j; + for (i = 0; i < elementgroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (elementgroups[i] === this.groups[j]) { + // This is a parent node of the grabbed node (used for dropping in empty sections). + validdrop = true; + // This node will have no text - so we get the first valid text from the parent. + labelroot = node.get('parentNode'); + break; + } + } + if (validdrop) { + break; + } + } + } + + if (validdrop) { + // It is a valid drop target - create a list item for it. + listitem = Y.Node.create('
        • '); + listlink = Y.Node.create(''); + nodetitle = this.find_element_text(labelroot); + + listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle); + listlink.setContent(listitemtext); + + // Add a data attribute so we can get the real drop target. + listlink.setAttribute('data-drop-target', node.get('id')); + // Notify the screen reader this is a valid drop target. + listlink.setAttribute('aria-dropeffect', 'move'); + // Allow tabbing to the link. + listlink.setAttribute('tabindex', '0'); + + // Set the event listeners for enter, space or click. + listlink.on('click', this.global_keyboard_drop, this); + listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this); + + // Add to the list or drop targets. + listitem.append(listlink); + droplist.append(listitem); + } + }, this); + + // Create the dialog for the interaction. + M.core.dragdrop.dropui = new M.core.dialogue({ + headerContent: dialogtitle, + bodyContent: droplist, + draggable: true, + visible: true, + centered: true + }); + + // Focus the first drop target. + if (droplist.one('a')) { + droplist.one('a').focus(); + } + }, + + /** + * This is used as a simulated drag/drop event in order to prevent any + * subtle bugs from creating a real instance of a drag drop event. This means + * there are no state changes in the Y.DD.DDM and any undefined functions + * will trigger an obvious and fatal error. + * The end result is that we call all our drag/drop handlers but do not bubble the + * event to anyone else. + * + * The functions/properties implemented in the wrapper are: + * e.target + * e.drag + * e.drop + * e.drag.get('node') + * e.drop.get('node') + * e.drag.addHandle() + * e.drag.removeHandle() + * + * @method simulated_drag_drop_event + * @param {Node} dragnode The drag container node + * @param {Node} dropnode The node to initiate the drop on + */ + simulated_drag_drop_event: function(dragnode, dropnode) { + + // Subclass for wrapping both drag and drop. + var DragDropWrapper = function(node) { + this.node = node; + }; + + // Method e.drag.get() - get the node. + DragDropWrapper.prototype.get = function(param) { + if (param === 'node' || param === 'dragNode' || param === 'dropNode') { + return this.node; + } + return null; + }; + + // Method e.drag.inGroup() - we have already run the group checks before triggering the event. + DragDropWrapper.prototype.inGroup = function() { + return true; + }; + + // Method e.drag.addHandle() - we don't want to run this. + DragDropWrapper.prototype.addHandle = function() {}; + // Method e.drag.removeHandle() - we don't want to run this. + DragDropWrapper.prototype.removeHandle = function() {}; + + // Create instances of the DragDropWrapper. + this.drop = new DragDropWrapper(dropnode); + this.drag = new DragDropWrapper(dragnode); + this.target = this.drop; + }, + + /** + * This is used to complete a keyboard version of a drag and drop. + * A drop event will be simulated based on the drag and drop nodes. + * @method global_keyboard_drop + * @param {Event} e The keydown / click event on the proxy drop node. + */ + global_keyboard_drop: function(e) { + // The drag node was saved. + var dragcontainer = M.core.dragdrop.keydragcontainer; + dragcontainer.setAttribute('aria-grabbed', 'false'); + // The real drop node is stored in an attribute of the proxy. + var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target')); + + // Close the dialog. + M.core.dragdrop.dropui.hide(); + // Cancel the event. + e.preventDefault(); + // Convert to drag drop events. + var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer); + var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget); + // Simulate the full sequence. + this.drag_start(dragevent); + this.global_drop_over(dropevent); + this.global_drop_hit(dropevent); + M.core.dragdrop.keydraghandle.focus(); + }, + + /** + * This is used to cancel a keyboard version of a drag and drop. + * + * @method global_cancel_keyboard_drag + */ + global_cancel_keyboard_drag: function() { + if (M.core.dragdrop.keydragcontainer) { + M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false'); + M.core.dragdrop.keydraghandle.focus(); + M.core.dragdrop.keydragcontainer = null; + } + }, + + /** + * Process key events on the drag handles. + * + * @method global_keydown + * @param {EventFacade} e The keydown / click event on the drag handle. + */ + global_keydown: function(e) { + var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true), + dragcontainer, + draggroups; + + if (draghandle === null) { + // The element clicked did not have a a draghandle in it's lineage. + return; + } + + if (e.keyCode === 27 ) { + // Escape to cancel from anywhere. + this.global_cancel_keyboard_drag(); + e.preventDefault(); + return; + } + + // Only process events on a drag handle. + if (!draghandle.hasClass(MOVEICON.cssclass)) { + return; + } + + // Do nothing if not space or enter. + if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') { + return; + } + + // Check the drag groups to see if we are the handler for this node. + draggroups = draghandle.getAttribute('data-draggroups').split(' '); + var i, j, validgroup = false; + + for (i = 0; i < draggroups.length; i++) { + for (j = 0; j < this.groups.length; j++) { + if (draggroups[i] === this.groups[j]) { + validgroup = true; + break; + } + } + if (validgroup) { + break; + } + } + if (!validgroup) { + return; + } + + // Valid event - start the keyboard drag. + dragcontainer = draghandle.ancestor('.yui3-dd-drop'); + this.global_start_keyboard_drag(e, draghandle, dragcontainer); + + e.preventDefault(); + }, + + + // Abstract functions definitions. + + /** + * Callback to use when dragging starts. + * + * @method drag_start + * @param {EventFacade} e + */ + drag_start: function() {}, + + /** + * Callback to use when dragging ends. + * + * @method drag_end + * @param {EventFacade} e + */ + drag_end: function() {}, + + /** + * Callback to use during dragging. + * + * @method drag_drag + * @param {EventFacade} e + */ + drag_drag: function() {}, + + /** + * Callback to use when dragging ends and is not over a drop target. + * + * @method drag_dropmiss + * @param {EventFacade} e + */ + drag_dropmiss: function() {}, + + /** + * Callback to use when a drop over event occurs. + * + * @method drop_over + * @param {EventFacade} e + */ + drop_over: function() {}, + + /** + * Callback to use on drop:hit. + * + * @method drop_hit + * @param {EventFacade} e + */ + drop_hit: function() {} +}, { + NAME: 'dragdrop', + ATTRS: {} +}); + +M.core = M.core || {}; +M.core.dragdrop = DRAGDROP; diff --git a/lib/yui/src/dragdrop/meta/dragdrop.json b/lib/yui/src/dragdrop/meta/dragdrop.json new file mode 100644 index 00000000000..4e4f51c655c --- /dev/null +++ b/lib/yui/src/dragdrop/meta/dragdrop.json @@ -0,0 +1,14 @@ +{ + "moodle-core-dragdrop": { + "requires": [ + "base", + "node", + "io", + "dom", + "dd", + "event-key", + "event-focus", + "moodle-core-notification" + ] + } +}