Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / layout / component / field / Field.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.layout.component.field.Field
17  * @extends Ext.layout.component.Component
18  * Layout class for components with {@link Ext.form.Labelable field labeling}, handling the sizing and alignment of
19  * the form control, label, and error message treatment.
20  * @private
21  */
22 Ext.define('Ext.layout.component.field.Field', {
23
24     /* Begin Definitions */
25
26     alias: ['layout.field'],
27
28     extend: 'Ext.layout.component.Component',
29
30     uses: ['Ext.tip.QuickTip', 'Ext.util.TextMetrics'],
31
32     /* End Definitions */
33
34     type: 'field',
35
36     beforeLayout: function(width, height) {
37         var me = this;
38         return me.callParent(arguments) || (!me.owner.preventMark && me.activeError !== me.owner.getActiveError());
39     },
40
41     onLayout: function(width, height) {
42         var me = this,
43             owner = me.owner,
44             labelStrategy = me.getLabelStrategy(),
45             errorStrategy = me.getErrorStrategy(),
46             isDefined = Ext.isDefined,
47             isNumber = Ext.isNumber,
48             lastSize, autoWidth, autoHeight, info, undef;
49
50         lastSize = me.lastComponentSize || {};
51         if (!isDefined(width)) {
52             width = lastSize.width;
53             if (width < 0) { //first pass lastComponentSize.width is -Infinity
54                 width = undef;
55             }
56         }
57         if (!isDefined(height)) {
58             height = lastSize.height;
59             if (height < 0) { //first pass lastComponentSize.height is -Infinity
60                 height = undef;
61             }
62         }
63         autoWidth = !isNumber(width);
64         autoHeight = !isNumber(height);
65
66         info = {
67             autoWidth: autoWidth,
68             autoHeight: autoHeight,
69             width: autoWidth ? owner.getBodyNaturalWidth() : width, //always give a pixel width
70             height: height,
71             setOuterWidth: false, //whether the outer el width should be set to the calculated width
72
73             // insets for the bodyEl from each side of the component layout area
74             insets: {
75                 top: 0,
76                 right: 0,
77                 bottom: 0,
78                 left: 0
79             }
80         };
81
82         // NOTE the order of calculating insets and setting styles here is very important; we must first
83         // calculate and set horizontal layout alone, as the horizontal sizing of elements can have an impact
84         // on the vertical sizes due to wrapping, then calculate and set the vertical layout.
85
86         // perform preparation on the label and error (setting css classes, qtips, etc.)
87         labelStrategy.prepare(owner, info);
88         errorStrategy.prepare(owner, info);
89
90         // calculate the horizontal insets for the label and error
91         labelStrategy.adjustHorizInsets(owner, info);
92         errorStrategy.adjustHorizInsets(owner, info);
93
94         // set horizontal styles for label and error based on the current insets
95         labelStrategy.layoutHoriz(owner, info);
96         errorStrategy.layoutHoriz(owner, info);
97
98         // calculate the vertical insets for the label and error
99         labelStrategy.adjustVertInsets(owner, info);
100         errorStrategy.adjustVertInsets(owner, info);
101
102         // set vertical styles for label and error based on the current insets
103         labelStrategy.layoutVert(owner, info);
104         errorStrategy.layoutVert(owner, info);
105
106         // perform sizing of the elements based on the final dimensions and insets
107         if (autoWidth && autoHeight) {
108             // Don't use setTargetSize if auto-sized, so the calculated size is not reused next time
109             me.setElementSize(owner.el, (info.setOuterWidth ? info.width : undef), info.height);
110         } else {
111             me.setTargetSize((!autoWidth || info.setOuterWidth ? info.width : undef), info.height);
112         }
113         me.sizeBody(info);
114
115         me.activeError = owner.getActiveError();
116     },
117     
118     onFocus: function(){
119         this.getErrorStrategy().onFocus(this.owner);    
120     },
121
122
123     /**
124      * Perform sizing and alignment of the bodyEl (and children) to match the calculated insets.
125      */
126     sizeBody: function(info) {
127         var me = this,
128             owner = me.owner,
129             insets = info.insets,
130             totalWidth = info.width,
131             totalHeight = info.height,
132             width = Ext.isNumber(totalWidth) ? totalWidth - insets.left - insets.right : totalWidth,
133             height = Ext.isNumber(totalHeight) ? totalHeight - insets.top - insets.bottom : totalHeight;
134
135         // size the bodyEl
136         me.setElementSize(owner.bodyEl, width, height);
137
138         // size the bodyEl's inner contents if necessary
139         me.sizeBodyContents(width, height);
140     },
141
142     /**
143      * Size the contents of the field body, given the full dimensions of the bodyEl. Does nothing by
144      * default, subclasses can override to handle their specific contents.
145      * @param {Number} width The bodyEl width
146      * @param {Number} height The bodyEl height
147      */
148     sizeBodyContents: Ext.emptyFn,
149
150
151     /**
152      * Return the set of strategy functions from the {@link #labelStrategies labelStrategies collection}
153      * that is appropriate for the field's {@link Ext.form.Labelable#labelAlign labelAlign} config.
154      */
155     getLabelStrategy: function() {
156         var me = this,
157             strategies = me.labelStrategies,
158             labelAlign = me.owner.labelAlign;
159         return strategies[labelAlign] || strategies.base;
160     },
161
162     /**
163      * Return the set of strategy functions from the {@link #errorStrategies errorStrategies collection}
164      * that is appropriate for the field's {@link Ext.form.Labelable#msgTarget msgTarget} config.
165      */
166     getErrorStrategy: function() {
167         var me = this,
168             owner = me.owner,
169             strategies = me.errorStrategies,
170             msgTarget = owner.msgTarget;
171         return !owner.preventMark && Ext.isString(msgTarget) ?
172                 (strategies[msgTarget] || strategies.elementId) :
173                 strategies.none;
174     },
175
176
177
178     /**
179      * Collection of named strategies for laying out and adjusting labels to accommodate error messages.
180      * An appropriate one will be chosen based on the owner field's {@link Ext.form.Labelable#labelAlign} config.
181      */
182     labelStrategies: (function() {
183         var applyIf = Ext.applyIf,
184             emptyFn = Ext.emptyFn,
185             base = {
186                 prepare: function(owner, info) {
187                     var cls = owner.labelCls + '-' + owner.labelAlign,
188                         labelEl = owner.labelEl;
189                     if (labelEl && !labelEl.hasCls(cls)) {
190                         labelEl.addCls(cls);
191                     }
192                 },
193                 adjustHorizInsets: emptyFn,
194                 adjustVertInsets: emptyFn,
195                 layoutHoriz: emptyFn,
196                 layoutVert: emptyFn
197             },
198             left = applyIf({
199                 prepare: function(owner, info) {
200                     base.prepare(owner, info);
201                     // If auto width, add the label width to the body's natural width.
202                     if (info.autoWidth) {
203                         info.width += (!owner.labelEl ? 0 : owner.labelWidth + owner.labelPad);
204                     }
205                     // Must set outer width to prevent field from wrapping below floated label
206                     info.setOuterWidth = true;
207                 },
208                 adjustHorizInsets: function(owner, info) {
209                     if (owner.labelEl) {
210                         info.insets.left += owner.labelWidth + owner.labelPad;
211                     }
212                 },
213                 layoutHoriz: function(owner, info) {
214                     // For content-box browsers we can't rely on Labelable.js#getLabelableRenderData
215                     // setting the width style because it needs to account for the final calculated
216                     // padding/border styles for the label. So we set the width programmatically here to
217                     // normalize content-box sizing, while letting border-box browsers use the original
218                     // width style.
219                     var labelEl = owner.labelEl;
220                     if (labelEl && !owner.isLabelSized && !Ext.isBorderBox) {
221                         labelEl.setWidth(owner.labelWidth);
222                         owner.isLabelSized = true;
223                     }
224                 }
225             }, base);
226
227
228         return {
229             base: base,
230
231             /**
232              * Label displayed above the bodyEl
233              */
234             top: applyIf({
235                 adjustVertInsets: function(owner, info) {
236                     var labelEl = owner.labelEl;
237                     if (labelEl) {
238                         info.insets.top += Ext.util.TextMetrics.measure(labelEl, owner.fieldLabel, info.width).height +
239                                            labelEl.getFrameWidth('tb') + owner.labelPad;
240                     }
241                 }
242             }, base),
243
244             /**
245              * Label displayed to the left of the bodyEl
246              */
247             left: left,
248
249             /**
250              * Same as left, only difference is text-align in CSS
251              */
252             right: left
253         };
254     })(),
255
256
257
258     /**
259      * Collection of named strategies for laying out and adjusting insets to accommodate error messages.
260      * An appropriate one will be chosen based on the owner field's {@link Ext.form.Labelable#msgTarget} config.
261      */
262     errorStrategies: (function() {
263         function setDisplayed(el, displayed) {
264             var wasDisplayed = el.getStyle('display') !== 'none';
265             if (displayed !== wasDisplayed) {
266                 el.setDisplayed(displayed);
267             }
268         }
269
270         function setStyle(el, name, value) {
271             if (el.getStyle(name) !== value) {
272                 el.setStyle(name, value);
273             }
274         }
275         
276         function showTip(owner) {
277             var tip = Ext.layout.component.field.Field.tip,
278                 target;
279                 
280             if (tip && tip.isVisible()) {
281                 target = tip.activeTarget;
282                 if (target && target.el === owner.getActionEl().dom) {
283                     tip.toFront(true);
284                 }
285             }
286         }
287
288         var applyIf = Ext.applyIf,
289             emptyFn = Ext.emptyFn,
290             base = {
291                 prepare: function(owner) {
292                     setDisplayed(owner.errorEl, false);
293                 },
294                 adjustHorizInsets: emptyFn,
295                 adjustVertInsets: emptyFn,
296                 layoutHoriz: emptyFn,
297                 layoutVert: emptyFn,
298                 onFocus: emptyFn
299             };
300
301         return {
302             none: base,
303
304             /**
305              * Error displayed as icon (with QuickTip on hover) to right of the bodyEl
306              */
307             side: applyIf({
308                 prepare: function(owner) {
309                     var errorEl = owner.errorEl;
310                     errorEl.addCls(Ext.baseCSSPrefix + 'form-invalid-icon');
311                     Ext.layout.component.field.Field.initTip();
312                     errorEl.dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
313                     setDisplayed(errorEl, owner.hasActiveError());
314                 },
315                 adjustHorizInsets: function(owner, info) {
316                     if (owner.autoFitErrors && owner.hasActiveError()) {
317                         info.insets.right += owner.errorEl.getWidth();
318                     }
319                 },
320                 layoutHoriz: function(owner, info) {
321                     if (owner.hasActiveError()) {
322                         setStyle(owner.errorEl, 'left', info.width - info.insets.right + 'px');
323                     }
324                 },
325                 layoutVert: function(owner, info) {
326                     if (owner.hasActiveError()) {
327                         setStyle(owner.errorEl, 'top', info.insets.top + 'px');
328                     }
329                 },
330                 onFocus: showTip
331             }, base),
332
333             /**
334              * Error message displayed underneath the bodyEl
335              */
336             under: applyIf({
337                 prepare: function(owner) {
338                     var errorEl = owner.errorEl,
339                         cls = Ext.baseCSSPrefix + 'form-invalid-under';
340                     if (!errorEl.hasCls(cls)) {
341                         errorEl.addCls(cls);
342                     }
343                     setDisplayed(errorEl, owner.hasActiveError());
344                 },
345                 adjustVertInsets: function(owner, info) {
346                     if (owner.autoFitErrors) {
347                         info.insets.bottom += owner.errorEl.getHeight();
348                     }
349                 },
350                 layoutHoriz: function(owner, info) {
351                     var errorEl = owner.errorEl,
352                         insets = info.insets;
353
354                     setStyle(errorEl, 'width', info.width - insets.right - insets.left + 'px');
355                     setStyle(errorEl, 'marginLeft', insets.left + 'px');
356                 }
357             }, base),
358
359             /**
360              * Error displayed as QuickTip on hover of the field container
361              */
362             qtip: applyIf({
363                 prepare: function(owner) {
364                     setDisplayed(owner.errorEl, false);
365                     Ext.layout.component.field.Field.initTip();
366                     owner.getActionEl().dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
367                 },
368                 onFocus: showTip
369             }, base),
370
371             /**
372              * Error displayed as title tip on hover of the field container
373              */
374             title: applyIf({
375                 prepare: function(owner) {
376                     setDisplayed(owner.errorEl, false);
377                     owner.el.dom.title = owner.getActiveError() || '';
378                 }
379             }, base),
380
381             /**
382              * Error message displayed as content of an element with a given id elsewhere in the app
383              */
384             elementId: applyIf({
385                 prepare: function(owner) {
386                     setDisplayed(owner.errorEl, false);
387                     var targetEl = Ext.fly(owner.msgTarget);
388                     if (targetEl) {
389                         targetEl.dom.innerHTML = owner.getActiveError() || '';
390                         targetEl.setDisplayed(owner.hasActiveError());
391                     }
392                 }
393             }, base)
394         };
395     })(),
396
397     statics: {
398         /**
399          * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we
400          * can give it a custom frame style. Responds to errorqtip rather than the qtip property.
401          */
402         initTip: function() {
403             var tip = this.tip;
404             if (!tip) {
405                 tip = this.tip = Ext.create('Ext.tip.QuickTip', {
406                     baseCls: Ext.baseCSSPrefix + 'form-invalid-tip',
407                     renderTo: Ext.getBody()
408                 });
409                 tip.tagConfig = Ext.apply({}, {attribute: 'errorqtip'}, tip.tagConfig);
410             }
411         },
412
413         /**
414          * Destroy the error tip instance.
415          */
416         destroyTip: function() {
417             var tip = this.tip;
418             if (tip) {
419                 tip.destroy();
420                 delete this.tip;
421             }
422         }
423     }
424
425 });
426