MDL-47368 core JS: dragdrop click move fills the dom with divs
[moodle.git] / lib / yui / build / moodle-core-dragdrop / moodle-core-dragdrop.js
CommitLineData
83cdecec
AN
1YUI.add('moodle-core-dragdrop', function (Y, NAME) {
2
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 */
9var MOVEICON = {
10 pix: "i/move_2d",
11 largepix: "i/dragdrop",
12 component: 'moodle',
13 cssclass: 'moodle-core-dragdrop-draghandle'
14};
15
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 */
24var DRAGDROP = function() {
25 DRAGDROP.superclass.constructor.apply(this, arguments);
26};
27
28Y.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,
38
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,
48
49 /**
50 * The class for the object.
51 *
52 * @property samenodeclass
53 * @type String
54 * @default null
55 */
56 samenodeclass: null,
57
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,
66
34bcc6a9
AN
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,
75
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,
84
83cdecec
AN
85 /**
86 * The groups for this instance.
87 *
88 * @property groups
89 * @type Array
90 * @default []
91 */
92 groups: [],
93
94 /**
95 * The previous drop location.
96 *
97 * @property lastdroptarget
98 * @type Node
99 * @default null
100 */
101 lastdroptarget: null,
102
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);
112
113 // Listen for all drag:end events.
114 Y.DD.DDM.on('drag:end', this.global_drag_end, this);
115
116 // Listen for all drag:drag events.
117 Y.DD.DDM.on('drag:drag', this.global_drag_drag, this);
118
119 // Listen for all drop:over events.
120 Y.DD.DDM.on('drop:over', this.global_drop_over, this);
121
122 // Listen for all drop:hit events.
123 Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
124
125 // Listen for all drop:miss events.
126 Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
127
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);
131
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 },
136
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 }
163
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)
02839a35 169 .setAttribute('role', 'button');
83cdecec
AN
170 dragelement.appendChild(dragicon);
171 dragelement.addClass(MOVEICON.cssclass);
172
173 return dragelement;
174 },
175
176 lock_drag_handle: function(drag, classname) {
177 drag.removeHandle('.'+classname);
178 },
179
180 unlock_drag_handle: function(drag, classname) {
181 drag.addHandle('.'+classname);
8659df93 182 drag.get('activeHandle').focus();
83cdecec
AN
183 },
184
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 },
192
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 },
222
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 },
236
237 global_drag_drag: function(e) {
238 var drag = e.target,
239 info = e.info;
240
241 // Check that drag object belongs to correct group
242 if (!this.in_group(drag)) {
243 return;
244 }
245
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.
249
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;
254
255 } else if (info.start[1] > info.xy[1]) {
256 // Otherwise we're going down.
257 this.absgoingup = false;
258 }
259
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;
264
265 } else if (info.delta[1] > 0) {
266 // Otherwise we're going down.
267 this.goingup = false;
268 }
269
270 this.drag_drag(e);
271 },
272
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 }
278
279 // Get a reference to our drag and drop nodes.
280 var drag = e.drag.get('node'),
281 drop = e.drop.get('node');
282
283 // Save last drop target for the case of missed target processing.
284 this.lastdroptarget = e.drop;
285
286 // Are we dropping within the same parent node?
287 if (drop.hasClass(this.samenodeclass)) {
288 var where;
289
290 if (this.goingup) {
291 where = "before";
292 } else {
293 where = "after";
294 }
295
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 },
308
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 },
325
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 },
333
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 = '';
345
346 nodes.each(function () {
347 if (text === '') {
348 if (Y.Lang.trim(this.get('text')) !== '') {
349 text = this.get('text');
350 }
351 }
352 });
353
354 if (text !== '') {
355 return text;
356 }
357 return M.util.get_string('emptydragdropregion', 'moodle');
358 },
359
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;
372
83cdecec
AN
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);
376
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;
382
383 // Search for possible drop targets.
384 var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
385
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 }
409
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);
415
34bcc6a9
AN
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 }
83cdecec
AN
423 listlink.setContent(listitemtext);
424
425 // Add a data attribute so we can get the real drop target.
426 listlink.setAttribute('data-drop-target', node.get('id'));
83cdecec
AN
427 // Allow tabbing to the link.
428 listlink.setAttribute('tabindex', '0');
429
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);
433
434 // Add to the list or drop targets.
435 listitem.append(listlink);
436 droplist.append(listitem);
437 }
438 }, this);
439
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,
8659df93
AN
446 center: true,
447 modal: true
83cdecec
AN
448 });
449
8659df93
AN
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);
457
83cdecec
AN
458 // Focus the first drop target.
459 if (droplist.one('a')) {
460 droplist.one('a').focus();
461 }
462 },
463
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) {
486
487 // Subclass for wrapping both drag and drop.
488 var DragDropWrapper = function(node) {
489 this.node = node;
490 };
491
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 }
8659df93
AN
497 if (param === 'activeHandle') {
498 return this.node.one('.editing_move');
499 }
83cdecec
AN
500 return null;
501 };
502
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 };
507
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() {};
512
513 // Create instances of the DragDropWrapper.
514 this.drop = new DragDropWrapper(dropnode);
515 this.drag = new DragDropWrapper(dragnode);
516 this.target = this.drop;
517 },
518
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;
83cdecec
AN
528 // The real drop node is stored in an attribute of the proxy.
529 var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
530
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);
2ab0238f
AN
541
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 }
549
83cdecec 550 this.global_drop_hit(dropevent);
83cdecec
AN
551 },
552
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) {
8659df93 560 // Focus on the node which was being dragged.
83cdecec
AN
561 M.core.dragdrop.keydraghandle.focus();
562 M.core.dragdrop.keydragcontainer = null;
563 }
08ab7a3c
TH
564 if (M.core.dragdrop.dropui) {
565 M.core.dragdrop.dropui.destroy();
566 }
83cdecec
AN
567 },
568
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;
579
580 if (draghandle === null) {
581 // The element clicked did not have a a draghandle in it's lineage.
582 return;
583 }
584
585 if (e.keyCode === 27 ) {
586 // Escape to cancel from anywhere.
587 this.global_cancel_keyboard_drag();
588 e.preventDefault();
589 return;
590 }
591
592 // Only process events on a drag handle.
593 if (!draghandle.hasClass(MOVEICON.cssclass)) {
594 return;
595 }
596
597 // Do nothing if not space or enter.
598 if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') {
599 return;
600 }
601
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;
605
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 }
620
621 // Valid event - start the keyboard drag.
622 dragcontainer = draghandle.ancestor('.yui3-dd-drop');
623 this.global_start_keyboard_drag(e, draghandle, dragcontainer);
624
625 e.preventDefault();
626 },
627
628
629 // Abstract functions definitions.
630
631 /**
632 * Callback to use when dragging starts.
633 *
634 * @method drag_start
635 * @param {EventFacade} e
636 */
637 drag_start: function() {},
638
639 /**
640 * Callback to use when dragging ends.
641 *
642 * @method drag_end
643 * @param {EventFacade} e
644 */
645 drag_end: function() {},
646
647 /**
648 * Callback to use during dragging.
649 *
650 * @method drag_drag
651 * @param {EventFacade} e
652 */
653 drag_drag: function() {},
654
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() {},
662
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() {},
670
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});
682
683M.core = M.core || {};
684M.core.dragdrop = DRAGDROP;
685
686
687}, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]});