2
0

jquery.fancytree.dnd.js 19 KB

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