Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / examples / ux / form / MultiSelect.js
1 /**
2  * @class Ext.ux.form.MultiSelect
3  * @extends Ext.form.field.Base
4  * A control that allows selection and form submission of multiple list items.
5  *
6  *  @history
7  *    2008-06-19 bpm Original code contributed by Toby Stuart (with contributions from Robert Williams)
8  *    2008-06-19 bpm Docs and demo code clean up
9  *
10  * @constructor
11  * Create a new MultiSelect
12  * @param {Object} config Configuration options
13  * @xtype multiselect
14  */
15 Ext.define('Ext.ux.form.MultiSelect', {
16     extend: 'Ext.form.field.Base',
17     alternateClassName: 'Ext.ux.Multiselect',
18     alias: ['widget.multiselect', 'widget.multiselectfield'],
19     uses: [
20         'Ext.view.BoundList',
21         'Ext.form.FieldSet',
22         'Ext.ux.layout.component.form.MultiSelect',
23         'Ext.view.DragZone',
24         'Ext.view.DropZone'
25     ],
26
27     /**
28      * @cfg {String} listTitle An optional title to be displayed at the top of the selection list.
29      */
30
31     /**
32      * @cfg {String/Array} dragGroup The ddgroup name(s) for the MultiSelect DragZone (defaults to undefined).
33      */
34
35     /**
36      * @cfg {String/Array} dropGroup The ddgroup name(s) for the MultiSelect DropZone (defaults to undefined).
37      */
38
39     /**
40      * @cfg {Boolean} ddReorder Whether the items in the MultiSelect list are drag/drop reorderable (defaults to false).
41      */
42     ddReorder: false,
43
44     /**
45      * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
46      * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
47      * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
48      */
49
50     /**
51      * @cfg {String} appendOnly True if the list should only allow append drops when drag/drop is enabled
52      * (use for lists which are sorted, defaults to false).
53      */
54     appendOnly: false,
55
56     /**
57      * @cfg {String} displayField Name of the desired display field in the dataset (defaults to 'text').
58      */
59     displayField: 'text',
60
61     /**
62      * @cfg {String} valueField Name of the desired value field in the dataset (defaults to the
63      * value of {@link #displayField}).
64      */
65
66     /**
67      * @cfg {Boolean} allowBlank False to require at least one item in the list to be selected, true to allow no
68      * selection (defaults to true).
69      */
70     allowBlank: true,
71
72     /**
73      * @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
74      */
75     minSelections: 0,
76
77     /**
78      * @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
79      */
80     maxSelections: Number.MAX_VALUE,
81
82     /**
83      * @cfg {String} blankText Default text displayed when the control contains no items (defaults to 'This field is required')
84      */
85     blankText: 'This field is required',
86
87     /**
88      * @cfg {String} minSelectionsText Validation message displayed when {@link #minSelections} is not met (defaults to 'Minimum {0}
89      * item(s) required').  The {0} token will be replaced by the value of {@link #minSelections}.
90      */
91     minSelectionsText: 'Minimum {0} item(s) required',
92
93     /**
94      * @cfg {String} maxSelectionsText Validation message displayed when {@link #maxSelections} is not met (defaults to 'Maximum {0}
95      * item(s) allowed').  The {0} token will be replaced by the value of {@link #maxSelections}.
96      */
97     maxSelectionsText: 'Maximum {0} item(s) allowed',
98
99     /**
100      * @cfg {String} delimiter The string used to delimit the selected values when {@link #getSubmitValue submitting}
101      * the field as part of a form. Defaults to ','. If you wish to have the selected values submitted as separate
102      * parameters rather than a single delimited parameter, set this to <tt>null</tt>.
103      */
104     delimiter: ',',
105
106     /**
107      * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
108      * Acceptable values for this property are:
109      * <div class="mdetail-params"><ul>
110      * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
111      * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
112      * <div class="mdetail-params"><ul>
113      * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
114      * A 1-dimensional array will automatically be expanded (each array item will be the combo
115      * {@link #valueField value} and {@link #displayField text})</div></li>
116      * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
117      * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
118      * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
119      * </div></li></ul></div></li></ul></div>
120      */
121
122     componentLayout: 'multiselectfield',
123
124     fieldBodyCls: Ext.baseCSSPrefix + 'form-multiselect-body',
125
126
127     // private
128     initComponent: function(){
129         var me = this;
130
131         me.bindStore(me.store, true);
132         if (me.store.autoCreated) {
133             me.valueField = me.displayField = 'field1';
134             if (!me.store.expanded) {
135                 me.displayField = 'field2';
136             }
137         }
138
139         if (!Ext.isDefined(me.valueField)) {
140             me.valueField = me.displayField;
141         }
142
143         me.callParent();
144     },
145
146     bindStore: function(store, initial) {
147         var me = this,
148             oldStore = me.store,
149             boundList = me.boundList;
150
151         if (oldStore && !initial && oldStore !== store && oldStore.autoDestroy) {
152             oldStore.destroy();
153         }
154
155         me.store = store ? Ext.data.StoreManager.lookup(store) : null;
156         if (boundList) {
157             boundList.bindStore(store || null);
158         }
159     },
160
161
162     // private
163     onRender: function(ct, position) {
164         var me = this,
165             panel, boundList, selModel;
166
167         me.callParent(arguments);
168
169         boundList = me.boundList = Ext.create('Ext.view.BoundList', {
170             multiSelect: true,
171             store: me.store,
172             displayField: me.displayField,
173             border: false
174         });
175
176         selModel = boundList.getSelectionModel();
177         me.mon(selModel, {
178             selectionChange: me.onSelectionChange,
179             scope: me
180         });
181
182         panel = me.panel = Ext.create('Ext.panel.Panel', {
183             title: me.listTitle,
184             tbar: me.tbar,
185             items: [boundList],
186             renderTo: me.bodyEl,
187             layout: 'fit'
188         });
189
190         // Must set upward link after first render
191         panel.ownerCt = me;
192
193         // Set selection to current value
194         me.setRawValue(me.rawValue);
195     },
196
197     // No content generated via template, it's all added components
198     getSubTplMarkup: function() {
199         return '';
200     },
201
202     // private
203     afterRender: function() {
204         var me = this;
205         me.callParent();
206
207         if (me.ddReorder && !me.dragGroup && !me.dropGroup){
208             me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
209         }
210
211         if (me.draggable || me.dragGroup){
212             me.dragZone = Ext.create('Ext.view.DragZone', {
213                 view: me.boundList,
214                 ddGroup: me.dragGroup,
215                 dragText: '{0} Item{1}'
216             });
217         }
218         if (me.droppable || me.dropGroup){
219             me.dropZone = Ext.create('Ext.view.DropZone', {
220                 view: me.boundList,
221                 ddGroup: me.dropGroup,
222                 handleNodeDrop: function(data, dropRecord, position) {
223                     var view = this.view,
224                         store = view.getStore(),
225                         records = data.records,
226                         index;
227
228                     // remove the Models from the source Store
229                     data.view.store.remove(records);
230
231                     index = store.indexOf(dropRecord);
232                     if (position === 'after') {
233                         index++;
234                     }
235                     store.insert(index, records);
236                     view.getSelectionModel().select(records);
237                 }
238             });
239         }
240     },
241
242     onSelectionChange: function() {
243         this.checkChange();
244     },
245
246     /**
247      * Clears any values currently selected.
248      */
249     clearValue: function() {
250         this.setValue([]);
251     },
252
253     /**
254      * Return the value(s) to be submitted for this field. The returned value depends on the {@link #delimiter}
255      * config: If it is set to a String value (like the default ',') then this will return the selected values
256      * joined by the delimiter. If it is set to <tt>null</tt> then the values will be returned as an Array.
257      */
258     getSubmitValue: function() {
259         var me = this,
260             delimiter = me.delimiter,
261             val = me.getValue();
262         return Ext.isString(delimiter) ? val.join(delimiter) : val;
263     },
264
265     // inherit docs
266     getRawValue: function() {
267         var me = this,
268             boundList = me.boundList;
269         if (boundList) {
270             me.rawValue = Ext.Array.map(boundList.getSelectionModel().getSelection(), function(model) {
271                 return model.get(me.valueField);
272             });
273         }
274         return me.rawValue;
275     },
276
277     // inherit docs
278     setRawValue: function(value) {
279         var me = this,
280             boundList = me.boundList,
281             models;
282
283         value = Ext.Array.from(value);
284         me.rawValue = value;
285
286         if (boundList) {
287             models = [];
288             Ext.Array.forEach(value, function(val) {
289                 var undef,
290                     model = me.store.findRecord(me.valueField, val, undef, undef, true, true);
291                 if (model) {
292                     models.push(model);
293                 }
294             });
295             boundList.getSelectionModel().select(models, false, true);
296         }
297
298         return value;
299     },
300
301     // no conversion
302     valueToRaw: function(value) {
303         return value;
304     },
305
306     // compare array values
307     isEqual: function(v1, v2) {
308         var fromArray = Ext.Array.from,
309             i, len;
310
311         v1 = fromArray(v1);
312         v2 = fromArray(v2);
313         len = v1.length;
314
315         if (len !== v2.length) {
316             return false;
317         }
318
319         for(i = 0; i < len; i++) {
320             if (v2[i] !== v1[i]) {
321                 return false;
322             }
323         }
324
325         return true;
326     },
327
328     getErrors : function(value) {
329         var me = this,
330             format = Ext.String.format,
331             errors = me.callParent(arguments),
332             numSelected;
333
334         value = Ext.Array.from(value || me.getValue());
335         numSelected = value.length;
336
337         if (!me.allowBlank && numSelected < 1) {
338             errors.push(me.blankText);
339         }
340         if (numSelected < this.minSelections) {
341             errors.push(format(me.minSelectionsText, me.minSelections));
342         }
343         if (numSelected > this.maxSelections) {
344             errors.push(format(me.maxSelectionsText, me.maxSelections));
345         }
346
347         return errors;
348     },
349
350     onDisable: function() {
351         this.callParent();
352         this.disabled = true;
353         this.updateReadOnly();
354     },
355
356     onEnable: function() {
357         this.callParent();
358         this.disabled = false;
359         this.updateReadOnly();
360     },
361
362     setReadOnly: function(readOnly) {
363         this.readOnly = readOnly;
364         this.updateReadOnly();
365     },
366
367     /**
368      * @private Lock or unlock the BoundList's selection model to match the current disabled/readonly state
369      */
370     updateReadOnly: function() {
371         var me = this,
372             boundList = me.boundList,
373             readOnly = me.readOnly || me.disabled;
374         if (boundList) {
375             boundList.getSelectionModel().setLocked(readOnly);
376         }
377     },
378
379     onDestroy: function(){
380         Ext.destroyMembers(this, 'panel', 'boundList', 'dragZone', 'dropZone');
381         this.callParent();
382     }
383 });
384
385