MDL-34209 JavaScript: Tidy up dragdrop code.
[moodle.git] / lib / yui / dragdrop / dragdrop.js
1 YUI.add('moodle-core-dragdrop', function(Y) {
2     var MOVEICON = {
3         pix: "i/move_2d",
4         largepix: "i/dragdrop",
5         component: 'moodle',
6         cssclass: 'moodle-core-dragdrop-draghandle'
7     };
9    /*
10     * General DRAGDROP class, this should not be used directly,
11     * it is supposed to be extended by your class
12     */
13     var DRAGDROP = function() {
14         DRAGDROP.superclass.constructor.apply(this, arguments);
15     };
17     Y.extend(DRAGDROP, Y.Base, {
18         goingup : null,
19         absgoingup : null,
20         samenodeclass : null,
21         parentnodeclass : null,
22         groups : [],
23         lastdroptarget : null,
24         initializer : function(params) {
25             // Listen for all drag:start events
26             Y.DD.DDM.on('drag:start', this.global_drag_start, this);
27             // Listen for all drag:end events
28             Y.DD.DDM.on('drag:end', this.global_drag_end, this);
29             // Listen for all drag:drag events
30             Y.DD.DDM.on('drag:drag', this.global_drag_drag, this);
31             // Listen for all drop:over events
32             Y.DD.DDM.on('drop:over', this.global_drop_over, this);
33             // Listen for all drop:hit events
34             Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
35             // Listen for all drop:miss events
36             Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
38             Y.on('key', this.global_keydown, window, 'down:32,enter,esc', this);
39         },
41         get_drag_handle: function(title, classname, iconclass, large) {
42             var iconname = MOVEICON.pix;
43             if (large) {
44                 iconname = MOVEICON.largepix;
45             }
46             var dragicon = Y.Node.create('<img />')
47                 .setStyle('cursor', 'move')
48                 .setAttrs({
49                     'src' : M.util.image_url(iconname, MOVEICON.component),
50                     'alt' : title
51                 });
52             if (iconclass) {
53                 dragicon.addClass(iconclass);
54             }
56             var dragelement = Y.Node.create('<span></span>')
57                 .addClass(classname)
58                 .setAttribute('title', title)
59                 .setAttribute('tabIndex', 0)
60                 .setAttribute('data-draggroups', this.groups);
61             dragelement.appendChild(dragicon);
62             dragelement.addClass(MOVEICON.cssclass);
64             return dragelement;
65         },
67         lock_drag_handle: function(drag, classname) {
68             drag.removeHandle('.'+classname);
69         },
71         unlock_drag_handle: function(drag, classname) {
72             drag.addHandle('.'+classname);
73         },
75         ajax_failure: function(response) {
76             var e = {
77                 name : response.status+' '+response.statusText,
78                 message : response.responseText
79             };
80             return new M.core.exception(e);
81         },
83         in_group: function(target) {
84             var ret = false;
85             Y.each(this.groups, function(v, k) {
86                 if (target._groups[v]) {
87                     ret = true;
88                 }
89             }, this);
90             return ret;
91         },
92         /*
93          * Drag-dropping related functions
94          */
95         global_drag_start : function(e) {
96             // Get our drag object
97             var drag = e.target;
98             // Check that drag object belongs to correct group
99             if (!this.in_group(drag)) {
100                 return;
101             }
102             // Set some general styles here
103             drag.get('node').setStyle('opacity', '.25');
104             drag.get('dragNode').setStyles({
105                 opacity: '.75',
106                 borderColor: drag.get('node').getStyle('borderColor'),
107                 backgroundColor: drag.get('node').getStyle('backgroundColor')
108             });
109             drag.get('dragNode').empty();
110             this.drag_start(e);
111         },
113         global_drag_end : function(e) {
114             var drag = e.target;
115             // Check that drag object belongs to correct group
116             if (!this.in_group(drag)) {
117                 return;
118             }
119             //Put our general styles back
120             drag.get('node').setStyles({
121                 visibility: '',
122                 opacity: '1'
123             });
124             this.drag_end(e);
125         },
127         global_drag_drag : function(e) {
128             var drag = e.target,
129                 info = e.info;
131             // Check that drag object belongs to correct group
132             if (!this.in_group(drag)) {
133                 return;
134             }
136             // Note, we test both < and > situations here. We don't want to
137             // effect a change in direction if the user is only moving side
138             // to side with no Y position change.
140             // Detect changes in the position relative to the start point.
141             if (info.start[1] < info.xy[1]) {
142                 // We are going up if our final position is higher than our start position.
143                 this.absgoingup = true;
145             } else if (info.start[1] > info.xy[1]) {
146                 // Otherwise we're going down.
147                 this.absgoingup = false;
148             }
150             // Detect changes in the position relative to the last movement.
151             if (info.delta[1] < 0) {
152                 // We are going up if our final position is higher than our start position.
153                 this.goingup = true;
155             } else if (info.delta[1] > 0) {
156                 // Otherwise we're going down.
157                 this.goingup = false;
158             }
160             this.drag_drag(e);
161         },
163         global_drop_over : function(e) {
164             // Check that drop object belong to correct group.
165             if (!e.drop || !e.drop.inGroup(this.groups)) {
166                 return;
167             }
169             // Get a reference to our drag and drop nodes.
170             var drag = e.drag.get('node'),
171                 drop = e.drop.get('node');
173             // Save last drop target for the case of missed target processing.
174             this.lastdroptarget = e.drop;
176             // Are we dropping within the same parent node?
177             if (drop.hasClass(this.samenodeclass)) {
178                 var where;
180                 if (this.goingup) {
181                     where = "before";
182                 } else {
183                     where = "after";
184                 }
186                 // Add the node contents so that it's moved, otherwise only the drag handle is moved.
187                 drop.insert(drag, where);
188             } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
189                 // We are dropping on parent node and it is empty
190                 if (this.goingup) {
191                     drop.append(drag);
192                 } else {
193                     drop.prepend(drag);
194                 }
195             }
196             this.drop_over(e);
197         },
199         global_drag_dropmiss : function(e) {
200             // drag:dropmiss does not have e.drag and e.drop properties
201             // we substitute them for the ease of use. For e.drop we use,
202             // this.lastdroptarget (ghost node we use for indicating where to drop)
203             e.drag = e.target;
204             e.drop = this.lastdroptarget;
205             // Check that drag object belongs to correct group
206             if (!this.in_group(e.drag)) {
207                 return;
208             }
209             // Check that drop object belong to correct group
210             if (!e.drop || !e.drop.inGroup(this.groups)) {
211                 return;
212             }
213             this.drag_dropmiss(e);
214         },
216         global_drop_hit : function(e) {
217             // Check that drop object belong to correct group
218             if (!e.drop || !e.drop.inGroup(this.groups)) {
219                 return;
220             }
221             this.drop_hit(e);
222         },
224         /**
225          * This is used to build the text for the heading of the keyboard
226          * drag drop menu and the text for the nodes in the list.
227          * @method find_element_text
228          * @param {Node} n The node to start searching for a valid text node.
229          * @returns {string} The text of the first text-like child node of n.
230          */
231         find_element_text : function(n) {
232             // The valid node types to get text from.
233             var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text');
234             var text = '';
235             debugger;
237             nodes.each(function () {
238                 if (text == '') {
239                     if (Y.Lang.trim(this.get('text')) != '') {
240                         text = this.get('text');
241                     }
242                 }
243             });
245             if (text != '') {
246                 return text;
247             }
248             return M.util.get_string('emptydragdropregion', 'moodle');
249         },
251         /**
252          * This is used to initiate a keyboard version of a drag and drop.
253          * A dialog will open listing all the valid drop targets that can be selected
254          * using tab, tab, tab, enter.
255          * @method global_start_keyboard_drag
256          * @param {Event} e The keydown / click event on the grab handle.
257          * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
258          * @param {Node} draghandle The node that triggered this action.
259          */
260         global_start_keyboard_drag : function(e, draghandle, dragcontainer) {
261             M.core.dragdrop.keydragcontainer = dragcontainer;
262             M.core.dragdrop.keydraghandle = draghandle;
264             // Indicate to a screenreader the node that is selected for drag and drop.
265             dragcontainer.setAttribute('aria-grabbed', 'true');
266             // Get the name of the thing to move.
267             var nodetitle = this.find_element_text(dragcontainer);
268             var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
270             // Build the list of drop targets.
271             var droplist = Y.Node.create('<ul></ul>');
272             droplist.addClass('dragdrop-keyboard-drag');
273             var listitem;
274             var listitemtext;
276             // Search for possible drop targets.
277             var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
279             droptargets.each(function (node) {
280                 var validdrop = false, labelroot = node;
281                 if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') != dragcontainer) {
282                     // This is a drag and drop target with the same class as the grabbed node.
283                     validdrop = true;
284                 } else {
285                     var elementgroups = node.getAttribute('data-draggroups').split(' ');
286                     var i, j;
287                     for (i = 0; i < elementgroups.length; i++) {
288                         for (j = 0; j < this.groups.length; j++) {
289                             if (elementgroups[i] == this.groups[j]) {
290                                 // This is a parent node of the grabbed node (used for dropping in empty sections).
291                                 validdrop = true;
292                                 // This node will have no text - so we get the first valid text from the parent.
293                                 labelroot = node.get('parentNode');
294                                 break;
295                             }
296                         }
297                         if (validdrop) {
298                             break;
299                         }
300                     }
301                 }
303                 if (validdrop) {
304                     // It is a valid drop target - create a list item for it.
305                     listitem = Y.Node.create('<li></li>');
306                     listlink = Y.Node.create('<a></a>');
307                     nodetitle = this.find_element_text(labelroot);
309                     listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle);
310                     listlink.setContent(listitemtext);
312                     // Add a data attribute so we can get the real drop target.
313                     listlink.setAttribute('data-drop-target', node.get('id'));
314                     // Notify the screen reader this is a valid drop target.
315                     listlink.setAttribute('aria-dropeffect', 'move');
316                     // Allow tabbing to the link.
317                     listlink.setAttribute('tabindex', '0');
319                     // Set the event listeners for enter, space or click.
320                     listlink.on('click', this.global_keyboard_drop, this);
321                     listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
323                     // Add to the list or drop targets.
324                     listitem.append(listlink);
325                     droplist.append(listitem);
326                 }
327             }, this);
329             // Create the dialog for the interaction.
330             M.core.dragdrop.dropui = new M.core.dialogue({
331                 headerContent : dialogtitle,
332                 bodyContent : droplist,
333                 draggable : true,
334                 visible : true
335             });
337             // Focus the first drop target.
338             if (droplist.one('a')) {
339                 droplist.one('a').focus();
340             }
341         },
343         /**
344          * This is used as a simulated drag/drop event in order to prevent any
345          * subtle bugs from creating a real instance of a drag drop event. This means
346          * there are no state changes in the Y.DD.DDM and any undefined functions
347          * will trigger an obvious and fatal error.
348          * The end result is that we call all our drag/drop handlers but do not bubble the
349          * event to anyone else.
350          *
351          * The functions/properties implemented in the wrapper are:
352          * e.target
353          * e.drag
354          * e.drop
355          * e.drag.get('node')
356          * e.drop.get('node')
357          * e.drag.addHandle()
358          * e.drag.removeHandle()
359          *
360          * @class simulated_drag_drop_event
361          * @param {Node} dragnode The drag container node
362          * @param {Node} dropnode The node to initiate the drop on
363          */
364         simulated_drag_drop_event : function(dragnode, dropnode) {
366             // Subclass for wrapping both drag and drop.
367             var dragdropwrapper = function(node) {
368                 this.node = node;
369             }
371             // Method e.drag.get() - get the node.
372             dragdropwrapper.prototype.get = function(param) {
373                 if (param == 'node' || param == 'dragNode' || param == 'dropNode') {
374                     return this.node;
375                 }
376                 return null;
377             };
379             // Method e.drag.inGroup() - we have already run the group checks before triggering the event.
380             dragdropwrapper.prototype.inGroup = function() {
381                 return true;
382             };
384             // Method e.drag.addHandle() - we don't want to run this.
385             dragdropwrapper.prototype.addHandle = function() {};
386             // Method e.drag.removeHandle() - we don't want to run this.
387             dragdropwrapper.prototype.removeHandle = function() {};
389             // Create instances of the dragdropwrapper.
390             this.drop = new dragdropwrapper(dropnode);
391             this.drag = new dragdropwrapper(dragnode);
392             this.target = this.drop;
393         },
395         /**
396          * This is used to complete a keyboard version of a drag and drop.
397          * A drop event will be simulated based on the drag and drop nodes.
398          * @method global_keyboard_drop
399          * @param {Event} e The keydown / click event on the proxy drop node.
400          */
401         global_keyboard_drop : function(e) {
402             // The drag node was saved.
403             var dragcontainer = M.core.dragdrop.keydragcontainer;
404             dragcontainer.setAttribute('aria-grabbed', 'false');
405             // The real drop node is stored in an attribute of the proxy.
406             var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
408             // Close the dialog.
409             M.core.dragdrop.dropui.hide();
410             // Cancel the event.
411             e.preventDefault();
412             // Convert to drag drop events.
413             var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
414             var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
415             // Simulate the full sequence.
416             this.drag_start(dragevent);
417             this.global_drop_over(dropevent);
418             this.global_drop_hit(dropevent);
419             M.core.dragdrop.keydraghandle.focus();
420         },
422         /**
423          * This is used to cancel a keyboard version of a drag and drop.
424          *
425          * @method global_cancel_keyboard_drag
426          */
427         global_cancel_keyboard_drag : function() {
428             if (M.core.dragdrop.keydragcontainer) {
429                 M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false');
430                 M.core.dragdrop.keydraghandle.focus();
431                 M.core.dragdrop.keydragcontainer = null;
432             }
433         },
435         /**
436          * Process key events on the drag handles.
437          * @method global_keydown
438          * @param {Event} e The keydown / click event on the drag handle.
439          */
440         global_keydown : function(e) {
441             var draghandle = e.target,
442                 dragcontainer,
443                 draggroups;
445             if (e.keyCode == 27 ) {
446                 // Escape to cancel from anywhere.
447                 this.global_cancel_keyboard_drag();
448                 e.preventDefault();
449                 return;
450             }
452             // Only process events on a drag handle.
453             if (!draghandle.hasClass(MOVEICON.cssclass)) {
454                 return;
455             }
456             // Do nothing if not space or enter.
457             if (e.keyCode != 13 && e.keyCode != 32) {
458                 return;
459             }
460             // Check the drag groups to see if we are the handler for this node.
461             draggroups = e.target.getAttribute('data-draggroups').split(' ');
462             var i, j, validgroup = false;
464             for (i = 0; i < draggroups.length; i++) {
465                 for (j = 0; j < this.groups.length; j++) {
466                     if (draggroups[i] == this.groups[j]) {
467                         validgroup = true;
468                         break;
469                     }
470                 }
471                 if (validgroup) {
472                     break;
473                 }
474             }
475             if (!validgroup) {
476                 return;
477             }
479             // Valid event - start the keyboard drag.
480             dragcontainer = draghandle.ancestor('.yui3-dd-drop');
481             this.global_start_keyboard_drag(e, draghandle, dragcontainer);
483             e.preventDefault();
484         },
486         /*
487          * Abstract functions definitions
488          */
489         drag_start : function(e) {},
490         drag_end : function(e) {},
491         drag_drag : function(e) {},
492         drag_dropmiss : function(e) {},
493         drop_over : function(e) {},
494         drop_hit : function(e) {}
495     }, {
496         NAME : 'dragdrop',
497         ATTRS : {}
498     });
500 M.core = M.core || {};
501 M.core.dragdrop = DRAGDROP;
503 }, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'event-key', 'event-focus', 'moodle-core-notification']});