[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 YUI.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 */ 9 var 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 */ 24 var DRAGDROP = function() { 25 DRAGDROP.superclass.constructor.apply(this, arguments); 26 }; 27 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, 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 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 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) 169 .setAttribute('role', 'button'); 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); 182 drag.get('activeHandle').focus(); 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 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 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); 424 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'); 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, 446 center: true, 447 modal: true 448 }); 449 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 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 } 497 if (param === 'activeHandle') { 498 return this.node.one('.editing_move'); 499 } 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; 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); 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 550 this.global_drop_hit(dropevent); 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) { 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 }, 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 683 M.core = M.core || {}; 684 M.core.dragdrop = DRAGDROP; 685 686 687 }, '@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: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |