3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
15 Ext.define('Ext.ux.BoxReorderer', {
17 observable: 'Ext.util.Observable'
21 * @cfg {String} itemSelector
22 * <p>Optional. Defaults to <code>'.x-box-item'</code>
23 * <p>A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child Components which participate in reordering.</p>
25 itemSelector: '.x-box-item',
28 * @cfg {Mixed} animate
29 * <p>Defaults to 300.</p>
30 * <p>If truthy, child reordering is animated so that moved boxes slide smoothly into position.
31 * If this option is numeric, it is used as the animation duration <b>in milliseconds</b>.</p>
35 constructor: function() {
39 * Fires when dragging of a child Component begins.
40 * @param {BoxReorder} this
41 * @param {Container} container The owning Container
42 * @param {Component} dragCmp The Component being dragged
43 * @param {Number} idx The start index of the Component being dragged.
48 * Fires during dragging of a child Component.
49 * @param {BoxReorder} this
50 * @param {Container} container The owning Container
51 * @param {Component} dragCmp The Component being dragged
52 * @param {Number} startIdx The index position from which the Component was initially dragged.
53 * @param {Number} idx The current closest index to which the Component would drop.
58 * Fires when dragging of a child Component causes its drop index to change.
59 * @param {BoxReorder} this
60 * @param {Container} container The owning Container
61 * @param {Component} dragCmp The Component being dragged
62 * @param {Number} startIdx The index position from which the Component was initially dragged.
63 * @param {Number} idx The current closest index to which the Component would drop.
68 * Fires when a child Component is dropped at a new index position.
69 * @param {BoxReorder} this
70 * @param {Container} container The owning Container
71 * @param {Component} dragCmp The Component being dropped
72 * @param {Number} startIdx The index position from which the Component was initially dragged.
73 * @param {Number} idx The index at which the Component is being dropped.
77 this.mixins.observable.constructor.apply(this, arguments);
80 init: function(container) {
81 this.container = container;
83 // Initialize the DD on first layout, when the innerCt has been created.
84 this.container.afterLayout = Ext.Function.createSequence(this.container.afterLayout, this.afterFirstLayout, this);
86 container.destroy = Ext.Function.createSequence(container.destroy, this.onContainerDestroy, this);
90 * @private Clear up on Container destroy
92 onContainerDestroy: function() {
98 afterFirstLayout: function() {
100 l = me.container.getLayout();
102 // delete the sequence
103 delete me.container.afterLayout;
105 // Create a DD instance. Poke the handlers in.
106 // TODO: Ext5's DD classes should apply config to themselves.
107 // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
108 // TODO: Ext5's DD classes should be Observable.
109 // TODO: When all the above are trus, this plugin should extend the DD class.
110 me.dd = Ext.create('Ext.dd.DD', l.innerCt, me.container.id + '-reorderer');
114 container: me.container,
115 getDragCmp: this.getDragCmp,
116 clickValidator: Ext.Function.createInterceptor(me.dd.clickValidator, me.clickValidator, me, false),
117 onMouseDown: me.onMouseDown,
118 startDrag: me.startDrag,
121 getNewIndex: me.getNewIndex,
123 findReorderable: me.findReorderable
126 // Decide which dimension we are measuring, and which measurement metric defines
127 // the *start* of the box depending upon orientation.
128 me.dd.dim = l.parallelPrefix;
129 me.dd.startAttr = l.parallelBefore;
130 me.dd.endAttr = l.parallelAfter;
133 getDragCmp: function(e) {
134 return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
137 // check if the clicked component is reorderable
138 clickValidator: function(e) {
139 var cmp = this.getDragCmp(e);
141 // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
142 return !!(cmp && cmp.reorderable !== false);
145 onMouseDown: function(e) {
147 container = me.container,
152 // Ascertain which child Component is being mousedowned
153 me.dragCmp = me.getDragCmp(e);
155 cmpEl = me.dragCmp.getEl();
156 me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
158 // Start position of dragged Component
159 cmpBox = cmpEl.getPageBox();
161 // Last tracked start position
162 me.lastPos = cmpBox[this.startAttr];
164 // Calculate constraints depending upon orientation
165 // Calculate offset from mouse to dragEl position
166 containerBox = container.el.getPageBox();
167 if (me.dim === 'width') {
168 me.minX = containerBox.left;
169 me.maxX = containerBox.right - cmpBox.width;
170 me.minY = me.maxY = cmpBox.top;
171 me.deltaX = e.getPageX() - cmpBox.left;
173 me.minY = containerBox.top;
174 me.maxY = containerBox.bottom - cmpBox.height;
175 me.minX = me.maxX = cmpBox.left;
176 me.deltaY = e.getPageY() - cmpBox.top;
178 me.constrainY = me.constrainX = true;
182 startDrag: function() {
185 // For the entire duration of dragging the *Element*, defeat any positioning of the dragged *Component*
186 me.dragCmp.setPosition = Ext.emptyFn;
188 // If the BoxLayout is not animated, animate it just for the duration of the drag operation.
189 if (!me.container.layout.animate && me.animate) {
190 me.container.layout.animate = me.animate;
191 me.removeAnimate = true;
193 // We drag the Component element
194 me.dragElId = me.dragCmp.getEl().id;
195 me.reorderer.fireEvent('StartDrag', me, me.container, me.dragCmp, me.curIndex);
196 // Suspend events, and set the disabled flag so that the mousedown and mouseup events
197 // that are going to take place do not cause any other UI interaction.
198 me.dragCmp.suspendEvents();
199 me.dragCmp.disabled = true;
200 me.dragCmp.el.setStyle('zIndex', 100);
210 * Find next or previous reorderable component index.
211 * @param {Number} newIndex The initial drop index.
212 * @return {Number} The index of the reorderable component.
214 findReorderable: function(newIndex) {
216 items = me.container.items,
219 if (items.getAt(newIndex).reorderable === false) {
220 newItem = items.getAt(newIndex);
221 if (newIndex > me.startIndex) {
222 while(newItem && newItem.reorderable === false) {
224 newItem = items.getAt(newIndex);
227 while(newItem && newItem.reorderable === false) {
229 newItem = items.getAt(newIndex);
234 newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
236 if (items.getAt(newIndex).reorderable === false) {
245 * @param {Number} newIndex The initial drop index.
247 doSwap: function(newIndex) {
249 items = me.container.items,
250 orig, dest, tmpIndex;
252 newIndex = me.findReorderable(newIndex);
254 if (newIndex === -1) {
258 me.reorderer.fireEvent('ChangeIndex', me, me.container, me.dragCmp, me.startIndex, newIndex);
259 orig = items.getAt(me.curIndex);
260 dest = items.getAt(newIndex);
262 tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
263 items.insert(tmpIndex, orig);
265 items.insert(me.curIndex, dest);
267 me.container.layout.layout();
268 me.curIndex = newIndex;
271 onDrag: function(e) {
275 newIndex = me.getNewIndex(e.getPoint());
276 if ((newIndex !== undefined)) {
277 me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
283 endDrag: function(e) {
289 me.container.layout.animate = {
290 // Call afterBoxReflow after the animation finishes.
291 callback: Ext.Function.bind(me.reorderer.afterBoxReflow, me)
295 // Reinstate the Component's positioning method after mouseup.
296 // Call the layout directly: Bypass the layoutBusy barrier
297 delete me.dragCmp.setPosition;
298 me.container.layout.layout();
300 if (me.removeAnimate) {
301 delete me.removeAnimate;
302 delete me.container.layout.animate;
304 me.reorderer.afterBoxReflow.call(me);
306 me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
312 * Called after the boxes have been reflowed after the drop.
314 afterBoxReflow: function() {
316 me.dragCmp.el.setStyle('zIndex', '');
317 me.dragCmp.disabled = false;
318 me.dragCmp.resumeEvents();
323 * Calculate drop index based upon the dragEl's position.
325 getNewIndex: function(pointerPos) {
327 dragEl = me.getDragEl(),
328 dragBox = Ext.fly(dragEl).getPageBox(),
333 it = me.container.items.items,
335 lastPos = me.lastPos;
337 me.lastPos = dragBox[me.startAttr];
339 for (; i < ln; i++) {
340 targetEl = it[i].getEl();
342 // Only look for a drop point if this found item is an item according to our selector
343 if (targetEl.is(me.reorderer.itemSelector)) {
344 targetBox = targetEl.getPageBox();
345 targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
346 if (i < me.curIndex) {
347 if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
350 } else if (i > me.curIndex) {
351 if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {