dndSource.js 18 KB

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