Skip to content
Martin Wendt edited this page Dec 27, 2021 · 38 revisions

About Fancytree drag-and-drop extension (jQuery UI based).

  • Status: production / deprecated
  • example

Note: This extension is deprecated!
Use the the HTML5-based version instead.
See "Migrate To ext-dnd5" below.

Add Drag-and-Drop support:

Example

In addition to jQuery, jQuery UI, and Fancytree, include jquery.fancytree.dnd.js:

  <script src="//code.jquery.com/jquery-3.6.0.min.js"></script>
  <script src="//code.jquery.com/ui/1.13.0/jquery-ui.min.js"></script>
  
  <link href="skin-win8/ui.fancytree.css" rel="stylesheet">
  <script src="js/jquery.fancytree.js"></script>
  <script src="js/jquery.fancytree.dnd.js"></script>

Enable dnd extension and pass options:

$("#tree").fancytree({
  extensions: ["dnd"],
  dnd: {
    // Available options with their default:
    autoExpandMS: 1000,   // Expand nodes after n milliseconds of hovering
    draggable: null,      // Additional options passed to jQuery UI draggable
    droppable: null,      // Additional options passed to jQuery UI droppable
    dropMarkerOffsetX: -24,  // absolute position offset for .fancytree-drop-marker
                             // relatively to ..fancytree-title (icon/img near a node accepting drop)
    dropMarkerInsertOffsetX: -16, // additional offset for drop-marker with hitMode = "before"/"after"
    focusOnClick: false,  // Focus, although draggable cancels mousedown event (#270)
    preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
    preventVoidMoves: true,      // Prevent dropping nodes 'before self', etc.
    smartRevert: true,    // set draggable.revert = true if drop was rejected

    // Events that make tree nodes draggable
    dragStart: null,      // Callback(sourceNode, data), return true to enable dnd
    dragStop: null,       // Callback(sourceNode, data)
    initHelper: null,     // Callback(sourceNode, data)
    updateHelper: null,   // Callback(sourceNode, data)

    // Events that make tree nodes accept draggables
    dragEnter: null,      // Callback(targetNode, data)
    dragExpand: null,     // Callback(targetNode, data), return false to prevent autoExpand
    dragOver: null,       // Callback(targetNode, data)
    dragDrop: null,       // Callback(targetNode, data)
    dragLeave: null       // Callback(targetNode, data)
  },
  [...]
});

All API function are passed a data object:

{
    node: ...,
    tree: ...,
    options: ...,
    originalEvent: ...,
    otherNode: ...,
    ui: ...,
    hitMode: ...,
    draggable: ...,
}

Example

$("#tree").fancytree({
  extensions: ["dnd"],

  // .. other options...

  dnd: {
    autoExpandMS: 400,
    draggable: { // modify default jQuery draggable options
      zIndex: 1000,
      scroll: false,
      containment: "parent",
      revert: "invalid"
    },
    preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
    preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.

    dragStart: function(node, data) {
      // This function MUST be defined to enable dragging for the tree.
      // Return false to cancel dragging of node.
//    if( data.originalEvent.shiftKey ) ...          
//    if( node.isFolder() ) { return false; }
      return true;
    },
    dragEnter: function(node, data) {
      /* data.otherNode may be null for non-fancytree droppables.
       * Return false to disallow dropping on node. In this case
       * dragOver and dragLeave are not called.
       * Return 'over', 'before, or 'after' to force a hitMode.
       * Return ['before', 'after'] to restrict available hitModes.
       * Any other return value will calc the hitMode from the cursor position.
       */
      // Prevent dropping a parent below another parent (only sort
      // nodes under the same parent):
//    if(node.parent !== data.otherNode.parent){
//      return false;
//    }
      // Don't allow dropping *over* a node (would create a child). Just
      // allow changing the order:
//    return ["before", "after"];
      // Accept everything:
      return true;
    },
    dragExpand: function(node, data) {
      // return false to prevent auto-expanding data.node on hover
    },
    dragOver: function(node, data) {
    },
    dragLeave: function(node, data) {
    },
    dragStop: function(node, data) {
    },
    dragDrop: function(node, data) {
      // This function MUST be defined to enable dropping of items on the tree.
      // data.hitMode is 'before', 'after', or 'over'.
      // We could for example move the source to the new target:
      data.otherNode.moveTo(node, data.hitMode);
    }
  }
});

Migrate To ext-dnd5

This extension ('ext-dnd') implements drag-and-drop support using the jQuery UI draggable and jQuery UI droppable API, which is a bit out-of-date now (2020).
We recommend to switch the HTML5-based alternative ('ext-dnd5') instead.

Transition should be straight-forward:

  • dragStop was renamed to dragEnd.
  • preventRecursiveMoves was renamed to preventRecursion
  • dragEnter must return true to allow dropping (this was recommended for 'ext-dnd' as well, but there undefined did also work).
  • Read the ExtDnd5 docs to look for enhanced features, that became available now.

See ExtDnd5 for details.

Recipes

[Howto] Copy a node on drop

This is accomplished by the node.copyTo() method.
When a node is copied from the same tree, we should define a new key, or let the tree generate one:

  dragDrop: function(node, data) {
    newNode = data.otherNode.copyTo(node, data.hitMode, function(n){
      n.title = "Copy of " + n.title;
      n.key = null; // make sure, a new key is generated
    });
  }

[Howto] Control scrolling inside the tree container while dragging

By default, jQuery UI Draggable options for auto scrolling are enabled for the tree:

scroll: true,
scrollSpeed: 7,
scrollSensitivity: 10,

Scrolling also requires that the Fancytree container has position: relative and the draggable helper is a child of the scroll parent.
Therefore the draggable helper is appended to the tree container by default and this rule is part of common CSS:

ul.fancytree-container {
    position: relative;
}

The container should also be sized by a custom rule, for example:

ul.fancytree-container {
    height: 200px;
    overflow-y: auto;
}

Prevent Scrolling

While auto-scrolling may be handy when nodes should be dragged inside one large tree, it may be undesirable when nodes should be dragged to outside targets.

In order to prevent scrolling inside the tree container, this can be turned off for draggable and container:

$("#tree").fancytree({
  ...  
  dnd: {
    ...
    draggable: { // modify default jQuery draggable options
      scroll: false  // disable auto-scrolling
      ...
    },

and custom CSS

ul.fancytree-container {
    position: inherit;  /* prevent clipping */
}

Note: position: inherit; does not play well with the wide extension.
In this case try to move the helper element outside the container:

$("#tree").fancytree({
  ...  
  dnd: {
    ...
    draggable: { // modify default jQuery draggable options
      appendTo: "body",
      ...
    },

[Howto] Drop on a lazy node

Dropping a node onto a lazy folder may not work as expected: The item that is dragged will appear in that folder but it stops the node from performing the Ajax request.
This is 'works as designed': lazy folders only generate an Ajax request if the children property is null or undefined (in order to prevent lazy-loading a second time).

We could however expand the node before adding the dropped node:

dragDrop: function(node, data) {
  node.setExpanded(true).always(function(){
    // Wait until expand finished, then add the additional child
    data.otherNode.moveTo(node, data.hitMode);
  });
}

(Another pattern could be: issue an Ajax request to notify the server about the new node. Then reload the branch.)

[Howto] Accept standard jQuery UI draggables as drop source

Assuming we have a standard jQuery UI draggable element:

<p class="draggable">
  Draggable.
</p>

Connect the draggable to the tree:

$(".draggable").draggable({
  revert: true, //"invalid",
  cursorAt: { top: -5, left: -5 },
  connectToFancytree: true,   // let Fancytree accept drag events
  ...
});

and handle drop events:

$("#tree").fancytree({
  extensions: ["dnd"],
  ...
  dnd: {
    ...
    dragEnter: function(node, data) {
       return true;
    },
    dragDrop: function(node, data) {
      if( !data.otherNode ){
        // It's a non-tree draggable
        alert("dropped " + $(data.draggable.element).text());
        return;
      }
      data.otherNode.moveTo(node, data.hitMode);
    }
  }
});

[Howto] Accept Fancytree nodes as source for a standard droppable

Assuming we have a standard jQuery UI droppable element:

<p class="droppable">
  Droppable.
</p>

and the tree has the dnd extension enabled:

$("#tree").fancytree({
  extensions: ["dnd"],
  ...
  dnd: {
    ...
    dragStart: function(node, data) {
      return true;
    },
    ...
  }
});

Notes: See also <[Howto] Control scrolling inside the tree container while dragging

Nodes can be dropped to the standard droppables, and we can access the original source node like so:

$(".droppable").droppable({
  drop: function(event, ui){
    var sourceNode = $(ui.helper).data("ftSourceNode");
    alert("Dropped source node " + sourceNode);
  },
  ...
});

[Howto] Prevent ext-dnd to suppress mouse clicks

Fancytree uses the standard jQuery UI draggable plugin to implement drag'n'drop. However draggable prevents mouse clicks from setting the focus (probably because this allows to drag an object without activating it).

In combination with keyboard navigation, this can prevent setting the focus to the tree container, so that keyboard input events are not dispatched to the tree.
Use the dnd.focusOnClick: true option in this case:

$("#tree").fancytree({
  dnd: {
    focusOnClick: false,  // Focus, although draggable cancels mousedown event (#270)
    ...
  }
  [...]
});

[Howto] Modify the draggable helper, helper parent, and others

The dnd extension allows to pass options to the standard jQuery UI draggable plugin.

$("#tree").fancytree({
  dnd: {
    ...
    draggable: {
      revert: "invalid"
      scroll: false,
      appendTo: "body", // Helper parent (defaults to tree.$container)
      helper: function(event) {
        var $helper,
          sourceNode = $.ui.fancytree.getNode(event.target),
          $nodeTag = $(sourceNode.span);

        $helper = $("<div class='fancytree-drag-helper'><span class='fancytree-drag-helper-img' /></div>")
          .append($nodeTag.find("span.fancytree-title").clone());

        // Attach node reference to helper object
        $helper.data("ftSourceNode", sourceNode);
        // we return an unconnected element, so `draggable` will add this
        // to the parent specified as `appendTo` option
        return $helper;
      },
    },
  }
  [...]
});

[Howto] Implement copy/move modifier keys

There is no built-in 'useModifiers' option, because the potential use cases are too diverse. But we can implement the desired behavior using callbacks.

See the example

[Howto] Implement multi-node drag'n'drop

There is no built-in 'multiDnd' option, because the potential use cases are too diverse. But we can implement the desired behavior using callbacks.

See the example

[Howto] Fix jumping container on mouse click

The combination of tabindex="0" and using jQuery UI draggable, causes large containers to scroll to the top of the page (see issue #577).

This may be solved by removing the tabindex attribute by passing the tree option .tabindex = "".