/src/jquery.fancytree.dnd.js

https://github.com/Octabits/fancytree · JavaScript · 523 lines · 367 code · 32 blank · 124 comment · 106 complexity · 95238dfb5c5bcc44f2917d42030edc7a MD5 · raw file

  1. /*!
  2. * jquery.fancytree.dnd.js
  3. *
  4. * Drag-and-drop support.
  5. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  6. *
  7. * Copyright (c) 2014, Martin Wendt (http://wwWendt.de)
  8. *
  9. * Released under the MIT license
  10. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11. *
  12. * @version @VERSION
  13. * @date @DATE
  14. */
  15. ;(function($, window, document, undefined) {
  16. "use strict";
  17. /* *****************************************************************************
  18. * Private functions and variables
  19. */
  20. var logMsg = $.ui.fancytree.debug,
  21. didRegisterDnd = false;
  22. /* Convert number to string and prepend +/-; return empty string for 0.*/
  23. function offsetString(n){
  24. return n === 0 ? "" : (( n > 0 ) ? ("+" + n) : ("" + n));
  25. }
  26. /* *****************************************************************************
  27. * Drag and drop support
  28. */
  29. function _initDragAndDrop(tree) {
  30. var dnd = tree.options.dnd || null;
  31. // Register 'connectToFancytree' option with ui.draggable
  32. if( dnd ) {
  33. _registerDnd();
  34. }
  35. // Attach ui.draggable to this Fancytree instance
  36. if(dnd && dnd.dragStart ) {
  37. tree.widget.element.draggable($.extend({
  38. addClasses: false,
  39. appendTo: "body",
  40. containment: false,
  41. delay: 0,
  42. distance: 4,
  43. // TODO: merge Dynatree issue 419
  44. revert: false,
  45. scroll: true, // issue 244: enable scrolling (if ul.fancytree-container)
  46. scrollSpeed: 7,
  47. scrollSensitivity: 10,
  48. // Delegate draggable.start, drag, and stop events to our handler
  49. connectToFancytree: true,
  50. // Let source tree create the helper element
  51. helper: function(event) {
  52. var sourceNode = $.ui.fancytree.getNode(event.target);
  53. if(!sourceNode){ // Dynatree issue 211
  54. // might happen, if dragging a table *header*
  55. return "<div>ERROR?: helper requested but sourceNode not found</div>";
  56. }
  57. return sourceNode.tree.ext.dnd._onDragEvent("helper", sourceNode, null, event, null, null);
  58. },
  59. start: function(event, ui) {
  60. var sourceNode = ui.helper.data("ftSourceNode");
  61. return !!sourceNode; // Abort dragging if no node could be found
  62. }
  63. }, tree.options.dnd.draggable));
  64. }
  65. // Attach ui.droppable to this Fancytree instance
  66. if(dnd && dnd.dragDrop) {
  67. tree.widget.element.droppable($.extend({
  68. addClasses: false,
  69. tolerance: "intersect",
  70. greedy: false
  71. /*
  72. activate: function(event, ui) {
  73. logMsg("droppable - activate", event, ui, this);
  74. },
  75. create: function(event, ui) {
  76. logMsg("droppable - create", event, ui);
  77. },
  78. deactivate: function(event, ui) {
  79. logMsg("droppable - deactivate", event, ui);
  80. },
  81. drop: function(event, ui) {
  82. logMsg("droppable - drop", event, ui);
  83. },
  84. out: function(event, ui) {
  85. logMsg("droppable - out", event, ui);
  86. },
  87. over: function(event, ui) {
  88. logMsg("droppable - over", event, ui);
  89. }
  90. */
  91. }, tree.options.dnd.droppable));
  92. }
  93. }
  94. //--- Extend ui.draggable event handling --------------------------------------
  95. function _registerDnd() {
  96. if(didRegisterDnd){
  97. return;
  98. }
  99. // Register proxy-functions for draggable.start/drag/stop
  100. $.ui.plugin.add("draggable", "connectToFancytree", {
  101. start: function(event, ui) {
  102. // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
  103. var draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
  104. sourceNode = ui.helper.data("ftSourceNode") || null;
  105. if(sourceNode) {
  106. // Adjust helper offset, so cursor is slightly outside top/left corner
  107. draggable.offset.click.top = -2;
  108. draggable.offset.click.left = + 16;
  109. // Trigger dragStart event
  110. // TODO: when called as connectTo..., the return value is ignored(?)
  111. return sourceNode.tree.ext.dnd._onDragEvent("start", sourceNode, null, event, ui, draggable);
  112. }
  113. },
  114. drag: function(event, ui) {
  115. var isHelper,
  116. // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
  117. draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
  118. sourceNode = ui.helper.data("ftSourceNode") || null,
  119. prevTargetNode = ui.helper.data("ftTargetNode") || null,
  120. targetNode = $.ui.fancytree.getNode(event.target);
  121. if(event.target && !targetNode){
  122. // We got a drag event, but the targetNode could not be found
  123. // at the event location. This may happen,
  124. // 1. if the mouse jumped over the drag helper,
  125. // 2. or if a non-fancytree element is dragged
  126. // We ignore it:
  127. isHelper = $(event.target).closest("div.fancytree-drag-helper,#fancytree-drop-marker").length > 0;
  128. if(isHelper){
  129. logMsg("Drag event over helper: ignored.");
  130. return;
  131. }
  132. }
  133. ui.helper.data("ftTargetNode", targetNode);
  134. // Leaving a tree node
  135. if(prevTargetNode && prevTargetNode !== targetNode ) {
  136. prevTargetNode.tree.ext.dnd._onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
  137. }
  138. if(targetNode){
  139. if(!targetNode.tree.options.dnd.dragDrop) {
  140. // not enabled as drop target
  141. } else if(targetNode === prevTargetNode) {
  142. // Moving over same node
  143. targetNode.tree.ext.dnd._onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
  144. }else{
  145. // Entering this node first time
  146. targetNode.tree.ext.dnd._onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
  147. }
  148. }
  149. // else go ahead with standard event handling
  150. },
  151. stop: function(event, ui) {
  152. // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
  153. var draggable = $(this).data("ui-draggable") || $(this).data("draggable"),
  154. sourceNode = ui.helper.data("ftSourceNode") || null,
  155. targetNode = ui.helper.data("ftTargetNode") || null,
  156. // mouseDownEvent = draggable._mouseDownEvent,
  157. eventType = event.type,
  158. dropped = (eventType === "mouseup" && event.which === 1);
  159. if(!dropped){
  160. logMsg("Drag was cancelled");
  161. }
  162. if(targetNode) {
  163. if(dropped){
  164. targetNode.tree.ext.dnd._onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
  165. }
  166. targetNode.tree.ext.dnd._onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
  167. }
  168. if(sourceNode){
  169. sourceNode.tree.ext.dnd._onDragEvent("stop", sourceNode, null, event, ui, draggable);
  170. }
  171. }
  172. });
  173. didRegisterDnd = true;
  174. }
  175. /* *****************************************************************************
  176. *
  177. */
  178. $.ui.fancytree.registerExtension({
  179. name: "dnd",
  180. version: "0.1.0",
  181. // Default options for this extension.
  182. options: {
  183. // Make tree nodes draggable:
  184. dragStart: null, // Callback(sourceNode, data), return true, to enable dnd
  185. dragStop: null, // Callback(sourceNode, data)
  186. // helper: null,
  187. // Make tree nodes accept draggables
  188. autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering.
  189. preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
  190. preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
  191. dragEnter: null, // Callback(targetNode, data)
  192. dragOver: null, // Callback(targetNode, data)
  193. dragDrop: null, // Callback(targetNode, data)
  194. dragLeave: null, // Callback(targetNode, data)
  195. //
  196. draggable: null, // Additional options passed to jQuery draggable
  197. droppable: null // Additional options passed to jQuery droppable
  198. },
  199. treeInit: function(ctx){
  200. var tree = ctx.tree;
  201. this._super(ctx);
  202. _initDragAndDrop(tree);
  203. },
  204. /* Override key handler in order to cancel dnd on escape.*/
  205. nodeKeydown: function(ctx) {
  206. var event = ctx.originalEvent;
  207. if( event.which === $.ui.keyCode.ESCAPE) {
  208. this._local._cancelDrag();
  209. }
  210. return this._super(ctx);
  211. },
  212. /* Display drop marker according to hitMode ('after', 'before', 'over', 'out', 'start', 'stop'). */
  213. _setDndStatus: function(sourceNode, targetNode, helper, hitMode, accept) {
  214. var posOpts,
  215. markerOffsetX = 0,
  216. markerAt = "center",
  217. instData = this._local,
  218. $source = sourceNode ? $(sourceNode.span) : null,
  219. $target = $(targetNode.span);
  220. if( !instData.$dropMarker ) {
  221. instData.$dropMarker = $("<div id='fancytree-drop-marker'></div>")
  222. .hide()
  223. .css({"z-index": 1000})
  224. .prependTo($(this.$div).parent());
  225. // .prependTo("body");
  226. }
  227. // this.$dropMarker.attr("class", hitMode);
  228. if(hitMode === "after" || hitMode === "before" || hitMode === "over"){
  229. // $source && $source.addClass("fancytree-drag-source");
  230. // $target.addClass("fancytree-drop-target");
  231. switch(hitMode){
  232. case "before":
  233. instData
  234. .$dropMarker.removeClass("fancytree-drop-after fancytree-drop-over")
  235. .addClass("fancytree-drop-before");
  236. markerAt = "top";
  237. break;
  238. case "after":
  239. instData.$dropMarker.removeClass("fancytree-drop-before fancytree-drop-over")
  240. .addClass("fancytree-drop-after");
  241. markerAt = "bottom";
  242. break;
  243. default:
  244. instData.$dropMarker.removeClass("fancytree-drop-after fancytree-drop-before")
  245. .addClass("fancytree-drop-over");
  246. $target.addClass("fancytree-drop-target");
  247. markerOffsetX = 8;
  248. }
  249. if( $.ui.fancytree.jquerySupports.positionMyOfs ){
  250. posOpts = {
  251. my: "left" + offsetString(markerOffsetX) + " center",
  252. at: "left " + markerAt,
  253. of: $target
  254. };
  255. } else {
  256. posOpts = {
  257. my: "left center",
  258. at: "left " + markerAt,
  259. of: $target,
  260. offset: "" + markerOffsetX + " 0"
  261. };
  262. }
  263. instData.$dropMarker
  264. .show()
  265. .position(posOpts);
  266. // helper.addClass("fancytree-drop-hover");
  267. } else {
  268. // $source && $source.removeClass("fancytree-drag-source");
  269. $target.removeClass("fancytree-drop-target");
  270. instData.$dropMarker.hide();
  271. // helper.removeClass("fancytree-drop-hover");
  272. }
  273. if(hitMode === "after"){
  274. $target.addClass("fancytree-drop-after");
  275. } else {
  276. $target.removeClass("fancytree-drop-after");
  277. }
  278. if(hitMode === "before"){
  279. $target.addClass("fancytree-drop-before");
  280. } else {
  281. $target.removeClass("fancytree-drop-before");
  282. }
  283. if(accept === true){
  284. if($source){
  285. $source.addClass("fancytree-drop-accept");
  286. }
  287. $target.addClass("fancytree-drop-accept");
  288. helper.addClass("fancytree-drop-accept");
  289. }else{
  290. if($source){
  291. $source.removeClass("fancytree-drop-accept");
  292. }
  293. $target.removeClass("fancytree-drop-accept");
  294. helper.removeClass("fancytree-drop-accept");
  295. }
  296. if(accept === false){
  297. if($source){
  298. $source.addClass("fancytree-drop-reject");
  299. }
  300. $target.addClass("fancytree-drop-reject");
  301. helper.addClass("fancytree-drop-reject");
  302. }else{
  303. if($source){
  304. $source.removeClass("fancytree-drop-reject");
  305. }
  306. $target.removeClass("fancytree-drop-reject");
  307. helper.removeClass("fancytree-drop-reject");
  308. }
  309. },
  310. /*
  311. * Handles drag'n'drop functionality.
  312. *
  313. * A standard jQuery drag-and-drop process may generate these calls:
  314. *
  315. * draggable helper():
  316. * _onDragEvent("helper", sourceNode, null, event, null, null);
  317. * start:
  318. * _onDragEvent("start", sourceNode, null, event, ui, draggable);
  319. * drag:
  320. * _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
  321. * _onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
  322. * _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
  323. * stop:
  324. * _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
  325. * _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
  326. * _onDragEvent("stop", sourceNode, null, event, ui, draggable);
  327. */
  328. _onDragEvent: function(eventName, node, otherNode, event, ui, draggable) {
  329. if(eventName !== "over"){
  330. logMsg("tree.ext.dnd._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this);
  331. }
  332. var $helper, nodeOfs, relPos, relPos2,
  333. enterResponse, hitMode, r,
  334. opts = this.options,
  335. dnd = opts.dnd,
  336. ctx = this._makeHookContext(node, event, {otherNode: otherNode, ui: ui, draggable: draggable}),
  337. res = null,
  338. $nodeTag = $(node.span);
  339. switch (eventName) {
  340. case "helper":
  341. // Only event and node argument is available
  342. $helper = $("<div class='fancytree-drag-helper'><span class='fancytree-drag-helper-img' /></div>")
  343. .css({zIndex: 3, position: "relative"}) // so it appears above ext-wide selection bar
  344. .append($nodeTag.find("span.fancytree-title").clone());
  345. // DT issue 244: helper should be child of scrollParent
  346. $("ul.fancytree-container", node.tree.$div).append($helper);
  347. // Attach node reference to helper object
  348. $helper.data("ftSourceNode", node);
  349. // logMsg("helper=%o", $helper);
  350. // logMsg("helper.sourceNode=%o", $helper.data("ftSourceNode"));
  351. res = $helper;
  352. break;
  353. case "start":
  354. if( node.isStatusNode() ) {
  355. res = false;
  356. } else if(dnd.dragStart) {
  357. res = dnd.dragStart(node, ctx);
  358. }
  359. if(res === false) {
  360. this.debug("tree.dragStart() cancelled");
  361. //draggable._clear();
  362. // NOTE: the return value seems to be ignored (drag is not canceled, when false is returned)
  363. // TODO: call this._cancelDrag()?
  364. ui.helper.trigger("mouseup")
  365. .hide();
  366. } else {
  367. $nodeTag.addClass("fancytree-drag-source");
  368. }
  369. break;
  370. case "enter":
  371. if(dnd.preventRecursiveMoves && node.isDescendantOf(otherNode)){
  372. r = false;
  373. }else{
  374. r = dnd.dragEnter ? dnd.dragEnter(node, ctx) : null;
  375. }
  376. if(!r){
  377. // convert null, undefined, false to false
  378. res = false;
  379. }else if ( $.isArray(r) ) {
  380. // TODO: also accept passing an object of this format directly
  381. res = {
  382. over: ($.inArray("over", r) >= 0),
  383. before: ($.inArray("before", r) >= 0),
  384. after: ($.inArray("after", r) >= 0)
  385. };
  386. }else{
  387. res = {
  388. over: ((r === true) || (r === "over")),
  389. before: ((r === true) || (r === "before")),
  390. after: ((r === true) || (r === "after"))
  391. };
  392. }
  393. ui.helper.data("enterResponse", res);
  394. logMsg("helper.enterResponse: %o", res);
  395. break;
  396. case "over":
  397. enterResponse = ui.helper.data("enterResponse");
  398. hitMode = null;
  399. if(enterResponse === false){
  400. // Don't call dragOver if onEnter returned false.
  401. // break;
  402. } else if(typeof enterResponse === "string") {
  403. // Use hitMode from onEnter if provided.
  404. hitMode = enterResponse;
  405. } else {
  406. // Calculate hitMode from relative cursor position.
  407. nodeOfs = $nodeTag.offset();
  408. relPos = { x: event.pageX - nodeOfs.left,
  409. y: event.pageY - nodeOfs.top };
  410. relPos2 = { x: relPos.x / $nodeTag.width(),
  411. y: relPos.y / $nodeTag.height() };
  412. if( enterResponse.after && relPos2.y > 0.75 ){
  413. hitMode = "after";
  414. } else if(!enterResponse.over && enterResponse.after && relPos2.y > 0.5 ){
  415. hitMode = "after";
  416. } else if(enterResponse.before && relPos2.y <= 0.25) {
  417. hitMode = "before";
  418. } else if(!enterResponse.over && enterResponse.before && relPos2.y <= 0.5) {
  419. hitMode = "before";
  420. } else if(enterResponse.over) {
  421. hitMode = "over";
  422. }
  423. // Prevent no-ops like 'before source node'
  424. // TODO: these are no-ops when moving nodes, but not in copy mode
  425. if( dnd.preventVoidMoves ){
  426. if(node === otherNode){
  427. logMsg(" drop over source node prevented");
  428. hitMode = null;
  429. }else if(hitMode === "before" && otherNode && node === otherNode.getNextSibling()){
  430. logMsg(" drop after source node prevented");
  431. hitMode = null;
  432. }else if(hitMode === "after" && otherNode && node === otherNode.getPrevSibling()){
  433. logMsg(" drop before source node prevented");
  434. hitMode = null;
  435. }else if(hitMode === "over" && otherNode && otherNode.parent === node && otherNode.isLastSibling() ){
  436. logMsg(" drop last child over own parent prevented");
  437. hitMode = null;
  438. }
  439. }
  440. // logMsg("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling());
  441. ui.helper.data("hitMode", hitMode);
  442. }
  443. // Auto-expand node (only when 'over' the node, not 'before', or 'after')
  444. if(hitMode === "over" && dnd.autoExpandMS && node.hasChildren() !== false && !node.expanded) {
  445. node.scheduleAction("expand", dnd.autoExpandMS);
  446. }
  447. if(hitMode && dnd.dragOver){
  448. // TODO: http://code.google.com/p/dynatree/source/detail?r=625
  449. ctx.hitMode = hitMode;
  450. res = dnd.dragOver(node, ctx);
  451. }
  452. // DT issue 332
  453. // this._setDndStatus(otherNode, node, ui.helper, hitMode, res!==false);
  454. this._local._setDndStatus(otherNode, node, ui.helper, hitMode, res!==false && hitMode !== null);
  455. break;
  456. case "drop":
  457. hitMode = ui.helper.data("hitMode");
  458. if(hitMode && dnd.dragDrop){
  459. ctx.hitMode = hitMode;
  460. dnd.dragDrop(node, ctx);
  461. }
  462. break;
  463. case "leave":
  464. // Cancel pending expand request
  465. node.scheduleAction("cancel");
  466. ui.helper.data("enterResponse", null);
  467. ui.helper.data("hitMode", null);
  468. this._local._setDndStatus(otherNode, node, ui.helper, "out", undefined);
  469. if(dnd.dragLeave){
  470. dnd.dragLeave(node, ctx);
  471. }
  472. break;
  473. case "stop":
  474. $nodeTag.removeClass("fancytree-drag-source");
  475. if(dnd.dragStop){
  476. dnd.dragStop(node, ctx);
  477. }
  478. break;
  479. default:
  480. $.error("Unsupported drag event: " + eventName);
  481. }
  482. return res;
  483. },
  484. _cancelDrag: function() {
  485. var dd = $.ui.ddmanager.current;
  486. if(dd){
  487. dd.cancel();
  488. }
  489. }
  490. });
  491. }(jQuery, window, document));