Skip to content
Martin Wendt edited this page Apr 9, 2021 · 29 revisions

About Fancytree drag-and-drop extension (HTML5 based).

Add Drag-and-Drop support:

  • Compatible with the HTML Drag and Drop API.
  • Drag nodes inside one tree, i.e. re-order, move, or copy.
  • Drag nodes between different trees (even on different iframes, windows, or pages).
  • Any element that has the draggable="true"attribute set may be dropped over a Fancytree node.
  • Drop a Fancytree node on any element that implements the HTML Drag and Drop API, including text editors or desktop items.
  • Requires IE 11 or later, or recent Chrome, Firefox, Safari.

Note: There is also another extension (ext-dnd) available that implements drag-and-drop support using the jQuery UI draggable and jQuery UI droppable API.
Unless you need to support those APIs, it is recommended to use ext-dnd5.

Options

(See below)

Events

(See below)

Methods

  • {FancytreeNode} $.ui.fancytree.getDragNode()
    Return the FancytreeNode that is currently dragged or null. In multiSource-mode, only the first node is returned. Note that this only works for trees inside the same window/frame. Use dataTransfer.getData("application/x-fancytree-node") for cross-window dragging.

  • {FancytreeNode[]} $.ui.fancytree.getDragNodeList()
    Return a list of FancytreeNodes that are currently dragged or an empyty list if no dragging is active. See also comments on 'getDragNode()'.

Usage

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

  <script src="//code.jquery.com/jquery-3.6.0.min.js"></script>

  <link href="skin-win8/ui.fancytree.css" rel="stylesheet">
  <script src="js/jquery-ui-dependencies/jquery.fancytree.ui-deps.js"></script>
  <script src="js/jquery.fancytree.js"></script>
  <script src="js/jquery.fancytree.dnd5.js"></script>

Enable dnd5 extension and pass options:

$("#tree").fancytree({
  extensions: ["dnd5"],
  dnd5: {
    // Available options with their default:
    autoExpandMS: 1500,           // Expand nodes after n milliseconds of hovering.
    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"
    dropMarkerParent: "body",     // Root Container used for drop marker (could be a shadow root)
    effectAllowed: "all",         // Restrict the possible cursor shapes and modifier operations 
                                  // (can also be set in the dragStart event)
    dropEffectDefault: "move",    // Default dropEffect ('copy', 'link', or 'move') 
                                  // when no modifier is pressed (overide in dragDrag, dragOver).
    multiSource: false,           // true: Drag multiple (i.e. selected) nodes. 
                                  // Also a callback() is allowed to return a node list
    preventForeignNodes: false,   // Prevent dropping nodes from another Fancytree
    preventLazyParents: true,     // Prevent dropping items on unloaded lazy Fancytree nodes
    preventNonNodes: false,       // Prevent dropping items other than Fancytree nodes
    preventRecursion: true,       // Prevent dropping nodes on own descendants when in move-mode
    preventSameParent: false,     // Prevent dropping nodes under same direct parent
    preventVoidMoves: true,       // Prevent moving nodes 'before self', etc.
    scroll: true,                 // Enable auto-scrolling while dragging
    scrollSensitivity: 20,        // Active top/bottom margin in pixel
    scrollSpeed: 5,               // Pixel per event
    setTextTypeJson: false,       // Allow dragging of nodes to different IE windows
    sourceCopyHook: null,         // Optional callback passed to `toDict` on dragStart @since 2.38
    // Events (drag support)
    dragStart: null,       // Callback(sourceNode, data), return true, to enable dragging
    dragDrag: $.noop,      // Callback(sourceNode, data)
    dragEnd: $.noop,       // Callback(sourceNode, data)
    // Events (drop support)
    dragEnter: null,       // Callback(targetNode, data), return true, to enable dropping
    dragOver: $.noop,      // Callback(targetNode, data)
    dragExpand: $.noop,    // Callback(targetNode, data)
    dragDrop: $.noop,      // Callback(targetNode, data)
    dragLeave: $.noop      // Callback(targetNode, data)
  },
  [...]
});

All callback methods are passed a data object:

