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