Upgrade to ExtJS 3.1.1 - Released 02/08/2010
[extjs.git] / src / widgets / form / CheckboxGroup.js
1 /*!
2  * Ext JS Library 3.1.1
3  * Copyright(c) 2006-2010 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.form.CheckboxGroup
9  * @extends Ext.form.Field
10  * <p>A grouping container for {@link Ext.form.Checkbox} controls.</p>
11  * <p>Sample usage:</p>
12  * <pre><code>
13 var myCheckboxGroup = new Ext.form.CheckboxGroup({
14     id:'myGroup',
15     xtype: 'checkboxgroup',
16     fieldLabel: 'Single Column',
17     itemCls: 'x-check-group-alt',
18     // Put all controls in a single column with width 100%
19     columns: 1,
20     items: [
21         {boxLabel: 'Item 1', name: 'cb-col-1'},
22         {boxLabel: 'Item 2', name: 'cb-col-2', checked: true},
23         {boxLabel: 'Item 3', name: 'cb-col-3'}
24     ]
25 });
26  * </code></pre>
27  * @constructor
28  * Creates a new CheckboxGroup
29  * @param {Object} config Configuration options
30  * @xtype checkboxgroup
31  */
32 Ext.form.CheckboxGroup = Ext.extend(Ext.form.Field, {
33     /**
34      * @cfg {Array} items An Array of {@link Ext.form.Checkbox Checkbox}es or Checkbox config objects
35      * to arrange in the group.
36      */
37     /**
38      * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped
39      * checkbox/radio controls using automatic layout.  This config can take several types of values:
40      * <ul><li><b>'auto'</b> : <p class="sub-desc">The controls will be rendered one per column on one row and the width
41      * of each column will be evenly distributed based on the width of the overall field container. This is the default.</p></li>
42      * <li><b>Number</b> : <p class="sub-desc">If you specific a number (e.g., 3) that number of columns will be
43      * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.</p></li>
44      * <li><b>Array</b> : Object<p class="sub-desc">You can also specify an array of column widths, mixing integer
45      * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will
46      * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float
47      * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field
48      * container you should do so.</p></li></ul>
49      */
50     columns : 'auto',
51     /**
52      * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column
53      * top to bottom before starting on the next column.  The number of controls in each column will be automatically
54      * calculated to keep columns as even as possible.  The default value is false, so that controls will be added
55      * to columns one at a time, completely filling each row left to right before starting on the next row.
56      */
57     vertical : false,
58     /**
59      * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true).
60      * If no items are selected at validation time, {@link @blankText} will be used as the error text.
61      */
62     allowBlank : true,
63     /**
64      * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must
65      * select at least one item in this group")
66      */
67     blankText : "You must select at least one item in this group",
68
69     // private
70     defaultType : 'checkbox',
71
72     // private
73     groupCls : 'x-form-check-group',
74
75     // private
76     initComponent: function(){
77         this.addEvents(
78             /**
79              * @event change
80              * Fires when the state of a child checkbox changes.
81              * @param {Ext.form.CheckboxGroup} this
82              * @param {Array} checked An array containing the checked boxes.
83              */
84             'change'
85         );
86         this.on('change', this.validate, this);
87         Ext.form.CheckboxGroup.superclass.initComponent.call(this);
88     },
89
90     // private
91     onRender : function(ct, position){
92         if(!this.el){
93             var panelCfg = {
94                 autoEl: {
95                     id: this.id
96                 },
97                 cls: this.groupCls,
98                 layout: 'column',
99                 renderTo: ct,
100                 bufferResize: false // Default this to false, since it doesn't really have a proper ownerCt.
101             };
102             var colCfg = {
103                 xtype: 'container',
104                 defaultType: this.defaultType,
105                 layout: 'form',
106                 defaults: {
107                     hideLabel: true,
108                     anchor: '100%'
109                 }
110             };
111
112             if(this.items[0].items){
113
114                 // The container has standard ColumnLayout configs, so pass them in directly
115
116                 Ext.apply(panelCfg, {
117                     layoutConfig: {columns: this.items.length},
118                     defaults: this.defaults,
119                     items: this.items
120                 });
121                 for(var i=0, len=this.items.length; i<len; i++){
122                     Ext.applyIf(this.items[i], colCfg);
123                 }
124
125             }else{
126
127                 // The container has field item configs, so we have to generate the column
128                 // panels first then move the items into the columns as needed.
129
130                 var numCols, cols = [];
131
132                 if(typeof this.columns == 'string'){ // 'auto' so create a col per item
133                     this.columns = this.items.length;
134                 }
135                 if(!Ext.isArray(this.columns)){
136                     var cs = [];
137                     for(var i=0; i<this.columns; i++){
138                         cs.push((100/this.columns)*.01); // distribute by even %
139                     }
140                     this.columns = cs;
141                 }
142
143                 numCols = this.columns.length;
144
145                 // Generate the column configs with the correct width setting
146                 for(var i=0; i<numCols; i++){
147                     var cc = Ext.apply({items:[]}, colCfg);
148                     cc[this.columns[i] <= 1 ? 'columnWidth' : 'width'] = this.columns[i];
149                     if(this.defaults){
150                         cc.defaults = Ext.apply(cc.defaults || {}, this.defaults)
151                     }
152                     cols.push(cc);
153                 };
154
155                 // Distribute the original items into the columns
156                 if(this.vertical){
157                     var rows = Math.ceil(this.items.length / numCols), ri = 0;
158                     for(var i=0, len=this.items.length; i<len; i++){
159                         if(i>0 && i%rows==0){
160                             ri++;
161                         }
162                         if(this.items[i].fieldLabel){
163                             this.items[i].hideLabel = false;
164                         }
165                         cols[ri].items.push(this.items[i]);
166                     };
167                 }else{
168                     for(var i=0, len=this.items.length; i<len; i++){
169                         var ci = i % numCols;
170                         if(this.items[i].fieldLabel){
171                             this.items[i].hideLabel = false;
172                         }
173                         cols[ci].items.push(this.items[i]);
174                     };
175                 }
176
177                 Ext.apply(panelCfg, {
178                     layoutConfig: {columns: numCols},
179                     items: cols
180                 });
181             }
182
183             this.panel = new Ext.Container(panelCfg);
184             this.panel.ownerCt = this;
185             this.el = this.panel.getEl();
186
187             if(this.forId && this.itemCls){
188                 var l = this.el.up(this.itemCls).child('label', true);
189                 if(l){
190                     l.setAttribute('htmlFor', this.forId);
191                 }
192             }
193
194             var fields = this.panel.findBy(function(c){
195                 return c.isFormField;
196             }, this);
197
198             this.items = new Ext.util.MixedCollection();
199             this.items.addAll(fields);
200         }
201         Ext.form.CheckboxGroup.superclass.onRender.call(this, ct, position);
202     },
203
204     initValue : function(){
205         if(this.value){
206             this.setValue.apply(this, this.buffered ? this.value : [this.value]);
207             delete this.buffered;
208             delete this.value;
209         }
210     },
211
212     afterRender : function(){
213         Ext.form.CheckboxGroup.superclass.afterRender.call(this);
214         this.eachItem(function(item){
215             item.on('check', this.fireChecked, this);
216             item.inGroup = true;
217         });
218     },
219
220     // private
221     doLayout: function(){
222         //ugly method required to layout hidden items
223         if(this.rendered){
224             this.panel.forceLayout = this.ownerCt.forceLayout;
225             this.panel.doLayout();
226         }
227     },
228
229     // private
230     fireChecked: function(){
231         var arr = [];
232         this.eachItem(function(item){
233             if(item.checked){
234                 arr.push(item);
235             }
236         });
237         this.fireEvent('change', this, arr);
238     },
239
240     // private
241     validateValue : function(value){
242         if(!this.allowBlank){
243             var blank = true;
244             this.eachItem(function(f){
245                 if(f.checked){
246                     return (blank = false);
247                 }
248             });
249             if(blank){
250                 this.markInvalid(this.blankText);
251                 return false;
252             }
253         }
254         return true;
255     },
256
257     // private
258     isDirty: function(){
259         //override the behaviour to check sub items.
260         if (this.disabled || !this.rendered) {
261             return false;
262         }
263
264         var dirty = false;
265         this.eachItem(function(item){
266             if(item.isDirty()){
267                 dirty = true;
268                 return false;
269             }
270         });
271         return dirty;
272     },
273
274     // private
275     onDisable : function(){
276         this.eachItem(function(item){
277             item.disable();
278         });
279     },
280
281     // private
282     onEnable : function(){
283         this.eachItem(function(item){
284             item.enable();
285         });
286     },
287
288     // private
289     doLayout: function(){
290         if(this.rendered){
291             this.panel.forceLayout = this.ownerCt.forceLayout;
292             this.panel.doLayout();
293         }
294     },
295
296     // private
297     onResize : function(w, h){
298         this.panel.setSize(w, h);
299         this.panel.doLayout();
300     },
301
302     // inherit docs from Field
303     reset : function(){
304         this.eachItem(function(c){
305             if(c.reset){
306                 c.reset();
307             }
308         });
309         // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete.
310         // Important because reset is being called on both the group and the individual items.
311         (function() {
312             this.clearInvalid();
313         }).defer(50, this);
314     },
315
316     /**
317      * {@link Ext.form.Checkbox#setValue Set the value(s)} of an item or items
318      * in the group. Examples illustrating how this method may be called:
319      * <pre><code>
320 // call with name and value
321 myCheckboxGroup.setValue('cb-col-1', true);
322 // call with an array of boolean values
323 myCheckboxGroup.setValue([true, false, false]);
324 // call with an object literal specifying item:value pairs
325 myCheckboxGroup.setValue({
326     'cb-col-2': false,
327     'cb-col-3': true
328 });
329 // use comma separated string to set items with name to true (checked)
330 myCheckboxGroup.setValue('cb-col-1,cb-col-3');
331      * </code></pre>
332      * See {@link Ext.form.Checkbox#setValue} for additional information.
333      * @param {Mixed} id The checkbox to check, or as described by example shown.
334      * @param {Boolean} value (optional) The value to set the item.
335      * @return {Ext.form.CheckboxGroup} this
336      */
337     setValue: function(){
338         if(this.rendered){
339             this.onSetValue.apply(this, arguments);
340         }else{
341             this.buffered = true;
342             this.value = arguments;
343         }
344         return this;
345     },
346
347     onSetValue: function(id, value){
348         if(arguments.length == 1){
349             if(Ext.isArray(id)){
350                 // an array of boolean values
351                 Ext.each(id, function(val, idx){
352                     var item = this.items.itemAt(idx);
353                     if(item){
354                         item.setValue(val);
355                     }
356                 }, this);
357             }else if(Ext.isObject(id)){
358                 // set of name/value pairs
359                 for(var i in id){
360                     var f = this.getBox(i);
361                     if(f){
362                         f.setValue(id[i]);
363                     }
364                 }
365             }else{
366                 this.setValueForItem(id);
367             }
368         }else{
369             var f = this.getBox(id);
370             if(f){
371                 f.setValue(value);
372             }
373         }
374     },
375
376     // private
377     beforeDestroy: function(){
378         Ext.destroy(this.panel);
379         Ext.form.CheckboxGroup.superclass.beforeDestroy.call(this);
380
381     },
382
383     setValueForItem : function(val){
384         val = String(val).split(',');
385         this.eachItem(function(item){
386             if(val.indexOf(item.inputValue)> -1){
387                 item.setValue(true);
388             }
389         });
390     },
391
392     // private
393     getBox : function(id){
394         var box = null;
395         this.eachItem(function(f){
396             if(id == f || f.dataIndex == id || f.id == id || f.getName() == id){
397                 box = f;
398                 return false;
399             }
400         });
401         return box;
402     },
403
404     /**
405      * Gets an array of the selected {@link Ext.form.Checkbox} in the group.
406      * @return {Array} An array of the selected checkboxes.
407      */
408     getValue : function(){
409         var out = [];
410         this.eachItem(function(item){
411             if(item.checked){
412                 out.push(item);
413             }
414         });
415         return out;
416     },
417
418     // private
419     eachItem: function(fn){
420         if(this.items && this.items.each){
421             this.items.each(fn, this);
422         }
423     },
424
425     /**
426      * @cfg {String} name
427      * @hide
428      */
429
430     /**
431      * @method getRawValue
432      * @hide
433      */
434     getRawValue : Ext.emptyFn,
435
436     /**
437      * @method setRawValue
438      * @hide
439      */
440     setRawValue : Ext.emptyFn
441
442 });
443
444 Ext.reg('checkboxgroup', Ext.form.CheckboxGroup);