{
  tree: {Fancytree},            // The tree that the event refers to
  node: {FancytreeNode},        // The node that the event refers to (also passed as first argument)
  options: {object},            // Tree options (plugin options accessible as `options.dnd5`)
  originalEvent: {Event},       // The original jQuery Event that caused this callback
  widget: {object},             // The jQuery UI tree widget
  dataTransfer: {DataTransfer}, // Access drag data, drag image, and system drop effect
  dropEffect: {string},         // ('move', 'copy', or 'link') access the requested drop effect
  dropEffectSuggested: {string},// Recommended effect derived from a common key mapping
  effectAllowed: {string},      // ('all', 'copyMove', 'link', 'move', ...) Settable on dragstart only
  useDefaultImage: {boolean},   // (Default: true) Developer can set this to false if a custom setDragImage() was called
  isCancelled: {boolean},       // Set for dragend and drop events
  isMove: {boolean},            // false for copy or link effects
  // Only on these events: dragenter, dragover, dragleave, drop:
  files: null,                  // list of `File` objects if any were dropped (may be [])
  hitMode: {string},            // 'over', 'after', 'before'
  otherNode: {FancytreeNode},   // If applicable: the other node, e.g. drag source, ...
  otherNodeList: {Array(FancytreeNode)},
  otherNodeData: {object},      // set by drop event
}

Example

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

  // .. other options...

  dnd5: {
    autoExpandMS: 1500,
    preventRecursion: true, // Prevent dropping nodes on own descendants
    preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.

    // --- Drag Support --------------------------------------------------------

    dragStart: function(node, data) {
      // Called on source node when user starts dragging `node`.
      // This method MUST be defined to enable dragging for tree nodes!
      // We can
      //   - Add or modify the drag data using `data.dataTransfer.setData()`.
      //   - Call `data.dataTransfer.setDragImage()` and set `data.useDefaultImage` to false.
      //   - Return false to cancel dragging of `node`.

      // Set the allowed effects (i.e. override the 'effectAllowed' option)
      data.effectAllowed = "all";  // or 'copyMove', 'link'', ...

      // Set a drop effect (i.e. override the 'dropEffectDefault' option)
      // One of 'copy', 'move', 'link'.
      // In order to use a common modifier key mapping, we can use the suggested value:
      data.dropEffect = data.dropEffectSuggested;

      // We could also define a custom image here (not on IE though):
      //data.dataTransfer.setDragImage($("<div>TEST</div>").appendTo("body")[0], -10, -10);
      //data.useDefaultImage = false;

      // Return true to allow the drag operation
      if( node.isFolder() ) { return false; }
      return true;
    },
    dragDrag: function(node, data) {
      // Called on source node every few milliseconds while `node` is dragged.
      // Implementation of this callback is optional and rarely required.
    },
    dragEnd: function(node, data) {
      // Called on source node when the drag operation has terminated.
      // Check `data.isCancelled` to see if a drop occurred.
      // Implementation of this callback is optional and rarely required.
      // Note caveat: 
      // If the drop handler removed or moved the dragged source element,  
      // `node` and `data` may not contain expected values, or this event
      // is not triggered at all.
    },

    // --- Drop Support --------------------------------------------------------

    dragEnter: function(node, data) {
      // Called on target node when s.th. is dragged over `node`.
      // `data.otherNode` may be a Fancytree source node or null for 
      // non-Fancytree droppables.
      // This method MUST be defined to enable dropping over tree nodes!
      //
      // We may
      //   - Set `data.dropEffect` (defaults to '')
      //   - Call `data.setDragImage()`
      //
      // Return
      //   - true to allow dropping (calc the hitMode from the cursor position)
      //   - false to prevent dropping (dragOver and dragLeave are not called)
      //   - a list (e.g. ["before", "after"]) to restrict available hitModes
      //   - a string "over", "before, or "after" to force a hitMode
      //   - Any other return value will calc the hitMode from the cursor position.

      // Example:
      // Prevent dropping a parent below another parent (only sort nodes under
      // the same parent):
      //if(node.parent !== data.otherNode.parent){
      //  return false;
      //}
      // Example:
      // Don't allow dropping *over* a node (which would create a child). Just
      // allow changing the order:
      //return ["before", "after"];

      // Accept everything:
      return true;
    },
    dragOver: function(node, data) {
      // Called on target node every few milliseconds while some source is 
      // dragged over it.
      // `data.hitMode` contains the calculated insertion point, based on cursor
      // position and the response of `dragEnter`.
      //
      // We may
      //   - Override `data.hitMode`
      //   - Set `data.dropEffect` (defaults to the value that of dragEnter)
      //     (Note: IE will ignore this and use the value from dragenter instead!)
      //   - Call `data.dataTransfer.setDragImage()`

      // Set a drop effect (i.e. override the 'dropEffectDefault' option)
      // One of 'copy', 'move', 'link'.
      // In order to use a common modifier key mapping, we can use the suggested value:
      data.dropEffect = data.dropEffectSuggested;
    },
    dragExpand: function(node, data) {
      // Called when a dragging cursor lingers over a parent node.
      // (Optional) Return false to prevent auto-expanding `node`.
    },
    dragLeave: function(node, data) {
      // Called when s.th. is no longer dragged over `node`.
      // Implementation of this callback is optional and rarely required.
    },
    dragDrop: function(node, data) {
      // This function MUST be defined to enable dropping of items on the tree.
      //
      // The source data is provided in several formats:
      //   `data.otherNode` (null if it's not a FancytreeNode from the same page)
      //   `data.otherNodeData` (Json object; null if it's not a FancytreeNode)
      //   `data.dataTransfer.getData()`
      //
      // We may access some meta data to decide what to do:
      //   `data.hitMode` ("before", "after", or "over").
      //   `data.dataTransfer.dropEffect`,`.effectAllowed`
      //   `data.originalEvent.shiftKey`, ...
      //
      // Example:

      var transfer = data.dataTransfer;

      node.debug("drop", data);

      if( data.otherNode ) {
        // Drop another Fancytree node from same frame
        // (maybe from another tree however)
        var sameTree = (data.otherNode.tree === data.tree);

        data.otherNode.moveTo(node, data.hitMode);
      } else if( data.otherNodeData ) {
        // Drop Fancytree node from different frame or window, so we only have
        // JSON representation available
        node.addChild(data.otherNodeData, data.hitMode);
      } else {
        // Drop a non-node
        node.addNode({
          title: transfer.getData("text")
        }, data.hitMode);
      }
      // Expand target node when a child was created:
      node.setExpanded();
    }
  }
});

