1/*! 2 * jquery.fancytree.dnd5.js 3 * 4 * Drag-and-drop support (native HTML5). 5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 6 * 7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8 * 9 * Released under the MIT license 10 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11 * 12 * @version 2.38.3 13 * @date 2023-02-01T20:52:50Z 14 */ 15 16/* 17 #TODO 18 Compatiblity when dragging between *separate* windows: 19 20 Drag from Chrome Edge FF IE11 Safari 21 To Chrome ok ok ok NO ? 22 Edge ok ok ok NO ? 23 FF ok ok ok NO ? 24 IE 11 ok ok ok ok ? 25 Safari ? ? ? ? ok 26 27 */ 28 29(function (factory) { 30 if (typeof define === "function" && define.amd) { 31 // AMD. Register as an anonymous module. 32 define(["jquery", "./jquery.fancytree"], factory); 33 } else if (typeof module === "object" && module.exports) { 34 // Node/CommonJS 35 require("./jquery.fancytree"); 36 module.exports = factory(require("jquery")); 37 } else { 38 // Browser globals 39 factory(jQuery); 40 } 41})(function ($) { 42 "use strict"; 43 44 /****************************************************************************** 45 * Private functions and variables 46 */ 47 var FT = $.ui.fancytree, 48 isMac = /Mac/.test(navigator.platform), 49 classDragSource = "fancytree-drag-source", 50 classDragRemove = "fancytree-drag-remove", 51 classDropAccept = "fancytree-drop-accept", 52 classDropAfter = "fancytree-drop-after", 53 classDropBefore = "fancytree-drop-before", 54 classDropOver = "fancytree-drop-over", 55 classDropReject = "fancytree-drop-reject", 56 classDropTarget = "fancytree-drop-target", 57 nodeMimeType = "application/x-fancytree-node", 58 $dropMarker = null, 59 $dragImage, 60 $extraHelper, 61 SOURCE_NODE = null, 62 SOURCE_NODE_LIST = null, 63 $sourceList = null, 64 DRAG_ENTER_RESPONSE = null, 65 // SESSION_DATA = null, // plain object passed to events as `data` 66 SUGGESTED_DROP_EFFECT = null, 67 REQUESTED_DROP_EFFECT = null, 68 REQUESTED_EFFECT_ALLOWED = null, 69 LAST_HIT_MODE = null, 70 DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode 71 72 /* */ 73 function _clearGlobals() { 74 DRAG_ENTER_RESPONSE = null; 75 DRAG_OVER_STAMP = null; 76 REQUESTED_DROP_EFFECT = null; 77 REQUESTED_EFFECT_ALLOWED = null; 78 SUGGESTED_DROP_EFFECT = null; 79 SOURCE_NODE = null; 80 SOURCE_NODE_LIST = null; 81 if ($sourceList) { 82 $sourceList.removeClass(classDragSource + " " + classDragRemove); 83 } 84 $sourceList = null; 85 if ($dropMarker) { 86 $dropMarker.hide(); 87 } 88 // Take this badge off of me - I can't use it anymore: 89 if ($extraHelper) { 90 $extraHelper.remove(); 91 $extraHelper = null; 92 } 93 } 94 95 /* Convert number to string and prepend +/-; return empty string for 0.*/ 96 function offsetString(n) { 97 // eslint-disable-next-line no-nested-ternary 98 return n === 0 ? "" : n > 0 ? "+" + n : "" + n; 99 } 100 101 /* Convert a dragEnter() or dragOver() response to a canonical form. 102 * Return false or plain object 103 * @param {string|object|boolean} r 104 * @return {object|false} 105 */ 106 function normalizeDragEnterResponse(r) { 107 var res; 108 109 if (!r) { 110 return false; 111 } 112 if ($.isPlainObject(r)) { 113 res = { 114 over: !!r.over, 115 before: !!r.before, 116 after: !!r.after, 117 }; 118 } else if (Array.isArray(r)) { 119 res = { 120 over: $.inArray("over", r) >= 0, 121 before: $.inArray("before", r) >= 0, 122 after: $.inArray("after", r) >= 0, 123 }; 124 } else { 125 res = { 126 over: r === true || r === "over", 127 before: r === true || r === "before", 128 after: r === true || r === "after", 129 }; 130 } 131 if (Object.keys(res).length === 0) { 132 return false; 133 } 134 // if( Object.keys(res).length === 1 ) { 135 // res.unique = res[0]; 136 // } 137 return res; 138 } 139 140 /* Convert a dataTransfer.effectAllowed to a canonical form. 141 * Return false or plain object 142 * @param {string|boolean} r 143 * @return {object|false} 144 */ 145 // function normalizeEffectAllowed(r) { 146 // if (!r || r === "none") { 147 // return false; 148 // } 149 // var all = r === "all", 150 // res = { 151 // copy: all || /copy/i.test(r), 152 // link: all || /link/i.test(r), 153 // move: all || /move/i.test(r), 154 // }; 155 156 // return res; 157 // } 158 159 /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ 160 function autoScroll(tree, event) { 161 var spOfs, 162 scrollTop, 163 delta, 164 dndOpts = tree.options.dnd5, 165 sp = tree.$scrollParent[0], 166 sensitivity = dndOpts.scrollSensitivity, 167 speed = dndOpts.scrollSpeed, 168 scrolled = 0; 169 170 if (sp !== document && sp.tagName !== "HTML") { 171 spOfs = tree.$scrollParent.offset(); 172 scrollTop = sp.scrollTop; 173 if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) { 174 delta = 175 sp.scrollHeight - 176 tree.$scrollParent.innerHeight() - 177 scrollTop; 178 // console.log ("sp.offsetHeight: " + sp.offsetHeight 179 // + ", spOfs.top: " + spOfs.top 180 // + ", scrollTop: " + scrollTop 181 // + ", innerHeight: " + tree.$scrollParent.innerHeight() 182 // + ", scrollHeight: " + sp.scrollHeight 183 // + ", delta: " + delta 184 // ); 185 if (delta > 0) { 186 sp.scrollTop = scrolled = scrollTop + speed; 187 } 188 } else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) { 189 sp.scrollTop = scrolled = scrollTop - speed; 190 } 191 } else { 192 scrollTop = $(document).scrollTop(); 193 if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) { 194 scrolled = scrollTop - speed; 195 $(document).scrollTop(scrolled); 196 } else if ( 197 $(window).height() - (event.pageY - scrollTop) < 198 sensitivity 199 ) { 200 scrolled = scrollTop + speed; 201 $(document).scrollTop(scrolled); 202 } 203 } 204 if (scrolled) { 205 tree.debug("autoScroll: " + scrolled + "px"); 206 } 207 return scrolled; 208 } 209 210 /* Guess dropEffect from modifier keys. 211 * Using rules suggested here: 212 * https://ux.stackexchange.com/a/83769 213 * @returns 214 * 'copy', 'link', 'move', or 'none' 215 */ 216 function evalEffectModifiers(tree, event, effectDefault) { 217 var res = effectDefault; 218 219 if (isMac) { 220 if (event.metaKey && event.altKey) { 221 // Mac: [Control] + [Option] 222 res = "link"; 223 } else if (event.ctrlKey) { 224 // Chrome on Mac: [Control] 225 res = "link"; 226 } else if (event.metaKey) { 227 // Mac: [Command] 228 res = "move"; 229 } else if (event.altKey) { 230 // Mac: [Option] 231 res = "copy"; 232 } 233 } else { 234 if (event.ctrlKey) { 235 // Windows: [Ctrl] 236 res = "copy"; 237 } else if (event.shiftKey) { 238 // Windows: [Shift] 239 res = "move"; 240 } else if (event.altKey) { 241 // Windows: [Alt] 242 res = "link"; 243 } 244 } 245 if (res !== SUGGESTED_DROP_EFFECT) { 246 tree.info( 247 "evalEffectModifiers: " + 248 event.type + 249 " - evalEffectModifiers(): " + 250 SUGGESTED_DROP_EFFECT + 251 " -> " + 252 res 253 ); 254 } 255 SUGGESTED_DROP_EFFECT = res; 256 // tree.debug("evalEffectModifiers: " + res); 257 return res; 258 } 259 /* 260 * Check if the previous callback (dragEnter, dragOver, ...) has changed 261 * the `data` object and apply those settings. 262 * 263 * Safari: 264 * It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain 265 * even if the cursor changes when [Alt] or [Ctrl] are pressed (?) 266 * Using rules suggested here: 267 * https://ux.stackexchange.com/a/83769 268 * @returns 269 * 'copy', 'link', 'move', or 'none' 270 */ 271 function prepareDropEffectCallback(event, data) { 272 var tree = data.tree, 273 dataTransfer = data.dataTransfer; 274 275 if (event.type === "dragstart") { 276 data.effectAllowed = tree.options.dnd5.effectAllowed; 277 data.dropEffect = tree.options.dnd5.dropEffectDefault; 278 } else { 279 data.effectAllowed = REQUESTED_EFFECT_ALLOWED; 280 data.dropEffect = REQUESTED_DROP_EFFECT; 281 } 282 data.dropEffectSuggested = evalEffectModifiers( 283 tree, 284 event, 285 tree.options.dnd5.dropEffectDefault 286 ); 287 data.isMove = data.dropEffect === "move"; 288 data.files = dataTransfer.files || []; 289 290 // if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) { 291 // tree.warn( 292 // "prepareDropEffectCallback(" + 293 // event.type + 294 // "): dataTransfer.effectAllowed changed from " + 295 // REQUESTED_EFFECT_ALLOWED + 296 // " -> " + 297 // dataTransfer.effectAllowed 298 // ); 299 // } 300 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 301 // tree.warn( 302 // "prepareDropEffectCallback(" + 303 // event.type + 304 // "): dataTransfer.dropEffect changed from requested " + 305 // REQUESTED_DROP_EFFECT + 306 // " to " + 307 // dataTransfer.dropEffect 308 // ); 309 // } 310 } 311 312 function applyDropEffectCallback(event, data, allowDrop) { 313 var tree = data.tree, 314 dataTransfer = data.dataTransfer; 315 316 if ( 317 event.type !== "dragstart" && 318 REQUESTED_EFFECT_ALLOWED !== data.effectAllowed 319 ) { 320 tree.warn( 321 "effectAllowed should only be changed in dragstart event: " + 322 event.type + 323 ": data.effectAllowed changed from " + 324 REQUESTED_EFFECT_ALLOWED + 325 " -> " + 326 data.effectAllowed 327 ); 328 } 329 330 if (allowDrop === false) { 331 tree.info("applyDropEffectCallback: allowDrop === false"); 332 data.effectAllowed = "none"; 333 data.dropEffect = "none"; 334 } 335 // if (REQUESTED_DROP_EFFECT !== data.dropEffect) { 336 // tree.debug( 337 // "applyDropEffectCallback(" + 338 // event.type + 339 // "): data.dropEffect changed from previous " + 340 // REQUESTED_DROP_EFFECT + 341 // " to " + 342 // data.dropEffect 343 // ); 344 // } 345 346 data.isMove = data.dropEffect === "move"; 347 // data.isMove = data.dropEffectSuggested === "move"; 348 349 // `effectAllowed` must only be defined in dragstart event, so we 350 // store it in a global variable for reference 351 if (event.type === "dragstart") { 352 REQUESTED_EFFECT_ALLOWED = data.effectAllowed; 353 REQUESTED_DROP_EFFECT = data.dropEffect; 354 } 355 356 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 357 // data.tree.info( 358 // "applyDropEffectCallback(" + 359 // event.type + 360 // "): dataTransfer.dropEffect changed from " + 361 // REQUESTED_DROP_EFFECT + 362 // " -> " + 363 // dataTransfer.dropEffect 364 // ); 365 // } 366 dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED; 367 dataTransfer.dropEffect = REQUESTED_DROP_EFFECT; 368 369 // tree.debug( 370 // "applyDropEffectCallback(" + 371 // event.type + 372 // "): set " + 373 // dataTransfer.dropEffect + 374 // "/" + 375 // dataTransfer.effectAllowed 376 // ); 377 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 378 // data.tree.warn( 379 // "applyDropEffectCallback(" + 380 // event.type + 381 // "): could not set dataTransfer.dropEffect to " + 382 // REQUESTED_DROP_EFFECT + 383 // ": got " + 384 // dataTransfer.dropEffect 385 // ); 386 // } 387 return REQUESTED_DROP_EFFECT; 388 } 389 390 /* Handle dragover event (fired every x ms) on valid drop targets. 391 * 392 * - Auto-scroll when cursor is in border regions 393 * - Apply restrictioan like 'preventVoidMoves' 394 * - Calculate hit mode 395 * - Calculate drop effect 396 * - Trigger dragOver() callback to let user modify hit mode and drop effect 397 * - Adjust the drop marker accordingly 398 * 399 * @returns hitMode 400 */ 401 function handleDragOver(event, data) { 402 // Implement auto-scrolling 403 if (data.options.dnd5.scroll) { 404 autoScroll(data.tree, event); 405 } 406 // Bail out with previous response if we get an invalid dragover 407 if (!data.node) { 408 data.tree.warn("Ignored dragover for non-node"); //, event, data); 409 return LAST_HIT_MODE; 410 } 411 412 var markerOffsetX, 413 nodeOfs, 414 pos, 415 relPosY, 416 hitMode = null, 417 tree = data.tree, 418 options = tree.options, 419 dndOpts = options.dnd5, 420 targetNode = data.node, 421 sourceNode = data.otherNode, 422 markerAt = "center", 423 $target = $(targetNode.span), 424 $targetTitle = $target.find("span.fancytree-title"); 425 426 if (DRAG_ENTER_RESPONSE === false) { 427 tree.debug("Ignored dragover, since dragenter returned false."); 428 return false; 429 } else if (typeof DRAG_ENTER_RESPONSE === "string") { 430 $.error("assert failed: dragenter returned string"); 431 } 432 // Calculate hitMode from relative cursor position. 433 nodeOfs = $target.offset(); 434 relPosY = (event.pageY - nodeOfs.top) / $target.height(); 435 if (event.pageY === undefined) { 436 tree.warn("event.pageY is undefined: see issue #1013."); 437 } 438 439 if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) { 440 hitMode = "after"; 441 } else if ( 442 !DRAG_ENTER_RESPONSE.over && 443 DRAG_ENTER_RESPONSE.after && 444 relPosY > 0.5 445 ) { 446 hitMode = "after"; 447 } else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) { 448 hitMode = "before"; 449 } else if ( 450 !DRAG_ENTER_RESPONSE.over && 451 DRAG_ENTER_RESPONSE.before && 452 relPosY <= 0.5 453 ) { 454 hitMode = "before"; 455 } else if (DRAG_ENTER_RESPONSE.over) { 456 hitMode = "over"; 457 } 458 // Prevent no-ops like 'before source node' 459 // TODO: these are no-ops when moving nodes, but not in copy mode 460 if (dndOpts.preventVoidMoves && data.dropEffect === "move") { 461 if (targetNode === sourceNode) { 462 targetNode.debug("Drop over source node prevented."); 463 hitMode = null; 464 } else if ( 465 hitMode === "before" && 466 sourceNode && 467 targetNode === sourceNode.getNextSibling() 468 ) { 469 targetNode.debug("Drop after source node prevented."); 470 hitMode = null; 471 } else if ( 472 hitMode === "after" && 473 sourceNode && 474 targetNode === sourceNode.getPrevSibling() 475 ) { 476 targetNode.debug("Drop before source node prevented."); 477 hitMode = null; 478 } else if ( 479 hitMode === "over" && 480 sourceNode && 481 sourceNode.parent === targetNode && 482 sourceNode.isLastSibling() 483 ) { 484 targetNode.debug("Drop last child over own parent prevented."); 485 hitMode = null; 486 } 487 } 488 // Let callback modify the calculated hitMode 489 data.hitMode = hitMode; 490 if (hitMode && dndOpts.dragOver) { 491 prepareDropEffectCallback(event, data); 492 dndOpts.dragOver(targetNode, data); 493 var allowDrop = !!hitMode; 494 applyDropEffectCallback(event, data, allowDrop); 495 hitMode = data.hitMode; 496 } 497 LAST_HIT_MODE = hitMode; 498 // 499 if (hitMode === "after" || hitMode === "before" || hitMode === "over") { 500 markerOffsetX = dndOpts.dropMarkerOffsetX || 0; 501 switch (hitMode) { 502 case "before": 503 markerAt = "top"; 504 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 505 break; 506 case "after": 507 markerAt = "bottom"; 508 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 509 break; 510 } 511 512 pos = { 513 my: "left" + offsetString(markerOffsetX) + " center", 514 at: "left " + markerAt, 515 of: $targetTitle, 516 }; 517 if (options.rtl) { 518 pos.my = "right" + offsetString(-markerOffsetX) + " center"; 519 pos.at = "right " + markerAt; 520 // console.log("rtl", pos); 521 } 522 $dropMarker 523 .toggleClass(classDropAfter, hitMode === "after") 524 .toggleClass(classDropOver, hitMode === "over") 525 .toggleClass(classDropBefore, hitMode === "before") 526 .show() 527 .position(FT.fixPositionOptions(pos)); 528 } else { 529 $dropMarker.hide(); 530 // console.log("hide dropmarker") 531 } 532 533 $(targetNode.span) 534 .toggleClass( 535 classDropTarget, 536 hitMode === "after" || 537 hitMode === "before" || 538 hitMode === "over" 539 ) 540 .toggleClass(classDropAfter, hitMode === "after") 541 .toggleClass(classDropBefore, hitMode === "before") 542 .toggleClass(classDropAccept, hitMode === "over") 543 .toggleClass(classDropReject, hitMode === false); 544 545 return hitMode; 546 } 547 548 /* 549 * Handle dragstart drag dragend events on the container 550 */ 551 function onDragEvent(event) { 552 var json, 553 tree = this, 554 dndOpts = tree.options.dnd5, 555 node = FT.getNode(event), 556 dataTransfer = 557 event.dataTransfer || event.originalEvent.dataTransfer, 558 data = { 559 tree: tree, 560 node: node, 561 options: tree.options, 562 originalEvent: event.originalEvent, 563 widget: tree.widget, 564 dataTransfer: dataTransfer, 565 useDefaultImage: true, 566 dropEffect: undefined, 567 dropEffectSuggested: undefined, 568 effectAllowed: undefined, // set by dragstart 569 files: undefined, // only for drop events 570 isCancelled: undefined, // set by dragend 571 isMove: undefined, 572 }; 573 574 switch (event.type) { 575 case "dragstart": 576 if (!node) { 577 tree.info("Ignored dragstart on a non-node."); 578 return false; 579 } 580 // Store current source node in different formats 581 SOURCE_NODE = node; 582 583 // Also optionally store selected nodes 584 if (dndOpts.multiSource === false) { 585 SOURCE_NODE_LIST = [node]; 586 } else if (dndOpts.multiSource === true) { 587 if (node.isSelected()) { 588 SOURCE_NODE_LIST = tree.getSelectedNodes(); 589 } else { 590 SOURCE_NODE_LIST = [node]; 591 } 592 } else { 593 SOURCE_NODE_LIST = dndOpts.multiSource(node, data); 594 } 595 // Cache as array of jQuery objects for faster access: 596 $sourceList = $( 597 $.map(SOURCE_NODE_LIST, function (n) { 598 return n.span; 599 }) 600 ); 601 // Set visual feedback 602 $sourceList.addClass(classDragSource); 603 604 // Set payload 605 // Note: 606 // Transfer data is only accessible on dragstart and drop! 607 // For all other events the formats and kinds in the drag 608 // data store list of items representing dragged data can be 609 // enumerated, but the data itself is unavailable and no new 610 // data can be added. 611 var nodeData = node.toDict(true, dndOpts.sourceCopyHook); 612 nodeData.treeId = node.tree._id; 613 json = JSON.stringify(nodeData); 614 try { 615 dataTransfer.setData(nodeMimeType, json); 616 dataTransfer.setData("text/html", $(node.span).html()); 617 dataTransfer.setData("text/plain", node.title); 618 } catch (ex) { 619 // IE only accepts 'text' type 620 tree.warn( 621 "Could not set data (IE only accepts 'text') - " + ex 622 ); 623 } 624 // We always need to set the 'text' type if we want to drag 625 // Because IE 11 only accepts this single type. 626 // If we pass JSON here, IE can can access all node properties, 627 // even when the source lives in another window. (D'n'd inside 628 // the same window will always work.) 629 // The drawback is, that in this case ALL browsers will see 630 // the JSON representation as 'text', so dragging 631 // to a text field will insert the JSON string instead of 632 // the node title. 633 if (dndOpts.setTextTypeJson) { 634 dataTransfer.setData("text", json); 635 } else { 636 dataTransfer.setData("text", node.title); 637 } 638 639 // Set the allowed drag modes (combinations of move, copy, and link) 640 // (effectAllowed can only be set in the dragstart event.) 641 // This can be overridden in the dragStart() callback 642 prepareDropEffectCallback(event, data); 643 644 // Let user cancel or modify above settings 645 // Realize potential changes by previous callback 646 if (dndOpts.dragStart(node, data) === false) { 647 // Cancel dragging 648 // dataTransfer.dropEffect = "none"; 649 _clearGlobals(); 650 return false; 651 } 652 applyDropEffectCallback(event, data); 653 654 // Unless user set `data.useDefaultImage` to false in dragStart, 655 // generata a default drag image now: 656 $extraHelper = null; 657 658 if (data.useDefaultImage) { 659 // Set the title as drag image (otherwise it would contain the expander) 660 $dragImage = $(node.span).find(".fancytree-title"); 661 662 if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) { 663 // Add a counter badge to node title if dragging more than one node. 664 // We want this, because the element that is used as drag image 665 // must be *visible* in the DOM, so we cannot create some hidden 666 // custom markup. 667 // See https://kryogenix.org/code/browser/custom-drag-image.html 668 // Also, since IE 11 and Edge don't support setDragImage() alltogether, 669 // it gives som feedback to the user. 670 // The badge will be removed later on drag end. 671 $extraHelper = $( 672 "<span class='fancytree-childcounter'/>" 673 ) 674 .text("+" + (SOURCE_NODE_LIST.length - 1)) 675 .appendTo($dragImage); 676 } 677 if (dataTransfer.setDragImage) { 678 // IE 11 and Edge do not support this 679 dataTransfer.setDragImage($dragImage[0], -10, -10); 680 } 681 } 682 return true; 683 684 case "drag": 685 // Called every few milliseconds (no matter if the 686 // cursor is over a valid drop target) 687 // data.tree.info("drag", SOURCE_NODE) 688 prepareDropEffectCallback(event, data); 689 dndOpts.dragDrag(node, data); 690 applyDropEffectCallback(event, data); 691 692 $sourceList.toggleClass(classDragRemove, data.isMove); 693 break; 694 695 case "dragend": 696 // Called at the end of a d'n'd process (after drop) 697 // Note caveat: If drop removed the dragged source element, 698 // we may not get this event, since the target does not exist 699 // anymore 700 prepareDropEffectCallback(event, data); 701 702 _clearGlobals(); 703 704 data.isCancelled = !LAST_HIT_MODE; 705 dndOpts.dragEnd(node, data, !LAST_HIT_MODE); 706 // applyDropEffectCallback(event, data); 707 break; 708 } 709 } 710 /* 711 * Handle dragenter dragover dragleave drop events on the container 712 */ 713 function onDropEvent(event) { 714 var json, 715 allowAutoExpand, 716 nodeData, 717 isSourceFtNode, 718 r, 719 res, 720 tree = this, 721 dndOpts = tree.options.dnd5, 722 allowDrop = null, 723 node = FT.getNode(event), 724 dataTransfer = 725 event.dataTransfer || event.originalEvent.dataTransfer, 726 data = { 727 tree: tree, 728 node: node, 729 options: tree.options, 730 originalEvent: event.originalEvent, 731 widget: tree.widget, 732 hitMode: DRAG_ENTER_RESPONSE, 733 dataTransfer: dataTransfer, 734 otherNode: SOURCE_NODE || null, 735 otherNodeList: SOURCE_NODE_LIST || null, 736 otherNodeData: null, // set by drop event 737 useDefaultImage: true, 738 dropEffect: undefined, 739 dropEffectSuggested: undefined, 740 effectAllowed: undefined, // set by dragstart 741 files: null, // list of File objects (may be []) 742 isCancelled: undefined, // set by drop event 743 isMove: undefined, 744 }; 745 746 // data.isMove = dropEffect === "move"; 747 748 switch (event.type) { 749 case "dragenter": 750 // The dragenter event is fired when a dragged element or 751 // text selection enters a valid drop target. 752 753 DRAG_OVER_STAMP = null; 754 if (!node) { 755 // Sometimes we get dragenter for the container element 756 tree.debug( 757 "Ignore non-node " + 758 event.type + 759 ": " + 760 event.target.tagName + 761 "." + 762 event.target.className 763 ); 764 DRAG_ENTER_RESPONSE = false; 765 break; 766 } 767 768 $(node.span) 769 .addClass(classDropOver) 770 .removeClass(classDropAccept + " " + classDropReject); 771 772 // Data is only readable in the dragstart and drop event, 773 // but we can check for the type: 774 isSourceFtNode = 775 $.inArray(nodeMimeType, dataTransfer.types) >= 0; 776 777 if (dndOpts.preventNonNodes && !isSourceFtNode) { 778 node.debug("Reject dropping a non-node."); 779 DRAG_ENTER_RESPONSE = false; 780 break; 781 } else if ( 782 dndOpts.preventForeignNodes && 783 (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree) 784 ) { 785 node.debug("Reject dropping a foreign node."); 786 DRAG_ENTER_RESPONSE = false; 787 break; 788 } else if ( 789 dndOpts.preventSameParent && 790 data.otherNode && 791 data.otherNode.tree === node.tree && 792 node.parent === data.otherNode.parent 793 ) { 794 node.debug("Reject dropping as sibling (same parent)."); 795 DRAG_ENTER_RESPONSE = false; 796 break; 797 } else if ( 798 dndOpts.preventRecursion && 799 data.otherNode && 800 data.otherNode.tree === node.tree && 801 node.isDescendantOf(data.otherNode) 802 ) { 803 node.debug("Reject dropping below own ancestor."); 804 DRAG_ENTER_RESPONSE = false; 805 break; 806 } else if (dndOpts.preventLazyParents && !node.isLoaded()) { 807 node.warn("Drop over unloaded target node prevented."); 808 DRAG_ENTER_RESPONSE = false; 809 break; 810 } 811 $dropMarker.show(); 812 813 // Call dragEnter() to figure out if (and where) dropping is allowed 814 prepareDropEffectCallback(event, data); 815 r = dndOpts.dragEnter(node, data); 816 817 res = normalizeDragEnterResponse(r); 818 // alert("res:" + JSON.stringify(res)) 819 DRAG_ENTER_RESPONSE = res; 820 821 allowDrop = res && (res.over || res.before || res.after); 822 823 applyDropEffectCallback(event, data, allowDrop); 824 break; 825 826 case "dragover": 827 if (!node) { 828 tree.debug( 829 "Ignore non-node " + 830 event.type + 831 ": " + 832 event.target.tagName + 833 "." + 834 event.target.className 835 ); 836 break; 837 } 838 // The dragover event is fired when an element or text 839 // selection is being dragged over a valid drop target 840 // (every few hundred milliseconds). 841 // tree.debug( 842 // event.type + 843 // ": dropEffect: " + 844 // dataTransfer.dropEffect 845 // ); 846 prepareDropEffectCallback(event, data); 847 LAST_HIT_MODE = handleDragOver(event, data); 848 849 // The flag controls the preventDefault() below: 850 allowDrop = !!LAST_HIT_MODE; 851 allowAutoExpand = 852 LAST_HIT_MODE === "over" || LAST_HIT_MODE === false; 853 854 if ( 855 allowAutoExpand && 856 !node.expanded && 857 node.hasChildren() !== false 858 ) { 859 if (!DRAG_OVER_STAMP) { 860 DRAG_OVER_STAMP = Date.now(); 861 } else if ( 862 dndOpts.autoExpandMS && 863 Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS && 864 !node.isLoading() && 865 (!dndOpts.dragExpand || 866 dndOpts.dragExpand(node, data) !== false) 867 ) { 868 node.setExpanded(); 869 } 870 } else { 871 DRAG_OVER_STAMP = null; 872 } 873 break; 874 875 case "dragleave": 876 // NOTE: dragleave is fired AFTER the dragenter event of the 877 // FOLLOWING element. 878 if (!node) { 879 tree.debug( 880 "Ignore non-node " + 881 event.type + 882 ": " + 883 event.target.tagName + 884 "." + 885 event.target.className 886 ); 887 break; 888 } 889 if (!$(node.span).hasClass(classDropOver)) { 890 node.debug("Ignore dragleave (multi)."); 891 break; 892 } 893 $(node.span).removeClass( 894 classDropOver + 895 " " + 896 classDropAccept + 897 " " + 898 classDropReject 899 ); 900 node.scheduleAction("cancel"); 901 dndOpts.dragLeave(node, data); 902 $dropMarker.hide(); 903 break; 904 905 case "drop": 906 // Data is only readable in the (dragstart and) drop event: 907 908 if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) { 909 nodeData = dataTransfer.getData(nodeMimeType); 910 tree.info( 911 event.type + 912 ": getData('application/x-fancytree-node'): '" + 913 nodeData + 914 "'" 915 ); 916 } 917 if (!nodeData) { 918 // 1. Source is not a Fancytree node, or 919 // 2. If the FT mime type was set, but returns '', this 920 // is probably IE 11 (which only supports 'text') 921 nodeData = dataTransfer.getData("text"); 922 tree.info( 923 event.type + ": getData('text'): '" + nodeData + "'" 924 ); 925 } 926 if (nodeData) { 927 try { 928 // 'text' type may contain JSON if IE is involved 929 // and setTextTypeJson option was set 930 json = JSON.parse(nodeData); 931 if (json.title !== undefined) { 932 data.otherNodeData = json; 933 } 934 } catch (ex) { 935 // assume 'text' type contains plain text, so `otherNodeData` 936 // should not be set 937 } 938 } 939 tree.debug( 940 event.type + 941 ": nodeData: '" + 942 nodeData + 943 "', otherNodeData: ", 944 data.otherNodeData 945 ); 946 947 $(node.span).removeClass( 948 classDropOver + 949 " " + 950 classDropAccept + 951 " " + 952 classDropReject 953 ); 954 955 // Let user implement the actual drop operation 956 data.hitMode = LAST_HIT_MODE; 957 prepareDropEffectCallback(event, data, !LAST_HIT_MODE); 958 data.isCancelled = !LAST_HIT_MODE; 959 960 var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span, 961 orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree; 962 963 dndOpts.dragDrop(node, data); 964 // applyDropEffectCallback(event, data); 965 966 // Prevent browser's default drop handling, i.e. open as link, ... 967 event.preventDefault(); 968 969 if (orgSourceElem && !document.body.contains(orgSourceElem)) { 970 // The drop handler removed the original drag source from 971 // the DOM, so the dragend event will probaly not fire. 972 if (orgSourceTree === tree) { 973 tree.debug( 974 "Drop handler removed source element: generating dragEnd." 975 ); 976 dndOpts.dragEnd(SOURCE_NODE, data); 977 } else { 978 tree.warn( 979 "Drop handler removed source element: dragend event may be lost." 980 ); 981 } 982 } 983 984 _clearGlobals(); 985 986 break; 987 } 988 // Dnd API madness: we must PREVENT default handling to enable dropping 989 if (allowDrop) { 990 event.preventDefault(); 991 return false; 992 } 993 } 994 995 /** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject. 996 * 997 * @returns {FancytreeNode[]} List of nodes (empty if no drag operation) 998 * @example 999 * $.ui.fancytree.getDragNodeList(); 1000 * 1001 * @alias Fancytree_Static#getDragNodeList 1002 * @requires jquery.fancytree.dnd5.js 1003 * @since 2.31 1004 */ 1005 $.ui.fancytree.getDragNodeList = function () { 1006 return SOURCE_NODE_LIST || []; 1007 }; 1008 1009 /** [ext-dnd5] Return the FancytreeNode that is currently being dragged. 1010 * 1011 * If multiple nodes are dragged, only the first is returned. 1012 * 1013 * @returns {FancytreeNode | null} dragged nodes or null if no drag operation 1014 * @example 1015 * $.ui.fancytree.getDragNode(); 1016 * 1017 * @alias Fancytree_Static#getDragNode 1018 * @requires jquery.fancytree.dnd5.js 1019 * @since 2.31 1020 */ 1021 $.ui.fancytree.getDragNode = function () { 1022 return SOURCE_NODE; 1023 }; 1024 1025 /****************************************************************************** 1026 * 1027 */ 1028 1029 $.ui.fancytree.registerExtension({ 1030 name: "dnd5", 1031 version: "2.38.3", 1032 // Default options for this extension. 1033 options: { 1034 autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering 1035 dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" 1036 dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) 1037 // #1021 `document.body` is not available yet 1038 dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) 1039 multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed 1040 effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) 1041 // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string. 1042 dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver). 1043 preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees 1044 preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes 1045 preventNonNodes: false, // Prevent dropping items other than Fancytree nodes 1046 preventRecursion: true, // Prevent dropping nodes on own descendants 1047 preventSameParent: false, // Prevent dropping nodes under same direct parent 1048 preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. 1049 scroll: true, // Enable auto-scrolling while dragging 1050 scrollSensitivity: 20, // Active top/bottom margin in pixel 1051 scrollSpeed: 5, // Pixel per event 1052 setTextTypeJson: false, // Allow dragging of nodes to different IE windows 1053 sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 1054 // Events (drag support) 1055 dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag 1056 dragDrag: $.noop, // Callback(sourceNode, data) 1057 dragEnd: $.noop, // Callback(sourceNode, data) 1058 // Events (drop support) 1059 dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop 1060 dragOver: $.noop, // Callback(targetNode, data) 1061 dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand 1062 dragDrop: $.noop, // Callback(targetNode, data) 1063 dragLeave: $.noop, // Callback(targetNode, data) 1064 }, 1065 1066 treeInit: function (ctx) { 1067 var $temp, 1068 tree = ctx.tree, 1069 opts = ctx.options, 1070 glyph = opts.glyph || null, 1071 dndOpts = opts.dnd5; 1072 1073 if ($.inArray("dnd", opts.extensions) >= 0) { 1074 $.error("Extensions 'dnd' and 'dnd5' are mutually exclusive."); 1075 } 1076 if (dndOpts.dragStop) { 1077 $.error( 1078 "dragStop is not used by ext-dnd5. Use dragEnd instead." 1079 ); 1080 } 1081 if (dndOpts.preventRecursiveMoves != null) { 1082 $.error( 1083 "preventRecursiveMoves was renamed to preventRecursion." 1084 ); 1085 } 1086 1087 // Implement `opts.createNode` event to add the 'draggable' attribute 1088 // #680: this must happen before calling super.treeInit() 1089 if (dndOpts.dragStart) { 1090 FT.overrideMethod( 1091 ctx.options, 1092 "createNode", 1093 function (event, data) { 1094 // Default processing if any 1095 this._super.apply(this, arguments); 1096 if (data.node.span) { 1097 data.node.span.draggable = true; 1098 } else { 1099 data.node.warn( 1100 "Cannot add `draggable`: no span tag" 1101 ); 1102 } 1103 } 1104 ); 1105 } 1106 this._superApply(arguments); 1107 1108 this.$container.addClass("fancytree-ext-dnd5"); 1109 1110 // Store the current scroll parent, which may be the tree 1111 // container, any enclosing div, or the document. 1112 // #761: scrollParent() always needs a container child 1113 $temp = $("<span>").appendTo(this.$container); 1114 this.$scrollParent = $temp.scrollParent(); 1115 $temp.remove(); 1116 1117 $dropMarker = $("#fancytree-drop-marker"); 1118 if (!$dropMarker.length) { 1119 $dropMarker = $("<div id='fancytree-drop-marker'></div>") 1120 .hide() 1121 .css({ 1122 "z-index": 1000, 1123 // Drop marker should not steal dragenter/dragover events: 1124 "pointer-events": "none", 1125 }) 1126 .prependTo(dndOpts.dropMarkerParent); 1127 if (glyph) { 1128 FT.setSpanIcon( 1129 $dropMarker[0], 1130 glyph.map._addClass, 1131 glyph.map.dropMarker 1132 ); 1133 } 1134 } 1135 $dropMarker.toggleClass("fancytree-rtl", !!opts.rtl); 1136 1137 // Enable drag support if dragStart() is specified: 1138 if (dndOpts.dragStart) { 1139 // Bind drag event handlers 1140 tree.$container.on( 1141 "dragstart drag dragend", 1142 onDragEvent.bind(tree) 1143 ); 1144 } 1145 // Enable drop support if dragEnter() is specified: 1146 if (dndOpts.dragEnter) { 1147 // Bind drop event handlers 1148 tree.$container.on( 1149 "dragenter dragover dragleave drop", 1150 onDropEvent.bind(tree) 1151 ); 1152 } 1153 }, 1154 }); 1155 // Value returned by `require('jquery.fancytree..')` 1156 return $.ui.fancytree; 1157}); // End of closure 1158