dddfee47e2634bfae5a8ab98d46e061e2d4c0545
[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             }
168             //Get a reference to our drag and drop nodes
169             var drag = e.drag.get('node');
170             var drop = e.drop.get('node');
171             // Save last drop target for the case of missed target processing
172             this.lastdroptarget = e.drop;
173             //Are we dropping on the same node?
174             if (drop.hasClass(this.samenodeclass)) {
175                 var where;
177                 if (this.goingup) {
178                     where = "before";
179                 } else {
180                     where = "after";
181                 }
183                 // Add the node contents so that it's moved, otherwise only the drag handle is moved.
184                 drop.insert(drag, where);
185             } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
186                 // We are dropping on parent node and it is empty
187                 if (this.goingup) {
188                     drop.append(drag);
189                 } else {
190                     drop.prepend(drag);
191                 }
192             }
193             this.drop_over(e);
194         },
196         global_drag_dropmiss : function(e) {
197             // drag:dropmiss does not have e.drag and e.drop properties
198             // we substitute them for the ease of use. For e.drop we use,
199             // this.lastdroptarget (ghost node we use for indicating where to drop)
200             e.drag = e.target;
201             e.drop = this.lastdroptarget;
202             // Check that drag object belongs to correct group
203             if (!this.in_group(e.drag)) {
204                 return;
205             }
206             // Check that drop object belong to correct group
207             if (!e.drop || !e.drop.inGroup(this.groups)) {
208                 return;
209             }
210             this.drag_dropmiss(e);
211         },
213         global_drop_hit : function(e) {
214             // Check that drop object belong to correct group
215             if (!e.drop || !e.drop.inGroup(this.groups)) {
216                 return;
217             }
218             this.drop_hit(e);
219         },
221         /**
222          * This is used to build the text for the heading of the keyboard
223          * drag drop menu and the text for the nodes in the list.
224          * @method find_element_text
225          * @param {Node} n The node to start searching for a valid text node.
226          * @returns {string} The text of the first text-like child node of n.
227          */
228         find_element_text : function(n) {
229             // The valid node types to get text from.
230             var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text');
231             var text = '';
232             debugger;
234             nodes.each(function () {
235                 if (text == '') {
236                     if (Y.Lang.trim(this.get('text')) != '') {
237                         text = this.get('text');
238                     }
239                 }
240             });
242             if (text != '') {
243                 return text;
244             }
245             return M.util.get_string('emptydragdropregion', 'moodle');
246         },
248         /**
249          * This is used to initiate a keyboard version of a drag and drop.
250          * A dialog will open listing all the valid drop targets that can be selected
251          * using tab, tab, tab, enter.
252          * @method global_start_keyboard_drag
253          * @param {Event} e The keydown / click event on the grab handle.
254          * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
255          * @param {Node} draghandle The node that triggered this action.
256          */
257         global_start_keyboard_drag : function(e, draghandle, dragcontainer) {
258             M.core.dragdrop.keydragcontainer = dragcontainer;
259             M.core.dragdrop.keydraghandle = draghandle;
261             // Indicate to a screenreader the node that is selected for drag and drop.
262             dragcontainer.setAttribute('aria-grabbed', 'true');
263             // Get the name of the thing to move.
264             var nodetitle = this.find_element_text(dragcontainer);
265             var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
267             // Build the list of drop targets.
268             var droplist = Y.Node.create('<ul></ul>');
269             droplist.addClass('dragdrop-keyboard-drag');
270             var listitem;
271             var listitemtext;
273             // Search for possible drop targets.
274             var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
276             droptargets.each(function (node) {
277                 var validdrop = false, labelroot = node;
278                 if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') != dragcontainer) {
279                     // This is a drag and drop target with the same class as the grabbed node.
280                     validdrop = true;
281                 } else {
282                     var elementgroups = node.getAttribute('data-draggroups').split(' ');
283                     var i, j;
284                     for (i = 0; i < elementgroups.length; i++) {
285                         for (j = 0; j < this.groups.length; j++) {
286                             if (elementgroups[i] == this.groups[j]) {
287                                 // This is a parent node of the grabbed node (used for dropping in empty sections).
288                                 validdrop = true;
289                                 // This node will have no text - so we get the first valid text from the parent.
290                                 labelroot = node.get('parentNode');
291                                 break;
292                             }
293                         }
294                         if (validdrop) {
295                             break;
296                         }
297                     }
298                 }
300                 if (validdrop) {
301                     // It is a valid drop target - create a list item for it.
302                     listitem = Y.Node.create('<li></li>');
303                     listlink = Y.Node.create('<a></a>');
304                     nodetitle = this.find_element_text(labelroot);
306                     listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle);
307                     listlink.setContent(listitemtext);
309                     // Add a data attribute so we can get the real drop target.
310                     listlink.setAttribute('data-drop-target', node.get('id'));
311                     // Notify the screen reader this is a valid drop target.
312                     listlink.setAttribute('aria-dropeffect', 'move');
313                     // Allow tabbing to the link.
314                     listlink.setAttribute('tabindex', '0');
316                     // Set the event listeners for enter, space or click.
317                     listlink.on('click', this.global_keyboard_drop, this);
318                     listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
320                     // Add to the list or drop targets.
321                     listitem.append(listlink);
322                     droplist.append(listitem);
323                 }
324             }, this);
326             // Create the dialog for the interaction.
327             M.core.dragdrop.dropui = new M.core.dialogue({
328                 headerContent : dialogtitle,
329                 bodyContent : droplist,
330                 draggable : true,
331                 visible : true
332             });
334             // Focus the first drop target.
335             if (droplist.one('a')) {
336                 droplist.one('a').focus();
337             }
338         },
340         /**
341          * This is used as a simulated drag/drop event in order to prevent any
342          * subtle bugs from creating a real instance of a drag drop event. This means
343          * there are no state changes in the Y.DD.DDM and any undefined functions
344          * will trigger an obvious and fatal error.
345          * The end result is that we call all our drag/drop handlers but do not bubble the
346          * event to anyone else.
347          *
348          * The functions/properties implemented in the wrapper are:
349          * e.target
350          * e.drag
351          * e.drop
352          * e.drag.get('node')
353          * e.drop.get('node')
354          * e.drag.addHandle()
355          * e.drag.removeHandle()
356          *
357          * @class simulated_drag_drop_event
358          * @param {Node} dragnode The drag container node
359          * @param {Node} dropnode The node to initiate the drop on
360          */
361         simulated_drag_drop_event : function(dragnode, dropnode) {
363             // Subclass for wrapping both drag and drop.
364             var dragdropwrapper = function(node) {
365                 this.node = node;
366             }
368             // Method e.drag.get() - get the node.
369             dragdropwrapper.prototype.get = function(param) {
370                 if (param == 'node' || param == 'dragNode' || param == 'dropNode') {
371                     return this.node;
372                 }
373                 return null;
374             };
376             // Method e.drag.inGroup() - we have already run the group checks before triggering the event.
377             dragdropwrapper.prototype.inGroup = function() {
378                 return true;
379             };
381             // Method e.drag.addHandle() - we don't want to run this.
382             dragdropwrapper.prototype.addHandle = function() {};
383             // Method e.drag.removeHandle() - we don't want to run this.
384             dragdropwrapper.prototype.removeHandle = function() {};
386             // Create instances of the dragdropwrapper.
387             this.drop = new dragdropwrapper(dropnode);
388             this.drag = new dragdropwrapper(dragnode);
389             this.target = this.drop;
390         },
392         /**
393          * This is used to complete a keyboard version of a drag and drop.
394          * A drop event will be simulated based on the drag and drop nodes.
395          * @method global_keyboard_drop
396          * @param {Event} e The keydown / click event on the proxy drop node.
397          */
398         global_keyboard_drop : function(e) {
399             // The drag node was saved.
400             var dragcontainer = M.core.dragdrop.keydragcontainer;
401             dragcontainer.setAttribute('aria-grabbed', 'false');
402             // The real drop node is stored in an attribute of the proxy.
403             var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
405             // Close the dialog.
406             M.core.dragdrop.dropui.hide();
407             // Cancel the event.
408             e.preventDefault();
409             // Convert to drag drop events.
410             var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
411             var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
412             // Simulate the full sequence.
413             this.drag_start(dragevent);
414             this.global_drop_over(dropevent);
415             this.global_drop_hit(dropevent);
416             M.core.dragdrop.keydraghandle.focus();
417         },
419         /**
420          * This is used to cancel a keyboard version of a drag and drop.
421          *
422          * @method global_cancel_keyboard_drag
423          */
424         global_cancel_keyboard_drag : function() {
425             if (M.core.dragdrop.keydragcontainer) {
426                 M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false');
427                 M.core.dragdrop.keydraghandle.focus();
428                 M.core.dragdrop.keydragcontainer = null;
429             }
430         },
432         /**
433          * Process key events on the drag handles.
434          * @method global_keydown
435          * @param {Event} e The keydown / click event on the drag handle.
436          */
437         global_keydown : function(e) {
438             var draghandle = e.target,
439                 dragcontainer,
440                 draggroups;
442             if (e.keyCode == 27 ) {
443                 // Escape to cancel from anywhere.
444                 this.global_cancel_keyboard_drag();
445                 e.preventDefault();
446                 return;
447             }
449             // Only process events on a drag handle.
450             if (!draghandle.hasClass(MOVEICON.cssclass)) {
451                 return;
452             }
453             // Do nothing if not space or enter.
454             if (e.keyCode != 13 && e.keyCode != 32) {
455                 return;
456             }
457             // Check the drag groups to see if we are the handler for this node.
458             draggroups = e.target.getAttribute('data-draggroups').split(' ');
459             var i, j, validgroup = false;
461             for (i = 0; i < draggroups.length; i++) {
462                 for (j = 0; j < this.groups.length; j++) {
463                     if (draggroups[i] == this.groups[j]) {
464                         validgroup = true;
465                         break;
466                     }
467                 }
468                 if (validgroup) {
469                     break;
470                 }
471             }
472             if (!validgroup) {
473                 return;
474             }
476             // Valid event - start the keyboard drag.
477             dragcontainer = draghandle.ancestor('.yui3-dd-drop');
478             this.global_start_keyboard_drag(e, draghandle, dragcontainer);
480             e.preventDefault();
481         },
483         /*
484          * Abstract functions definitions
485          */
486         drag_start : function(e) {},
487         drag_end : function(e) {},
488         drag_drag : function(e) {},
489         drag_dropmiss : function(e) {},
490         drop_over : function(e) {},
491         drop_hit : function(e) {}
492     }, {
493         NAME : 'dragdrop',
494         ATTRS : {}
495     });
497 M.core = M.core || {};
498 M.core.dragdrop = DRAGDROP;
500 }, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'event-key', 'event-focus', 'moodle-core-notification']});