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