You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1158 lines
33 KiB

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