MDL-47368 core JS: dragdrop click move fills the dom with divs
[moodle.git] / lib / yui / build / moodle-core-dragdrop / moodle-core-dragdrop-debug.js
1 YUI.add('moodle-core-dragdrop', function (Y, NAME) {
3 /**
4  * The core drag and drop module for Moodle which extends the YUI drag and
5  * drop functionality with additional features.
6  *
7  * @module moodle-core-dragdrop
8  */
9 var MOVEICON = {
10     pix: "i/move_2d",
11     largepix: "i/dragdrop",
12     component: 'moodle',
13     cssclass: 'moodle-core-dragdrop-draghandle'
14 };
16 /**
17  * General DRAGDROP class, this should not be used directly,
18  * it is supposed to be extended by your class
19  *
20  * @class M.core.dragdrop
21  * @constructor
22  * @extends Base
23  */
24 var DRAGDROP = function() {
25     DRAGDROP.superclass.constructor.apply(this, arguments);
26 };
28 Y.extend(DRAGDROP, Y.Base, {
29     /**
30      * Whether the item is being moved upwards compared with the last
31      * location.
32      *
33      * @property goingup
34      * @type Boolean
35      * @default null
36      */
37     goingup: null,
39     /**
40      * Whether the item is being moved upwards compared with the start
41      * point.
42      *
43      * @property absgoingup
44      * @type Boolean
45      * @default null
46      */
47     absgoingup: null,
49     /**
50      * The class for the object.
51      *
52      * @property samenodeclass
53      * @type String
54      * @default null
55      */
56     samenodeclass: null,
58     /**
59      * The class on the parent of the item being moved.
60      *
61      * @property parentnodeclass
62      * @type String
63      * @default
64      */
65     parentnodeclass: null,
67     /**
68      * The label to use with keyboard drag/drop to describe items of the same Node.
69      *
70      * @property samenodelabel
71      * @type Object
72      * @default null
73      */
74     samenodelabel : null,
76     /**
77      * The label to use with keyboard drag/drop to describe items of the parent Node.
78      *
79      * @property samenodelabel
80      * @type Object
81      * @default null
82      */
83     parentnodelabel : null,
85     /**
86      * The groups for this instance.
87      *
88      * @property groups
89      * @type Array
90      * @default []
91      */
92     groups: [],
94     /**
95      * The previous drop location.
96      *
97      * @property lastdroptarget
98      * @type Node
99      * @default null
100      */
101     lastdroptarget: null,
103     /**
104      * The initializer which sets up the move action.
105      *
106      * @method initializer
107      * @protected
108      */
109     initializer: function() {
110         // Listen for all drag:start events.
111         Y.DD.DDM.on('drag:start', this.global_drag_start, this);
113         // Listen for all drag:end events.
114         Y.DD.DDM.on('drag:end', this.global_drag_end, this);
116         // Listen for all drag:drag events.
117         Y.DD.DDM.on('drag:drag', this.global_drag_drag, this);
119         // Listen for all drop:over events.
120         Y.DD.DDM.on('drop:over', this.global_drop_over, this);
122         // Listen for all drop:hit events.
123         Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
125         // Listen for all drop:miss events.
126         Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
128         // Add keybaord listeners for accessible drag/drop
129         Y.one(Y.config.doc.body).delegate('key', this.global_keydown,
130                 'down:32, enter, esc', '.' + MOVEICON.cssclass, this);
132         // Make the accessible drag/drop respond to a single click.
133         Y.one(Y.config.doc.body).delegate('click', this.global_keydown,
134                 '.' + MOVEICON.cssclass , this);
135     },
137     /**
138      * Build a new drag handle Node.
139      *
140      * @method get_drag_handle
141      * @param {String} title The title on the drag handle
142      * @param {String} classname The name of the class to add to the node
143      * wrapping the drag icon
144      * @param {String} [iconclass] The class to add to the icon
145      * @param {Boolean} [large=false] whether to use the larger version of
146      * the drag icon
147      * @return Node The built drag handle.
148      */
149     get_drag_handle: function(title, classname, iconclass, large) {
150         var iconname = MOVEICON.pix;
151         if (large) {
152             iconname = MOVEICON.largepix;
153         }
154         var dragicon = Y.Node.create('<img />')
155             .setStyle('cursor', 'move')
156             .setAttrs({
157                 'src': M.util.image_url(iconname, MOVEICON.component),
158                 'alt': title
159             });
160         if (iconclass) {
161             dragicon.addClass(iconclass);
162         }
164         var dragelement = Y.Node.create('<span></span>')
165             .addClass(classname)
166             .setAttribute('title', title)
167             .setAttribute('tabIndex', 0)
168             .setAttribute('data-draggroups', this.groups)
169             .setAttribute('role', 'button');
170         dragelement.appendChild(dragicon);
171         dragelement.addClass(MOVEICON.cssclass);
173         return dragelement;
174     },
176     lock_drag_handle: function(drag, classname) {
177         drag.removeHandle('.'+classname);
178     },
180     unlock_drag_handle: function(drag, classname) {
181         drag.addHandle('.'+classname);
182         drag.get('activeHandle').focus();
183     },
185     ajax_failure: function(response) {
186         var e = {
187             name: response.status+' '+response.statusText,
188             message: response.responseText
189         };
190         return new M.core.exception(e);
191     },
193     in_group: function(target) {
194         var ret = false;
195         Y.each(this.groups, function(v) {
196             if (target._groups[v]) {
197                 ret = true;
198             }
199         }, this);
200         return ret;
201     },
202     /*
203         * Drag-dropping related functions
204         */
205     global_drag_start: function(e) {
206         // Get our drag object
207         var drag = e.target;
208         // Check that drag object belongs to correct group
209         if (!this.in_group(drag)) {
210             return;
211         }
212         // Set some general styles here
213         drag.get('node').setStyle('opacity', '.25');
214         drag.get('dragNode').setStyles({
215             opacity: '.75',
216             borderColor: drag.get('node').getStyle('borderColor'),
217             backgroundColor: drag.get('node').getStyle('backgroundColor')
218         });
219         drag.get('dragNode').empty();
220         this.drag_start(e);
221     },
223     global_drag_end: function(e) {
224         var drag = e.target;
225         // Check that drag object belongs to correct group
226         if (!this.in_group(drag)) {
227             return;
228         }
229         //Put our general styles back
230         drag.get('node').setStyles({
231             visibility: '',
232             opacity: '1'
233         });
234         this.drag_end(e);
235     },
237     global_drag_drag: function(e) {
238         var drag = e.target,
239             info = e.info;
241         // Check that drag object belongs to correct group
242         if (!this.in_group(drag)) {
243             return;
244         }
246         // Note, we test both < and > situations here. We don't want to
247         // effect a change in direction if the user is only moving side
248         // to side with no Y position change.
250         // Detect changes in the position relative to the start point.
251         if (info.start[1] < info.xy[1]) {
252             // We are going up if our final position is higher than our start position.
253             this.absgoingup = true;
255         } else if (info.start[1] > info.xy[1]) {
256             // Otherwise we're going down.
257             this.absgoingup = false;
258         }
260         // Detect changes in the position relative to the last movement.
261         if (info.delta[1] < 0) {
262             // We are going up if our final position is higher than our start position.
263             this.goingup = true;
265         } else if (info.delta[1] > 0) {
266             // Otherwise we're going down.
267             this.goingup = false;
268         }
270         this.drag_drag(e);
271     },
273     global_drop_over: function(e) {
274         // Check that drop object belong to correct group.
275         if (!e.drop || !e.drop.inGroup(this.groups)) {
276             return;
277         }
279         // Get a reference to our drag and drop nodes.
280         var drag = e.drag.get('node'),
281             drop = e.drop.get('node');
283         // Save last drop target for the case of missed target processing.
284         this.lastdroptarget = e.drop;
286         // Are we dropping within the same parent node?
287         if (drop.hasClass(this.samenodeclass)) {
288             var where;
290             if (this.goingup) {
291                 where = "before";
292             } else {
293                 where = "after";
294             }
296             // Add the node contents so that it's moved, otherwise only the drag handle is moved.
297             drop.insert(drag, where);
298         } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
299             // We are dropping on parent node and it is empty
300             if (this.goingup) {
301                 drop.append(drag);
302             } else {
303                 drop.prepend(drag);
304             }
305         }
306         this.drop_over(e);
307     },
309     global_drag_dropmiss: function(e) {
310         // drag:dropmiss does not have e.drag and e.drop properties
311         // we substitute them for the ease of use. For e.drop we use,
312         // this.lastdroptarget (ghost node we use for indicating where to drop)
313         e.drag = e.target;
314         e.drop = this.lastdroptarget;
315         // Check that drag object belongs to correct group
316         if (!this.in_group(e.drag)) {
317             return;
318         }
319         // Check that drop object belong to correct group
320         if (!e.drop || !e.drop.inGroup(this.groups)) {
321             return;
322         }
323         this.drag_dropmiss(e);
324     },
326     global_drop_hit: function(e) {
327         // Check that drop object belong to correct group
328         if (!e.drop || !e.drop.inGroup(this.groups)) {
329             return;
330         }
331         this.drop_hit(e);
332     },
334     /**
335      * This is used to build the text for the heading of the keyboard
336      * drag drop menu and the text for the nodes in the list.
337      * @method find_element_text
338      * @param {Node} n The node to start searching for a valid text node.
339      * @return {string} The text of the first text-like child node of n.
340      */
341     find_element_text: function(n) {
342         // The valid node types to get text from.
343         var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text');
344         var text = '';
346         nodes.each(function () {
347             if (text === '') {
348                 if (Y.Lang.trim(this.get('text')) !== '') {
349                     text = this.get('text');
350                 }
351             }
352         });
354         if (text !== '') {
355             return text;
356         }
357         return M.util.get_string('emptydragdropregion', 'moodle');
358     },
360     /**
361      * This is used to initiate a keyboard version of a drag and drop.
362      * A dialog will open listing all the valid drop targets that can be selected
363      * using tab, tab, tab, enter.
364      * @method global_start_keyboard_drag
365      * @param {Event} e The keydown / click event on the grab handle.
366      * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
367      * @param {Node} draghandle The node that triggered this action.
368      */
369     global_start_keyboard_drag: function(e, draghandle, dragcontainer) {
370         M.core.dragdrop.keydragcontainer = dragcontainer;
371         M.core.dragdrop.keydraghandle = draghandle;
373         // Get the name of the thing to move.
374         var nodetitle = this.find_element_text(dragcontainer);
375         var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
377         // Build the list of drop targets.
378         var droplist = Y.Node.create('<ul></ul>');
379         droplist.addClass('dragdrop-keyboard-drag');
380         var listitem;
381         var listitemtext;
383         // Search for possible drop targets.
384         var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
386         droptargets.each(function (node) {
387             var validdrop = false, labelroot = node;
388             if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) {
389                 // This is a drag and drop target with the same class as the grabbed node.
390                 validdrop = true;
391             } else {
392                 var elementgroups = node.getAttribute('data-draggroups').split(' ');
393                 var i, j;
394                 for (i = 0; i < elementgroups.length; i++) {
395                     for (j = 0; j < this.groups.length; j++) {
396                         if (elementgroups[i] === this.groups[j]) {
397                             // This is a parent node of the grabbed node (used for dropping in empty sections).
398                             validdrop = true;
399                             // This node will have no text - so we get the first valid text from the parent.
400                             labelroot = node.get('parentNode');
401                             break;
402                         }
403                     }
404                     if (validdrop) {
405                         break;
406                     }
407                 }
408             }
410             if (validdrop) {
411                 // It is a valid drop target - create a list item for it.
412                 listitem = Y.Node.create('<li></li>');
413                 listlink = Y.Node.create('<a></a>');
414                 nodetitle = this.find_element_text(labelroot);
416                 if (this.samenodelabel && node.hasClass(this.samenodeclass)) {
417                     listitemtext = M.util.get_string(this.samenodelabel.identifier, this.samenodelabel.component, nodetitle);
418                 } else if (this.parentnodelabel && node.hasClass(this.parentnodeclass)) {
419                     listitemtext = M.util.get_string(this.parentnodelabel.identifier, this.parentnodelabel.component, nodetitle);
420                 } else {
421                     listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle);
422                 }
423                 listlink.setContent(listitemtext);
425                 // Add a data attribute so we can get the real drop target.
426                 listlink.setAttribute('data-drop-target', node.get('id'));
427                 // Allow tabbing to the link.
428                 listlink.setAttribute('tabindex', '0');
430                 // Set the event listeners for enter, space or click.
431                 listlink.on('click', this.global_keyboard_drop, this);
432                 listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
434                 // Add to the list or drop targets.
435                 listitem.append(listlink);
436                 droplist.append(listitem);
437             }
438         }, this);
440         // Create the dialog for the interaction.
441         M.core.dragdrop.dropui = new M.core.dialogue({
442             headerContent: dialogtitle,
443             bodyContent: droplist,
444             draggable: true,
445             visible: true,
446             center: true,
447             modal: true
448         });
450         M.core.dragdrop.dropui.after('visibleChange', function(e) {
451             // After the dialogue has been closed, we call the cancel function. This will
452             // ensure that tidying up happens (e.g. focusing on the start Node).
453             if (e.prevVal && !e.newVal) {
454                 this.global_cancel_keyboard_drag();
455             }
456         }, this);
458         // Focus the first drop target.
459         if (droplist.one('a')) {
460             droplist.one('a').focus();
461         }
462     },
464     /**
465      * This is used as a simulated drag/drop event in order to prevent any
466      * subtle bugs from creating a real instance of a drag drop event. This means
467      * there are no state changes in the Y.DD.DDM and any undefined functions
468      * will trigger an obvious and fatal error.
469      * The end result is that we call all our drag/drop handlers but do not bubble the
470      * event to anyone else.
471      *
472      * The functions/properties implemented in the wrapper are:
473      * e.target
474      * e.drag
475      * e.drop
476      * e.drag.get('node')
477      * e.drop.get('node')
478      * e.drag.addHandle()
479      * e.drag.removeHandle()
480      *
481      * @method simulated_drag_drop_event
482      * @param {Node} dragnode The drag container node
483      * @param {Node} dropnode The node to initiate the drop on
484      */
485     simulated_drag_drop_event: function(dragnode, dropnode) {
487         // Subclass for wrapping both drag and drop.
488         var DragDropWrapper = function(node) {
489             this.node = node;
490         };
492         // Method e.drag.get() - get the node.
493         DragDropWrapper.prototype.get = function(param) {
494             if (param === 'node' || param === 'dragNode' || param === 'dropNode') {
495                 return this.node;
496             }
497             if (param === 'activeHandle') {
498                 return this.node.one('.editing_move');
499             }
500             return null;
501         };
503         // Method e.drag.inGroup() - we have already run the group checks before triggering the event.
504         DragDropWrapper.prototype.inGroup = function() {
505             return true;
506         };
508         // Method e.drag.addHandle() - we don't want to run this.
509         DragDropWrapper.prototype.addHandle = function() {};
510         // Method e.drag.removeHandle() - we don't want to run this.
511         DragDropWrapper.prototype.removeHandle = function() {};
513         // Create instances of the DragDropWrapper.
514         this.drop = new DragDropWrapper(dropnode);
515         this.drag = new DragDropWrapper(dragnode);
516         this.target = this.drop;
517     },
519     /**
520      * This is used to complete a keyboard version of a drag and drop.
521      * A drop event will be simulated based on the drag and drop nodes.
522      * @method global_keyboard_drop
523      * @param {Event} e The keydown / click event on the proxy drop node.
524      */
525     global_keyboard_drop: function(e) {
526         // The drag node was saved.
527         var dragcontainer = M.core.dragdrop.keydragcontainer;
528         // The real drop node is stored in an attribute of the proxy.
529         var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
531         // Close the dialog.
532         M.core.dragdrop.dropui.hide();
533         // Cancel the event.
534         e.preventDefault();
535         // Convert to drag drop events.
536         var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
537         var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
538         // Simulate the full sequence.
539         this.drag_start(dragevent);
540         this.global_drop_over(dropevent);
542         if (droptarget.hasClass(this.parentnodeclass) && droptarget.contains(dragcontainer)) {
543             // The global_drop_over function does not handle the case where an item was moved up, without the
544             // 'goingup' variable being set, as is the case wih keyboard drag/drop. We must detect this case and
545             // apply it after the drop_over, but before the drop_hit event in order for it to be moved to the
546             // correct location.
547             droptarget.prepend(dragcontainer);
548         }
550         this.global_drop_hit(dropevent);
551     },
553     /**
554      * This is used to cancel a keyboard version of a drag and drop.
555      *
556      * @method global_cancel_keyboard_drag
557      */
558     global_cancel_keyboard_drag: function() {
559         if (M.core.dragdrop.keydragcontainer) {
560             // Focus on the node which was being dragged.
561             M.core.dragdrop.keydraghandle.focus();
562             M.core.dragdrop.keydragcontainer = null;
563         }
564         if (M.core.dragdrop.dropui) {
565             M.core.dragdrop.dropui.destroy();
566         }
567     },
569     /**
570      * Process key events on the drag handles.
571      *
572      * @method global_keydown
573      * @param {EventFacade} e The keydown / click event on the drag handle.
574      */
575     global_keydown: function(e) {
576         var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true),
577             dragcontainer,
578             draggroups;
580         if (draghandle === null) {
581             // The element clicked did not have a a draghandle in it's lineage.
582             return;
583         }
585         if (e.keyCode === 27 ) {
586             // Escape to cancel from anywhere.
587             this.global_cancel_keyboard_drag();
588             e.preventDefault();
589             return;
590         }
592         // Only process events on a drag handle.
593         if (!draghandle.hasClass(MOVEICON.cssclass)) {
594             return;
595         }
597         // Do nothing if not space or enter.
598         if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') {
599             return;
600         }
602         // Check the drag groups to see if we are the handler for this node.
603         draggroups = draghandle.getAttribute('data-draggroups').split(' ');
604         var i, j, validgroup = false;
606         for (i = 0; i < draggroups.length; i++) {
607             for (j = 0; j < this.groups.length; j++) {
608                 if (draggroups[i] === this.groups[j]) {
609                     validgroup = true;
610                     break;
611                 }
612             }
613             if (validgroup) {
614                 break;
615             }
616         }
617         if (!validgroup) {
618             return;
619         }
621         // Valid event - start the keyboard drag.
622         dragcontainer = draghandle.ancestor('.yui3-dd-drop');
623         this.global_start_keyboard_drag(e, draghandle, dragcontainer);
625         e.preventDefault();
626     },
629     // Abstract functions definitions.
631     /**
632      * Callback to use when dragging starts.
633      *
634      * @method drag_start
635      * @param {EventFacade} e
636      */
637     drag_start: function() {},
639     /**
640      * Callback to use when dragging ends.
641      *
642      * @method drag_end
643      * @param {EventFacade} e
644      */
645     drag_end: function() {},
647     /**
648      * Callback to use during dragging.
649      *
650      * @method drag_drag
651      * @param {EventFacade} e
652      */
653     drag_drag: function() {},
655     /**
656      * Callback to use when dragging ends and is not over a drop target.
657      *
658      * @method drag_dropmiss
659      * @param {EventFacade} e
660      */
661     drag_dropmiss: function() {},
663     /**
664      * Callback to use when a drop over event occurs.
665      *
666      * @method drop_over
667      * @param {EventFacade} e
668      */
669     drop_over: function() {},
671     /**
672      * Callback to use on drop:hit.
673      *
674      * @method drop_hit
675      * @param {EventFacade} e
676      */
677     drop_hit: function() {}
678 }, {
679     NAME: 'dragdrop',
680     ATTRS: {}
681 });
683 M.core = M.core || {};
684 M.core.dragdrop = DRAGDROP;
687 }, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]});