dndSource.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit.tree.dndSource"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.tree.dndSource"] = true;
  8. dojo.provide("dijit.tree.dndSource");
  9. dojo.require("dijit.tree._dndSelector");
  10. dojo.require("dojo.dnd.Manager");
  11. /*=====
  12. dijit.tree.__SourceArgs = function(){
  13. // summary:
  14. // A dict of parameters for Tree source configuration.
  15. // isSource: Boolean?
  16. // Can be used as a DnD source. Defaults to true.
  17. // accept: String[]
  18. // List of accepted types (text strings) for a target; defaults to
  19. // ["text", "treeNode"]
  20. // copyOnly: Boolean?
  21. // Copy items, if true, use a state of Ctrl key otherwise,
  22. // dragThreshold: Number
  23. // The move delay in pixels before detecting a drag; 0 by default
  24. // betweenThreshold: Integer
  25. // Distance from upper/lower edge of node to allow drop to reorder nodes
  26. this.isSource = isSource;
  27. this.accept = accept;
  28. this.autoSync = autoSync;
  29. this.copyOnly = copyOnly;
  30. this.dragThreshold = dragThreshold;
  31. this.betweenThreshold = betweenThreshold;
  32. }
  33. =====*/
  34. dojo.declare("dijit.tree.dndSource", dijit.tree._dndSelector, {
  35. // summary:
  36. // Handles drag and drop operations (as a source or a target) for `dijit.Tree`
  37. // isSource: [private] Boolean
  38. // Can be used as a DnD source.
  39. isSource: true,
  40. // accept: String[]
  41. // List of accepted types (text strings) for the Tree; defaults to
  42. // ["text"]
  43. accept: ["text", "treeNode"],
  44. // copyOnly: [private] Boolean
  45. // Copy items, if true, use a state of Ctrl key otherwise
  46. copyOnly: false,
  47. // dragThreshold: Number
  48. // The move delay in pixels before detecting a drag; 5 by default
  49. dragThreshold: 5,
  50. // betweenThreshold: Integer
  51. // Distance from upper/lower edge of node to allow drop to reorder nodes
  52. betweenThreshold: 0,
  53. constructor: function(/*dijit.Tree*/ tree, /*dijit.tree.__SourceArgs*/ params){
  54. // summary:
  55. // a constructor of the Tree DnD Source
  56. // tags:
  57. // private
  58. if(!params){ params = {}; }
  59. dojo.mixin(this, params);
  60. this.isSource = typeof params.isSource == "undefined" ? true : params.isSource;
  61. var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"];
  62. this.accept = null;
  63. if(type.length){
  64. this.accept = {};
  65. for(var i = 0; i < type.length; ++i){
  66. this.accept[type[i]] = 1;
  67. }
  68. }
  69. // class-specific variables
  70. this.isDragging = false;
  71. this.mouseDown = false;
  72. this.targetAnchor = null; // DOMNode corresponding to the currently moused over TreeNode
  73. this.targetBox = null; // coordinates of this.targetAnchor
  74. this.dropPosition = ""; // whether mouse is over/after/before this.targetAnchor
  75. this._lastX = 0;
  76. this._lastY = 0;
  77. // states
  78. this.sourceState = "";
  79. if(this.isSource){
  80. dojo.addClass(this.node, "dojoDndSource");
  81. }
  82. this.targetState = "";
  83. if(this.accept){
  84. dojo.addClass(this.node, "dojoDndTarget");
  85. }
  86. // set up events
  87. this.topics = [
  88. dojo.subscribe("/dnd/source/over", this, "onDndSourceOver"),
  89. dojo.subscribe("/dnd/start", this, "onDndStart"),
  90. dojo.subscribe("/dnd/drop", this, "onDndDrop"),
  91. dojo.subscribe("/dnd/cancel", this, "onDndCancel")
  92. ];
  93. },
  94. // methods
  95. checkAcceptance: function(source, nodes){
  96. // summary:
  97. // Checks if the target can accept nodes from this source
  98. // source: dijit.tree.dndSource
  99. // The source which provides items
  100. // nodes: DOMNode[]
  101. // Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
  102. // source is a dijit.Tree.
  103. // tags:
  104. // extension
  105. return true; // Boolean
  106. },
  107. copyState: function(keyPressed){
  108. // summary:
  109. // Returns true, if we need to copy items, false to move.
  110. // It is separated to be overwritten dynamically, if needed.
  111. // keyPressed: Boolean
  112. // The "copy" control key was pressed
  113. // tags:
  114. // protected
  115. return this.copyOnly || keyPressed; // Boolean
  116. },
  117. destroy: function(){
  118. // summary:
  119. // Prepares the object to be garbage-collected.
  120. this.inherited("destroy",arguments);
  121. dojo.forEach(this.topics, dojo.unsubscribe);
  122. this.targetAnchor = null;
  123. },
  124. _onDragMouse: function(e){
  125. // summary:
  126. // Helper method for processing onmousemove/onmouseover events while drag is in progress.
  127. // Keeps track of current drop target.
  128. var m = dojo.dnd.manager(),
  129. oldTarget = this.targetAnchor, // the TreeNode corresponding to TreeNode mouse was previously over
  130. newTarget = this.current, // TreeNode corresponding to TreeNode mouse is currently over
  131. oldDropPosition = this.dropPosition; // the previous drop position (over/before/after)
  132. // calculate if user is indicating to drop the dragged node before, after, or over
  133. // (i.e., to become a child of) the target node
  134. var newDropPosition = "Over";
  135. if(newTarget && this.betweenThreshold > 0){
  136. // If mouse is over a new TreeNode, then get new TreeNode's position and size
  137. if(!this.targetBox || oldTarget != newTarget){
  138. this.targetBox = dojo.position(newTarget.rowNode, true);
  139. }
  140. if((e.pageY - this.targetBox.y) <= this.betweenThreshold){
  141. newDropPosition = "Before";
  142. }else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){
  143. newDropPosition = "After";
  144. }
  145. }
  146. if(newTarget != oldTarget || newDropPosition != oldDropPosition){
  147. if(oldTarget){
  148. this._removeItemClass(oldTarget.rowNode, oldDropPosition);
  149. }
  150. if(newTarget){
  151. this._addItemClass(newTarget.rowNode, newDropPosition);
  152. }
  153. // Check if it's ok to drop the dragged node on/before/after the target node.
  154. if(!newTarget){
  155. m.canDrop(false);
  156. }else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){
  157. // Can't drop before or after tree's root node; the dropped node would just disappear (at least visually)
  158. m.canDrop(false);
  159. }else if(m.source == this && (newTarget.id in this.selection)){
  160. // Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140)
  161. m.canDrop(false);
  162. }else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase())
  163. && !this._isParentChildDrop(m.source, newTarget.rowNode)){
  164. m.canDrop(true);
  165. }else{
  166. m.canDrop(false);
  167. }
  168. this.targetAnchor = newTarget;
  169. this.dropPosition = newDropPosition;
  170. }
  171. },
  172. onMouseMove: function(e){
  173. // summary:
  174. // Called for any onmousemove events over the Tree
  175. // e: Event
  176. // onmousemouse event
  177. // tags:
  178. // private
  179. if(this.isDragging && this.targetState == "Disabled"){ return; }
  180. this.inherited(arguments);
  181. var m = dojo.dnd.manager();
  182. if(this.isDragging){
  183. this._onDragMouse(e);
  184. }else{
  185. if(this.mouseDown && this.isSource &&
  186. (Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){
  187. var nodes = this.getSelectedTreeNodes();
  188. if(nodes.length){
  189. if(nodes.length > 1){
  190. //filter out all selected items which has one of their ancestor selected as well
  191. var seen = this.selection, i = 0, r = [], n, p;
  192. nextitem: while((n = nodes[i++])){
  193. for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){
  194. if(seen[p.id]){ //parent is already selected, skip this node
  195. continue nextitem;
  196. }
  197. }
  198. //this node does not have any ancestors selected, add it
  199. r.push(n);
  200. }
  201. nodes = r;
  202. }
  203. nodes = dojo.map(nodes, function(n){return n.domNode});
  204. m.startDrag(this, nodes, this.copyState(dojo.isCopyKey(e)));
  205. }
  206. }
  207. }
  208. },
  209. onMouseDown: function(e){
  210. // summary:
  211. // Event processor for onmousedown
  212. // e: Event
  213. // onmousedown event
  214. // tags:
  215. // private
  216. this.mouseDown = true;
  217. this.mouseButton = e.button;
  218. this._lastX = e.pageX;
  219. this._lastY = e.pageY;
  220. this.inherited(arguments);
  221. },
  222. onMouseUp: function(e){
  223. // summary:
  224. // Event processor for onmouseup
  225. // e: Event
  226. // onmouseup event
  227. // tags:
  228. // private
  229. if(this.mouseDown){
  230. this.mouseDown = false;
  231. this.inherited(arguments);
  232. }
  233. },
  234. onMouseOut: function(){
  235. // summary:
  236. // Event processor for when mouse is moved away from a TreeNode
  237. // tags:
  238. // private
  239. this.inherited(arguments);
  240. this._unmarkTargetAnchor();
  241. },
  242. checkItemAcceptance: function(target, source, position){
  243. // summary:
  244. // Stub function to be overridden if one wants to check for the ability to drop at the node/item level
  245. // description:
  246. // In the base case, this is called to check if target can become a child of source.
  247. // When betweenThreshold is set, position="before" or "after" means that we
  248. // are asking if the source node can be dropped before/after the target node.
  249. // target: DOMNode
  250. // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
  251. // Use dijit.getEnclosingWidget(target) to get the TreeNode.
  252. // source: dijit.tree.dndSource
  253. // The (set of) nodes we are dropping
  254. // position: String
  255. // "over", "before", or "after"
  256. // tags:
  257. // extension
  258. return true;
  259. },
  260. // topic event processors
  261. onDndSourceOver: function(source){
  262. // summary:
  263. // Topic event processor for /dnd/source/over, called when detected a current source.
  264. // source: Object
  265. // The dijit.tree.dndSource / dojo.dnd.Source which has the mouse over it
  266. // tags:
  267. // private
  268. if(this != source){
  269. this.mouseDown = false;
  270. this._unmarkTargetAnchor();
  271. }else if(this.isDragging){
  272. var m = dojo.dnd.manager();
  273. m.canDrop(false);
  274. }
  275. },
  276. onDndStart: function(source, nodes, copy){
  277. // summary:
  278. // Topic event processor for /dnd/start, called to initiate the DnD operation
  279. // source: Object
  280. // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
  281. // nodes: DomNode[]
  282. // The list of transferred items, dndTreeNode nodes if dragging from a Tree
  283. // copy: Boolean
  284. // Copy items, if true, move items otherwise
  285. // tags:
  286. // private
  287. if(this.isSource){
  288. this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : "");
  289. }
  290. var accepted = this.checkAcceptance(source, nodes);
  291. this._changeState("Target", accepted ? "" : "Disabled");
  292. if(this == source){
  293. dojo.dnd.manager().overSource(this);
  294. }
  295. this.isDragging = true;
  296. },
  297. itemCreator: function(/*DomNode[]*/ nodes, target, /*dojo.dnd.Source*/ source){
  298. // summary:
  299. // Returns objects passed to `Tree.model.newItem()` based on DnD nodes
  300. // dropped onto the tree. Developer must override this method to enable
  301. // dropping from external sources onto this Tree, unless the Tree.model's items
  302. // happen to look like {id: 123, name: "Apple" } with no other attributes.
  303. // description:
  304. // For each node in nodes[], which came from source, create a hash of name/value
  305. // pairs to be passed to Tree.model.newItem(). Returns array of those hashes.
  306. // returns: Object[]
  307. // Array of name/value hashes for each new item to be added to the Tree, like:
  308. // | [
  309. // | { id: 123, label: "apple", foo: "bar" },
  310. // | { id: 456, label: "pear", zaz: "bam" }
  311. // | ]
  312. // tags:
  313. // extension
  314. // TODO: for 2.0 refactor so itemCreator() is called once per drag node, and
  315. // make signature itemCreator(sourceItem, node, target) (or similar).
  316. return dojo.map(nodes, function(node){
  317. return {
  318. "id": node.id,
  319. "name": node.textContent || node.innerText || ""
  320. };
  321. }); // Object[]
  322. },
  323. onDndDrop: function(source, nodes, copy){
  324. // summary:
  325. // Topic event processor for /dnd/drop, called to finish the DnD operation.
  326. // description:
  327. // Updates data store items according to where node was dragged from and dropped
  328. // to. The tree will then respond to those data store updates and redraw itself.
  329. // source: Object
  330. // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
  331. // nodes: DomNode[]
  332. // The list of transferred items, dndTreeNode nodes if dragging from a Tree
  333. // copy: Boolean
  334. // Copy items, if true, move items otherwise
  335. // tags:
  336. // protected
  337. if(this.containerState == "Over"){
  338. var tree = this.tree,
  339. model = tree.model,
  340. target = this.targetAnchor,
  341. requeryRoot = false; // set to true iff top level items change
  342. this.isDragging = false;
  343. // Compute the new parent item
  344. var targetWidget = target;
  345. var newParentItem;
  346. var insertIndex;
  347. newParentItem = (targetWidget && targetWidget.item) || tree.item;
  348. if(this.dropPosition == "Before" || this.dropPosition == "After"){
  349. // TODO: if there is no parent item then disallow the drop.
  350. // Actually this should be checked during onMouseMove too, to make the drag icon red.
  351. newParentItem = (targetWidget.getParent() && targetWidget.getParent().item) || tree.item;
  352. // Compute the insert index for reordering
  353. insertIndex = targetWidget.getIndexInParent();
  354. if(this.dropPosition == "After"){
  355. insertIndex = targetWidget.getIndexInParent() + 1;
  356. }
  357. }else{
  358. newParentItem = (targetWidget && targetWidget.item) || tree.item;
  359. }
  360. // If necessary, use this variable to hold array of hashes to pass to model.newItem()
  361. // (one entry in the array for each dragged node).
  362. var newItemsParams;
  363. dojo.forEach(nodes, function(node, idx){
  364. // dojo.dnd.Item representing the thing being dropped.
  365. // Don't confuse the use of item here (meaning a DnD item) with the
  366. // uses below where item means dojo.data item.
  367. var sourceItem = source.getItem(node.id);
  368. // Information that's available if the source is another Tree
  369. // (possibly but not necessarily this tree, possibly but not
  370. // necessarily the same model as this Tree)
  371. if(dojo.indexOf(sourceItem.type, "treeNode") != -1){
  372. var childTreeNode = sourceItem.data,
  373. childItem = childTreeNode.item,
  374. oldParentItem = childTreeNode.getParent().item;
  375. }
  376. if(source == this){
  377. // This is a node from my own tree, and we are moving it, not copying.
  378. // Remove item from old parent's children attribute.
  379. // TODO: dijit.tree.dndSelector should implement deleteSelectedNodes()
  380. // and this code should go there.
  381. if(typeof insertIndex == "number"){
  382. if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){
  383. insertIndex -= 1;
  384. }
  385. }
  386. model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
  387. }else if(model.isItem(childItem)){
  388. // Item from same model
  389. // (maybe we should only do this branch if the source is a tree?)
  390. model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
  391. }else{
  392. // Get the hash to pass to model.newItem(). A single call to
  393. // itemCreator() returns an array of hashes, one for each drag source node.
  394. if(!newItemsParams){
  395. newItemsParams = this.itemCreator(nodes, target.rowNode, source);
  396. }
  397. // Create new item in the tree, based on the drag source.
  398. model.newItem(newItemsParams[idx], newParentItem, insertIndex);
  399. }
  400. }, this);
  401. // Expand the target node (if it's currently collapsed) so the user can see
  402. // where their node was dropped. In particular since that node is still selected.
  403. this.tree._expandNode(targetWidget);
  404. }
  405. this.onDndCancel();
  406. },
  407. onDndCancel: function(){
  408. // summary:
  409. // Topic event processor for /dnd/cancel, called to cancel the DnD operation
  410. // tags:
  411. // private
  412. this._unmarkTargetAnchor();
  413. this.isDragging = false;
  414. this.mouseDown = false;
  415. delete this.mouseButton;
  416. this._changeState("Source", "");
  417. this._changeState("Target", "");
  418. },
  419. // When focus moves in/out of the entire Tree
  420. onOverEvent: function(){
  421. // summary:
  422. // This method is called when mouse is moved over our container (like onmouseenter)
  423. // tags:
  424. // private
  425. this.inherited(arguments);
  426. dojo.dnd.manager().overSource(this);
  427. },
  428. onOutEvent: function(){
  429. // summary:
  430. // This method is called when mouse is moved out of our container (like onmouseleave)
  431. // tags:
  432. // private
  433. this._unmarkTargetAnchor();
  434. var m = dojo.dnd.manager();
  435. if(this.isDragging){
  436. m.canDrop(false);
  437. }
  438. m.outSource(this);
  439. this.inherited(arguments);
  440. },
  441. _isParentChildDrop: function(source, targetRow){
  442. // summary:
  443. // Checks whether the dragged items are parent rows in the tree which are being
  444. // dragged into their own children.
  445. //
  446. // source:
  447. // The DragSource object.
  448. //
  449. // targetRow:
  450. // The tree row onto which the dragged nodes are being dropped.
  451. //
  452. // tags:
  453. // private
  454. // If the dragged object is not coming from the tree this widget belongs to,
  455. // it cannot be invalid.
  456. if(!source.tree || source.tree != this.tree){
  457. return false;
  458. }
  459. var root = source.tree.domNode;
  460. var ids = source.selection;
  461. var node = targetRow.parentNode;
  462. // Iterate up the DOM hierarchy from the target drop row,
  463. // checking of any of the dragged nodes have the same ID.
  464. while(node != root && !ids[node.id]){
  465. node = node.parentNode;
  466. }
  467. return node.id && ids[node.id];
  468. },
  469. _unmarkTargetAnchor: function(){
  470. // summary:
  471. // Removes hover class of the current target anchor
  472. // tags:
  473. // private
  474. if(!this.targetAnchor){ return; }
  475. this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition);
  476. this.targetAnchor = null;
  477. this.targetBox = null;
  478. this.dropPosition = null;
  479. },
  480. _markDndStatus: function(copy){
  481. // summary:
  482. // Changes source's state based on "copy" status
  483. this._changeState("Source", copy ? "Copied" : "Moved");
  484. }
  485. });
  486. }