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