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