BoxReorderer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /*
  2. This file is part of Ext JS 4
  3. Copyright (c) 2011 Sencha Inc
  4. Contact: http://www.sencha.com/contact
  5. GNU General Public License Usage
  6. This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
  7. If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
  8. */
  9. Ext.define('Ext.ux.BoxReorderer', {
  10. mixins: {
  11. observable: 'Ext.util.Observable'
  12. },
  13. /**
  14. * @cfg {String} itemSelector
  15. * <p>Optional. Defaults to <code>'.x-box-item'</code>
  16. * <p>A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child Components which participate in reordering.</p>
  17. */
  18. itemSelector: '.x-box-item',
  19. /**
  20. * @cfg {Mixed} animate
  21. * <p>Defaults to 300.</p>
  22. * <p>If truthy, child reordering is animated so that moved boxes slide smoothly into position.
  23. * If this option is numeric, it is used as the animation duration <b>in milliseconds</b>.</p>
  24. */
  25. animate: 100,
  26. constructor: function() {
  27. this.addEvents(
  28. /**
  29. * @event StartDrag
  30. * Fires when dragging of a child Component begins.
  31. * @param {BoxReorder} this
  32. * @param {Container} container The owning Container
  33. * @param {Component} dragCmp The Component being dragged
  34. * @param {Number} idx The start index of the Component being dragged.
  35. */
  36. 'StartDrag',
  37. /**
  38. * @event Drag
  39. * Fires during dragging of a child Component.
  40. * @param {BoxReorder} this
  41. * @param {Container} container The owning Container
  42. * @param {Component} dragCmp The Component being dragged
  43. * @param {Number} startIdx The index position from which the Component was initially dragged.
  44. * @param {Number} idx The current closest index to which the Component would drop.
  45. */
  46. 'Drag',
  47. /**
  48. * @event ChangeIndex
  49. * Fires when dragging of a child Component causes its drop index to change.
  50. * @param {BoxReorder} this
  51. * @param {Container} container The owning Container
  52. * @param {Component} dragCmp The Component being dragged
  53. * @param {Number} startIdx The index position from which the Component was initially dragged.
  54. * @param {Number} idx The current closest index to which the Component would drop.
  55. */
  56. 'ChangeIndex',
  57. /**
  58. * @event Drop
  59. * Fires when a child Component is dropped at a new index position.
  60. * @param {BoxReorder} this
  61. * @param {Container} container The owning Container
  62. * @param {Component} dragCmp The Component being dropped
  63. * @param {Number} startIdx The index position from which the Component was initially dragged.
  64. * @param {Number} idx The index at which the Component is being dropped.
  65. */
  66. 'Drop'
  67. );
  68. this.mixins.observable.constructor.apply(this, arguments);
  69. },
  70. init: function(container) {
  71. this.container = container;
  72. // Initialize the DD on first layout, when the innerCt has been created.
  73. this.container.afterLayout = Ext.Function.createSequence(this.container.afterLayout, this.afterFirstLayout, this);
  74. container.destroy = Ext.Function.createSequence(container.destroy, this.onContainerDestroy, this);
  75. },
  76. /**
  77. * @private Clear up on Container destroy
  78. */
  79. onContainerDestroy: function() {
  80. if (this.dd) {
  81. this.dd.unreg();
  82. }
  83. },
  84. afterFirstLayout: function() {
  85. var me = this,
  86. l = me.container.getLayout();
  87. // delete the sequence
  88. delete me.container.afterLayout;
  89. // Create a DD instance. Poke the handlers in.
  90. // TODO: Ext5's DD classes should apply config to themselves.
  91. // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
  92. // TODO: Ext5's DD classes should be Observable.
  93. // TODO: When all the above are trus, this plugin should extend the DD class.
  94. me.dd = Ext.create('Ext.dd.DD', l.innerCt, me.container.id + '-reorderer');
  95. Ext.apply(me.dd, {
  96. animate: me.animate,
  97. reorderer: me,
  98. container: me.container,
  99. getDragCmp: this.getDragCmp,
  100. clickValidator: Ext.Function.createInterceptor(me.dd.clickValidator, me.clickValidator, me, false),
  101. onMouseDown: me.onMouseDown,
  102. startDrag: me.startDrag,
  103. onDrag: me.onDrag,
  104. endDrag: me.endDrag,
  105. getNewIndex: me.getNewIndex,
  106. doSwap: me.doSwap,
  107. findReorderable: me.findReorderable
  108. });
  109. // Decide which dimension we are measuring, and which measurement metric defines
  110. // the *start* of the box depending upon orientation.
  111. me.dd.dim = l.parallelPrefix;
  112. me.dd.startAttr = l.parallelBefore;
  113. me.dd.endAttr = l.parallelAfter;
  114. },
  115. getDragCmp: function(e) {
  116. return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
  117. },
  118. // check if the clicked component is reorderable
  119. clickValidator: function(e) {
  120. var cmp = this.getDragCmp(e);
  121. // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
  122. return !!(cmp && cmp.reorderable !== false);
  123. },
  124. onMouseDown: function(e) {
  125. var me = this,
  126. container = me.container,
  127. containerBox,
  128. cmpEl,
  129. cmpBox;
  130. // Ascertain which child Component is being mousedowned
  131. me.dragCmp = me.getDragCmp(e);
  132. if (me.dragCmp) {
  133. cmpEl = me.dragCmp.getEl();
  134. me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
  135. // Start position of dragged Component
  136. cmpBox = cmpEl.getPageBox();
  137. // Last tracked start position
  138. me.lastPos = cmpBox[this.startAttr];
  139. // Calculate constraints depending upon orientation
  140. // Calculate offset from mouse to dragEl position
  141. containerBox = container.el.getPageBox();
  142. if (me.dim === 'width') {
  143. me.minX = containerBox.left;
  144. me.maxX = containerBox.right - cmpBox.width;
  145. me.minY = me.maxY = cmpBox.top;
  146. me.deltaX = e.getPageX() - cmpBox.left;
  147. } else {
  148. me.minY = containerBox.top;
  149. me.maxY = containerBox.bottom - cmpBox.height;
  150. me.minX = me.maxX = cmpBox.left;
  151. me.deltaY = e.getPageY() - cmpBox.top;
  152. }
  153. me.constrainY = me.constrainX = true;
  154. }
  155. },
  156. startDrag: function() {
  157. var me = this;
  158. if (me.dragCmp) {
  159. // For the entire duration of dragging the *Element*, defeat any positioning of the dragged *Component*
  160. me.dragCmp.setPosition = Ext.emptyFn;
  161. // If the BoxLayout is not animated, animate it just for the duration of the drag operation.
  162. if (!me.container.layout.animate && me.animate) {
  163. me.container.layout.animate = me.animate;
  164. me.removeAnimate = true;
  165. }
  166. // We drag the Component element
  167. me.dragElId = me.dragCmp.getEl().id;
  168. me.reorderer.fireEvent('StartDrag', me, me.container, me.dragCmp, me.curIndex);
  169. // Suspend events, and set the disabled flag so that the mousedown and mouseup events
  170. // that are going to take place do not cause any other UI interaction.
  171. me.dragCmp.suspendEvents();
  172. me.dragCmp.disabled = true;
  173. me.dragCmp.el.setStyle('zIndex', 100);
  174. } else {
  175. me.dragElId = null;
  176. }
  177. },
  178. /**
  179. * @private
  180. * Find next or previous reorderable component index.
  181. * @param {Number} newIndex The initial drop index.
  182. * @return {Number} The index of the reorderable component.
  183. */
  184. findReorderable: function(newIndex) {
  185. var me = this,
  186. items = me.container.items,
  187. newItem;
  188. if (items.getAt(newIndex).reorderable === false) {
  189. newItem = items.getAt(newIndex);
  190. if (newIndex > me.startIndex) {
  191. while(newItem && newItem.reorderable === false) {
  192. newIndex++;
  193. newItem = items.getAt(newIndex);
  194. }
  195. } else {
  196. while(newItem && newItem.reorderable === false) {
  197. newIndex--;
  198. newItem = items.getAt(newIndex);
  199. }
  200. }
  201. }
  202. newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
  203. if (items.getAt(newIndex).reorderable === false) {
  204. return -1;
  205. }
  206. return newIndex;
  207. },
  208. /**
  209. * @private
  210. * Swap 2 components.
  211. * @param {Number} newIndex The initial drop index.
  212. */
  213. doSwap: function(newIndex) {
  214. var me = this,
  215. items = me.container.items,
  216. orig, dest, tmpIndex;
  217. newIndex = me.findReorderable(newIndex);
  218. if (newIndex === -1) {
  219. return;
  220. }
  221. me.reorderer.fireEvent('ChangeIndex', me, me.container, me.dragCmp, me.startIndex, newIndex);
  222. orig = items.getAt(me.curIndex);
  223. dest = items.getAt(newIndex);
  224. items.remove(orig);
  225. tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
  226. items.insert(tmpIndex, orig);
  227. items.remove(dest);
  228. items.insert(me.curIndex, dest);
  229. me.container.layout.layout();
  230. me.curIndex = newIndex;
  231. },
  232. onDrag: function(e) {
  233. var me = this,
  234. newIndex;
  235. newIndex = me.getNewIndex(e.getPoint());
  236. if ((newIndex !== undefined)) {
  237. me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
  238. me.doSwap(newIndex);
  239. }
  240. },
  241. endDrag: function(e) {
  242. e.stopEvent();
  243. var me = this;
  244. if (me.dragCmp) {
  245. delete me.dragElId;
  246. if (me.animate) {
  247. me.container.layout.animate = {
  248. // Call afterBoxReflow after the animation finishes.
  249. callback: Ext.Function.bind(me.reorderer.afterBoxReflow, me)
  250. };
  251. }
  252. // Reinstate the Component's positioning method after mouseup.
  253. // Call the layout directly: Bypass the layoutBusy barrier
  254. delete me.dragCmp.setPosition;
  255. me.container.layout.layout();
  256. if (me.removeAnimate) {
  257. delete me.removeAnimate;
  258. delete me.container.layout.animate;
  259. } else {
  260. me.reorderer.afterBoxReflow.call(me);
  261. }
  262. me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
  263. }
  264. },
  265. /**
  266. * @private
  267. * Called after the boxes have been reflowed after the drop.
  268. */
  269. afterBoxReflow: function() {
  270. var me = this;
  271. me.dragCmp.el.setStyle('zIndex', '');
  272. me.dragCmp.disabled = false;
  273. me.dragCmp.resumeEvents();
  274. },
  275. /**
  276. * @private
  277. * Calculate drop index based upon the dragEl's position.
  278. */
  279. getNewIndex: function(pointerPos) {
  280. var me = this,
  281. dragEl = me.getDragEl(),
  282. dragBox = Ext.fly(dragEl).getPageBox(),
  283. targetEl,
  284. targetBox,
  285. targetMidpoint,
  286. i = 0,
  287. it = me.container.items.items,
  288. ln = it.length,
  289. lastPos = me.lastPos;
  290. me.lastPos = dragBox[me.startAttr];
  291. for (; i < ln; i++) {
  292. targetEl = it[i].getEl();
  293. // Only look for a drop point if this found item is an item according to our selector
  294. if (targetEl.is(me.reorderer.itemSelector)) {
  295. targetBox = targetEl.getPageBox();
  296. targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
  297. if (i < me.curIndex) {
  298. if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
  299. return i;
  300. }
  301. } else if (i > me.curIndex) {
  302. if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {
  303. return i;
  304. }
  305. }
  306. }
  307. }
  308. }
  309. });