MDL-38774 JavaScript: Migrate moodle-core-dragdrop to Shifter
[moodle.git] / lib / yui / build / moodle-core-dragdrop / moodle-core-dragdrop.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 groups for this instance.
69      *
70      * @property groups
71      * @type Array
72      * @default []
73      */
74     groups: [],
76     /**
77      * The previous drop location.
78      *
79      * @property lastdroptarget
80      * @type Node
81      * @default null
82      */
83     lastdroptarget: null,
85     /**
86      * The initializer which sets up the move action.
87      *
88      * @method initializer
89      * @protected
90      */
91     initializer: function() {
92         // Listen for all drag:start events.
93         Y.DD.DDM.on('drag:start', this.global_drag_start, this);
95         // Listen for all drag:end events.
96         Y.DD.DDM.on('drag:end', this.global_drag_end, this);
98         // Listen for all drag:drag events.
99         Y.DD.DDM.on('drag:drag', this.global_drag_drag, this);
101         // Listen for all drop:over events.
102         Y.DD.DDM.on('drop:over', this.global_drop_over, this);
104         // Listen for all drop:hit events.
105         Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
107         // Listen for all drop:miss events.
108         Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
110         // Add keybaord listeners for accessible drag/drop
111         Y.one(Y.config.doc.body).delegate('key', this.global_keydown,
112                 'down:32, enter, esc', '.' + MOVEICON.cssclass, this);
114         // Make the accessible drag/drop respond to a single click.
115         Y.one(Y.config.doc.body).delegate('click', this.global_keydown,
116                 '.' + MOVEICON.cssclass , this);
117     },
119     /**
120      * Build a new drag handle Node.
121      *
122      * @method get_drag_handle
123      * @param {String} title The title on the drag handle
124      * @param {String} classname The name of the class to add to the node
125      * wrapping the drag icon
126      * @param {String} [iconclass] The class to add to the icon
127      * @param {Boolean} [large=false] whether to use the larger version of
128      * the drag icon
129      * @return Node The built drag handle.
130      */
131     get_drag_handle: function(title, classname, iconclass, large) {
132         var iconname = MOVEICON.pix;
133         if (large) {
134             iconname = MOVEICON.largepix;
135         }
136         var dragicon = Y.Node.create('<img />')
137             .setStyle('cursor', 'move')
138             .setAttrs({
139                 'src': M.util.image_url(iconname, MOVEICON.component),
140                 'alt': title
141             });
142         if (iconclass) {
143             dragicon.addClass(iconclass);
144         }
146         var dragelement = Y.Node.create('<span></span>')
147             .addClass(classname)
148             .setAttribute('title', title)
149             .setAttribute('tabIndex', 0)
150             .setAttribute('data-draggroups', this.groups)
151             .setAttribute('role', 'button')
152             .setAttribute('aria-grabbed', 'false');
153         dragelement.appendChild(dragicon);
154         dragelement.addClass(MOVEICON.cssclass);
156         return dragelement;
157     },
159     lock_drag_handle: function(drag, classname) {
160         drag.removeHandle('.'+classname);
161     },
163     unlock_drag_handle: function(drag, classname) {
164         drag.addHandle('.'+classname);
165     },
167     ajax_failure: function(response) {
168         var e = {
169             name: response.status+' '+response.statusText,
170             message: response.responseText
171         };
172         return new M.core.exception(e);
173     },
175     in_group: function(target) {
176         var ret = false;
177         Y.each(this.groups, function(v) {
178             if (target._groups[v]) {
179                 ret = true;
180             }
181         }, this);
182         return ret;
183     },
184     /*
185         * Drag-dropping related functions
186         */
187     global_drag_start: function(e) {
188         // Get our drag object
189         var drag = e.target;
190         // Check that drag object belongs to correct group
191         if (!this.in_group(drag)) {
192             return;
193         }
194         // Set some general styles here
195         drag.get('node').setStyle('opacity', '.25');
196         drag.get('dragNode').setStyles({
197             opacity: '.75',
198             borderColor: drag.get('node').getStyle('borderColor'),
199             backgroundColor: drag.get('node').getStyle('backgroundColor')
200         });
201         drag.get('dragNode').empty();
202         this.drag_start(e);
203     },
205     global_drag_end: function(e) {
206         var drag = e.target;
207         // Check that drag object belongs to correct group
208         if (!this.in_group(drag)) {
209             return;
210         }
211         //Put our general styles back
212         drag.get('node').setStyles({
213             visibility: '',
214             opacity: '1'
215         });
216         this.drag_end(e);
217     },
219     global_drag_drag: function(e) {
220         var drag = e.target,
221             info = e.info;
223         // Check that drag object belongs to correct group
224         if (!this.in_group(drag)) {
225             return;
226         }
228         // Note, we test both < and > situations here. We don't want to
229         // effect a change in direction if the user is only moving side
230         // to side with no Y position change.
232         // Detect changes in the position relative to the start point.
233         if (info.start[1] < info.xy[1]) {
234             // We are going up if our final position is higher than our start position.
235             this.absgoingup = true;
237         } else if (info.start[1] > info.xy[1]) {
238             // Otherwise we're going down.
239             this.absgoingup = false;
240         }
242         // Detect changes in the position relative to the last movement.
243         if (info.delta[1] < 0) {
244             // We are going up if our final position is higher than our start position.
245             this.goingup = true;
247         } else if (info.delta[1] > 0) {
248             // Otherwise we're going down.
249             this.goingup = false;
250         }
252         this.drag_drag(e);
253     },
255     global_drop_over: function(e) {
256         // Check that drop object belong to correct group.
257         if (!e.drop || !e.drop.inGroup(this.groups)) {
258             return;
259         }
261         // Get a reference to our drag and drop nodes.
262         var drag = e.drag.get('node'),
263             drop = e.drop.get('node');
265         // Save last drop target for the case of missed target processing.
266         this.lastdroptarget = e.drop;
268         // Are we dropping within the same parent node?
269         if (drop.hasClass(this.samenodeclass)) {
270             var where;
272             if (this.goingup) {
273                 where = "before";
274             } else {
275                 where = "after";
276             }
278             // Add the node contents so that it's moved, otherwise only the drag handle is moved.
279             drop.insert(drag, where);
280         } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
281             // We are dropping on parent node and it is empty
282             if (this.goingup) {
283                 drop.append(drag);
284             } else {
285                 drop.prepend(drag);
286             }
287         }
288         this.drop_over(e);
289     },
291     global_drag_dropmiss: function(e) {
292         // drag:dropmiss does not have e.drag and e.drop properties
293         // we substitute them for the ease of use. For e.drop we use,
294         // this.lastdroptarget (ghost node we use for indicating where to drop)
295         e.drag = e.target;
296         e.drop = this.lastdroptarget;
297         // Check that drag object belongs to correct group
298         if (!this.in_group(e.drag)) {
299             return;
300         }
301         // Check that drop object belong to correct group
302         if (!e.drop || !e.drop.inGroup(this.groups)) {
303             return;
304         }
305         this.drag_dropmiss(e);
306     },
308     global_drop_hit: function(e) {
309         // Check that drop object belong to correct group
310         if (!e.drop || !e.drop.inGroup(this.groups)) {
311             return;
312         }
313         this.drop_hit(e);
314     },
316     /**
317      * This is used to build the text for the heading of the keyboard
318      * drag drop menu and the text for the nodes in the list.
319      * @method find_element_text
320      * @param {Node} n The node to start searching for a valid text node.
321      * @return {string} The text of the first text-like child node of n.
322      */
323     find_element_text: function(n) {
324         // The valid node types to get text from.
325         var nodes = n.all('h2, h3, h4, h5, span, p, div.no-overflow, div.dimmed_text');
326         var text = '';
328         nodes.each(function () {
329             if (text === '') {
330                 if (Y.Lang.trim(this.get('text')) !== '') {
331                     text = this.get('text');
332                 }
333             }
334         });
336         if (text !== '') {
337             return text;
338         }
339         return M.util.get_string('emptydragdropregion', 'moodle');
340     },
342     /**
343      * This is used to initiate a keyboard version of a drag and drop.
344      * A dialog will open listing all the valid drop targets that can be selected
345      * using tab, tab, tab, enter.
346      * @method global_start_keyboard_drag
347      * @param {Event} e The keydown / click event on the grab handle.
348      * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
349      * @param {Node} draghandle The node that triggered this action.
350      */
351     global_start_keyboard_drag: function(e, draghandle, dragcontainer) {
352         M.core.dragdrop.keydragcontainer = dragcontainer;
353         M.core.dragdrop.keydraghandle = draghandle;
355         // Indicate to a screenreader the node that is selected for drag and drop.
356         dragcontainer.setAttribute('aria-grabbed', 'true');
357         // Get the name of the thing to move.
358         var nodetitle = this.find_element_text(dragcontainer);
359         var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
361         // Build the list of drop targets.
362         var droplist = Y.Node.create('<ul></ul>');
363         droplist.addClass('dragdrop-keyboard-drag');
364         var listitem;
365         var listitemtext;
367         // Search for possible drop targets.
368         var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
370         droptargets.each(function (node) {
371             var validdrop = false, labelroot = node;
372             if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) {
373                 // This is a drag and drop target with the same class as the grabbed node.
374                 validdrop = true;
375             } else {
376                 var elementgroups = node.getAttribute('data-draggroups').split(' ');
377                 var i, j;
378                 for (i = 0; i < elementgroups.length; i++) {
379                     for (j = 0; j < this.groups.length; j++) {
380                         if (elementgroups[i] === this.groups[j]) {
381                             // This is a parent node of the grabbed node (used for dropping in empty sections).
382                             validdrop = true;
383                             // This node will have no text - so we get the first valid text from the parent.
384                             labelroot = node.get('parentNode');
385                             break;
386                         }
387                     }
388                     if (validdrop) {
389                         break;
390                     }
391                 }
392             }
394             if (validdrop) {
395                 // It is a valid drop target - create a list item for it.
396                 listitem = Y.Node.create('<li></li>');
397                 listlink = Y.Node.create('<a></a>');
398                 nodetitle = this.find_element_text(labelroot);
400                 listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle);
401                 listlink.setContent(listitemtext);
403                 // Add a data attribute so we can get the real drop target.
404                 listlink.setAttribute('data-drop-target', node.get('id'));
405                 // Notify the screen reader this is a valid drop target.
406                 listlink.setAttribute('aria-dropeffect', 'move');
407                 // Allow tabbing to the link.
408                 listlink.setAttribute('tabindex', '0');
410                 // Set the event listeners for enter, space or click.
411                 listlink.on('click', this.global_keyboard_drop, this);
412                 listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
414                 // Add to the list or drop targets.
415                 listitem.append(listlink);
416                 droplist.append(listitem);
417             }
418         }, this);
420         // Create the dialog for the interaction.
421         M.core.dragdrop.dropui = new M.core.dialogue({
422             headerContent: dialogtitle,
423             bodyContent: droplist,
424             draggable: true,
425             visible: true,
426             centered: true
427         });
429         // Focus the first drop target.
430         if (droplist.one('a')) {
431             droplist.one('a').focus();
432         }
433     },
435     /**
436      * This is used as a simulated drag/drop event in order to prevent any
437      * subtle bugs from creating a real instance of a drag drop event. This means
438      * there are no state changes in the Y.DD.DDM and any undefined functions
439      * will trigger an obvious and fatal error.
440      * The end result is that we call all our drag/drop handlers but do not bubble the
441      * event to anyone else.
442      *
443      * The functions/properties implemented in the wrapper are:
444      * e.target
445      * e.drag
446      * e.drop
447      * e.drag.get('node')
448      * e.drop.get('node')
449      * e.drag.addHandle()
450      * e.drag.removeHandle()
451      *
452      * @method simulated_drag_drop_event
453      * @param {Node} dragnode The drag container node
454      * @param {Node} dropnode The node to initiate the drop on
455      */
456     simulated_drag_drop_event: function(dragnode, dropnode) {
458         // Subclass for wrapping both drag and drop.
459         var DragDropWrapper = function(node) {
460             this.node = node;
461         };
463         // Method e.drag.get() - get the node.
464         DragDropWrapper.prototype.get = function(param) {
465             if (param === 'node' || param === 'dragNode' || param === 'dropNode') {
466                 return this.node;
467             }
468             return null;
469         };
471         // Method e.drag.inGroup() - we have already run the group checks before triggering the event.
472         DragDropWrapper.prototype.inGroup = function() {
473             return true;
474         };
476         // Method e.drag.addHandle() - we don't want to run this.
477         DragDropWrapper.prototype.addHandle = function() {};
478         // Method e.drag.removeHandle() - we don't want to run this.
479         DragDropWrapper.prototype.removeHandle = function() {};
481         // Create instances of the DragDropWrapper.
482         this.drop = new DragDropWrapper(dropnode);
483         this.drag = new DragDropWrapper(dragnode);
484         this.target = this.drop;
485     },
487     /**
488      * This is used to complete a keyboard version of a drag and drop.
489      * A drop event will be simulated based on the drag and drop nodes.
490      * @method global_keyboard_drop
491      * @param {Event} e The keydown / click event on the proxy drop node.
492      */
493     global_keyboard_drop: function(e) {
494         // The drag node was saved.
495         var dragcontainer = M.core.dragdrop.keydragcontainer;
496         dragcontainer.setAttribute('aria-grabbed', 'false');
497         // The real drop node is stored in an attribute of the proxy.
498         var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
500         // Close the dialog.
501         M.core.dragdrop.dropui.hide();
502         // Cancel the event.
503         e.preventDefault();
504         // Convert to drag drop events.
505         var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
506         var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
507         // Simulate the full sequence.
508         this.drag_start(dragevent);
509         this.global_drop_over(dropevent);
510         this.global_drop_hit(dropevent);
511         M.core.dragdrop.keydraghandle.focus();
512     },
514     /**
515      * This is used to cancel a keyboard version of a drag and drop.
516      *
517      * @method global_cancel_keyboard_drag
518      */
519     global_cancel_keyboard_drag: function() {
520         if (M.core.dragdrop.keydragcontainer) {
521             M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false');
522             M.core.dragdrop.keydraghandle.focus();
523             M.core.dragdrop.keydragcontainer = null;
524         }
525     },
527     /**
528      * Process key events on the drag handles.
529      *
530      * @method global_keydown
531      * @param {EventFacade} e The keydown / click event on the drag handle.
532      */
533     global_keydown: function(e) {
534         var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true),
535             dragcontainer,
536             draggroups;
538         if (draghandle === null) {
539             // The element clicked did not have a a draghandle in it's lineage.
540             return;
541         }
543         if (e.keyCode === 27 ) {
544             // Escape to cancel from anywhere.
545             this.global_cancel_keyboard_drag();
546             e.preventDefault();
547             return;
548         }
550         // Only process events on a drag handle.
551         if (!draghandle.hasClass(MOVEICON.cssclass)) {
552             return;
553         }
555         // Do nothing if not space or enter.
556         if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') {
557             return;
558         }
560         // Check the drag groups to see if we are the handler for this node.
561         draggroups = draghandle.getAttribute('data-draggroups').split(' ');
562         var i, j, validgroup = false;
564         for (i = 0; i < draggroups.length; i++) {
565             for (j = 0; j < this.groups.length; j++) {
566                 if (draggroups[i] === this.groups[j]) {
567                     validgroup = true;
568                     break;
569                 }
570             }
571             if (validgroup) {
572                 break;
573             }
574         }
575         if (!validgroup) {
576             return;
577         }
579         // Valid event - start the keyboard drag.
580         dragcontainer = draghandle.ancestor('.yui3-dd-drop');
581         this.global_start_keyboard_drag(e, draghandle, dragcontainer);
583         e.preventDefault();
584     },
587     // Abstract functions definitions.
589     /**
590      * Callback to use when dragging starts.
591      *
592      * @method drag_start
593      * @param {EventFacade} e
594      */
595     drag_start: function() {},
597     /**
598      * Callback to use when dragging ends.
599      *
600      * @method drag_end
601      * @param {EventFacade} e
602      */
603     drag_end: function() {},
605     /**
606      * Callback to use during dragging.
607      *
608      * @method drag_drag
609      * @param {EventFacade} e
610      */
611     drag_drag: function() {},
613     /**
614      * Callback to use when dragging ends and is not over a drop target.
615      *
616      * @method drag_dropmiss
617      * @param {EventFacade} e
618      */
619     drag_dropmiss: function() {},
621     /**
622      * Callback to use when a drop over event occurs.
623      *
624      * @method drop_over
625      * @param {EventFacade} e
626      */
627     drop_over: function() {},
629     /**
630      * Callback to use on drop:hit.
631      *
632      * @method drop_hit
633      * @param {EventFacade} e
634      */
635     drop_hit: function() {}
636 }, {
637     NAME: 'dragdrop',
638     ATTRS: {}
639 });
641 M.core = M.core || {};
642 M.core.dragdrop = DRAGDROP;
645 }, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]});