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