]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the\r | |
3 | * {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is rendered with\r | |
4 | * a {@link #fieldLabel field label} and optional {@link #msgTarget error message} around its sub-items.\r | |
5 | * This is useful for arranging a group of fields or other components within a single item in a form, so\r | |
6 | * that it lines up nicely with other fields. A common use is for grouping a set of related fields under\r | |
7 | * a single label in a form.\r | |
8 | * \r | |
9 | * The container's configured {@link #cfg-items} will be layed out within the field body area according to the\r | |
10 | * configured {@link #layout} type. The default layout is `'autocontainer'`.\r | |
11 | * \r | |
12 | * Like regular fields, FieldContainer can inherit its decoration configuration from the\r | |
13 | * {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition,\r | |
14 | * FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields}\r | |
15 | * it may itself contain.\r | |
16 | * \r | |
17 | * If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or {@link Ext.form.field.Radio Radio}\r | |
18 | * fields in a single labeled container, consider using a {@link Ext.form.CheckboxGroup}\r | |
19 | * or {@link Ext.form.RadioGroup} instead as they are specialized for handling those types.\r | |
20 | *\r | |
21 | * # Example\r | |
22 | * \r | |
23 | * @example\r | |
24 | * Ext.create('Ext.form.Panel', {\r | |
25 | * title: 'FieldContainer Example',\r | |
26 | * width: 550,\r | |
27 | * bodyPadding: 10,\r | |
28 | * \r | |
29 | * items: [{\r | |
30 | * xtype: 'fieldcontainer',\r | |
31 | * fieldLabel: 'Last Three Jobs',\r | |
32 | * labelWidth: 100,\r | |
33 | * \r | |
34 | * // The body area will contain three text fields, arranged\r | |
35 | * // horizontally, separated by draggable splitters.\r | |
36 | * layout: 'hbox',\r | |
37 | * items: [{\r | |
38 | * xtype: 'textfield',\r | |
39 | * flex: 1\r | |
40 | * }, {\r | |
41 | * xtype: 'splitter'\r | |
42 | * }, {\r | |
43 | * xtype: 'textfield',\r | |
44 | * flex: 1\r | |
45 | * }, {\r | |
46 | * xtype: 'splitter'\r | |
47 | * }, {\r | |
48 | * xtype: 'textfield',\r | |
49 | * flex: 1\r | |
50 | * }]\r | |
51 | * }],\r | |
52 | * renderTo: Ext.getBody()\r | |
53 | * });\r | |
54 | * \r | |
55 | * # Usage of fieldDefaults\r | |
56 | *\r | |
57 | * @example\r | |
58 | * Ext.create('Ext.form.Panel', {\r | |
59 | * title: 'FieldContainer Example',\r | |
60 | * width: 350,\r | |
61 | * bodyPadding: 10,\r | |
62 | * \r | |
63 | * items: [{\r | |
64 | * xtype: 'fieldcontainer',\r | |
65 | * fieldLabel: 'Your Name',\r | |
66 | * labelWidth: 75,\r | |
67 | * defaultType: 'textfield',\r | |
68 | * \r | |
69 | * // Arrange fields vertically, stretched to full width\r | |
70 | * layout: 'anchor',\r | |
71 | * defaults: {\r | |
72 | * layout: '100%'\r | |
73 | * },\r | |
74 | * \r | |
75 | * // These config values will be applied to both sub-fields, except\r | |
76 | * // for Last Name which will use its own msgTarget.\r | |
77 | * fieldDefaults: {\r | |
78 | * msgTarget: 'under',\r | |
79 | * labelAlign: 'top'\r | |
80 | * },\r | |
81 | * \r | |
82 | * items: [{\r | |
83 | * fieldLabel: 'First Name',\r | |
84 | * name: 'firstName'\r | |
85 | * }, {\r | |
86 | * fieldLabel: 'Last Name',\r | |
87 | * name: 'lastName',\r | |
88 | * msgTarget: 'under'\r | |
89 | * }]\r | |
90 | * }],\r | |
91 | * renderTo: Ext.getBody()\r | |
92 | * });\r | |
93 | */\r | |
94 | Ext.define('Ext.form.FieldContainer', {\r | |
95 | extend: 'Ext.container.Container',\r | |
96 | mixins: {\r | |
97 | labelable: 'Ext.form.Labelable',\r | |
98 | fieldAncestor: 'Ext.form.FieldAncestor'\r | |
99 | },\r | |
100 | requires: 'Ext.layout.component.field.FieldContainer',\r | |
101 | \r | |
102 | alias: 'widget.fieldcontainer',\r | |
103 | \r | |
104 | componentLayout: 'fieldcontainer',\r | |
105 | \r | |
106 | componentCls: Ext.baseCSSPrefix + 'form-fieldcontainer',\r | |
107 | \r | |
108 | shrinkWrap: true,\r | |
109 | \r | |
110 | autoEl: {\r | |
111 | tag: 'div',\r | |
112 | role: 'presentation'\r | |
113 | },\r | |
114 | \r | |
115 | childEls: [\r | |
116 | 'containerEl'\r | |
117 | ],\r | |
118 | \r | |
119 | /**\r | |
120 | * @cfg {Boolean} combineLabels\r | |
121 | * If set to true, and there is no defined {@link #fieldLabel}, the field container will automatically\r | |
122 | * generate its label by combining the labels of all the fields it contains. Defaults to false.\r | |
123 | */\r | |
124 | combineLabels: false,\r | |
125 | \r | |
126 | //<locale>\r | |
127 | /**\r | |
128 | * @cfg {String} labelConnector\r | |
129 | * The string to use when joining the labels of individual sub-fields, when {@link #combineLabels} is\r | |
130 | * set to true. Defaults to ', '.\r | |
131 | */\r | |
132 | labelConnector: ', ',\r | |
133 | //</locale>\r | |
134 | \r | |
135 | /**\r | |
136 | * @cfg {Boolean} combineErrors\r | |
137 | * If set to true, the field container will automatically combine and display the validation errors from\r | |
138 | * all the fields it contains as a single error on the container, according to the configured\r | |
139 | * {@link #msgTarget}. Defaults to false.\r | |
140 | */\r | |
141 | combineErrors: false,\r | |
142 | \r | |
143 | maskOnDisable: false,\r | |
144 | // If we allow this to mark with the invalidCls it will cascade to all\r | |
145 | // child fields, let them handle themselves\r | |
146 | invalidCls: '',\r | |
147 | \r | |
148 | fieldSubTpl: [\r | |
149 | '<div id="{id}-containerEl" data-ref="containerEl" class="{containerElCls}"',\r | |
150 | '<tpl if="ariaAttributes">',\r | |
151 | '<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>',\r | |
152 | '<tpl else>',\r | |
153 | ' role="presentation"',\r | |
154 | '</tpl>',\r | |
155 | '>',\r | |
156 | '{%this.renderContainer(out,values)%}',\r | |
157 | '</div>'\r | |
158 | ],\r | |
159 | \r | |
160 | initComponent: function() {\r | |
161 | var me = this;\r | |
162 | \r | |
163 | // Init mixins\r | |
164 | me.initLabelable();\r | |
165 | me.initFieldAncestor();\r | |
166 | \r | |
167 | me.callParent();\r | |
168 | me.initMonitor();\r | |
169 | },\r | |
170 | \r | |
171 | /**\r | |
172 | * @protected\r | |
173 | * Called when a {@link Ext.form.Labelable} instance is added to the container's subtree.\r | |
174 | * @param {Ext.form.Labelable} labelItem The instance that was added\r | |
175 | */\r | |
176 | onAdd: function(labelItem) {\r | |
177 | var me = this;\r | |
178 | \r | |
179 | // Fix for https://sencha.jira.com/browse/EXTJSIV-6424 Which was *sneakily* fixed fixed in version 37\r | |
180 | // In FF < 37, positioning absolutely within a TD positions relative to the TR!\r | |
181 | // So we must add the width of a visible, left-aligned label cell to the x coordinate.\r | |
182 | if (labelItem.isLabelable && Ext.isGecko && Ext.firefoxVersion < 37 && me.layout.type === 'absolute' && !me.hideLabel && me.labelAlign !== 'top') {\r | |
183 | labelItem.x += (me.labelWidth + me.labelPad);\r | |
184 | }\r | |
185 | me.callParent(arguments);\r | |
186 | if (labelItem.isLabelable && me.combineLabels) {\r | |
187 | labelItem.oldHideLabel = labelItem.hideLabel;\r | |
188 | labelItem.hideLabel = true;\r | |
189 | }\r | |
190 | me.updateLabel();\r | |
191 | },\r | |
192 | \r | |
193 | /**\r | |
194 | * @protected\r | |
195 | * Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree.\r | |
196 | * @param {Ext.form.Labelable} labelItem The instance that was removed\r | |
197 | */\r | |
198 | onRemove: function(labelItem, isDestroying) {\r | |
199 | var me = this;\r | |
200 | me.callParent(arguments);\r | |
201 | if (!isDestroying) {\r | |
202 | if (labelItem.isLabelable && me.combineLabels) {\r | |
203 | labelItem.hideLabel = labelItem.oldHideLabel;\r | |
204 | }\r | |
205 | me.updateLabel();\r | |
206 | } \r | |
207 | },\r | |
208 | \r | |
209 | initRenderData: function() {\r | |
210 | var me = this,\r | |
211 | data = me.callParent();\r | |
212 | \r | |
213 | data.containerElCls = me.containerElCls;\r | |
214 | data = Ext.applyIf(data, me.getLabelableRenderData());\r | |
215 | data.tipAnchorTarget = me.id + '-containerEl';\r | |
216 | return data;\r | |
217 | },\r | |
218 | \r | |
219 | /**\r | |
220 | * Returns the combined field label if {@link #combineLabels} is set to true and if there is no\r | |
221 | * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override\r | |
222 | * this method to provide a custom generated label.\r | |
223 | * @template\r | |
224 | * @return {String} The label, or empty string if none.\r | |
225 | */\r | |
226 | getFieldLabel: function() {\r | |
227 | var label = this.fieldLabel || '';\r | |
228 | if (!label && this.combineLabels) {\r | |
229 | label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) {\r | |
230 | return field.getFieldLabel();\r | |
231 | }).join(this.labelConnector);\r | |
232 | }\r | |
233 | return label;\r | |
234 | },\r | |
235 | \r | |
236 | getSubTplData: function() {\r | |
237 | var ret = this.initRenderData();\r | |
238 | \r | |
239 | Ext.apply(ret, this.subTplData);\r | |
240 | return ret;\r | |
241 | },\r | |
242 | \r | |
243 | getSubTplMarkup: function(fieldData) {\r | |
244 | var me = this,\r | |
245 | tpl = me.getTpl('fieldSubTpl'),\r | |
246 | html;\r | |
247 | \r | |
248 | if (!tpl.renderContent) {\r | |
249 | me.setupRenderTpl(tpl);\r | |
250 | }\r | |
251 | \r | |
252 | html = tpl.apply(me.getSubTplData(fieldData));\r | |
253 | return html;\r | |
254 | },\r | |
255 | \r | |
256 | /**\r | |
257 | * @private\r | |
258 | * Updates the content of the labelEl if it is rendered\r | |
259 | */\r | |
260 | updateLabel: function() {\r | |
261 | var me = this,\r | |
262 | label = me.labelEl;\r | |
263 | \r | |
264 | if (label) {\r | |
265 | me.setFieldLabel(me.getFieldLabel());\r | |
266 | }\r | |
267 | },\r | |
268 | \r | |
269 | \r | |
270 | /**\r | |
271 | * @private\r | |
272 | * Fired when the error message of any field within the container changes, and updates the\r | |
273 | * combined error message to match.\r | |
274 | */\r | |
275 | onFieldErrorChange: function() {\r | |
276 | if (this.combineErrors) {\r | |
277 | var me = this,\r | |
278 | oldError = me.getActiveError(),\r | |
279 | invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) {\r | |
280 | return field.hasActiveError();\r | |
281 | }),\r | |
282 | newErrors = me.getCombinedErrors(invalidFields);\r | |
283 | \r | |
284 | if (newErrors) {\r | |
285 | me.setActiveErrors(newErrors);\r | |
286 | } else {\r | |
287 | me.unsetActiveError();\r | |
288 | }\r | |
289 | \r | |
290 | if (oldError !== me.getActiveError()) {\r | |
291 | me.updateLayout();\r | |
292 | }\r | |
293 | }\r | |
294 | },\r | |
295 | \r | |
296 | /**\r | |
297 | * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list of error\r | |
298 | * messages from them. Defaults to prepending each message by the field name and a colon. This\r | |
299 | * can be overridden to provide custom combined error message handling, for instance changing\r | |
300 | * the format of each message or sorting the array (it is sorted in order of appearance by default).\r | |
301 | * @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently invalid.\r | |
302 | * @return {String[]} The combined list of error messages\r | |
303 | */\r | |
304 | getCombinedErrors: function(invalidFields) {\r | |
305 | var errors = [],\r | |
306 | f,\r | |
307 | fLen = invalidFields.length,\r | |
308 | field,\r | |
309 | activeErrors, a, aLen,\r | |
310 | error, label;\r | |
311 | \r | |
312 | for (f = 0; f < fLen; f++) {\r | |
313 | field = invalidFields[f];\r | |
314 | activeErrors = field.getActiveErrors();\r | |
315 | aLen = activeErrors.length;\r | |
316 | \r | |
317 | for (a = 0; a < aLen; a++) {\r | |
318 | error = activeErrors[a];\r | |
319 | label = field.getFieldLabel();\r | |
320 | \r | |
321 | errors.push((label ? label + ': ' : '') + error);\r | |
322 | }\r | |
323 | }\r | |
324 | \r | |
325 | return errors;\r | |
326 | },\r | |
327 | \r | |
328 | privates: {\r | |
329 | applyTargetCls: function(targetCls) {\r | |
330 | var containerElCls = this.containerElCls;\r | |
331 | \r | |
332 | this.containerElCls = containerElCls ? containerElCls + ' ' + targetCls : targetCls;\r | |
333 | },\r | |
334 | \r | |
335 | getTargetEl: function() {\r | |
336 | return this.containerEl;\r | |
337 | },\r | |
338 | \r | |
339 | initRenderTpl: function() {\r | |
340 | var me = this;\r | |
341 | if (!me.hasOwnProperty('renderTpl')) {\r | |
342 | me.renderTpl = me.getTpl('labelableRenderTpl');\r | |
343 | }\r | |
344 | return me.callParent();\r | |
345 | }\r | |
346 | }\r | |
347 | });\r |