mar10/fancytree

View on GitHub
src/jquery.fancytree.dnd5.js

Summary

Maintainability
F
6 days
Test Coverage
/*!
 * 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