Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / resizer / Resizer.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.resizer.Resizer
17  * <p>Applies drag handles to an element or component to make it resizable. The
18  * drag handles are inserted into the element (or component's element) and
19  * positioned absolute.</p>
20  *
21  * <p>Textarea and img elements will be wrapped with an additional div because
22  * these elements do not support child nodes. The original element can be accessed
23  * through the originalTarget property.</p>
24  *
25  * <p>Here is the list of valid resize handles:</p>
26  * <pre>
27 Value   Description
28 ------  -------------------
29  'n'     north
30  's'     south
31  'e'     east
32  'w'     west
33  'nw'    northwest
34  'sw'    southwest
35  'se'    southeast
36  'ne'    northeast
37  'all'   all
38 </pre>
39  * {@img Ext.resizer.Resizer/Ext.resizer.Resizer.png Ext.resizer.Resizer component}
40  * <p>Here's an example showing the creation of a typical Resizer:</p>
41  * <pre><code>
42     <div id="elToResize" style="width:200px; height:100px; background-color:#000000;"></div>
43
44     Ext.create('Ext.resizer.Resizer', {
45         el: 'elToResize',
46         handles: 'all',
47         minWidth: 200,
48         minHeight: 100,
49         maxWidth: 500,
50         maxHeight: 400,
51         pinned: true
52     });
53 </code></pre>
54 */
55 Ext.define('Ext.resizer.Resizer', {
56     mixins: {
57         observable: 'Ext.util.Observable'
58     },
59     uses: ['Ext.resizer.ResizeTracker', 'Ext.Component'],
60
61     alternateClassName: 'Ext.Resizable',
62
63     handleCls: Ext.baseCSSPrefix + 'resizable-handle',
64     pinnedCls: Ext.baseCSSPrefix + 'resizable-pinned',
65     overCls:   Ext.baseCSSPrefix + 'resizable-over',
66     proxyCls:  Ext.baseCSSPrefix + 'resizable-proxy',
67     wrapCls:   Ext.baseCSSPrefix + 'resizable-wrap',
68
69     /**
70      * @cfg {Boolean} dynamic
71      * <p>Specify as true to update the {@link #target} (Element or {@link Ext.Component Component}) dynamically during dragging.
72      * This is <code>true</code> by default, but the {@link Ext.Component Component} class passes <code>false</code> when it
73      * is configured as {@link Ext.Component#resizable}.</p>
74      * <p>If specified as <code>false</code>, a proxy element is displayed during the resize operation, and the {@link #target}
75      * is updated on mouseup.</p>
76      */
77     dynamic: true,
78
79     /**
80      * @cfg {String} handles String consisting of the resize handles to display. Defaults to 's e se' for
81      * Elements and fixed position Components. Defaults to 8 point resizing for floating Components (such as Windows).
82      * Specify either <code>'all'</code> or any of <code>'n s e w ne nw se sw'</code>.
83      */
84     handles: 's e se',
85
86     /**
87      * @cfg {Number} height Optional. The height to set target to in pixels (defaults to null)
88      */
89     height : null,
90
91     /**
92      * @cfg {Number} width Optional. The width to set the target to in pixels (defaults to null)
93      */
94     width : null,
95
96     /**
97      * @cfg {Number} heightIncrement The increment to snap the height resize in pixels.
98      * Defaults to <code>0</code>.
99      */
100     heightIncrement : 0,
101
102     /**
103      * @cfg {Number} widthIncrement The increment to snap the width resize in pixels
104      * Defaults to <code>0</code>.
105      */
106     widthIncrement : 0,
107
108     /**
109      * @cfg {Number} minHeight The minimum height for the element (defaults to 20)
110      */
111     minHeight : 20,
112
113     /**
114      * @cfg {Number} minWidth The minimum width for the element (defaults to 20)
115      */
116     minWidth : 20,
117
118     /**
119      * @cfg {Number} maxHeight The maximum height for the element (defaults to 10000)
120      */
121     maxHeight : 10000,
122
123     /**
124      * @cfg {Number} maxWidth The maximum width for the element (defaults to 10000)
125      */
126     maxWidth : 10000,
127
128     /**
129      * @cfg {Boolean} pinned True to ensure that the resize handles are always
130      * visible, false indicates resizing by cursor changes only (defaults to false)
131      */
132     pinned: false,
133
134     /**
135      * @cfg {Boolean} preserveRatio True to preserve the original ratio between height
136      * and width during resize (defaults to false)
137      */
138     preserveRatio: false,
139
140     /**
141      * @cfg {Boolean} transparent True for transparent handles. This is only applied at config time. (defaults to false)
142      */
143     transparent: false,
144
145     /**
146      * @cfg {Mixed} constrainTo Optional. An element, or a {@link Ext.util.Region} into which the resize operation
147      * must be constrained.
148      */
149
150     possiblePositions: {
151         n:  'north',
152         s:  'south',
153         e:  'east',
154         w:  'west',
155         se: 'southeast',
156         sw: 'southwest',
157         nw: 'northwest',
158         ne: 'northeast'
159     },
160
161     /**
162      * @cfg {Mixed} target The Element or Component to resize.
163      */
164
165     /**
166      * Outer element for resizing behavior.
167      * @type Ext.core.Element
168      * @property el
169      */
170
171     constructor: function(config) {
172         var me = this,
173             target,
174             tag,
175             handles = me.handles,
176             handleCls,
177             possibles,
178             len,
179             i = 0,
180             pos;
181
182         this.addEvents(
183             /**
184              * @event beforeresize
185              * Fired before resize is allowed. Return false to cancel resize.
186              * @param {Ext.resizer.Resizer} this
187              * @param {Number} width The start width
188              * @param {Number} height The start height
189              * @param {Ext.EventObject} e The mousedown event
190              */
191             'beforeresize',
192             /**
193              * @event resizedrag
194              * Fires during resizing. Return false to cancel resize.
195              * @param {Ext.resizer.Resizer} this
196              * @param {Number} width The new width
197              * @param {Number} height The new height
198              * @param {Ext.EventObject} e The mousedown event
199              */
200             'resizedrag',
201             /**
202              * @event resize
203              * Fired after a resize.
204              * @param {Ext.resizer.Resizer} this
205              * @param {Number} width The new width
206              * @param {Number} height The new height
207              * @param {Ext.EventObject} e The mouseup event
208              */
209             'resize'
210         );
211
212         if (Ext.isString(config) || Ext.isElement(config) || config.dom) {
213             target = config;
214             config = arguments[1] || {};
215             config.target = target;
216         }
217         // will apply config to this
218         me.mixins.observable.constructor.call(me, config);
219
220         // If target is a Component, ensure that we pull the element out.
221         // Resizer must examine the underlying Element.
222         target = me.target;
223         if (target) {
224             if (target.isComponent) {
225                 me.el = target.getEl();
226                 if (target.minWidth) {
227                     me.minWidth = target.minWidth;
228                 }
229                 if (target.minHeight) {
230                     me.minHeight = target.minHeight;
231                 }
232                 if (target.maxWidth) {
233                     me.maxWidth = target.maxWidth;
234                 }
235                 if (target.maxHeight) {
236                     me.maxHeight = target.maxHeight;
237                 }
238                 if (target.floating) {
239                     if (!this.hasOwnProperty('handles')) {
240                         this.handles = 'n ne e se s sw w nw';
241                     }
242                 }
243             } else {
244                 me.el = me.target = Ext.get(target);
245             }
246         }
247         // Backwards compatibility with Ext3.x's Resizable which used el as a config.
248         else {
249             me.target = me.el = Ext.get(me.el);
250         }
251
252         // Tags like textarea and img cannot
253         // have children and therefore must
254         // be wrapped
255         tag = me.el.dom.tagName;
256         if (tag == 'TEXTAREA' || tag == 'IMG') {
257             /**
258              * Reference to the original resize target if the element of the original
259              * resize target was an IMG or a TEXTAREA which must be wrapped in a DIV.
260              * @type Mixed
261              * @property originalTarget
262              */
263             me.originalTarget = me.target;
264             me.target = me.el = me.el.wrap({
265                 cls: me.wrapCls,
266                 id: me.el.id + '-rzwrap'
267             });
268
269             // Transfer originalTarget's positioning/sizing
270             me.el.setPositioning(me.originalTarget.getPositioning());
271             me.originalTarget.clearPositioning();
272             var box = me.originalTarget.getBox();
273             me.el.setBox(box);
274         }
275
276         // Position the element, this enables us to absolute position
277         // the handles within this.el
278         me.el.position();
279         if (me.pinned) {
280             me.el.addCls(me.pinnedCls);
281         }
282
283         /**
284          * @type Ext.resizer.ResizeTracker
285          * @property resizeTracker
286          */
287         me.resizeTracker = Ext.create('Ext.resizer.ResizeTracker', {
288             disabled: me.disabled,
289             target: me.target,
290             constrainTo: me.constrainTo,
291             overCls: me.overCls,
292             throttle: me.throttle,
293             originalTarget: me.originalTarget,
294             delegate: '.' + me.handleCls,
295             dynamic: me.dynamic,
296             preserveRatio: me.preserveRatio,
297             heightIncrement: me.heightIncrement,
298             widthIncrement: me.widthIncrement,
299             minHeight: me.minHeight,
300             maxHeight: me.maxHeight,
301             minWidth: me.minWidth,
302             maxWidth: me.maxWidth
303         });
304
305         // Relay the ResizeTracker's superclass events as our own resize events
306         me.resizeTracker.on('mousedown', me.onBeforeResize, me);
307         me.resizeTracker.on('drag', me.onResize, me);
308         me.resizeTracker.on('dragend', me.onResizeEnd, me);
309
310         if (me.handles == 'all') {
311             me.handles = 'n s e w ne nw se sw';
312         }
313
314         handles = me.handles = me.handles.split(/ |\s*?[,;]\s*?/);
315         possibles = me.possiblePositions;
316         len = handles.length;
317         handleCls = me.handleCls + ' ' + (this.target.isComponent ? (me.target.baseCls + '-handle ') : '') + me.handleCls + '-';
318
319         for(; i < len; i++){
320             // if specified and possible, create
321             if (handles[i] && possibles[handles[i]]) {
322                 pos = possibles[handles[i]];
323                 // store a reference in this.east, this.west, etc
324
325                 me[pos] = Ext.create('Ext.Component', {
326                     owner: this,
327                     region: pos,
328                     cls: handleCls + pos,
329                     renderTo: me.el
330                 });
331                 me[pos].el.unselectable();
332                 if (me.transparent) {
333                     me[pos].el.setOpacity(0);
334                 }
335             }
336         }
337
338         // Constrain within configured maxima
339         if (Ext.isNumber(me.width)) {
340             me.width = Ext.Number.constrain(me.width, me.minWidth, me.maxWidth);
341         }
342         if (Ext.isNumber(me.height)) {
343             me.height = Ext.Number.constrain(me.height, me.minHeight, me.maxHeight);
344         }
345
346         // Size the element
347         if (me.width != null || me.height != null) {
348             if (me.originalTarget) {
349                 me.originalTarget.setWidth(me.width);
350                 me.originalTarget.setHeight(me.height);
351             }
352             me.resizeTo(me.width, me.height);
353         }
354
355         me.forceHandlesHeight();
356     },
357
358     disable: function() {
359         this.resizeTracker.disable();
360     },
361
362     enable: function() {
363         this.resizeTracker.enable();
364     },
365
366     /**
367      * @private Relay the Tracker's mousedown event as beforeresize
368      * @param tracker The Resizer
369      * @param e The Event
370      */
371     onBeforeResize: function(tracker, e) {
372         var b = this.target.getBox();
373         return this.fireEvent('beforeresize', this, b.width, b.height, e);
374     },
375
376     /**
377      * @private Relay the Tracker's drag event as resizedrag
378      * @param tracker The Resizer
379      * @param e The Event
380      */
381     onResize: function(tracker, e) {
382         var me = this,
383             b = me.target.getBox();
384         me.forceHandlesHeight();
385         return me.fireEvent('resizedrag', me, b.width, b.height, e);
386     },
387
388     /**
389      * @private Relay the Tracker's dragend event as resize
390      * @param tracker The Resizer
391      * @param e The Event
392      */
393     onResizeEnd: function(tracker, e) {
394         var me = this,
395             b = me.target.getBox();
396         me.forceHandlesHeight();
397         return me.fireEvent('resize', me, b.width, b.height, e);
398     },
399
400     /**
401      * Perform a manual resize and fires the 'resize' event.
402      * @param {Number} width
403      * @param {Number} height
404      */
405     resizeTo : function(width, height){
406         this.target.setSize(width, height);
407         this.fireEvent('resize', this, width, height, null);
408     },
409
410     /**
411      * <p>Returns the element that was configured with the el or target config property.
412      * If a component was configured with the target property then this will return the
413      * element of this component.<p>
414      * <p>Textarea and img elements will be wrapped with an additional div because
415       * these elements do not support child nodes. The original element can be accessed
416      * through the originalTarget property.</p>
417      * @return {Element} element
418      */
419     getEl : function() {
420         return this.el;
421     },
422
423     /**
424      * <p>Returns the element or component that was configured with the target config property.<p>
425      * <p>Textarea and img elements will be wrapped with an additional div because
426       * these elements do not support child nodes. The original element can be accessed
427      * through the originalTarget property.</p>
428      * @return {Element/Component}
429      */
430     getTarget: function() {
431         return this.target;
432     },
433
434     destroy: function() {
435         var h;
436         for (var i = 0, l = this.handles.length; i < l; i++) {
437             h = this[this.possiblePositions[this.handles[i]]];
438             delete h.owner;
439             Ext.destroy(h);
440         }
441     },
442
443     /**
444      * @private
445      * Fix IE6 handle height issue.
446      */
447     forceHandlesHeight : function() {
448         var me = this,
449             handle;
450         if (Ext.isIE6) {
451             handle = me.east;
452             if (handle) {
453                 handle.setHeight(me.el.getHeight());
454             }
455             handle = me.west;
456             if (handle) {
457                 handle.setHeight(me.el.getHeight());
458             }
459             me.el.repaint();
460         }
461     }
462 });
463