1158 lines
33 KiB
JavaScript
1158 lines
33 KiB
JavaScript
/*!
|
|
* jquery.fancytree.dnd5.js
|
|
*
|
|
* Drag-and-drop support (native HTML5).
|
|
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
|
*
|
|
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
|
*
|
|
* Released under the MIT license
|
|
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
|
*
|
|
* @version 2.38.2
|
|
* @date 2022-06-30T18:24:06Z
|
|
*/
|
|
|
|
/*
|
|
#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: "2.38.2",
|
|
// 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
|