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