Status Classes

By default, some classes are added to the nodes, to enable visual feedback:

  • TODO

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

The auto scroll feature is on by default. It works by scrolling the whole tree when the drag cursor is near the upper or lower margin of the scroll parent:

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

The container may also be set to a fixed sized by a custom rule, to enable scrolling inside the tree-container only:

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

[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 non-nodes as drop source

We can drop any element onto Fancytree, that satisfies the HTML Drag and Drop API, for example:

<span class="drag-source" draggable="true"
  ondragstart="event.dataTransfer.setData('text/plain', 'Drag me');">
  Drag me</span>

Also other elements may generate drop-events by default:

  • <input> elements and <a> tags
  • selected text from a text editor
  • Files from your computer's desktop, ...
$("#tree").fancytree({
  extensions: ["dnd5"],
  ...
  dnd5: {
    preventNonNodes: false,      // Allow dropping items other than Fancytree nodes
    ...
    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());
        node.addNode({
          title: data.dataTransfer.getData("text")
        }, data.hitMode);
        return;
      }
      data.otherNode.moveTo(node, data.hitMode);
    }
  }
});

Note: ext-dnd5 does not make use of the jQuery UI draggable API.

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

We can drop a Fancytree node on any target, that implements the HTML Drag and Drop API.

For example these elements are potential drop targets:

  • Another Fancytree widget on the same page or another browser window
  • <input> and <textarea> elements
  • Text editor windows
  • File Explorer, your computer's desktop, ...

Note: ext-dnd5 does not make use of the jQuery UI droppable API.

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

[Howto] Modify the drag icon

$("#tree").fancytree({
  dnd5: {
    ... 
    dragStart: function(node, data) {
      ...
      // Image must exist in DOM
      var $image = $("#myDragImage");
      if( !$image.length ) {
        $image = $("<div id='myDragImage'>TEST</div>").appendTo("body");
      }
      data.dataTransfer.setDragImage($image[0], -10, -10);
      // Prevent henerating the default echo
      data.useDefaultImage = false;
      ...
    },
  }
  [...]
});

[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] Drop files

$("#tree").fancytree({
  dnd5: {
    ...
    dragDrop: function(node, data) {
      var newNode,
        transfer = data.dataTransfer,
        mode = data.dropEffect;

      // Browser should not open links, files, etc.
      data.originalEvent.preventDefault();

      if (data.files.length) {
          for(var i=0; i<data.files.length; i++) {
            var file = data.files[i];
            node.addNode( { title: "'" + file.name + "' (" + file.size + " bytes)" }, data.hitMode );
            // var url = "'https://example.com/upload",
            //     formData = new FormData();

            // formData.append("file", transfer.files[0])
            // fetch(url, {
            //   method: "POST",
            //   body: formData
            // }).then(function() { /* Done. Inform the user */ })
            // .catch(function() { /* Error. Inform the user */ });
          }
      } 
      ...
    },
  }
  [...]
});

See the example

Clone this wiki locally