BoxReorderer.js 13 KB

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