Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / core / src / dom / Element.alignment.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.core.Element
17  */
18 Ext.core.Element.addMethods({
19     /**
20      * Gets the x,y coordinates specified by the anchor position on the element.
21      * @param {String} anchor (optional) The specified anchor position (defaults to "c").  See {@link #alignTo}
22      * for details on supported anchor positions.
23      * @param {Boolean} local (optional) True to get the local (element top/left-relative) anchor position instead
24      * of page coordinates
25      * @param {Object} size (optional) An object containing the size to use for calculating anchor position
26      * {width: (target width), height: (target height)} (defaults to the element's current size)
27      * @return {Array} [x, y] An array containing the element's x and y coordinates
28      */
29     getAnchorXY : function(anchor, local, s){
30         //Passing a different size is useful for pre-calculating anchors,
31         //especially for anchored animations that change the el size.
32         anchor = (anchor || "tl").toLowerCase();
33         s = s || {};
34
35         var me = this,
36             vp = me.dom == document.body || me.dom == document,
37             w = s.width || vp ? Ext.core.Element.getViewWidth() : me.getWidth(),
38             h = s.height || vp ? Ext.core.Element.getViewHeight() : me.getHeight(),
39             xy,
40             r = Math.round,
41             o = me.getXY(),
42             scroll = me.getScroll(),
43             extraX = vp ? scroll.left : !local ? o[0] : 0,
44             extraY = vp ? scroll.top : !local ? o[1] : 0,
45             hash = {
46                 c  : [r(w * 0.5), r(h * 0.5)],
47                 t  : [r(w * 0.5), 0],
48                 l  : [0, r(h * 0.5)],
49                 r  : [w, r(h * 0.5)],
50                 b  : [r(w * 0.5), h],
51                 tl : [0, 0],
52                 bl : [0, h],
53                 br : [w, h],
54                 tr : [w, 0]
55             };
56
57         xy = hash[anchor];
58         return [xy[0] + extraX, xy[1] + extraY];
59     },
60
61     /**
62      * Anchors an element to another element and realigns it when the window is resized.
63      * @param {Mixed} element The element to align to.
64      * @param {String} position The position to align to.
65      * @param {Array} offsets (optional) Offset the positioning by [x, y]
66      * @param {Boolean/Object} animate (optional) True for the default animation or a standard Element animation config object
67      * @param {Boolean/Number} monitorScroll (optional) True to monitor body scroll and reposition. If this parameter
68      * is a number, it is used as the buffer delay (defaults to 50ms).
69      * @param {Function} callback The function to call after the animation finishes
70      * @return {Ext.core.Element} this
71      */
72     anchorTo : function(el, alignment, offsets, animate, monitorScroll, callback){
73         var me = this,
74             dom = me.dom,
75             scroll = !Ext.isEmpty(monitorScroll),
76             action = function(){
77                 Ext.fly(dom).alignTo(el, alignment, offsets, animate);
78                 Ext.callback(callback, Ext.fly(dom));
79             },
80             anchor = this.getAnchor();
81
82         // previous listener anchor, remove it
83         this.removeAnchor();
84         Ext.apply(anchor, {
85             fn: action,
86             scroll: scroll
87         });
88
89         Ext.EventManager.onWindowResize(action, null);
90
91         if(scroll){
92             Ext.EventManager.on(window, 'scroll', action, null,
93                 {buffer: !isNaN(monitorScroll) ? monitorScroll : 50});
94         }
95         action.call(me); // align immediately
96         return me;
97     },
98
99     /**
100      * Remove any anchor to this element. See {@link #anchorTo}.
101      * @return {Ext.core.Element} this
102      */
103     removeAnchor : function(){
104         var me = this,
105             anchor = this.getAnchor();
106
107         if(anchor && anchor.fn){
108             Ext.EventManager.removeResizeListener(anchor.fn);
109             if(anchor.scroll){
110                 Ext.EventManager.un(window, 'scroll', anchor.fn);
111             }
112             delete anchor.fn;
113         }
114         return me;
115     },
116
117     // private
118     getAnchor : function(){
119         var data = Ext.core.Element.data,
120             dom = this.dom;
121             if (!dom) {
122                 return;
123             }
124             var anchor = data(dom, '_anchor');
125
126         if(!anchor){
127             anchor = data(dom, '_anchor', {});
128         }
129         return anchor;
130     },
131
132     getAlignVector: function(el, spec, offset) {
133         var me = this,
134             side = {t:"top", l:"left", r:"right", b: "bottom"},
135             thisRegion = me.getRegion(),
136             elRegion;
137
138         el = Ext.get(el);
139         if(!el || !el.dom){
140             //<debug>
141             Ext.Error.raise({
142                 sourceClass: 'Ext.core.Element',
143                 sourceMethod: 'getAlignVector',
144                 msg: 'Attempted to align an element that doesn\'t exist'
145             });
146             //</debug>
147         }
148
149         elRegion = el.getRegion();
150     },
151
152     /**
153      * Gets the x,y coordinates to align this element with another element. See {@link #alignTo} for more info on the
154      * supported position values.
155      * @param {Mixed} element The element to align to.
156      * @param {String} position (optional, defaults to "tl-bl?") The position to align to.
157      * @param {Array} offsets (optional) Offset the positioning by [x, y]
158      * @return {Array} [x, y]
159      */
160     getAlignToXY : function(el, p, o){
161         el = Ext.get(el);
162
163         if(!el || !el.dom){
164             //<debug>
165             Ext.Error.raise({
166                 sourceClass: 'Ext.core.Element',
167                 sourceMethod: 'getAlignToXY',
168                 msg: 'Attempted to align an element that doesn\'t exist'
169             });
170             //</debug>
171         }
172
173         o = o || [0,0];
174         p = (!p || p == "?" ? "tl-bl?" : (!(/-/).test(p) && p !== "" ? "tl-" + p : p || "tl-bl")).toLowerCase();
175
176         var me = this,
177             d = me.dom,
178             a1,
179             a2,
180             x,
181             y,
182             //constrain the aligned el to viewport if necessary
183             w,
184             h,
185             r,
186             dw = Ext.core.Element.getViewWidth() -10, // 10px of margin for ie
187             dh = Ext.core.Element.getViewHeight()-10, // 10px of margin for ie
188             p1y,
189             p1x,
190             p2y,
191             p2x,
192             swapY,
193             swapX,
194             doc = document,
195             docElement = doc.documentElement,
196             docBody = doc.body,
197             scrollX = (docElement.scrollLeft || docBody.scrollLeft || 0)+5,
198             scrollY = (docElement.scrollTop || docBody.scrollTop || 0)+5,
199             c = false, //constrain to viewport
200             p1 = "",
201             p2 = "",
202             m = p.match(/^([a-z]+)-([a-z]+)(\?)?$/);
203
204         if(!m){
205             //<debug>
206             Ext.Error.raise({
207                 sourceClass: 'Ext.core.Element',
208                 sourceMethod: 'getAlignToXY',
209                 el: el,
210                 position: p,
211                 offset: o,
212                 msg: 'Attemmpted to align an element with an invalid position: "' + p + '"'
213             });
214             //</debug>
215         }
216
217         p1 = m[1];
218         p2 = m[2];
219         c = !!m[3];
220
221         //Subtract the aligned el's internal xy from the target's offset xy
222         //plus custom offset to get the aligned el's new offset xy
223         a1 = me.getAnchorXY(p1, true);
224         a2 = el.getAnchorXY(p2, false);
225
226         x = a2[0] - a1[0] + o[0];
227         y = a2[1] - a1[1] + o[1];
228
229         if(c){
230            w = me.getWidth();
231            h = me.getHeight();
232            r = el.getRegion();
233            //If we are at a viewport boundary and the aligned el is anchored on a target border that is
234            //perpendicular to the vp border, allow the aligned el to slide on that border,
235            //otherwise swap the aligned el to the opposite border of the target.
236            p1y = p1.charAt(0);
237            p1x = p1.charAt(p1.length-1);
238            p2y = p2.charAt(0);
239            p2x = p2.charAt(p2.length-1);
240            swapY = ((p1y=="t" && p2y=="b") || (p1y=="b" && p2y=="t"));
241            swapX = ((p1x=="r" && p2x=="l") || (p1x=="l" && p2x=="r"));
242
243
244            if (x + w > dw + scrollX) {
245                 x = swapX ? r.left-w : dw+scrollX-w;
246            }
247            if (x < scrollX) {
248                x = swapX ? r.right : scrollX;
249            }
250            if (y + h > dh + scrollY) {
251                 y = swapY ? r.top-h : dh+scrollY-h;
252             }
253            if (y < scrollY){
254                y = swapY ? r.bottom : scrollY;
255            }
256         }
257         return [x,y];
258     },
259
260     /**
261      * Aligns this element with another element relative to the specified anchor points. If the other element is the
262      * document it aligns it to the viewport.
263      * The position parameter is optional, and can be specified in any one of the following formats:
264      * <ul>
265      *   <li><b>Blank</b>: Defaults to aligning the element's top-left corner to the target's bottom-left corner ("tl-bl").</li>
266      *   <li><b>One anchor (deprecated)</b>: The passed anchor position is used as the target element's anchor point.
267      *       The element being aligned will position its top-left corner (tl) to that point.  <i>This method has been
268      *       deprecated in favor of the newer two anchor syntax below</i>.</li>
269      *   <li><b>Two anchors</b>: If two values from the table below are passed separated by a dash, the first value is used as the
270      *       element's anchor point, and the second value is used as the target's anchor point.</li>
271      * </ul>
272      * In addition to the anchor points, the position parameter also supports the "?" character.  If "?" is passed at the end of
273      * the position string, the element will attempt to align as specified, but the position will be adjusted to constrain to
274      * the viewport if necessary.  Note that the element being aligned might be swapped to align to a different position than
275      * that specified in order to enforce the viewport constraints.
276      * Following are all of the supported anchor positions:
277 <pre>
278 Value  Description
279 -----  -----------------------------
280 tl     The top left corner (default)
281 t      The center of the top edge
282 tr     The top right corner
283 l      The center of the left edge
284 c      In the center of the element
285 r      The center of the right edge
286 bl     The bottom left corner
287 b      The center of the bottom edge
288 br     The bottom right corner
289 </pre>
290 Example Usage:
291 <pre><code>
292 // align el to other-el using the default positioning ("tl-bl", non-constrained)
293 el.alignTo("other-el");
294
295 // align the top left corner of el with the top right corner of other-el (constrained to viewport)
296 el.alignTo("other-el", "tr?");
297
298 // align the bottom right corner of el with the center left edge of other-el
299 el.alignTo("other-el", "br-l?");
300
301 // align the center of el with the bottom left corner of other-el and
302 // adjust the x position by -6 pixels (and the y position by 0)
303 el.alignTo("other-el", "c-bl", [-6, 0]);
304 </code></pre>
305      * @param {Mixed} element The element to align to.
306      * @param {String} position (optional, defaults to "tl-bl?") The position to align to.
307      * @param {Array} offsets (optional) Offset the positioning by [x, y]
308      * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
309      * @return {Ext.core.Element} this
310      */
311     alignTo : function(element, position, offsets, animate){
312         var me = this;
313         return me.setXY(me.getAlignToXY(element, position, offsets),
314                         me.anim && !!animate ? me.anim(animate) : false);
315     },
316
317     // private ==>  used outside of core
318     adjustForConstraints : function(xy, parent) {
319         var vector = this.getConstrainVector(parent, xy);
320         if (vector) {
321             xy[0] += vector[0];
322             xy[1] += vector[1];
323         }
324         return xy;
325     },
326
327     /**
328      * <p>Returns the <code>[X, Y]</code> vector by which this element must be translated to make a best attempt
329      * to constrain within the passed constraint. Returns <code>false</code> is this element does not need to be moved.</p>
330      * <p>Priority is given to constraining the top and left within the constraint.</p>
331      * <p>The constraint may either be an existing element into which this element is to be constrained, or
332      * an {@link Ext.util.Region Region} into which this element is to be constrained.</p>
333      * @param constrainTo {Mixed} The Element or {@link Ext.util.Region Region} into which this element is to be constrained.
334      * @param proposedPosition {Array} A proposed <code>[X, Y]</code> position to test for validity and to produce a vector for instead
335      * of using this Element's current position;
336      * @returns {Array} <b>If</b> this element <i>needs</i> to be translated, an <code>[X, Y]</code>
337      * vector by which this element must be translated. Otherwise, <code>false</code>.
338      */
339     getConstrainVector: function(constrainTo, proposedPosition) {
340         if (!(constrainTo instanceof Ext.util.Region)) {
341             constrainTo = Ext.get(constrainTo).getViewRegion();
342         }
343         var thisRegion = this.getRegion(),
344             vector = [0, 0],
345             shadowSize = this.shadow && this.shadow.offset,
346             overflowed = false;
347
348         // Shift this region to occupy the proposed position
349         if (proposedPosition) {
350             thisRegion.translateBy(proposedPosition[0] - thisRegion.x, proposedPosition[1] - thisRegion.y);
351         }
352
353         // Reduce the constrain region to allow for shadow
354         // TODO: Rewrite the Shadow class. When that's done, get the extra for each side from the Shadow.
355         if (shadowSize) {
356             constrainTo.adjust(0, -shadowSize, -shadowSize, shadowSize);
357         }
358
359         // Constrain the X coordinate by however much this Element overflows
360         if (thisRegion.right > constrainTo.right) {
361             overflowed = true;
362             vector[0] = (constrainTo.right - thisRegion.right);    // overflowed the right
363         }
364         if (thisRegion.left + vector[0] < constrainTo.left) {
365             overflowed = true;
366             vector[0] = (constrainTo.left - thisRegion.left);      // overflowed the left
367         }
368
369         // Constrain the Y coordinate by however much this Element overflows
370         if (thisRegion.bottom > constrainTo.bottom) {
371             overflowed = true;
372             vector[1] = (constrainTo.bottom - thisRegion.bottom);  // overflowed the bottom
373         }
374         if (thisRegion.top + vector[1] < constrainTo.top) {
375             overflowed = true;
376             vector[1] = (constrainTo.top - thisRegion.top);        // overflowed the top
377         }
378         return overflowed ? vector : false;
379     },
380
381     /**
382     * Calculates the x, y to center this element on the screen
383     * @return {Array} The x, y values [x, y]
384     */
385     getCenterXY : function(){
386         return this.getAlignToXY(document, 'c-c');
387     },
388
389     /**
390     * Centers the Element in either the viewport, or another Element.
391     * @param {Mixed} centerIn (optional) The element in which to center the element.
392     */
393     center : function(centerIn){
394         return this.alignTo(centerIn || document, 'c-c');
395     }
396 });
397