1/*!
2 * jquery.fancytree.dnd5.js
3 *
4 * Drag-and-drop support (native HTML5).
5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
6 *
7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
8 *
9 * Released under the MIT license
10 * https://github.com/mar10/fancytree/wiki/LicenseInfo
11 *
12 * @version 2.38.3
13 * @date 2023-02-01T20:52:50Z
14 */
15
16/*
17 #TODO
18	Compatiblity when dragging between *separate* windows:
19
20		   Drag from Chrome   Edge    FF    IE11    Safari
21	  To Chrome      ok       ok      ok    NO      ?
22		 Edge        ok       ok      ok    NO      ?
23		 FF          ok       ok      ok    NO      ?
24		 IE 11       ok       ok      ok    ok      ?
25		 Safari      ?        ?       ?     ?       ok
26
27 */
28
29(function (factory) {
30	if (typeof define === "function" && define.amd) {
31		// AMD. Register as an anonymous module.
32		define(["jquery", "./jquery.fancytree"], factory);
33	} else if (typeof module === "object" && module.exports) {
34		// Node/CommonJS
35		require("./jquery.fancytree");
36		module.exports = factory(require("jquery"));
37	} else {
38		// Browser globals
39		factory(jQuery);
40	}
41})(function ($) {
42	"use strict";
43
44	/******************************************************************************
45	 * Private functions and variables
46	 */
47	var FT = $.ui.fancytree,
48		isMac = /Mac/.test(navigator.platform),
49		classDragSource = "fancytree-drag-source",
50		classDragRemove = "fancytree-drag-remove",
51		classDropAccept = "fancytree-drop-accept",
52		classDropAfter = "fancytree-drop-after",
53		classDropBefore = "fancytree-drop-before",
54		classDropOver = "fancytree-drop-over",
55		classDropReject = "fancytree-drop-reject",
56		classDropTarget = "fancytree-drop-target",
57		nodeMimeType = "application/x-fancytree-node",
58		$dropMarker = null,
59		$dragImage,
60		$extraHelper,
61		SOURCE_NODE = null,
62		SOURCE_NODE_LIST = null,
63		$sourceList = null,
64		DRAG_ENTER_RESPONSE = null,
65		// SESSION_DATA = null, // plain object passed to events as `data`
66		SUGGESTED_DROP_EFFECT = null,
67		REQUESTED_DROP_EFFECT = null,
68		REQUESTED_EFFECT_ALLOWED = null,
69		LAST_HIT_MODE = null,
70		DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode
71
72	/* */
73	function _clearGlobals() {
74		DRAG_ENTER_RESPONSE = null;
75		DRAG_OVER_STAMP = null;
76		REQUESTED_DROP_EFFECT = null;
77		REQUESTED_EFFECT_ALLOWED = null;
78		SUGGESTED_DROP_EFFECT = null;
79		SOURCE_NODE = null;
80		SOURCE_NODE_LIST = null;
81		if ($sourceList) {
82			$sourceList.removeClass(classDragSource + " " + classDragRemove);
83		}
84		$sourceList = null;
85		if ($dropMarker) {
86			$dropMarker.hide();
87		}
88		// Take this badge off of me - I can't use it anymore:
89		if ($extraHelper) {
90			$extraHelper.remove();
91			$extraHelper = null;
92		}
93	}
94
95	/* Convert number to string and prepend +/-; return empty string for 0.*/
96	function offsetString(n) {
97		// eslint-disable-next-line no-nested-ternary
98		return n === 0 ? "" : n > 0 ? "+" + n : "" + n;
99	}
100
101	/* Convert a dragEnter() or dragOver() response to a canonical form.
102	 * Return false or plain object
103	 * @param {string|object|boolean} r
104	 * @return {object|false}
105	 */
106	function normalizeDragEnterResponse(r) {
107		var res;
108
109		if (!r) {
110			return false;
111		}
112		if ($.isPlainObject(r)) {
113			res = {
114				over: !!r.over,
115				before: !!r.before,
116				after: !!r.after,
117			};
118		} else if (Array.isArray(r)) {
119			res = {
120				over: $.inArray("over", r) >= 0,
121				before: $.inArray("before", r) >= 0,
122				after: $.inArray("after", r) >= 0,
123			};
124		} else {
125			res = {
126				over: r === true || r === "over",
127				before: r === true || r === "before",
128				after: r === true || r === "after",
129			};
130		}
131		if (Object.keys(res).length === 0) {
132			return false;
133		}
134		// if( Object.keys(res).length === 1 ) {
135		// 	res.unique = res[0];
136		// }
137		return res;
138	}
139
140	/* Convert a dataTransfer.effectAllowed to a canonical form.
141	 * Return false or plain object
142	 * @param {string|boolean} r
143	 * @return {object|false}
144	 */
145	// function normalizeEffectAllowed(r) {
146	// 	if (!r || r === "none") {
147	// 		return false;
148	// 	}
149	// 	var all = r === "all",
150	// 		res = {
151	// 			copy: all || /copy/i.test(r),
152	// 			link: all || /link/i.test(r),
153	// 			move: all || /move/i.test(r),
154	// 		};
155
156	// 	return res;
157	// }
158
159	/* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
160	function autoScroll(tree, event) {
161		var spOfs,
162			scrollTop,
163			delta,
164			dndOpts = tree.options.dnd5,
165			sp = tree.$scrollParent[0],
166			sensitivity = dndOpts.scrollSensitivity,
167			speed = dndOpts.scrollSpeed,
168			scrolled = 0;
169
170		if (sp !== document && sp.tagName !== "HTML") {
171			spOfs = tree.$scrollParent.offset();
172			scrollTop = sp.scrollTop;
173			if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) {
174				delta =
175					sp.scrollHeight -
176					tree.$scrollParent.innerHeight() -
177					scrollTop;
178				// console.log ("sp.offsetHeight: " + sp.offsetHeight
179				// 	+ ", spOfs.top: " + spOfs.top
180				// 	+ ", scrollTop: " + scrollTop
181				// 	+ ", innerHeight: " + tree.$scrollParent.innerHeight()
182				// 	+ ", scrollHeight: " + sp.scrollHeight
183				// 	+ ", delta: " + delta
184				// 	);
185				if (delta > 0) {
186					sp.scrollTop = scrolled = scrollTop + speed;
187				}
188			} else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) {
189				sp.scrollTop = scrolled = scrollTop - speed;
190			}
191		} else {
192			scrollTop = $(document).scrollTop();
193			if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
194				scrolled = scrollTop - speed;
195				$(document).scrollTop(scrolled);
196			} else if (
197				$(window).height() - (event.pageY - scrollTop) <
198				sensitivity
199			) {
200				scrolled = scrollTop + speed;
201				$(document).scrollTop(scrolled);
202			}
203		}
204		if (scrolled) {
205			tree.debug("autoScroll: " + scrolled + "px");
206		}
207		return scrolled;
208	}
209
210	/* Guess dropEffect from modifier keys.
211	 * Using rules suggested here:
212	 *     https://ux.stackexchange.com/a/83769
213	 * @returns
214	 *     'copy', 'link', 'move', or 'none'
215	 */
216	function evalEffectModifiers(tree, event, effectDefault) {
217		var res = effectDefault;
218
219		if (isMac) {
220			if (event.metaKey && event.altKey) {
221				// Mac: [Control] + [Option]
222				res = "link";
223			} else if (event.ctrlKey) {
224				// Chrome on Mac: [Control]
225				res = "link";
226			} else if (event.metaKey) {
227				// Mac: [Command]
228				res = "move";
229			} else if (event.altKey) {
230				// Mac: [Option]
231				res = "copy";
232			}
233		} else {
234			if (event.ctrlKey) {
235				// Windows: [Ctrl]
236				res = "copy";
237			} else if (event.shiftKey) {
238				// Windows: [Shift]
239				res = "move";
240			} else if (event.altKey) {
241				// Windows: [Alt]
242				res = "link";
243			}
244		}
245		if (res !== SUGGESTED_DROP_EFFECT) {
246			tree.info(
247				"evalEffectModifiers: " +
248					event.type +
249					" - evalEffectModifiers(): " +
250					SUGGESTED_DROP_EFFECT +
251					" -> " +
252					res
253			);
254		}
255		SUGGESTED_DROP_EFFECT = res;
256		// tree.debug("evalEffectModifiers: " + res);
257		return res;
258	}
259	/*
260	 * Check if the previous callback (dragEnter, dragOver, ...) has changed
261	 * the `data` object and apply those settings.
262	 *
263	 * Safari:
264	 *     It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain
265	 *     even if the cursor changes when [Alt] or [Ctrl] are pressed (?)
266	 * Using rules suggested here:
267	 *     https://ux.stackexchange.com/a/83769
268	 * @returns
269	 *     'copy', 'link', 'move', or 'none'
270	 */
271	function prepareDropEffectCallback(event, data) {
272		var tree = data.tree,
273			dataTransfer = data.dataTransfer;
274
275		if (event.type === "dragstart") {
276			data.effectAllowed = tree.options.dnd5.effectAllowed;
277			data.dropEffect = tree.options.dnd5.dropEffectDefault;
278		} else {
279			data.effectAllowed = REQUESTED_EFFECT_ALLOWED;
280			data.dropEffect = REQUESTED_DROP_EFFECT;
281		}
282		data.dropEffectSuggested = evalEffectModifiers(
283			tree,
284			event,
285			tree.options.dnd5.dropEffectDefault
286		);
287		data.isMove = data.dropEffect === "move";
288		data.files = dataTransfer.files || [];
289
290		// if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) {
291		// 	tree.warn(
292		// 		"prepareDropEffectCallback(" +
293		// 			event.type +
294		// 			"): dataTransfer.effectAllowed changed from " +
295		// 			REQUESTED_EFFECT_ALLOWED +
296		// 			" -> " +
297		// 			dataTransfer.effectAllowed
298		// 	);
299		// }
300		// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
301		// 	tree.warn(
302		// 		"prepareDropEffectCallback(" +
303		// 			event.type +
304		// 			"): dataTransfer.dropEffect changed from requested " +
305		// 			REQUESTED_DROP_EFFECT +
306		// 			" to " +
307		// 			dataTransfer.dropEffect
308		// 	);
309		// }
310	}
311
312	function applyDropEffectCallback(event, data, allowDrop) {
313		var tree = data.tree,
314			dataTransfer = data.dataTransfer;
315
316		if (
317			event.type !== "dragstart" &&
318			REQUESTED_EFFECT_ALLOWED !== data.effectAllowed
319		) {
320			tree.warn(
321				"effectAllowed should only be changed in dragstart event: " +
322					event.type +
323					": data.effectAllowed changed from " +
324					REQUESTED_EFFECT_ALLOWED +
325					" -> " +
326					data.effectAllowed
327			);
328		}
329
330		if (allowDrop === false) {
331			tree.info("applyDropEffectCallback: allowDrop === false");
332			data.effectAllowed = "none";
333			data.dropEffect = "none";
334		}
335		// if (REQUESTED_DROP_EFFECT !== data.dropEffect) {
336		// 	tree.debug(
337		// 		"applyDropEffectCallback(" +
338		// 			event.type +
339		// 			"): data.dropEffect changed from previous " +
340		// 			REQUESTED_DROP_EFFECT +
341		// 			" to " +
342		// 			data.dropEffect
343		// 	);
344		// }
345
346		data.isMove = data.dropEffect === "move";
347		// data.isMove = data.dropEffectSuggested === "move";
348
349		// `effectAllowed` must only be defined in dragstart event, so we
350		// store it in a global variable for reference
351		if (event.type === "dragstart") {
352			REQUESTED_EFFECT_ALLOWED = data.effectAllowed;
353			REQUESTED_DROP_EFFECT = data.dropEffect;
354		}
355
356		// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
357		// 	data.tree.info(
358		// 		"applyDropEffectCallback(" +
359		// 			event.type +
360		// 			"): dataTransfer.dropEffect changed from " +
361		// 			REQUESTED_DROP_EFFECT +
362		// 			" -> " +
363		// 			dataTransfer.dropEffect
364		// 	);
365		// }
366		dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED;
367		dataTransfer.dropEffect = REQUESTED_DROP_EFFECT;
368
369		// tree.debug(
370		// 	"applyDropEffectCallback(" +
371		// 		event.type +
372		// 		"): set " +
373		// 		dataTransfer.dropEffect +
374		// 		"/" +
375		// 		dataTransfer.effectAllowed
376		// );
377		// if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
378		// 	data.tree.warn(
379		// 		"applyDropEffectCallback(" +
380		// 			event.type +
381		// 			"): could not set dataTransfer.dropEffect to " +
382		// 			REQUESTED_DROP_EFFECT +
383		// 			": got " +
384		// 			dataTransfer.dropEffect
385		// 	);
386		// }
387		return REQUESTED_DROP_EFFECT;
388	}
389
390	/* Handle dragover event (fired every x ms) on valid drop targets.
391	 *
392	 * - Auto-scroll when cursor is in border regions
393	 * - Apply restrictioan like 'preventVoidMoves'
394	 * - Calculate hit mode
395	 * - Calculate drop effect
396	 * - Trigger dragOver() callback to let user modify hit mode and drop effect
397	 * - Adjust the drop marker accordingly
398	 *
399	 * @returns hitMode
400	 */
401	function handleDragOver(event, data) {
402		// Implement auto-scrolling
403		if (data.options.dnd5.scroll) {
404			autoScroll(data.tree, event);
405		}
406		// Bail out with previous response if we get an invalid dragover
407		if (!data.node) {
408			data.tree.warn("Ignored dragover for non-node"); //, event, data);
409			return LAST_HIT_MODE;
410		}
411
412		var markerOffsetX,
413			nodeOfs,
414			pos,
415			relPosY,
416			hitMode = null,
417			tree = data.tree,
418			options = tree.options,
419			dndOpts = options.dnd5,
420			targetNode = data.node,
421			sourceNode = data.otherNode,
422			markerAt = "center",
423			$target = $(targetNode.span),
424			$targetTitle = $target.find("span.fancytree-title");
425
426		if (DRAG_ENTER_RESPONSE === false) {
427			tree.debug("Ignored dragover, since dragenter returned false.");
428			return false;
429		} else if (typeof DRAG_ENTER_RESPONSE === "string") {
430			$.error("assert failed: dragenter returned string");
431		}
432		// Calculate hitMode from relative cursor position.
433		nodeOfs = $target.offset();
434		relPosY = (event.pageY - nodeOfs.top) / $target.height();
435		if (event.pageY === undefined) {
436			tree.warn("event.pageY is undefined: see issue #1013.");
437		}
438
439		if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) {
440			hitMode = "after";
441		} else if (
442			!DRAG_ENTER_RESPONSE.over &&
443			DRAG_ENTER_RESPONSE.after &&
444			relPosY > 0.5
445		) {
446			hitMode = "after";
447		} else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) {
448			hitMode = "before";
449		} else if (
450			!DRAG_ENTER_RESPONSE.over &&
451			DRAG_ENTER_RESPONSE.before &&
452			relPosY <= 0.5
453		) {
454			hitMode = "before";
455		} else if (DRAG_ENTER_RESPONSE.over) {
456			hitMode = "over";
457		}
458		// Prevent no-ops like 'before source node'
459		// TODO: these are no-ops when moving nodes, but not in copy mode
460		if (dndOpts.preventVoidMoves && data.dropEffect === "move") {
461			if (targetNode === sourceNode) {
462				targetNode.debug("Drop over source node prevented.");
463				hitMode = null;
464			} else if (
465				hitMode === "before" &&
466				sourceNode &&
467				targetNode === sourceNode.getNextSibling()
468			) {
469				targetNode.debug("Drop after source node prevented.");
470				hitMode = null;
471			} else if (
472				hitMode === "after" &&
473				sourceNode &&
474				targetNode === sourceNode.getPrevSibling()
475			) {
476				targetNode.debug("Drop before source node prevented.");
477				hitMode = null;
478			} else if (
479				hitMode === "over" &&
480				sourceNode &&
481				sourceNode.parent === targetNode &&
482				sourceNode.isLastSibling()
483			) {
484				targetNode.debug("Drop last child over own parent prevented.");
485				hitMode = null;
486			}
487		}
488		// Let callback modify the calculated hitMode
489		data.hitMode = hitMode;
490		if (hitMode && dndOpts.dragOver) {
491			prepareDropEffectCallback(event, data);
492			dndOpts.dragOver(targetNode, data);
493			var allowDrop = !!hitMode;
494			applyDropEffectCallback(event, data, allowDrop);
495			hitMode = data.hitMode;
496		}
497		LAST_HIT_MODE = hitMode;
498		//
499		if (hitMode === "after" || hitMode === "before" || hitMode === "over") {
500			markerOffsetX = dndOpts.dropMarkerOffsetX || 0;
501			switch (hitMode) {
502				case "before":
503					markerAt = "top";
504					markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
505					break;
506				case "after":
507					markerAt = "bottom";
508					markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
509					break;
510			}
511
512			pos = {
513				my: "left" + offsetString(markerOffsetX) + " center",
514				at: "left " + markerAt,
515				of: $targetTitle,
516			};
517			if (options.rtl) {
518				pos.my = "right" + offsetString(-markerOffsetX) + " center";
519				pos.at = "right " + markerAt;
520				// console.log("rtl", pos);
521			}
522			$dropMarker
523				.toggleClass(classDropAfter, hitMode === "after")
524				.toggleClass(classDropOver, hitMode === "over")
525				.toggleClass(classDropBefore, hitMode === "before")
526				.show()
527				.position(FT.fixPositionOptions(pos));
528		} else {
529			$dropMarker.hide();
530			// console.log("hide dropmarker")
531		}
532
533		$(targetNode.span)
534			.toggleClass(
535				classDropTarget,
536				hitMode === "after" ||
537					hitMode === "before" ||
538					hitMode === "over"
539			)
540			.toggleClass(classDropAfter, hitMode === "after")
541			.toggleClass(classDropBefore, hitMode === "before")
542			.toggleClass(classDropAccept, hitMode === "over")
543			.toggleClass(classDropReject, hitMode === false);
544
545		return hitMode;
546	}
547
548	/*
549	 * Handle dragstart drag dragend events on the container
550	 */
551	function onDragEvent(event) {
552		var json,
553			tree = this,
554			dndOpts = tree.options.dnd5,
555			node = FT.getNode(event),
556			dataTransfer =
557				event.dataTransfer || event.originalEvent.dataTransfer,
558			data = {
559				tree: tree,
560				node: node,
561				options: tree.options,
562				originalEvent: event.originalEvent,
563				widget: tree.widget,
564				dataTransfer: dataTransfer,
565				useDefaultImage: true,
566				dropEffect: undefined,
567				dropEffectSuggested: undefined,
568				effectAllowed: undefined, // set by dragstart
569				files: undefined, // only for drop events
570				isCancelled: undefined, // set by dragend
571				isMove: undefined,
572			};
573
574		switch (event.type) {
575			case "dragstart":
576				if (!node) {
577					tree.info("Ignored dragstart on a non-node.");
578					return false;
579				}
580				// Store current source node in different formats
581				SOURCE_NODE = node;
582
583				// Also optionally store selected nodes
584				if (dndOpts.multiSource === false) {
585					SOURCE_NODE_LIST = [node];
586				} else if (dndOpts.multiSource === true) {
587					if (node.isSelected()) {
588						SOURCE_NODE_LIST = tree.getSelectedNodes();
589					} else {
590						SOURCE_NODE_LIST = [node];
591					}
592				} else {
593					SOURCE_NODE_LIST = dndOpts.multiSource(node, data);
594				}
595				// Cache as array of jQuery objects for faster access:
596				$sourceList = $(
597					$.map(SOURCE_NODE_LIST, function (n) {
598						return n.span;
599					})
600				);
601				// Set visual feedback
602				$sourceList.addClass(classDragSource);
603
604				// Set payload
605				// Note:
606				// Transfer data is only accessible on dragstart and drop!
607				// For all other events the formats and kinds in the drag
608				// data store list of items representing dragged data can be
609				// enumerated, but the data itself is unavailable and no new
610				// data can be added.
611				var nodeData = node.toDict(true, dndOpts.sourceCopyHook);
612				nodeData.treeId = node.tree._id;
613				json = JSON.stringify(nodeData);
614				try {
615					dataTransfer.setData(nodeMimeType, json);
616					dataTransfer.setData("text/html", $(node.span).html());
617					dataTransfer.setData("text/plain", node.title);
618				} catch (ex) {
619					// IE only accepts 'text' type
620					tree.warn(
621						"Could not set data (IE only accepts 'text') - " + ex
622					);
623				}
624				// We always need to set the 'text' type if we want to drag
625				// Because IE 11 only accepts this single type.
626				// If we pass JSON here, IE can can access all node properties,
627				// even when the source lives in another window. (D'n'd inside
628				// the same window will always work.)
629				// The drawback is, that in this case ALL browsers will see
630				// the JSON representation as 'text', so dragging
631				// to a text field will insert the JSON string instead of
632				// the node title.
633				if (dndOpts.setTextTypeJson) {
634					dataTransfer.setData("text", json);
635				} else {
636					dataTransfer.setData("text", node.title);
637				}
638
639				// Set the allowed drag modes (combinations of move, copy, and link)
640				// (effectAllowed can only be set in the dragstart event.)
641				// This can be overridden in the dragStart() callback
642				prepareDropEffectCallback(event, data);
643
644				// Let user cancel or modify above settings
645				// Realize potential changes by previous callback
646				if (dndOpts.dragStart(node, data) === false) {
647					// Cancel dragging
648					// dataTransfer.dropEffect = "none";
649					_clearGlobals();
650					return false;
651				}
652				applyDropEffectCallback(event, data);
653
654				// Unless user set `data.useDefaultImage` to false in dragStart,
655				// generata a default drag image now:
656				$extraHelper = null;
657
658				if (data.useDefaultImage) {
659					// Set the title as drag image (otherwise it would contain the expander)
660					$dragImage = $(node.span).find(".fancytree-title");
661
662					if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) {
663						// Add a counter badge to node title if dragging more than one node.
664						// We want this, because the element that is used as drag image
665						// must be *visible* in the DOM, so we cannot create some hidden
666						// custom markup.
667						// See https://kryogenix.org/code/browser/custom-drag-image.html
668						// Also, since IE 11 and Edge don't support setDragImage() alltogether,
669						// it gives som feedback to the user.
670						// The badge will be removed later on drag end.
671						$extraHelper = $(
672							"<span class='fancytree-childcounter'/>"
673						)
674							.text("+" + (SOURCE_NODE_LIST.length - 1))
675							.appendTo($dragImage);
676					}
677					if (dataTransfer.setDragImage) {
678						// IE 11 and Edge do not support this
679						dataTransfer.setDragImage($dragImage[0], -10, -10);
680					}
681				}
682				return true;
683
684			case "drag":
685				// Called every few milliseconds (no matter if the
686				// cursor is over a valid drop target)
687				// data.tree.info("drag", SOURCE_NODE)
688				prepareDropEffectCallback(event, data);
689				dndOpts.dragDrag(node, data);
690				applyDropEffectCallback(event, data);
691
692				$sourceList.toggleClass(classDragRemove, data.isMove);
693				break;
694
695			case "dragend":
696				// Called at the end of a d'n'd process (after drop)
697				// Note caveat: If drop removed the dragged source element,
698				// we may not get this event, since the target does not exist
699				// anymore
700				prepareDropEffectCallback(event, data);
701
702				_clearGlobals();
703
704				data.isCancelled = !LAST_HIT_MODE;
705				dndOpts.dragEnd(node, data, !LAST_HIT_MODE);
706				// applyDropEffectCallback(event, data);
707				break;
708		}
709	}
710	/*
711	 * Handle dragenter dragover dragleave drop events on the container
712	 */
713	function onDropEvent(event) {
714		var json,
715			allowAutoExpand,
716			nodeData,
717			isSourceFtNode,
718			r,
719			res,
720			tree = this,
721			dndOpts = tree.options.dnd5,
722			allowDrop = null,
723			node = FT.getNode(event),
724			dataTransfer =
725				event.dataTransfer || event.originalEvent.dataTransfer,
726			data = {
727				tree: tree,
728				node: node,
729				options: tree.options,
730				originalEvent: event.originalEvent,
731				widget: tree.widget,
732				hitMode: DRAG_ENTER_RESPONSE,
733				dataTransfer: dataTransfer,
734				otherNode: SOURCE_NODE || null,
735				otherNodeList: SOURCE_NODE_LIST || null,
736				otherNodeData: null, // set by drop event
737				useDefaultImage: true,
738				dropEffect: undefined,
739				dropEffectSuggested: undefined,
740				effectAllowed: undefined, // set by dragstart
741				files: null, // list of File objects (may be [])
742				isCancelled: undefined, // set by drop event
743				isMove: undefined,
744			};
745
746		// data.isMove = dropEffect === "move";
747
748		switch (event.type) {
749			case "dragenter":
750				// The dragenter event is fired when a dragged element or
751				// text selection enters a valid drop target.
752
753				DRAG_OVER_STAMP = null;
754				if (!node) {
755					// Sometimes we get dragenter for the container element
756					tree.debug(
757						"Ignore non-node " +
758							event.type +
759							": " +
760							event.target.tagName +
761							"." +
762							event.target.className
763					);
764					DRAG_ENTER_RESPONSE = false;
765					break;
766				}
767
768				$(node.span)
769					.addClass(classDropOver)
770					.removeClass(classDropAccept + " " + classDropReject);
771
772				// Data is only readable in the dragstart and drop event,
773				// but we can check for the type:
774				isSourceFtNode =
775					$.inArray(nodeMimeType, dataTransfer.types) >= 0;
776
777				if (dndOpts.preventNonNodes && !isSourceFtNode) {
778					node.debug("Reject dropping a non-node.");
779					DRAG_ENTER_RESPONSE = false;
780					break;
781				} else if (
782					dndOpts.preventForeignNodes &&
783					(!SOURCE_NODE || SOURCE_NODE.tree !== node.tree)
784				) {
785					node.debug("Reject dropping a foreign node.");
786					DRAG_ENTER_RESPONSE = false;
787					break;
788				} else if (
789					dndOpts.preventSameParent &&
790					data.otherNode &&
791					data.otherNode.tree === node.tree &&
792					node.parent === data.otherNode.parent
793				) {
794					node.debug("Reject dropping as sibling (same parent).");
795					DRAG_ENTER_RESPONSE = false;
796					break;
797				} else if (
798					dndOpts.preventRecursion &&
799					data.otherNode &&
800					data.otherNode.tree === node.tree &&
801					node.isDescendantOf(data.otherNode)
802				) {
803					node.debug("Reject dropping below own ancestor.");
804					DRAG_ENTER_RESPONSE = false;
805					break;
806				} else if (dndOpts.preventLazyParents && !node.isLoaded()) {
807					node.warn("Drop over unloaded target node prevented.");
808					DRAG_ENTER_RESPONSE = false;
809					break;
810				}
811				$dropMarker.show();
812
813				// Call dragEnter() to figure out if (and where) dropping is allowed
814				prepareDropEffectCallback(event, data);
815				r = dndOpts.dragEnter(node, data);
816
817				res = normalizeDragEnterResponse(r);
818				// alert("res:" + JSON.stringify(res))
819				DRAG_ENTER_RESPONSE = res;
820
821				allowDrop = res && (res.over || res.before || res.after);
822
823				applyDropEffectCallback(event, data, allowDrop);
824				break;
825
826			case "dragover":
827				if (!node) {
828					tree.debug(
829						"Ignore non-node " +
830							event.type +
831							": " +
832							event.target.tagName +
833							"." +
834							event.target.className
835					);
836					break;
837				}
838				// The dragover event is fired when an element or text
839				// selection is being dragged over a valid drop target
840				// (every few hundred milliseconds).
841				// tree.debug(
842				// 	event.type +
843				// 		": dropEffect: " +
844				// 		dataTransfer.dropEffect
845				// );
846				prepareDropEffectCallback(event, data);
847				LAST_HIT_MODE = handleDragOver(event, data);
848
849				// The flag controls the preventDefault() below:
850				allowDrop = !!LAST_HIT_MODE;
851				allowAutoExpand =
852					LAST_HIT_MODE === "over" || LAST_HIT_MODE === false;
853
854				if (
855					allowAutoExpand &&
856					!node.expanded &&
857					node.hasChildren() !== false
858				) {
859					if (!DRAG_OVER_STAMP) {
860						DRAG_OVER_STAMP = Date.now();
861					} else if (
862						dndOpts.autoExpandMS &&
863						Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS &&
864						!node.isLoading() &&
865						(!dndOpts.dragExpand ||
866							dndOpts.dragExpand(node, data) !== false)
867					) {
868						node.setExpanded();
869					}
870				} else {
871					DRAG_OVER_STAMP = null;
872				}
873				break;
874
875			case "dragleave":
876				// NOTE: dragleave is fired AFTER the dragenter event of the
877				// FOLLOWING element.
878				if (!node) {
879					tree.debug(
880						"Ignore non-node " +
881							event.type +
882							": " +
883							event.target.tagName +
884							"." +
885							event.target.className
886					);
887					break;
888				}
889				if (!$(node.span).hasClass(classDropOver)) {
890					node.debug("Ignore dragleave (multi).");
891					break;
892				}
893				$(node.span).removeClass(
894					classDropOver +
895						" " +
896						classDropAccept +
897						" " +
898						classDropReject
899				);
900				node.scheduleAction("cancel");
901				dndOpts.dragLeave(node, data);
902				$dropMarker.hide();
903				break;
904
905			case "drop":
906				// Data is only readable in the (dragstart and) drop event:
907
908				if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) {
909					nodeData = dataTransfer.getData(nodeMimeType);
910					tree.info(
911						event.type +
912							": getData('application/x-fancytree-node'): '" +
913							nodeData +
914							"'"
915					);
916				}
917				if (!nodeData) {
918					// 1. Source is not a Fancytree node, or
919					// 2. If the FT mime type was set, but returns '', this
920					//    is probably IE 11 (which only supports 'text')
921					nodeData = dataTransfer.getData("text");
922					tree.info(
923						event.type + ": getData('text'): '" + nodeData + "'"
924					);
925				}
926				if (nodeData) {
927					try {
928						// 'text' type may contain JSON if IE is involved
929						// and setTextTypeJson option was set
930						json = JSON.parse(nodeData);
931						if (json.title !== undefined) {
932							data.otherNodeData = json;
933						}
934					} catch (ex) {
935						// assume 'text' type contains plain text, so `otherNodeData`
936						// should not be set
937					}
938				}
939				tree.debug(
940					event.type +
941						": nodeData: '" +
942						nodeData +
943						"', otherNodeData: ",
944					data.otherNodeData
945				);
946
947				$(node.span).removeClass(
948					classDropOver +
949						" " +
950						classDropAccept +
951						" " +
952						classDropReject
953				);
954
955				// Let user implement the actual drop operation
956				data.hitMode = LAST_HIT_MODE;
957				prepareDropEffectCallback(event, data, !LAST_HIT_MODE);
958				data.isCancelled = !LAST_HIT_MODE;
959
960				var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span,
961					orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree;
962
963				dndOpts.dragDrop(node, data);
964				// applyDropEffectCallback(event, data);
965
966				// Prevent browser's default drop handling, i.e. open as link, ...
967				event.preventDefault();
968
969				if (orgSourceElem && !document.body.contains(orgSourceElem)) {
970					// The drop handler removed the original drag source from
971					// the DOM, so the dragend event will probaly not fire.
972					if (orgSourceTree === tree) {
973						tree.debug(
974							"Drop handler removed source element: generating dragEnd."
975						);
976						dndOpts.dragEnd(SOURCE_NODE, data);
977					} else {
978						tree.warn(
979							"Drop handler removed source element: dragend event may be lost."
980						);
981					}
982				}
983
984				_clearGlobals();
985
986				break;
987		}
988		// Dnd API madness: we must PREVENT default handling to enable dropping
989		if (allowDrop) {
990			event.preventDefault();
991			return false;
992		}
993	}
994
995	/** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject.
996	 *
997	 * @returns {FancytreeNode[]} List of nodes (empty if no drag operation)
998	 * @example
999	 * $.ui.fancytree.getDragNodeList();
1000	 *
1001	 * @alias Fancytree_Static#getDragNodeList
1002	 * @requires jquery.fancytree.dnd5.js
1003	 * @since 2.31
1004	 */
1005	$.ui.fancytree.getDragNodeList = function () {
1006		return SOURCE_NODE_LIST || [];
1007	};
1008
1009	/** [ext-dnd5] Return the FancytreeNode that is currently being dragged.
1010	 *
1011	 * If multiple nodes are dragged, only the first is returned.
1012	 *
1013	 * @returns {FancytreeNode | null} dragged nodes or null if no drag operation
1014	 * @example
1015	 * $.ui.fancytree.getDragNode();
1016	 *
1017	 * @alias Fancytree_Static#getDragNode
1018	 * @requires jquery.fancytree.dnd5.js
1019	 * @since 2.31
1020	 */
1021	$.ui.fancytree.getDragNode = function () {
1022		return SOURCE_NODE;
1023	};
1024
1025	/******************************************************************************
1026	 *
1027	 */
1028
1029	$.ui.fancytree.registerExtension({
1030		name: "dnd5",
1031		version: "2.38.3",
1032		// Default options for this extension.
1033		options: {
1034			autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
1035			dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after"
1036			dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
1037			// #1021 `document.body` is not available yet
1038			dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root)
1039			multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed
1040			effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)
1041			// dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string.
1042			dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver).
1043			preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees
1044			preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes
1045			preventNonNodes: false, // Prevent dropping items other than Fancytree nodes
1046			preventRecursion: true, // Prevent dropping nodes on own descendants
1047			preventSameParent: false, // Prevent dropping nodes under same direct parent
1048			preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
1049			scroll: true, // Enable auto-scrolling while dragging
1050			scrollSensitivity: 20, // Active top/bottom margin in pixel
1051			scrollSpeed: 5, // Pixel per event
1052			setTextTypeJson: false, // Allow dragging of nodes to different IE windows
1053			sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
1054			// Events (drag support)
1055			dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
1056			dragDrag: $.noop, // Callback(sourceNode, data)
1057			dragEnd: $.noop, // Callback(sourceNode, data)
1058			// Events (drop support)
1059			dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop
1060			dragOver: $.noop, // Callback(targetNode, data)
1061			dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand
1062			dragDrop: $.noop, // Callback(targetNode, data)
1063			dragLeave: $.noop, // Callback(targetNode, data)
1064		},
1065
1066		treeInit: function (ctx) {
1067			var $temp,
1068				tree = ctx.tree,
1069				opts = ctx.options,
1070				glyph = opts.glyph || null,
1071				dndOpts = opts.dnd5;
1072
1073			if ($.inArray("dnd", opts.extensions) >= 0) {
1074				$.error("Extensions 'dnd' and 'dnd5' are mutually exclusive.");
1075			}
1076			if (dndOpts.dragStop) {
1077				$.error(
1078					"dragStop is not used by ext-dnd5. Use dragEnd instead."
1079				);
1080			}
1081			if (dndOpts.preventRecursiveMoves != null) {
1082				$.error(
1083					"preventRecursiveMoves was renamed to preventRecursion."
1084				);
1085			}
1086
1087			// Implement `opts.createNode` event to add the 'draggable' attribute
1088			// #680: this must happen before calling super.treeInit()
1089			if (dndOpts.dragStart) {
1090				FT.overrideMethod(
1091					ctx.options,
1092					"createNode",
1093					function (event, data) {
1094						// Default processing if any
1095						this._super.apply(this, arguments);
1096						if (data.node.span) {
1097							data.node.span.draggable = true;
1098						} else {
1099							data.node.warn(
1100								"Cannot add `draggable`: no span tag"
1101							);
1102						}
1103					}
1104				);
1105			}
1106			this._superApply(arguments);
1107
1108			this.$container.addClass("fancytree-ext-dnd5");
1109
1110			// Store the current scroll parent, which may be the tree
1111			// container, any enclosing div, or the document.
1112			// #761: scrollParent() always needs a container child
1113			$temp = $("<span>").appendTo(this.$container);
1114			this.$scrollParent = $temp.scrollParent();
1115			$temp.remove();
1116
1117			$dropMarker = $("#fancytree-drop-marker");
1118			if (!$dropMarker.length) {
1119				$dropMarker = $("<div id='fancytree-drop-marker'></div>")
1120					.hide()
1121					.css({
1122						"z-index": 1000,
1123						// Drop marker should not steal dragenter/dragover events:
1124						"pointer-events": "none",
1125					})
1126					.prependTo(dndOpts.dropMarkerParent);
1127				if (glyph) {
1128					FT.setSpanIcon(
1129						$dropMarker[0],
1130						glyph.map._addClass,
1131						glyph.map.dropMarker
1132					);
1133				}
1134			}
1135			$dropMarker.toggleClass("fancytree-rtl", !!opts.rtl);
1136
1137			// Enable drag support if dragStart() is specified:
1138			if (dndOpts.dragStart) {
1139				// Bind drag event handlers
1140				tree.$container.on(
1141					"dragstart drag dragend",
1142					onDragEvent.bind(tree)
1143				);
1144			}
1145			// Enable drop support if dragEnter() is specified:
1146			if (dndOpts.dragEnter) {
1147				// Bind drop event handlers
1148				tree.$container.on(
1149					"dragenter dragover dragleave drop",
1150					onDropEvent.bind(tree)
1151				);
1152			}
1153		},
1154	});
1155	// Value returned by `require('jquery.fancytree..')`
1156	return $.ui.fancytree;
1157}); // End of closure
1158