DnD.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. define([
  2. "dojo/_base/declare",
  3. "dojo/_base/lang",
  4. "dojo/_base/array",
  5. "dojo/_base/Deferred",
  6. "dojo/aspect",
  7. "dojo/on",
  8. "dojo/topic",
  9. "dojo/has",
  10. "dojo/dnd/Source",
  11. "dojo/dnd/Manager",
  12. "dojo/_base/NodeList",
  13. "put-selector/put",
  14. "../Selection",
  15. "dojo/has!touch?../util/touch",
  16. "dojo/has!touch?./_DnD-touch-autoscroll",
  17. "xstyle/css!dojo/resources/dnd.css"
  18. ], function(declare, lang, arrayUtil, Deferred, aspect, on, topic, has, DnDSource, DnDManager, NodeList, put, Selection, touchUtil){
  19. // Requirements
  20. // * requires a store (sounds obvious, but not all Lists/Grids have stores...)
  21. // * must support options.before in put calls
  22. // (if undefined, put at end)
  23. // * should support copy
  24. // (copy should also support options.before as above)
  25. // TODOs
  26. // * consider sending items rather than nodes to onDropExternal/Internal
  27. // * consider emitting store errors via OnDemandList._trackError
  28. var GridDnDSource = declare(DnDSource, {
  29. grid: null,
  30. getObject: function(node){
  31. // summary:
  32. // getObject is a method which should be defined on any source intending
  33. // on interfacing with dgrid DnD.
  34. var grid = this.grid;
  35. // Extract item id from row node id (gridID-row-*).
  36. return grid.store.get(node.id.slice(grid.id.length + 5));
  37. },
  38. _legalMouseDown: function(evt){
  39. // Fix _legalMouseDown to only allow starting drag from an item
  40. // (not from bodyNode outside contentNode).
  41. var legal = this.inherited(arguments);
  42. return legal && evt.target != this.grid.bodyNode;
  43. },
  44. // DnD method overrides
  45. onDrop: function(sourceSource, nodes, copy){
  46. var targetSource = this,
  47. targetRow = this._targetAnchor = this.targetAnchor, // save for Internal
  48. grid = this.grid,
  49. store = grid.store;
  50. if(!this.before && targetRow){
  51. // target before next node if dropped within bottom half of this node
  52. // (unless there's no node to target at all)
  53. targetRow = targetRow.nextSibling;
  54. }
  55. targetRow = targetRow && grid.row(targetRow);
  56. Deferred.when(targetRow && store.get(targetRow.id), function(target){
  57. // Note: if dropping after the last row, or into an empty grid,
  58. // target will be undefined. Thus, it is important for store to place
  59. // item last in order if options.before is undefined.
  60. // Delegate to onDropInternal or onDropExternal for rest of logic.
  61. // These are passed the target item as an additional argument.
  62. if(targetSource != sourceSource){
  63. targetSource.onDropExternal(sourceSource, nodes, copy, target);
  64. }else{
  65. targetSource.onDropInternal(nodes, copy, target);
  66. }
  67. });
  68. },
  69. onDropInternal: function(nodes, copy, targetItem){
  70. var store = this.grid.store,
  71. targetSource = this,
  72. grid = this.grid,
  73. anchor = targetSource._targetAnchor,
  74. targetRow;
  75. if(anchor){ // (falsy if drop occurred in empty space after rows)
  76. targetRow = this.before ? anchor.previousSibling : anchor.nextSibling;
  77. }
  78. // Don't bother continuing if the drop is really not moving anything.
  79. // (Don't need to worry about edge first/last cases since dropping
  80. // directly on self doesn't fire onDrop, but we do have to worry about
  81. // dropping last node into empty space beyond rendered rows.)
  82. if(!copy && (targetRow === nodes[0] ||
  83. (!targetItem && grid.down(grid.row(nodes[0])).element == nodes[0]))){
  84. return;
  85. }
  86. nodes.forEach(function(node){
  87. Deferred.when(targetSource.getObject(node), function(object){
  88. // For copy DnD operations, copy object, if supported by store;
  89. // otherwise settle for put anyway.
  90. // (put will relocate an existing item with the same id, i.e. move).
  91. store[copy && store.copy ? "copy" : "put"](object, {
  92. before: targetItem
  93. });
  94. });
  95. });
  96. },
  97. onDropExternal: function(sourceSource, nodes, copy, targetItem){
  98. // Note: this default implementation expects that two grids do not
  99. // share the same store. There may be more ideal implementations in the
  100. // case of two grids using the same store (perhaps differentiated by
  101. // query), dragging to each other.
  102. var store = this.grid.store,
  103. sourceGrid = sourceSource.grid;
  104. // TODO: bail out if sourceSource.getObject isn't defined?
  105. nodes.forEach(function(node, i){
  106. Deferred.when(sourceSource.getObject(node), function(object){
  107. if(!copy){
  108. if(sourceGrid){
  109. // Remove original in the case of inter-grid move.
  110. // (Also ensure dnd source is cleaned up properly)
  111. Deferred.when(sourceGrid.store.getIdentity(object), function(id){
  112. !i && sourceSource.selectNone(); // deselect all, one time
  113. sourceSource.delItem(node.id);
  114. sourceGrid.store.remove(id);
  115. });
  116. }else{
  117. sourceSource.deleteSelectedNodes();
  118. }
  119. }
  120. // Copy object, if supported by store; otherwise settle for put
  121. // (put will relocate an existing item with the same id).
  122. // Note that we use store.copy if available even for non-copy dnd:
  123. // since this coming from another dnd source, always behave as if
  124. // it is a new store item if possible, rather than replacing existing.
  125. store[store.copy ? "copy" : "put"](object, {
  126. before: targetItem
  127. });
  128. });
  129. });
  130. },
  131. onDndStart: function(source, nodes, copy){
  132. // Listen for start events to apply style change to avatar.
  133. this.inherited(arguments); // DnDSource.prototype.onDndStart.apply(this, arguments);
  134. if(source == this){
  135. // If TouchScroll is in use, cancel any pending scroll operation.
  136. if(this.grid.cancelTouchScroll){ this.grid.cancelTouchScroll(); }
  137. // Set avatar width to half the grid's width.
  138. // Kind of a naive default, but prevents ridiculously wide avatars.
  139. DnDManager.manager().avatar.node.style.width =
  140. this.grid.domNode.offsetWidth / 2 + "px";
  141. }
  142. },
  143. onMouseDown: function(evt){
  144. // Cancel the drag operation on presence of more than one contact point.
  145. // (This check will evaluate to false under non-touch circumstances.)
  146. if(has("touch") && this.isDragging &&
  147. touchUtil.countCurrentTouches(evt, this.grid.touchNode) > 1){
  148. topic.publish("/dnd/cancel");
  149. DnDManager.manager().stopDrag();
  150. }else{
  151. this.inherited(arguments);
  152. }
  153. },
  154. onMouseMove: function(evt){
  155. // If we're handling touchmove, only respond to single-contact events.
  156. if(!has("touch") || touchUtil.countCurrentTouches(evt, this.grid.touchNode) === 1){
  157. this.inherited(arguments);
  158. }
  159. },
  160. checkAcceptance: function(source, nodes){
  161. // Augment checkAcceptance to block drops from sources without getObject.
  162. return source.getObject &&
  163. DnDSource.prototype.checkAcceptance.apply(this, arguments);
  164. },
  165. getSelectedNodes: function(){
  166. // If dgrid's Selection mixin is in use, synchronize with it, using a
  167. // map of node references (updated on dgrid-[de]select events).
  168. if(!this.grid.selection){
  169. return this.inherited(arguments);
  170. }
  171. var t = new NodeList(),
  172. id;
  173. for(id in this.grid.selection){
  174. t.push(this._selectedNodes[id]);
  175. }
  176. return t; // NodeList
  177. }
  178. // TODO: could potentially also implement copyState to jive with default
  179. // onDrop* implementations (checking whether store.copy is available);
  180. // not doing that just yet until we're sure about default impl.
  181. });
  182. // Mix in Selection for more resilient dnd handling, particularly when part
  183. // of the selection is scrolled out of view and unrendered (which we
  184. // handle below).
  185. var DnD = declare(Selection, {
  186. // dndSourceType: String
  187. // Specifies the type which will be set for DnD items in the grid,
  188. // as well as what will be accepted by it by default.
  189. dndSourceType: "dgrid-row",
  190. // dndParams: Object
  191. // Object containing params to be passed to the DnD Source constructor.
  192. dndParams: null,
  193. // dndConstructor: Function
  194. // Constructor from which to instantiate the DnD Source.
  195. // Defaults to the GridSource constructor defined/exposed by this module.
  196. dndConstructor: GridDnDSource,
  197. postMixInProperties: function(){
  198. this.inherited(arguments);
  199. // ensure dndParams is initialized
  200. this.dndParams = lang.mixin({ accept: [this.dndSourceType] }, this.dndParams);
  201. },
  202. postCreate: function(){
  203. this.inherited(arguments);
  204. // Make the grid's content a DnD source/target.
  205. this.dndSource = new (this.dndConstructor || GridDnDSource)(
  206. this.bodyNode,
  207. lang.mixin(this.dndParams, {
  208. // add cross-reference to grid for potential use in inter-grid drop logic
  209. grid: this,
  210. dropParent: this.contentNode
  211. })
  212. );
  213. // Set up select/deselect handlers to maintain references, in case selected
  214. // rows are scrolled out of view and unrendered, but then dragged.
  215. var selectedNodes = this.dndSource._selectedNodes = {};
  216. function selectRow(row){
  217. selectedNodes[row.id] = row.element;
  218. }
  219. function deselectRow(row){
  220. delete selectedNodes[row.id];
  221. }
  222. this.on("dgrid-select", function(event){
  223. arrayUtil.forEach(event.rows, selectRow);
  224. });
  225. this.on("dgrid-deselect", function(event){
  226. arrayUtil.forEach(event.rows, deselectRow);
  227. });
  228. aspect.after(this, "destroy", function(){
  229. delete this.dndSource._selectedNodes;
  230. selectedNodes = null;
  231. this.dndSource.destroy();
  232. }, true);
  233. },
  234. insertRow: function(object){
  235. // override to add dojoDndItem class to make the rows draggable
  236. var row = this.inherited(arguments),
  237. type = typeof this.getObjectDndType == "function" ?
  238. this.getObjectDndType(object) : [this.dndSourceType];
  239. put(row, ".dojoDndItem");
  240. this.dndSource.setItem(row.id, {
  241. data: object,
  242. type: type instanceof Array ? type : [type]
  243. });
  244. return row;
  245. },
  246. removeRow: function (rowElement) {
  247. this.dndSource.delItem(this.row(rowElement));
  248. this.inherited(arguments);
  249. }
  250. });
  251. DnD.GridSource = GridDnDSource;
  252. return DnD;
  253. });