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