ExtDnd - mar10/fancytree GitHub Wiki
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:
- Drag nodes inside one tree, i.e. re-order.
- Drag nodes between different trees on the same page.
- Drop standard jQuery UI draggables on tree nodes.
- Drop tree nodes on standard jQuery UI droppables.
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: ...,
}
$("#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);
}
}
});
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 todragEnd
. -
preventRecursiveMoves
was renamed topreventRecursion
-
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.
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
});
}
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",
...
},
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.)
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);
}
}
});
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);
},
...
});
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)
...
}
[...]
});
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;
},
},
}
[...]
});
There is no built-in 'useModifiers' option, because the potential use cases are too diverse. But we can implement the desired behavior using callbacks.
There is no built-in 'multiDnd' option, because the potential use cases are too diverse. But we can implement the desired behavior using callbacks.
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 = ""
.