src/jquery.fancytree.dnd5.js
/*!
* jquery.fancytree.dnd5.js
*
* Drag-and-drop support (native HTML5).
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
/*
#TODO
Compatiblity when dragging between *separate* windows:
Drag from Chrome Edge FF IE11 Safari
To Chrome ok ok ok NO ?
Edge ok ok ok NO ?
FF ok ok ok NO ?
IE 11 ok ok ok ok ?
Safari ? ? ? ? ok
*/
(function (factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery", "./jquery.fancytree"], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
require("./jquery.fancytree");
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function ($) {
"use strict";
/******************************************************************************
* Private functions and variables
*/
var FT = $.ui.fancytree,
isMac = /Mac/.test(navigator.platform),
classDragSource = "fancytree-drag-source",
classDragRemove = "fancytree-drag-remove",
classDropAccept = "fancytree-drop-accept",
classDropAfter = "fancytree-drop-after",
classDropBefore = "fancytree-drop-before",
classDropOver = "fancytree-drop-over",
classDropReject = "fancytree-drop-reject",
classDropTarget = "fancytree-drop-target",
nodeMimeType = "application/x-fancytree-node",
$dropMarker = null,
$dragImage,
$extraHelper,
SOURCE_NODE = null,
SOURCE_NODE_LIST = null,
$sourceList = null,
DRAG_ENTER_RESPONSE = null,
// SESSION_DATA = null, // plain object passed to events as `data`
SUGGESTED_DROP_EFFECT = null,
REQUESTED_DROP_EFFECT = null,
REQUESTED_EFFECT_ALLOWED = null,
LAST_HIT_MODE = null,
DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode
/* */
function _clearGlobals() {
DRAG_ENTER_RESPONSE = null;
DRAG_OVER_STAMP = null;
REQUESTED_DROP_EFFECT = null;
REQUESTED_EFFECT_ALLOWED = null;
SUGGESTED_DROP_EFFECT = null;
SOURCE_NODE = null;
SOURCE_NODE_LIST = null;
if ($sourceList) {
$sourceList.removeClass(classDragSource + " " + classDragRemove);
}
$sourceList = null;
if ($dropMarker) {
$dropMarker.hide();
}
// Take this badge off of me - I can't use it anymore:
if ($extraHelper) {
$extraHelper.remove();
$extraHelper = null;
}
}
/* Convert number to string and prepend +/-; return empty string for 0.*/
function offsetString(n) {
// eslint-disable-next-line no-nested-ternary
return n === 0 ? "" : n > 0 ? "+" + n : "" + n;
}
/* Convert a dragEnter() or dragOver() response to a canonical form.
* Return false or plain object
* @param {string|object|boolean} r
* @return {object|false}
*/
function normalizeDragEnterResponse(r) {
var res;
if (!r) {
return false;
}
if ($.isPlainObject(r)) {
res = {
over: !!r.over,
before: !!r.before,
after: !!r.after,
};
} else if (Array.isArray(r)) {
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",
};
}
if (Object.keys(res).length === 0) {
return false;
}
// if( Object.keys(res).length === 1 ) {
// res.unique = res[0];
// }
return res;
}
/* Convert a dataTransfer.effectAllowed to a canonical form.
* Return false or plain object
* @param {string|boolean} r
* @return {object|false}
*/
// function normalizeEffectAllowed(r) {
// if (!r || r === "none") {
// return false;
// }
// var all = r === "all",
// res = {
// copy: all || /copy/i.test(r),
// link: all || /link/i.test(r),
// move: all || /move/i.test(r),
// };
// return res;
// }
/* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
function autoScroll(tree, event) {
var spOfs,
scrollTop,
delta,
dndOpts = tree.options.dnd5,
sp = tree.$scrollParent[0],
sensitivity = dndOpts.scrollSensitivity,
speed = dndOpts.scrollSpeed,
scrolled = 0;
if (sp !== document && sp.tagName !== "HTML") {
spOfs = tree.$scrollParent.offset();
scrollTop = sp.scrollTop;
if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) {
delta =
sp.scrollHeight -
tree.$scrollParent.innerHeight() -
scrollTop;
// console.log ("sp.offsetHeight: " + sp.offsetHeight
// + ", spOfs.top: " + spOfs.top
// + ", scrollTop: " + scrollTop
// + ", innerHeight: " + tree.$scrollParent.innerHeight()
// + ", scrollHeight: " + sp.scrollHeight
// + ", delta: " + delta
// );
if (delta > 0) {
sp.scrollTop = scrolled = scrollTop + speed;
}
} else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) {
sp.scrollTop = scrolled = scrollTop - speed;
}
} else {
scrollTop = $(document).scrollTop();
if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
scrolled = scrollTop - speed;
$(document).scrollTop(scrolled);
} else if (
$(window).height() - (event.pageY - scrollTop) <
sensitivity
) {
scrolled = scrollTop + speed;
$(document).scrollTop(scrolled);
}
}
if (scrolled) {
tree.debug("autoScroll: " + scrolled + "px");
}
return scrolled;
}
/* Guess dropEffect from modifier keys.
* Using rules suggested here:
* https://ux.stackexchange.com/a/83769
* @returns
* 'copy', 'link', 'move', or 'none'
*/
function evalEffectModifiers(tree, event, effectDefault) {
var res = effectDefault;
if (isMac) {
if (event.metaKey && event.altKey) {
// Mac: [Control] + [Option]
res = "link";
} else if (event.ctrlKey) {
// Chrome on Mac: [Control]
res = "link";
} else if (event.metaKey) {
// Mac: [Command]
res = "move";
} else if (event.altKey) {
// Mac: [Option]
res = "copy";
}
} else {
if (event.ctrlKey) {
// Windows: [Ctrl]
res = "copy";
} else if (event.shiftKey) {
// Windows: [Shift]
res = "move";
} else if (event.altKey) {
// Windows: [Alt]
res = "link";
}
}
if (res !== SUGGESTED_DROP_EFFECT) {
tree.info(
"evalEffectModifiers: " +
event.type +
" - evalEffectModifiers(): " +
SUGGESTED_DROP_EFFECT +
" -> " +
res
);
}
SUGGESTED_DROP_EFFECT = res;
// tree.debug("evalEffectModifiers: " + res);
return res;
}
/*
* Check if the previous callback (dragEnter, dragOver, ...) has changed
* the `data` object and apply those settings.
*
* Safari:
* It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain
* even if the cursor changes when [Alt] or [Ctrl] are pressed (?)
* Using rules suggested here:
* https://ux.stackexchange.com/a/83769
* @returns
* 'copy', 'link', 'move', or 'none'
*/
function prepareDropEffectCallback(event, data) {
var tree = data.tree,
dataTransfer = data.dataTransfer;
if (event.type === "dragstart") {
data.effectAllowed = tree.options.dnd5.effectAllowed;
data.dropEffect = tree.options.dnd5.dropEffectDefault;
} else {
data.effectAllowed = REQUESTED_EFFECT_ALLOWED;
data.dropEffect = REQUESTED_DROP_EFFECT;
}
data.dropEffectSuggested = evalEffectModifiers(
tree,
event,
tree.options.dnd5.dropEffectDefault
);
data.isMove = data.dropEffect === "move";
data.files = dataTransfer.files || [];
// if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) {
// tree.warn(
// "prepareDropEffectCallback(" +
// event.type +
// "): dataTransfer.effectAllowed changed from " +
// REQUESTED_EFFECT_ALLOWED +
// " -> " +
// dataTransfer.effectAllowed
// );
// }
// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
// tree.warn(
// "prepareDropEffectCallback(" +
// event.type +
// "): dataTransfer.dropEffect changed from requested " +
// REQUESTED_DROP_EFFECT +
// " to " +
// dataTransfer.dropEffect
// );
// }
}
function applyDropEffectCallback(event, data, allowDrop) {
var tree = data.tree,
dataTransfer = data.dataTransfer;
if (
event.type !== "dragstart" &&
REQUESTED_EFFECT_ALLOWED !== data.effectAllowed
) {
tree.warn(
"effectAllowed should only be changed in dragstart event: " +
event.type +
": data.effectAllowed changed from " +
REQUESTED_EFFECT_ALLOWED +
" -> " +
data.effectAllowed
);
}
if (allowDrop === false) {
tree.info("applyDropEffectCallback: allowDrop === false");
data.effectAllowed = "none";
data.dropEffect = "none";
}
// if (REQUESTED_DROP_EFFECT !== data.dropEffect) {
// tree.debug(
// "applyDropEffectCallback(" +
// event.type +
// "): data.dropEffect changed from previous " +
// REQUESTED_DROP_EFFECT +
// " to " +
// data.dropEffect
// );
// }
data.isMove = data.dropEffect === "move";
// data.isMove = data.dropEffectSuggested === "move";
// `effectAllowed` must only be defined in dragstart event, so we
// store it in a global variable for reference
if (event.type === "dragstart") {
REQUESTED_EFFECT_ALLOWED = data.effectAllowed;
REQUESTED_DROP_EFFECT = data.dropEffect;
}
// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
// data.tree.info(
// "applyDropEffectCallback(" +
// event.type +
// "): dataTransfer.dropEffect changed from " +
// REQUESTED_DROP_EFFECT +
// " -> " +
// dataTransfer.dropEffect
// );
// }
dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED;
dataTransfer.dropEffect = REQUESTED_DROP_EFFECT;
// tree.debug(
// "applyDropEffectCallback(" +
// event.type +
// "): set " +
// dataTransfer.dropEffect +
// "/" +
// dataTransfer.effectAllowed
// );
// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
// data.tree.warn(
// "applyDropEffectCallback(" +
// event.type +
// "): could not set dataTransfer.dropEffect to " +
// REQUESTED_DROP_EFFECT +
// ": got " +
// dataTransfer.dropEffect
// );
// }
return REQUESTED_DROP_EFFECT;
}
/* Handle dragover event (fired every x ms) on valid drop targets.
*
* - Auto-scroll when cursor is in border regions
* - Apply restrictioan like 'preventVoidMoves'
* - Calculate hit mode
* - Calculate drop effect
* - Trigger dragOver() callback to let user modify hit mode and drop effect
* - Adjust the drop marker accordingly
*
* @returns hitMode
*/
function handleDragOver(event, data) {
// Implement auto-scrolling
if (data.options.dnd5.scroll) {
autoScroll(data.tree, event);
}
// Bail out with previous response if we get an invalid dragover
if (!data.node) {
data.tree.warn("Ignored dragover for non-node"); //, event, data);
return LAST_HIT_MODE;
}
var markerOffsetX,
nodeOfs,
pos,
relPosY,
hitMode = null,
tree = data.tree,
options = tree.options,
dndOpts = options.dnd5,
targetNode = data.node,
sourceNode = data.otherNode,
markerAt = "center",
$target = $(targetNode.span),
$targetTitle = $target.find("span.fancytree-title");
if (DRAG_ENTER_RESPONSE === false) {
tree.debug("Ignored dragover, since dragenter returned false.");
return false;
} else if (typeof DRAG_ENTER_RESPONSE === "string") {
$.error("assert failed: dragenter returned string");
}
// Calculate hitMode from relative cursor position.
nodeOfs = $target.offset();
relPosY = (event.pageY - nodeOfs.top) / $target.height();
if (event.pageY === undefined) {
tree.warn("event.pageY is undefined: see issue #1013.");
}
if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) {
hitMode = "after";
} else if (
!DRAG_ENTER_RESPONSE.over &&
DRAG_ENTER_RESPONSE.after &&
relPosY > 0.5
) {
hitMode = "after";
} else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) {
hitMode = "before";
} else if (
!DRAG_ENTER_RESPONSE.over &&
DRAG_ENTER_RESPONSE.before &&
relPosY <= 0.5
) {
hitMode = "before";
} else if (DRAG_ENTER_RESPONSE.over) {
hitMode = "over";
}
// Prevent no-ops like 'before source node'
// TODO: these are no-ops when moving nodes, but not in copy mode
if (dndOpts.preventVoidMoves && data.dropEffect === "move") {
if (targetNode === sourceNode) {
targetNode.debug("Drop over source node prevented.");
hitMode = null;
} else if (
hitMode === "before" &&
sourceNode &&
targetNode === sourceNode.getNextSibling()
) {
targetNode.debug("Drop after source node prevented.");
hitMode = null;
} else if (
hitMode === "after" &&
sourceNode &&
targetNode === sourceNode.getPrevSibling()
) {
targetNode.debug("Drop before source node prevented.");
hitMode = null;
} else if (
hitMode === "over" &&
sourceNode &&
sourceNode.parent === targetNode &&
sourceNode.isLastSibling()
) {
targetNode.debug("Drop last child over own parent prevented.");
hitMode = null;
}
}
// Let callback modify the calculated hitMode
data.hitMode = hitMode;
if (hitMode && dndOpts.dragOver) {
prepareDropEffectCallback(event, data);
dndOpts.dragOver(targetNode, data);
var allowDrop = !!hitMode;
applyDropEffectCallback(event, data, allowDrop);
hitMode = data.hitMode;
}
LAST_HIT_MODE = hitMode;
//
if (hitMode === "after" || hitMode === "before" || hitMode === "over") {
markerOffsetX = dndOpts.dropMarkerOffsetX || 0;
switch (hitMode) {
case "before":
markerAt = "top";
markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
break;
case "after":
markerAt = "bottom";
markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
break;
}
pos = {
my: "left" + offsetString(markerOffsetX) + " center",
at: "left " + markerAt,
of: $targetTitle,
};
if (options.rtl) {
pos.my = "right" + offsetString(-markerOffsetX) + " center";
pos.at = "right " + markerAt;
// console.log("rtl", pos);
}
$dropMarker
.toggleClass(classDropAfter, hitMode === "after")
.toggleClass(classDropOver, hitMode === "over")
.toggleClass(classDropBefore, hitMode === "before")
.show()
.position(FT.fixPositionOptions(pos));
} else {
$dropMarker.hide();
// console.log("hide dropmarker")
}
$(targetNode.span)
.toggleClass(
classDropTarget,
hitMode === "after" ||
hitMode === "before" ||
hitMode === "over"
)
.toggleClass(classDropAfter, hitMode === "after")
.toggleClass(classDropBefore, hitMode === "before")
.toggleClass(classDropAccept, hitMode === "over")
.toggleClass(classDropReject, hitMode === false);
return hitMode;
}
/*
* Handle dragstart drag dragend events on the container
*/
function onDragEvent(event) {
var json,
tree = this,
dndOpts = tree.options.dnd5,
node = FT.getNode(event),
dataTransfer =
event.dataTransfer || event.originalEvent.dataTransfer,
data = {
tree: tree,
node: node,
options: tree.options,
originalEvent: event.originalEvent,
widget: tree.widget,
dataTransfer: dataTransfer,
useDefaultImage: true,
dropEffect: undefined,
dropEffectSuggested: undefined,
effectAllowed: undefined, // set by dragstart
files: undefined, // only for drop events
isCancelled: undefined, // set by dragend
isMove: undefined,
};
switch (event.type) {
case "dragstart":
if (!node) {
tree.info("Ignored dragstart on a non-node.");
return false;
}
// Store current source node in different formats
SOURCE_NODE = node;
// Also optionally store selected nodes
if (dndOpts.multiSource === false) {
SOURCE_NODE_LIST = [node];
} else if (dndOpts.multiSource === true) {
if (node.isSelected()) {
SOURCE_NODE_LIST = tree.getSelectedNodes();
} else {
SOURCE_NODE_LIST = [node];
}
} else {
SOURCE_NODE_LIST = dndOpts.multiSource(node, data);
}
// Cache as array of jQuery objects for faster access:
$sourceList = $(
$.map(SOURCE_NODE_LIST, function (n) {
return n.span;
})
);
// Set visual feedback
$sourceList.addClass(classDragSource);
// Set payload
// Note:
// Transfer data is only accessible on dragstart and drop!
// For all other events the formats and kinds in the drag
// data store list of items representing dragged data can be
// enumerated, but the data itself is unavailable and no new
// data can be added.
var nodeData = node.toDict(true, dndOpts.sourceCopyHook);
nodeData.treeId = node.tree._id;
json = JSON.stringify(nodeData);
try {
dataTransfer.setData(nodeMimeType, json);
dataTransfer.setData("text/html", $(node.span).html());
dataTransfer.setData("text/plain", node.title);
} catch (ex) {
// IE only accepts 'text' type
tree.warn(
"Could not set data (IE only accepts 'text') - " + ex
);
}
// We always need to set the 'text' type if we want to drag
// Because IE 11 only accepts this single type.
// If we pass JSON here, IE can can access all node properties,
// even when the source lives in another window. (D'n'd inside
// the same window will always work.)
// The drawback is, that in this case ALL browsers will see
// the JSON representation as 'text', so dragging
// to a text field will insert the JSON string instead of
// the node title.
if (dndOpts.setTextTypeJson) {
dataTransfer.setData("text", json);
} else {
dataTransfer.setData("text", node.title);
}
// Set the allowed drag modes (combinations of move, copy, and link)
// (effectAllowed can only be set in the dragstart event.)
// This can be overridden in the dragStart() callback
prepareDropEffectCallback(event, data);
// Let user cancel or modify above settings
// Realize potential changes by previous callback
if (dndOpts.dragStart(node, data) === false) {
// Cancel dragging
// dataTransfer.dropEffect = "none";
_clearGlobals();
return false;
}
applyDropEffectCallback(event, data);
// Unless user set `data.useDefaultImage` to false in dragStart,
// generata a default drag image now:
$extraHelper = null;
if (data.useDefaultImage) {
// Set the title as drag image (otherwise it would contain the expander)
$dragImage = $(node.span).find(".fancytree-title");
if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) {
// Add a counter badge to node title if dragging more than one node.
// We want this, because the element that is used as drag image
// must be *visible* in the DOM, so we cannot create some hidden
// custom markup.
// See https://kryogenix.org/code/browser/custom-drag-image.html
// Also, since IE 11 and Edge don't support setDragImage() alltogether,
// it gives som feedback to the user.
// The badge will be removed later on drag end.
$extraHelper = $(
"<span class='fancytree-childcounter'/>"
)
.text("+" + (SOURCE_NODE_LIST.length - 1))
.appendTo($dragImage);
}
if (dataTransfer.setDragImage) {
// IE 11 and Edge do not support this
dataTransfer.setDragImage($dragImage[0], -10, -10);
}
}
return true;
case "drag":
// Called every few milliseconds (no matter if the
// cursor is over a valid drop target)
// data.tree.info("drag", SOURCE_NODE)
prepareDropEffectCallback(event, data);
dndOpts.dragDrag(node, data);
applyDropEffectCallback(event, data);
$sourceList.toggleClass(classDragRemove, data.isMove);
break;
case "dragend":
// Called at the end of a d'n'd process (after drop)
// Note caveat: If drop removed the dragged source element,
// we may not get this event, since the target does not exist
// anymore
prepareDropEffectCallback(event, data);
_clearGlobals();
data.isCancelled = !LAST_HIT_MODE;
dndOpts.dragEnd(node, data, !LAST_HIT_MODE);
// applyDropEffectCallback(event, data);
break;
}
}
/*
* Handle dragenter dragover dragleave drop events on the container
*/
function onDropEvent(event) {
var json,
allowAutoExpand,
nodeData,
isSourceFtNode,
r,
res,
tree = this,
dndOpts = tree.options.dnd5,
allowDrop = null,
node = FT.getNode(event),
dataTransfer =
event.dataTransfer || event.originalEvent.dataTransfer,
data = {
tree: tree,
node: node,
options: tree.options,
originalEvent: event.originalEvent,
widget: tree.widget,
hitMode: DRAG_ENTER_RESPONSE,
dataTransfer: dataTransfer,
otherNode: SOURCE_NODE || null,
otherNodeList: SOURCE_NODE_LIST || null,
otherNodeData: null, // set by drop event
useDefaultImage: true,
dropEffect: undefined,
dropEffectSuggested: undefined,
effectAllowed: undefined, // set by dragstart
files: null, // list of File objects (may be [])
isCancelled: undefined, // set by drop event
isMove: undefined,
};
// data.isMove = dropEffect === "move";
switch (event.type) {
case "dragenter":
// The dragenter event is fired when a dragged element or
// text selection enters a valid drop target.
DRAG_OVER_STAMP = null;
if (!node) {
// Sometimes we get dragenter for the container element
tree.debug(
"Ignore non-node " +
event.type +
": " +
event.target.tagName +
"." +
event.target.className
);
DRAG_ENTER_RESPONSE = false;
break;
}
$(node.span)
.addClass(classDropOver)
.removeClass(classDropAccept + " " + classDropReject);
// Data is only readable in the dragstart and drop event,
// but we can check for the type:
isSourceFtNode =
$.inArray(nodeMimeType, dataTransfer.types) >= 0;
if (dndOpts.preventNonNodes && !isSourceFtNode) {
node.debug("Reject dropping a non-node.");
DRAG_ENTER_RESPONSE = false;
break;
} else if (
dndOpts.preventForeignNodes &&
(!SOURCE_NODE || SOURCE_NODE.tree !== node.tree)
) {
node.debug("Reject dropping a foreign node.");
DRAG_ENTER_RESPONSE = false;
break;
} else if (
dndOpts.preventSameParent &&
data.otherNode &&
data.otherNode.tree === node.tree &&
node.parent === data.otherNode.parent
) {
node.debug("Reject dropping as sibling (same parent).");
DRAG_ENTER_RESPONSE = false;
break;
} else if (
dndOpts.preventRecursion &&
data.otherNode &&
data.otherNode.tree === node.tree &&
node.isDescendantOf(data.otherNode)
) {
node.debug("Reject dropping below own ancestor.");
DRAG_ENTER_RESPONSE = false;
break;
} else if (dndOpts.preventLazyParents && !node.isLoaded()) {
node.warn("Drop over unloaded target node prevented.");
DRAG_ENTER_RESPONSE = false;
break;
}
$dropMarker.show();
// Call dragEnter() to figure out if (and where) dropping is allowed
prepareDropEffectCallback(event, data);
r = dndOpts.dragEnter(node, data);
res = normalizeDragEnterResponse(r);
// alert("res:" + JSON.stringify(res))
DRAG_ENTER_RESPONSE = res;
allowDrop = res && (res.over || res.before || res.after);
applyDropEffectCallback(event, data, allowDrop);
break;
case "dragover":
if (!node) {
tree.debug(
"Ignore non-node " +
event.type +
": " +
event.target.tagName +
"." +
event.target.className
);
break;
}
// The dragover event is fired when an element or text
// selection is being dragged over a valid drop target
// (every few hundred milliseconds).
// tree.debug(
// event.type +
// ": dropEffect: " +
// dataTransfer.dropEffect
// );
prepareDropEffectCallback(event, data);
LAST_HIT_MODE = handleDragOver(event, data);
// The flag controls the preventDefault() below:
allowDrop = !!LAST_HIT_MODE;
allowAutoExpand =
LAST_HIT_MODE === "over" || LAST_HIT_MODE === false;
if (
allowAutoExpand &&
!node.expanded &&
node.hasChildren() !== false
) {
if (!DRAG_OVER_STAMP) {
DRAG_OVER_STAMP = Date.now();
} else if (
dndOpts.autoExpandMS &&
Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS &&
!node.isLoading() &&
(!dndOpts.dragExpand ||
dndOpts.dragExpand(node, data) !== false)
) {
node.setExpanded();
}
} else {
DRAG_OVER_STAMP = null;
}
break;
case "dragleave":
// NOTE: dragleave is fired AFTER the dragenter event of the
// FOLLOWING element.
if (!node) {
tree.debug(
"Ignore non-node " +
event.type +
": " +
event.target.tagName +
"." +
event.target.className
);
break;
}
if (!$(node.span).hasClass(classDropOver)) {
node.debug("Ignore dragleave (multi).");
break;
}
$(node.span).removeClass(
classDropOver +
" " +
classDropAccept +
" " +
classDropReject
);
node.scheduleAction("cancel");
dndOpts.dragLeave(node, data);
$dropMarker.hide();
break;
case "drop":
// Data is only readable in the (dragstart and) drop event:
if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) {
nodeData = dataTransfer.getData(nodeMimeType);
tree.info(
event.type +
": getData('application/x-fancytree-node'): '" +
nodeData +
"'"
);
}
if (!nodeData) {
// 1. Source is not a Fancytree node, or
// 2. If the FT mime type was set, but returns '', this
// is probably IE 11 (which only supports 'text')
nodeData = dataTransfer.getData("text");
tree.info(
event.type + ": getData('text'): '" + nodeData + "'"
);
}
if (nodeData) {
try {
// 'text' type may contain JSON if IE is involved
// and setTextTypeJson option was set
json = JSON.parse(nodeData);
if (json.title !== undefined) {
data.otherNodeData = json;
}
} catch (ex) {
// assume 'text' type contains plain text, so `otherNodeData`
// should not be set
}
}
tree.debug(
event.type +
": nodeData: '" +
nodeData +
"', otherNodeData: ",
data.otherNodeData
);
$(node.span).removeClass(
classDropOver +
" " +
classDropAccept +
" " +
classDropReject
);
// Let user implement the actual drop operation
data.hitMode = LAST_HIT_MODE;
prepareDropEffectCallback(event, data, !LAST_HIT_MODE);
data.isCancelled = !LAST_HIT_MODE;
var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span,
orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree;
dndOpts.dragDrop(node, data);
// applyDropEffectCallback(event, data);
// Prevent browser's default drop handling, i.e. open as link, ...
event.preventDefault();
if (orgSourceElem && !document.body.contains(orgSourceElem)) {
// The drop handler removed the original drag source from
// the DOM, so the dragend event will probaly not fire.
if (orgSourceTree === tree) {
tree.debug(
"Drop handler removed source element: generating dragEnd."
);
dndOpts.dragEnd(SOURCE_NODE, data);
} else {
tree.warn(
"Drop handler removed source element: dragend event may be lost."
);
}
}
_clearGlobals();
break;
}
// Dnd API madness: we must PREVENT default handling to enable dropping
if (allowDrop) {
event.preventDefault();
return false;
}
}
/** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject.
*
* @returns {FancytreeNode[]} List of nodes (empty if no drag operation)
* @example
* $.ui.fancytree.getDragNodeList();
*
* @alias Fancytree_Static#getDragNodeList
* @requires jquery.fancytree.dnd5.js
* @since 2.31
*/
$.ui.fancytree.getDragNodeList = function () {
return SOURCE_NODE_LIST || [];
};
/** [ext-dnd5] Return the FancytreeNode that is currently being dragged.
*
* If multiple nodes are dragged, only the first is returned.
*
* @returns {FancytreeNode | null} dragged nodes or null if no drag operation
* @example
* $.ui.fancytree.getDragNode();
*
* @alias Fancytree_Static#getDragNode
* @requires jquery.fancytree.dnd5.js
* @since 2.31
*/
$.ui.fancytree.getDragNode = function () {
return SOURCE_NODE;
};
/******************************************************************************
*
*/
$.ui.fancytree.registerExtension({
name: "dnd5",
version: "@VERSION",
// Default options for this extension.
options: {
autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after"
dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
// #1021 `document.body` is not available yet
dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root)
multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed
effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)
// dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string.
dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver).
preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees
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
preventSameParent: false, // Prevent dropping nodes under same direct parent
preventVoidMoves: true, // Prevent dropping 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 dnd drag
dragDrag: $.noop, // Callback(sourceNode, data)
dragEnd: $.noop, // Callback(sourceNode, data)
// Events (drop support)
dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop
dragOver: $.noop, // Callback(targetNode, data)
dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand
dragDrop: $.noop, // Callback(targetNode, data)
dragLeave: $.noop, // Callback(targetNode, data)
},
treeInit: function (ctx) {
var $temp,
tree = ctx.tree,
opts = ctx.options,
glyph = opts.glyph || null,
dndOpts = opts.dnd5;
if ($.inArray("dnd", opts.extensions) >= 0) {
$.error("Extensions 'dnd' and 'dnd5' are mutually exclusive.");
}
if (dndOpts.dragStop) {
$.error(
"dragStop is not used by ext-dnd5. Use dragEnd instead."
);
}
if (dndOpts.preventRecursiveMoves != null) {
$.error(
"preventRecursiveMoves was renamed to preventRecursion."
);
}
// Implement `opts.createNode` event to add the 'draggable' attribute
// #680: this must happen before calling super.treeInit()
if (dndOpts.dragStart) {
FT.overrideMethod(
ctx.options,
"createNode",
function (event, data) {
// Default processing if any
this._super.apply(this, arguments);
if (data.node.span) {
data.node.span.draggable = true;
} else {
data.node.warn(
"Cannot add `draggable`: no span tag"
);
}
}
);
}
this._superApply(arguments);
this.$container.addClass("fancytree-ext-dnd5");
// Store the current scroll parent, which may be the tree
// container, any enclosing div, or the document.
// #761: scrollParent() always needs a container child
$temp = $("<span>").appendTo(this.$container);
this.$scrollParent = $temp.scrollParent();
$temp.remove();
$dropMarker = $("#fancytree-drop-marker");
if (!$dropMarker.length) {
$dropMarker = $("<div id='fancytree-drop-marker'></div>")
.hide()
.css({
"z-index": 1000,
// Drop marker should not steal dragenter/dragover events:
"pointer-events": "none",
})
.prependTo(dndOpts.dropMarkerParent);
if (glyph) {
FT.setSpanIcon(
$dropMarker[0],
glyph.map._addClass,
glyph.map.dropMarker
);
}
}
$dropMarker.toggleClass("fancytree-rtl", !!opts.rtl);
// Enable drag support if dragStart() is specified:
if (dndOpts.dragStart) {
// Bind drag event handlers
tree.$container.on(
"dragstart drag dragend",
onDragEvent.bind(tree)
);
}
// Enable drop support if dragEnter() is specified:
if (dndOpts.dragEnter) {
// Bind drop event handlers
tree.$container.on(
"dragenter dragover dragleave drop",
onDropEvent.bind(tree)
);
}
},
});
// Value returned by `require('jquery.fancytree..')`
return $.ui.fancytree;
}); // End of closure