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