Upgrade to ExtJS 3.2.2 - Released 06/02/2010
[extjs.git] / src / widgets / form / CheckboxGroup.js
1 /*!
2  * Ext JS Library 3.2.2
3  * Copyright(c) 2006-2010 Ext JS, Inc.
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     /**
241      * Runs CheckboxGroup's validations and returns an array of any errors. The only error by default
242      * is if allowBlank is set to true and no items are checked.
243      * @return {Array} Array of all validation errors
244      */
245     getErrors: function() {
246         var errors = Ext.form.CheckboxGroup.superclass.getErrors.apply(this, arguments);
247         
248         if (!this.allowBlank) {
249             var blank = true;
250             
251             this.eachItem(function(f){
252                 if (f.checked) {
253                     return (blank = false);
254                 }
255             });
256             
257             if (blank) errors.push(this.blankText);
258         }
259         
260         return errors;
261     },
262
263     // private
264     isDirty: function(){
265         //override the behaviour to check sub items.
266         if (this.disabled || !this.rendered) {
267             return false;
268         }
269
270         var dirty = false;
271         
272         this.eachItem(function(item){
273             if(item.isDirty()){
274                 dirty = true;
275                 return false;
276             }
277         });
278         
279         return dirty;
280     },
281
282     // private
283     setReadOnly : function(readOnly){
284         if(this.rendered){
285             this.eachItem(function(item){
286                 item.setReadOnly(readOnly);
287             });
288         }
289         this.readOnly = readOnly;
290     },
291
292     // private
293     onDisable : function(){
294         this.eachItem(function(item){
295             item.disable();
296         });
297     },
298
299     // private
300     onEnable : function(){
301         this.eachItem(function(item){
302             item.enable();
303         });
304     },
305
306     // private
307     onResize : function(w, h){
308         this.panel.setSize(w, h);
309         this.panel.doLayout();
310     },
311
312     // inherit docs from Field
313     reset : function(){
314         if (this.originalValue) {
315             // Clear all items
316             this.eachItem(function(c){
317                 if(c.setValue){
318                     c.setValue(false);
319                     c.originalValue = c.getValue();
320                 }
321             });
322             // Set items stored in originalValue, ugly - set a flag to reset the originalValue
323             // during the horrible onSetValue.  This will allow trackResetOnLoad to function.
324             this.resetOriginal = true;
325             this.setValue(this.originalValue);
326             delete this.resetOriginal;
327         } else {
328             this.eachItem(function(c){
329                 if(c.reset){
330                     c.reset();
331                 }
332             });
333         }
334         // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete.
335         // Important because reset is being called on both the group and the individual items.
336         (function() {
337             this.clearInvalid();
338         }).defer(50, this);
339     },
340
341     /**
342      * {@link Ext.form.Checkbox#setValue Set the value(s)} of an item or items
343      * in the group. Examples illustrating how this method may be called:
344      * <pre><code>
345 // call with name and value
346 myCheckboxGroup.setValue('cb-col-1', true);
347 // call with an array of boolean values
348 myCheckboxGroup.setValue([true, false, false]);
349 // call with an object literal specifying item:value pairs
350 myCheckboxGroup.setValue({
351     'cb-col-2': false,
352     'cb-col-3': true
353 });
354 // use comma separated string to set items with name to true (checked)
355 myCheckboxGroup.setValue('cb-col-1,cb-col-3');
356      * </code></pre>
357      * See {@link Ext.form.Checkbox#setValue} for additional information.
358      * @param {Mixed} id The checkbox to check, or as described by example shown.
359      * @param {Boolean} value (optional) The value to set the item.
360      * @return {Ext.form.CheckboxGroup} this
361      */
362     setValue: function(){
363         if(this.rendered){
364             this.onSetValue.apply(this, arguments);
365         }else{
366             this.buffered = true;
367             this.value = arguments;
368         }
369         return this;
370     },
371
372     /**
373      * @private
374      * Sets the values of one or more of the items within the CheckboxGroup
375      * @param {String|Array|Object} id Can take multiple forms. Can be optionally:
376      * <ul>
377      *   <li>An ID string to be used with a second argument</li>
378      *   <li>An array of the form ['some', 'list', 'of', 'ids', 'to', 'mark', 'checked']</li>
379      *   <li>An array in the form [true, true, false, true, false] etc, where each item relates to the check status of
380      *       the checkbox at the same index</li>
381      *   <li>An object containing ids of the checkboxes as keys and check values as properties</li>
382      * </ul>
383      * @param {String} value The value to set the field to if the first argument was a string
384      */
385     onSetValue: function(id, value){
386         if(arguments.length == 1){
387             if(Ext.isArray(id)){
388                 Ext.each(id, function(val, idx){
389                     if (Ext.isObject(val) && val.setValue){ // array of checkbox components to be checked
390                         val.setValue(true);
391                         if (this.resetOriginal === true) {
392                             val.originalValue = val.getValue();
393                         }
394                     } else { // an array of boolean values
395                         var item = this.items.itemAt(idx);
396                         if(item){
397                             item.setValue(val);
398                         }
399                     }
400                 }, this);
401             }else if(Ext.isObject(id)){
402                 // set of name/value pairs
403                 for(var i in id){
404                     var f = this.getBox(i);
405                     if(f){
406                         f.setValue(id[i]);
407                     }
408                 }
409             }else{
410                 this.setValueForItem(id);
411             }
412         }else{
413             var f = this.getBox(id);
414             if(f){
415                 f.setValue(value);
416             }
417         }
418     },
419
420     // private
421     beforeDestroy: function(){
422         Ext.destroy(this.panel);
423         Ext.form.CheckboxGroup.superclass.beforeDestroy.call(this);
424
425     },
426
427     setValueForItem : function(val){
428         val = String(val).split(',');
429         this.eachItem(function(item){
430             if(val.indexOf(item.inputValue)> -1){
431                 item.setValue(true);
432             }
433         });
434     },
435
436     // private
437     getBox : function(id){
438         var box = null;
439         this.eachItem(function(f){
440             if(id == f || f.dataIndex == id || f.id == id || f.getName() == id){
441                 box = f;
442                 return false;
443             }
444         });
445         return box;
446     },
447
448     /**
449      * Gets an array of the selected {@link Ext.form.Checkbox} in the group.
450      * @return {Array} An array of the selected checkboxes.
451      */
452     getValue : function(){
453         var out = [];
454         this.eachItem(function(item){
455             if(item.checked){
456                 out.push(item);
457             }
458         });
459         return out;
460     },
461
462     /**
463      * @private
464      * Convenience function which passes the given function to every item in the composite
465      * @param {Function} fn The function to call
466      * @param {Object} scope Optional scope object
467      */
468     eachItem: function(fn, scope) {
469         if(this.items && this.items.each){
470             this.items.each(fn, scope || this);
471         }
472     },
473
474     /**
475      * @cfg {String} name
476      * @hide
477      */
478
479     /**
480      * @method getRawValue
481      * @hide
482      */
483     getRawValue : Ext.emptyFn,
484
485     /**
486      * @method setRawValue
487      * @hide
488      */
489     setRawValue : Ext.emptyFn
490
491 });
492
493 Ext.reg('checkboxgroup', Ext.form.CheckboxGroup);