BoxReorderer.js 13 KB

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