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