/*! * jquery.fancytree.dnd.js * * Drag-and-drop support (jQuery UI draggable/droppable). * (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 */ (function(factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define([ "jquery", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable", "./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 didRegisterDnd = false, classDropAccept = "fancytree-drop-accept", classDropAfter = "fancytree-drop-after", classDropBefore = "fancytree-drop-before", classDropOver = "fancytree-drop-over", classDropReject = "fancytree-drop-reject", classDropTarget = "fancytree-drop-target"; /* 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; } //--- Extend ui.draggable event handling -------------------------------------- function _registerDnd() { if (didRegisterDnd) { return; } // Register proxy-functions for draggable.start/drag/stop $.ui.plugin.add("draggable", "connectToFancytree", { start: function(event, ui) { // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10 var draggable = $(this).data("ui-draggable") || $(this).data("draggable"), sourceNode = ui.helper.data("ftSourceNode") || null; if (sourceNode) { // Adjust helper offset, so cursor is slightly outside top/left corner draggable.offset.click.top = -2; draggable.offset.click.left = +16; // Trigger dragStart event // TODO: when called as connectTo..., the return value is ignored(?) return sourceNode.tree.ext.dnd._onDragEvent( "start", sourceNode, null, event, ui, draggable ); } }, drag: function(event, ui) { var ctx, isHelper, logObject, // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10 draggable = $(this).data("ui-draggable") || $(this).data("draggable"), sourceNode = ui.helper.data("ftSourceNode") || null, prevTargetNode = ui.helper.data("ftTargetNode") || null, targetNode = $.ui.fancytree.getNode(event.target), dndOpts = sourceNode && sourceNode.tree.options.dnd; // logObject = sourceNode || prevTargetNode || $.ui.fancytree; // logObject.debug("Drag event:", event, event.shiftKey); if (event.target && !targetNode) { // We got a drag event, but the targetNode could not be found // at the event location. This may happen, // 1. if the mouse jumped over the drag helper, // 2. or if a non-fancytree element is dragged // We ignore it: isHelper = $(event.target).closest( "div.fancytree-drag-helper,#fancytree-drop-marker" ).length > 0; if (isHelper) { logObject = sourceNode || prevTargetNode || $.ui.fancytree; logObject.debug("Drag event over helper: ignored."); return; } } ui.helper.data("ftTargetNode", targetNode); if (dndOpts && dndOpts.updateHelper) { ctx = sourceNode.tree._makeHookContext(sourceNode, event, { otherNode: targetNode, ui: ui, draggable: draggable, dropMarker: $("#fancytree-drop-marker"), }); dndOpts.updateHelper.call(sourceNode.tree, sourceNode, ctx); } // Leaving a tree node if (prevTargetNode && prevTargetNode !== targetNode) { prevTargetNode.tree.ext.dnd._onDragEvent( "leave", prevTargetNode, sourceNode, event, ui, draggable ); } if (targetNode) { if (!targetNode.tree.options.dnd.dragDrop) { // not enabled as drop target } else if (targetNode === prevTargetNode) { // Moving over same node targetNode.tree.ext.dnd._onDragEvent( "over", targetNode, sourceNode, event, ui, draggable ); } else { // Entering this node first time targetNode.tree.ext.dnd._onDragEvent( "enter", targetNode, sourceNode, event, ui, draggable ); targetNode.tree.ext.dnd._onDragEvent( "over", targetNode, sourceNode, event, ui, draggable ); } } // else go ahead with standard event handling }, stop: function(event, ui) { var logObject, // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10: draggable = $(this).data("ui-draggable") || $(this).data("draggable"), sourceNode = ui.helper.data("ftSourceNode") || null, targetNode = ui.helper.data("ftTargetNode") || null, dropped = event.type === "mouseup" && event.which === 1; if (!dropped) { logObject = sourceNode || targetNode || $.ui.fancytree; logObject.debug("Drag was cancelled"); } if (targetNode) { if (dropped) { targetNode.tree.ext.dnd._onDragEvent( "drop", targetNode, sourceNode, event, ui, draggable ); } targetNode.tree.ext.dnd._onDragEvent( "leave", targetNode, sourceNode, event, ui, draggable ); } if (sourceNode) { sourceNode.tree.ext.dnd._onDragEvent( "stop", sourceNode, null, event, ui, draggable ); } }, }); didRegisterDnd = true; } /****************************************************************************** * Drag and drop support */ function _initDragAndDrop(tree) { var dnd = tree.options.dnd || null, glyph = tree.options.glyph || null; // Register 'connectToFancytree' option with ui.draggable if (dnd) { _registerDnd(); } // Attach ui.draggable to this Fancytree instance if (dnd && dnd.dragStart) { tree.widget.element.draggable( $.extend( { addClasses: false, // DT issue 244: helper should be child of scrollParent: appendTo: tree.$container, // appendTo: "body", containment: false, // containment: "parent", delay: 0, distance: 4, revert: false, scroll: true, // to disable, also set css 'position: inherit' on ul.fancytree-container scrollSpeed: 7, scrollSensitivity: 10, // Delegate draggable.start, drag, and stop events to our handler connectToFancytree: true, // Let source tree create the helper element helper: function(event) { var $helper, $nodeTag, opts, sourceNode = $.ui.fancytree.getNode( event.target ); if (!sourceNode) { // #405, DT issue 211: might happen, if dragging a table *header* return "
ERROR?: helper requested but sourceNode not found
"; } opts = sourceNode.tree.options.dnd; $nodeTag = $(sourceNode.span); // Only event and node argument is available $helper = $( "
" ) .css({ zIndex: 3, position: "relative" }) // so it appears above ext-wide selection bar .append( $nodeTag .find("span.fancytree-title") .clone() ); // Attach node reference to helper object $helper.data("ftSourceNode", sourceNode); // Support glyph symbols instead of icons if (glyph) { $helper .find(".fancytree-drag-helper-img") .addClass( glyph.map._addClass + " " + glyph.map.dragHelper ); } // Allow to modify the helper, e.g. to add multi-node-drag feedback if (opts.initHelper) { opts.initHelper.call( sourceNode.tree, sourceNode, { node: sourceNode, tree: sourceNode.tree, originalEvent: event, ui: { helper: $helper }, } ); } // We return an unconnected element, so `draggable` will add this // to the parent specified as `appendTo` option return $helper; }, start: function(event, ui) { var sourceNode = ui.helper.data("ftSourceNode"); return !!sourceNode; // Abort dragging if no node could be found }, }, tree.options.dnd.draggable ) ); } // Attach ui.droppable to this Fancytree instance if (dnd && dnd.dragDrop) { tree.widget.element.droppable( $.extend( { addClasses: false, tolerance: "intersect", greedy: false, /* activate: function(event, ui) { tree.debug("droppable - activate", event, ui, this); }, create: function(event, ui) { tree.debug("droppable - create", event, ui); }, deactivate: function(event, ui) { tree.debug("droppable - deactivate", event, ui); }, drop: function(event, ui) { tree.debug("droppable - drop", event, ui); }, out: function(event, ui) { tree.debug("droppable - out", event, ui); }, over: function(event, ui) { tree.debug("droppable - over", event, ui); } */ }, tree.options.dnd.droppable ) ); } } /****************************************************************************** * */ $.ui.fancytree.registerExtension({ name: "dnd", version: "2.38.0", // Default options for this extension. options: { // Make tree nodes accept draggables autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering. draggable: null, // Additional options passed to jQuery draggable droppable: null, // Additional options passed to jQuery droppable focusOnClick: false, // Focus, although draggable cancels mousedown event (#270) preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. preventRecursiveMoves: true, // Prevent dropping nodes on own descendants smartRevert: true, // set draggable.revert = true if drop was rejected dropMarkerOffsetX: -24, // absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) dropMarkerInsertOffsetX: -16, // additional offset for drop-marker with hitMode = "before"/"after" // Events (drag support) dragStart: null, // Callback(sourceNode, data), return true, to enable dnd dragStop: null, // Callback(sourceNode, data) initHelper: null, // Callback(sourceNode, data) updateHelper: null, // Callback(sourceNode, data) // Events (drop support) dragEnter: null, // Callback(targetNode, data) dragOver: null, // Callback(targetNode, data) dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand dragDrop: null, // Callback(targetNode, data) dragLeave: null, // Callback(targetNode, data) }, treeInit: function(ctx) { var tree = ctx.tree; this._superApply(arguments); // issue #270: draggable eats mousedown events if (tree.options.dnd.dragStart) { tree.$container.on("mousedown", function(event) { // if( !tree.hasFocus() && ctx.options.dnd.focusOnClick ) { if (ctx.options.dnd.focusOnClick) { // #270 var node = $.ui.fancytree.getNode(event); if (node) { node.debug( "Re-enable focus that was prevented by jQuery UI draggable." ); // node.setFocus(); // $(node.span).closest(":tabbable").focus(); // $(event.target).trigger("focus"); // $(event.target).closest(":tabbable").trigger("focus"); } setTimeout(function() { // #300 $(event.target) .closest(":tabbable") .focus(); }, 10); } }); } _initDragAndDrop(tree); }, /* Display drop marker according to hitMode ('after', 'before', 'over'). */ _setDndStatus: function( sourceNode, targetNode, helper, hitMode, accept ) { var markerOffsetX, pos, markerAt = "center", instData = this._local, dndOpt = this.options.dnd, glyphOpt = this.options.glyph, $source = sourceNode ? $(sourceNode.span) : null, $target = $(targetNode.span), $targetTitle = $target.find("span.fancytree-title"); if (!instData.$dropMarker) { instData.$dropMarker = $( "
" ) .hide() .css({ "z-index": 1000 }) .prependTo($(this.$div).parent()); // .prependTo("body"); if (glyphOpt) { instData.$dropMarker.addClass( glyphOpt.map._addClass + " " + glyphOpt.map.dropMarker ); } } if ( hitMode === "after" || hitMode === "before" || hitMode === "over" ) { markerOffsetX = dndOpt.dropMarkerOffsetX || 0; switch (hitMode) { case "before": markerAt = "top"; markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0; break; case "after": markerAt = "bottom"; markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0; break; } pos = { my: "left" + offsetString(markerOffsetX) + " center", at: "left " + markerAt, of: $targetTitle, }; if (this.options.rtl) { pos.my = "right" + offsetString(-markerOffsetX) + " center"; pos.at = "right " + markerAt; } instData.$dropMarker .toggleClass(classDropAfter, hitMode === "after") .toggleClass(classDropOver, hitMode === "over") .toggleClass(classDropBefore, hitMode === "before") .toggleClass("fancytree-rtl", !!this.options.rtl) .show() .position($.ui.fancytree.fixPositionOptions(pos)); } else { instData.$dropMarker.hide(); } if ($source) { $source .toggleClass(classDropAccept, accept === true) .toggleClass(classDropReject, accept === false); } $target .toggleClass( classDropTarget, hitMode === "after" || hitMode === "before" || hitMode === "over" ) .toggleClass(classDropAfter, hitMode === "after") .toggleClass(classDropBefore, hitMode === "before") .toggleClass(classDropAccept, accept === true) .toggleClass(classDropReject, accept === false); helper .toggleClass(classDropAccept, accept === true) .toggleClass(classDropReject, accept === false); }, /* * Handles drag'n'drop functionality. * * A standard jQuery drag-and-drop process may generate these calls: * * start: * _onDragEvent("start", sourceNode, null, event, ui, draggable); * drag: * _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable); * _onDragEvent("over", targetNode, sourceNode, event, ui, draggable); * _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable); * stop: * _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable); * _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable); * _onDragEvent("stop", sourceNode, null, event, ui, draggable); */ _onDragEvent: function( eventName, node, otherNode, event, ui, draggable ) { // if(eventName !== "over"){ // this.debug("tree.ext.dnd._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this); // } var accept, nodeOfs, parentRect, rect, relPos, relPos2, enterResponse, hitMode, r, opts = this.options, dnd = opts.dnd, ctx = this._makeHookContext(node, event, { otherNode: otherNode, ui: ui, draggable: draggable, }), res = null, self = this, $nodeTag = $(node.span); if (dnd.smartRevert) { draggable.options.revert = "invalid"; } switch (eventName) { case "start": if (node.isStatusNode()) { res = false; } else if (dnd.dragStart) { res = dnd.dragStart(node, ctx); } if (res === false) { this.debug("tree.dragStart() cancelled"); //draggable._clear(); // NOTE: the return value seems to be ignored (drag is not cancelled, when false is returned) // TODO: call this._cancelDrag()? ui.helper.trigger("mouseup").hide(); } else { if (dnd.smartRevert) { // #567, #593: fix revert position // rect = node.li.getBoundingClientRect(); rect = node[ ctx.tree.nodeContainerAttrName ].getBoundingClientRect(); parentRect = $( draggable.options.appendTo )[0].getBoundingClientRect(); draggable.originalPosition.left = Math.max( 0, rect.left - parentRect.left ); draggable.originalPosition.top = Math.max( 0, rect.top - parentRect.top ); } $nodeTag.addClass("fancytree-drag-source"); // Register global handlers to allow cancel $(document).on( "keydown.fancytree-dnd,mousedown.fancytree-dnd", function(event) { // node.tree.debug("dnd global event", event.type, event.which); if ( event.type === "keydown" && event.which === $.ui.keyCode.ESCAPE ) { self.ext.dnd._cancelDrag(); } else if (event.type === "mousedown") { self.ext.dnd._cancelDrag(); } } ); } break; case "enter": if ( dnd.preventRecursiveMoves && node.isDescendantOf(otherNode) ) { r = false; } else { r = dnd.dragEnter ? dnd.dragEnter(node, ctx) : null; } if (!r) { // convert null, undefined, false to false res = false; } else if ($.isArray(r)) { // TODO: also accept passing an object of this format directly res = { over: $.inArray("over", r) >= 0, before: $.inArray("before", r) >= 0, after: $.inArray("after", r) >= 0, }; } else { res = { over: r === true || r === "over", before: r === true || r === "before", after: r === true || r === "after", }; } ui.helper.data("enterResponse", res); // this.debug("helper.enterResponse: %o", res); break; case "over": enterResponse = ui.helper.data("enterResponse"); hitMode = null; if (enterResponse === false) { // Don't call dragOver if onEnter returned false. // break; } else if (typeof enterResponse === "string") { // Use hitMode from onEnter if provided. hitMode = enterResponse; } else { // Calculate hitMode from relative cursor position. nodeOfs = $nodeTag.offset(); relPos = { x: event.pageX - nodeOfs.left, y: event.pageY - nodeOfs.top, }; relPos2 = { x: relPos.x / $nodeTag.width(), y: relPos.y / $nodeTag.height(), }; if (enterResponse.after && relPos2.y > 0.75) { hitMode = "after"; } else if ( !enterResponse.over && enterResponse.after && relPos2.y > 0.5 ) { hitMode = "after"; } else if (enterResponse.before && relPos2.y <= 0.25) { hitMode = "before"; } else if ( !enterResponse.over && enterResponse.before && relPos2.y <= 0.5 ) { hitMode = "before"; } else if (enterResponse.over) { hitMode = "over"; } // Prevent no-ops like 'before source node' // TODO: these are no-ops when moving nodes, but not in copy mode if (dnd.preventVoidMoves) { if (node === otherNode) { this.debug( " drop over source node prevented" ); hitMode = null; } else if ( hitMode === "before" && otherNode && node === otherNode.getNextSibling() ) { this.debug( " drop after source node prevented" ); hitMode = null; } else if ( hitMode === "after" && otherNode && node === otherNode.getPrevSibling() ) { this.debug( " drop before source node prevented" ); hitMode = null; } else if ( hitMode === "over" && otherNode && otherNode.parent === node && otherNode.isLastSibling() ) { this.debug( " drop last child over own parent prevented" ); hitMode = null; } } // this.debug("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling()); ui.helper.data("hitMode", hitMode); } // Auto-expand node (only when 'over' the node, not 'before', or 'after') if ( hitMode !== "before" && hitMode !== "after" && dnd.autoExpandMS && node.hasChildren() !== false && !node.expanded && (!dnd.dragExpand || dnd.dragExpand(node, ctx) !== false) ) { node.scheduleAction("expand", dnd.autoExpandMS); } if (hitMode && dnd.dragOver) { // TODO: http://code.google.com/p/dynatree/source/detail?r=625 ctx.hitMode = hitMode; res = dnd.dragOver(node, ctx); } accept = res !== false && hitMode !== null; if (dnd.smartRevert) { draggable.options.revert = !accept; } this._local._setDndStatus( otherNode, node, ui.helper, hitMode, accept ); break; case "drop": hitMode = ui.helper.data("hitMode"); if (hitMode && dnd.dragDrop) { ctx.hitMode = hitMode; dnd.dragDrop(node, ctx); } break; case "leave": // Cancel pending expand request node.scheduleAction("cancel"); ui.helper.data("enterResponse", null); ui.helper.data("hitMode", null); this._local._setDndStatus( otherNode, node, ui.helper, "out", undefined ); if (dnd.dragLeave) { dnd.dragLeave(node, ctx); } break; case "stop": $nodeTag.removeClass("fancytree-drag-source"); $(document).off(".fancytree-dnd"); if (dnd.dragStop) { dnd.dragStop(node, ctx); } break; default: $.error("Unsupported drag event: " + eventName); } return res; }, _cancelDrag: function() { var dd = $.ui.ddmanager.current; if (dd) { dd.cancel(); } }, }); // Value returned by `require('jquery.fancytree..')` return $.ui.fancytree; }); // End of closure