/*! * 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.0 * @date 2021-02-09T20:03:49Z */ /* #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 ($.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 = $( "" ) .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.0", // 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 = $("").appendTo(this.$container); this.$scrollParent = $temp.scrollParent(); $temp.remove(); $dropMarker = $("#fancytree-drop-marker"); if (!$dropMarker.length) { $dropMarker = $("
") .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