/*!
* jquery.fancytree.dnd.js
* Drag'n'drop extension for jquery.fancytree.js.
*
* Copyright (c) 2013, Martin Wendt (http://wwWendt.de)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://code.google.com/p/fancytree/wiki/LicenseInfo
*
* A current version and some documentation is available at
* https://github.com/mar10/fancytree/
*/
;(function($, window, document, undefined) {
"use strict";
/* *****************************************************************************
* Private functions and variables
*/
var logMsg = $.ui.fancytree.debug,
didRegisterDnd = false;
/* Convert number to string and prepend +/-; return empty string for 0.*/
function offsetString(n){
return n === 0 ? "" : (( n > 0 ) ? ("+" + n) : ("" + n));
}
/* *****************************************************************************
* Drag and drop support
*/
function _initDragAndDrop(tree) {
var dnd = tree.options.dnd || null;
// Register 'connectToFancytree' option with ui.draggable
if(dnd /*&& (dnd.onDragStart || dnd.onDrop)*/) {
_registerDnd();
}
// Attach ui.draggable to this Fancytree instance
if(dnd && dnd.onDragStart ) {
tree.widget.element.draggable({
addClasses: false,
appendTo: "body",
containment: false,
delay: 0,
distance: 4,
// TODO: merge Dynatree issue 419
revert: false,
scroll: true, // issue 244: enable scrolling (if ul.fancytree-container)
scrollSpeed: 7,
scrollSensitivity: 10,
// Delegate draggable.start, drag, and stop events to our handler
connectToFancytree: true,
// Let source tree create the helper element
helper: function(event) {
var sourceNode = $.ui.fancytree.getNode(event.target);
if(!sourceNode){ // issue 211
// TODO: remove this hint, when we understand when it happens
return "
ERROR?: helper requested but sourceNode not found
";
}
return sourceNode.tree.dnd._onDragEvent("helper", sourceNode, null, event, null, null);
},
start: function(event, ui) {
// var sourceNode = $.ui.fancytree.getNode(event.target);
// don't return false if sourceNode == null (see issue 268)
}
});
}
// Attach ui.droppable to this Fancytree instance
if(dnd && dnd.onDrop) {
tree.widget.element.droppable({
addClasses: false,
tolerance: "intersect",
greedy: false
/*
,
activate: function(event, ui) {
logMsg("droppable - activate", event, ui, this);
},
create: function(event, ui) {
logMsg("droppable - create", event, ui);
},
deactivate: function(event, ui) {
logMsg("droppable - deactivate", event, ui);
},
drop: function(event, ui) {
logMsg("droppable - drop", event, ui);
},
out: function(event, ui) {
logMsg("droppable - out", event, ui);
},
over: function(event, ui) {
logMsg("droppable - over", event, ui);
}
*/
});
}
}
//--- Extend ui.draggable event handling --------------------------------------
function _registerDnd() {
if(didRegisterDnd){
return;
}
// Register proxy-functions for draggable.start/drag/stop
$.ui.plugin.add("draggable", "connectToFancytree", {
start: function(event, ui) {
// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
var draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
sourceNode = ui.helper.data("ftSourceNode") || null;
// logMsg("draggable-connectToFancytree.start, %s", sourceNode);
// logMsg(" this: %o", this);
// logMsg(" event: %o", event);
// logMsg(" draggable: %o", draggable);
// logMsg(" ui: %o", ui);
if(sourceNode) {
// Adjust helper offset, so cursor is slightly outside top/left corner
draggable.offset.click.top = -2;
draggable.offset.click.left = + 16;
// logMsg(" draggable2: %o", draggable);
// logMsg(" draggable.offset.click FIXED: %s/%s", draggable.offset.click.left, draggable.offset.click.top);
// Trigger onDragStart event
// TODO: when called as connectTo..., the return value is ignored(?)
return sourceNode.tree.dnd._onDragEvent("start", sourceNode, null, event, ui, draggable);
}
},
drag: function(event, ui) {
// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
var isHelper,
draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
sourceNode = ui.helper.data("ftSourceNode") || null,
prevTargetNode = ui.helper.data("ftTargetNode") || null,
targetNode = $.ui.fancytree.getNode(event.target);
// logMsg("$.ui.fancytree.getNode(%o): %s", event.target, targetNode);
// logMsg("connectToFancytree.drag: helper: %o", ui.helper[0]);
if(event.target && !targetNode){
// We got a drag event, but the targetNode could not be found
// at the event location. This may happen,
// 1. if the mouse jumped over the drag helper,
// 2. or if a non-fancytree element is dragged
// We ignore it:
isHelper = $(event.target).closest("div.fancytree-drag-helper,#fancytree-drop-marker").length > 0;
if(isHelper){
logMsg("Drag event over helper: ignored.");
return;
}
}
// logMsg("draggable-connectToFancytree.drag: targetNode(from event): %s, ftTargetNode: %s", targetNode, ui.helper.data("ftTargetNode"));
ui.helper.data("ftTargetNode", targetNode);
// Leaving a tree node
if(prevTargetNode && prevTargetNode !== targetNode ) {
prevTargetNode.tree.dnd._onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
}
if(targetNode){
if(!targetNode.tree.options.dnd.onDrop) {
// not enabled as drop target
} else if(targetNode === prevTargetNode) {
// Moving over same node
targetNode.tree.dnd._onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
}else{
// Entering this node first time
targetNode.tree.dnd._onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
}
}
// else go ahead with standard event handling
},
stop: function(event, ui) {
// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
var draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
sourceNode = ui.helper.data("ftSourceNode") || null,
targetNode = ui.helper.data("ftTargetNode") || null,
// mouseDownEvent = draggable._mouseDownEvent,
eventType = event.type,
dropped = (eventType === "mouseup" && event.which === 1);
// logMsg("draggable-connectToFancytree.stop: targetNode(from event): %s, ftTargetNode: %s", targetNode, ui.helper.data("ftTargetNode"));
// logMsg("draggable-connectToFancytree.stop, %s", sourceNode);
// logMsg(" type: %o, downEvent: %o, upEvent: %o", eventType, mouseDownEvent, event);
// logMsg(" targetNode: %o", targetNode);
if(!dropped){
logMsg("Drag was cancelled");
}
if(targetNode) {
if(dropped){
targetNode.tree.dnd._onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
}
targetNode.tree.dnd._onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
}
if(sourceNode){
sourceNode.tree.dnd._onDragEvent("stop", sourceNode, null, event, ui, draggable);
}
}
});
didRegisterDnd = true;
}
/* *****************************************************************************
*
*/
/** @namespace $.ui.fancytree.dnd */
$.ui.fancytree.registerExtension("dnd",
/** @scope ui_fancytree
* @lends $.ui.fancytree.dnd.prototype
*/
{
// Default options for this extension.
options: {
// Make tree nodes draggable:
onDragStart: null, // Callback(sourceNode), return true, to enable dnd
onDragStop: null, // Callback(sourceNode)
// helper: null,
// Make tree nodes accept draggables
autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering.
preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
onDragEnter: null, // Callback(targetNode, sourceNode)
onDragOver: null, // Callback(targetNode, sourceNode, hitMode)
onDrop: null, // Callback(targetNode, sourceNode, hitMode)
onDragLeave: null // Callback(targetNode, sourceNode)
},
// Override virtual methods for this extension.
// `this` : Fancytree instance
// `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
treeInit: function(ctx){
var tree = ctx.tree;
this._super(ctx);
_initDragAndDrop(tree);
},
/* Override key handler in order to cancel dnd on escape.*/
nodeKeydown: function(ctx) {
var event = ctx.originalEvent;
if( event.which === $.ui.keyCode.ESCAPE) {
this.dnd._cancelDrag();
}
this._super(ctx);
},
/* Display drop marker according to hitMode ('after', 'before', 'over', 'out', 'start', 'stop'). */
_setDndStatus: function(sourceNode, targetNode, helper, hitMode, accept) {
var posOpts,
markerOffsetX = 0,
markerAt = "center",
instData = this.dnd,
$source = sourceNode ? $(sourceNode.span) : null,
$target = $(targetNode.span);
if( !instData.$dropMarker ) {
instData.$dropMarker = $("")
.hide()
.css({"z-index": 1000})
.prependTo($(this.$div).parent());
// .prependTo("body");
// logMsg("Creating marker: %o", this.$dropMarker);
}
/*
if(hitMode === "start"){
}
if(hitMode === "stop"){
// sourceNode.removeClass("fancytree-drop-target");
}
*/
// this.$dropMarker.attr("class", hitMode);
if(hitMode === "after" || hitMode === "before" || hitMode === "over"){
// $source && $source.addClass("fancytree-drag-source");
// $target.addClass("fancytree-drop-target");
switch(hitMode){
case "before":
instData.$dropMarker.removeClass("fancytree-drop-after fancytree-drop-over");
instData.$dropMarker.addClass("fancytree-drop-before");
markerAt = "top";
break;
case "after":
instData.$dropMarker.removeClass("fancytree-drop-before fancytree-drop-over");
instData.$dropMarker.addClass("fancytree-drop-after");
markerAt = "bottom";
break;
default:
instData.$dropMarker.removeClass("fancytree-drop-after fancytree-drop-before");
instData.$dropMarker.addClass("fancytree-drop-over");
$target.addClass("fancytree-drop-target");
markerOffsetX = 8;
}
if( $.ui.fancytree.jquerySupports.positionMyOfs ){
posOpts = {
my: "left" + offsetString(markerOffsetX) + " center",
at: "left " + markerAt,
of: $target
};
} else {
posOpts = {
my: "left center",
at: "left " + markerAt,
of: $target,
offset: "" + markerOffsetX + " 0"
};
}
instData.$dropMarker
.show()
.position(posOpts);
// helper.addClass("fancytree-drop-hover");
} else {
// $source && $source.removeClass("fancytree-drag-source");
$target.removeClass("fancytree-drop-target");
instData.$dropMarker.hide();
// helper.removeClass("fancytree-drop-hover");
}
if(hitMode === "after"){
$target.addClass("fancytree-drop-after");
} else {
$target.removeClass("fancytree-drop-after");
}
if(hitMode === "before"){
$target.addClass("fancytree-drop-before");
} else {
$target.removeClass("fancytree-drop-before");
}
if(accept === true){
if($source){
$source.addClass("fancytree-drop-accept");
}
$target.addClass("fancytree-drop-accept");
helper.addClass("fancytree-drop-accept");
}else{
if($source){
$source.removeClass("fancytree-drop-accept");
}
$target.removeClass("fancytree-drop-accept");
helper.removeClass("fancytree-drop-accept");
}
if(accept === false){
if($source){
$source.addClass("fancytree-drop-reject");
}
$target.addClass("fancytree-drop-reject");
helper.addClass("fancytree-drop-reject");
}else{
if($source){
$source.removeClass("fancytree-drop-reject");
}
$target.removeClass("fancytree-drop-reject");
helper.removeClass("fancytree-drop-reject");
}
},
/*
* Handles drag'n'drop functionality.
*
* A standard jQuery drag-and-drop process may generate these calls:
*
* draggable helper():
* _onDragEvent("helper", sourceNode, null, event, null, null);
* start:
* _onDragEvent("start", sourceNode, null, event, ui, draggable);
* drag:
* _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
* _onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
* _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
* stop:
* _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
* _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
* _onDragEvent("stop", sourceNode, null, event, ui, draggable);
*/
_onDragEvent: function(eventName, node, otherNode, event, ui, draggable) {
if(eventName !== "over"){
logMsg("tree.dnd._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this);
}
var $helper, nodeOfs, relPos, relPos2,
enterResponse, hitMode, r,
opts = this.options,
dnd = opts.dnd,
res = null,
nodeTag = $(node.span);
switch (eventName) {
case "helper":
// Only event and node argument is available
$helper = $("
")
// .append($(event.target).closest("a").clone());
.append($(event.target).closest("span.fancytree-title").clone());
// issue 244: helper should be child of scrollParent
$("ul.fancytree-container", node.tree.$div).append($helper);
// $(node.tree.divTree).append($helper);
// Attach node reference to helper object
$helper.data("ftSourceNode", node);
logMsg("helper=%o", $helper);
logMsg("helper.sourceNode=%o", $helper.data("ftSourceNode"));
res = $helper;
break;
case "start":
if( node.isStatusNode ) {
res = false;
} else if(dnd.onDragStart) {
res = dnd.onDragStart(node);
}
if(res === false) {
this.debug("tree.onDragStart() cancelled");
//draggable._clear();
// NOTE: the return value seems to be ignored (drag is not canceled, when false is returned)
// TODO: call this._cancelDrag()?
ui.helper.trigger("mouseup");
ui.helper.hide();
} else {
nodeTag.addClass("fancytree-drag-source");
}
break;
case "enter":
if(dnd.preventRecursiveMoves && node.isDescendantOf(otherNode)){
r = false;
}else{
r = dnd.onDragEnter ? dnd.onDragEnter(node, otherNode, ui, draggable) : null;
}
if(!r){
// convert null, undefined, false to false
res = false;
}else if ( $.isArray(r) ) {
// TODO: also accept passing an object of this format directly
res = {
over: ($.inArray("over", r) >= 0),
before: ($.inArray("before", r) >= 0),
after: ($.inArray("after", r) >= 0)
};
}else{
res = {
over: ((r === true) || (r === "over")),
before: ((r === true) || (r === "before")),
after: ((r === true) || (r === "after"))
};
}
ui.helper.data("enterResponse", res);
logMsg("helper.enterResponse: %o", res);
break;
case "over":
enterResponse = ui.helper.data("enterResponse");
hitMode = null;
if(enterResponse === false){
// Don't call onDragOver if onEnter returned false.
// break;
} else if(typeof enterResponse === "string") {
// Use hitMode from onEnter if provided.
hitMode = enterResponse;
} else {
// Calculate hitMode from relative cursor position.
nodeOfs = nodeTag.offset();
relPos = { x: event.pageX - nodeOfs.left,
y: event.pageY - nodeOfs.top };
relPos2 = { x: relPos.x / nodeTag.width(),
y: relPos.y / nodeTag.height() };
if( enterResponse.after && relPos2.y > 0.75 ){
hitMode = "after";
} else if(!enterResponse.over && enterResponse.after && relPos2.y > 0.5 ){
hitMode = "after";
} else if(enterResponse.before && relPos2.y <= 0.25) {
hitMode = "before";
} else if(!enterResponse.over && enterResponse.before && relPos2.y <= 0.5) {
hitMode = "before";
} else if(enterResponse.over) {
hitMode = "over";
}
// Prevent no-ops like 'before source node'
// TODO: these are no-ops when moving nodes, but not in copy mode
if( dnd.preventVoidMoves ){
if(node === otherNode){
logMsg(" drop over source node prevented");
hitMode = null;
}else if(hitMode === "before" && otherNode && node === otherNode.getNextSibling()){
logMsg(" drop after source node prevented");
hitMode = null;
}else if(hitMode === "after" && otherNode && node === otherNode.getPrevSibling()){
logMsg(" drop before source node prevented");
hitMode = null;
}else if(hitMode === "over" && otherNode && otherNode.parent === node && otherNode.isLastSibling() ){
logMsg(" drop last child over own parent prevented");
hitMode = null;
}
}
// logMsg("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling());
ui.helper.data("hitMode", hitMode);
}
// Auto-expand node (only when 'over' the node, not 'before', or 'after')
if(hitMode === "over" && dnd.autoExpandMS && node.hasChildren() !== false && !node.expanded) {
node.scheduleAction("expand", dnd.autoExpandMS);
}
if(hitMode && dnd.onDragOver){
// TODO: http://code.google.com/p/dynatree/source/detail?r=625
res = dnd.onDragOver(node, otherNode, hitMode, ui, draggable);
}
// issue 332
// this._setDndStatus(otherNode, node, ui.helper, hitMode, res!==false);
this.dnd._setDndStatus(otherNode, node, ui.helper, hitMode, res!==false && hitMode !== null);
break;
case "drop":
hitMode = ui.helper.data("hitMode");
if(hitMode && dnd.onDrop){
dnd.onDrop(node, otherNode, hitMode, ui, draggable);
}
break;
case "leave":
// Cancel pending expand request
node.scheduleAction("cancel");
ui.helper.data("enterResponse", null);
ui.helper.data("hitMode", null);
this.dnd._setDndStatus(otherNode, node, ui.helper, "out", undefined);
if(dnd.onDragLeave){
dnd.onDragLeave(node, otherNode, ui, draggable);
}
break;
case "stop":
nodeTag.removeClass("fancytree-drag-source");
if(dnd.onDragStop){
dnd.onDragStop(node);
}
break;
default:
throw "Unsupported drag event: " + eventName;
}
return res;
},
_cancelDrag: function() {
var dd = $.ui.ddmanager.current;
if(dd){
dd.cancel();
}
}
});
}(jQuery, window, document));