BoxReorderer.js 13 KB

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