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