3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
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.
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
25 * Create a new MultiSelect
26 * @param {Object} config Configuration options
29 Ext.define('Ext.ux.form.MultiSelect', {
30 extend: 'Ext.form.field.Base',
31 alternateClassName: 'Ext.ux.Multiselect',
32 alias: ['widget.multiselect', 'widget.multiselectfield'],
36 'Ext.ux.layout.component.form.MultiSelect',
42 * @cfg {String} listTitle An optional title to be displayed at the top of the selection list.
46 * @cfg {String/Array} dragGroup The ddgroup name(s) for the MultiSelect DragZone (defaults to undefined).
50 * @cfg {String/Array} dropGroup The ddgroup name(s) for the MultiSelect DropZone (defaults to undefined).
54 * @cfg {Boolean} ddReorder Whether the items in the MultiSelect list are drag/drop reorderable (defaults to false).
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}.
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).
71 * @cfg {String} displayField Name of the desired display field in the dataset (defaults to 'text').
76 * @cfg {String} valueField Name of the desired value field in the dataset (defaults to the
77 * value of {@link #displayField}).
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).
87 * @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
92 * @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
94 maxSelections: Number.MAX_VALUE,
97 * @cfg {String} blankText Default text displayed when the control contains no items (defaults to 'This field is required')
99 blankText: 'This field is required',
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}.
105 minSelectionsText: 'Minimum {0} item(s) required',
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}.
111 maxSelectionsText: 'Maximum {0} item(s) allowed',
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>.
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>
136 componentLayout: 'multiselectfield',
138 fieldBodyCls: Ext.baseCSSPrefix + 'form-multiselect-body',
142 initComponent: function(){
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';
153 if (!Ext.isDefined(me.valueField)) {
154 me.valueField = me.displayField;
160 bindStore: function(store, initial) {
163 boundList = me.boundList;
165 if (oldStore && !initial && oldStore !== store && oldStore.autoDestroy) {
166 oldStore.destroyStore();
169 me.store = store ? Ext.data.StoreManager.lookup(store) : null;
171 boundList.bindStore(store || null);
177 onRender: function(ct, position) {
179 panel, boundList, selModel;
181 me.callParent(arguments);
183 boundList = me.boundList = Ext.create('Ext.view.BoundList', {
184 deferInitialRefresh: false,
187 displayField: me.displayField,
189 disabled: me.disabled
192 selModel = boundList.getSelectionModel();
194 selectionChange: me.onSelectionChange,
198 panel = me.panel = Ext.create('Ext.panel.Panel', {
206 // Must set upward link after first render
209 // Set selection to current value
210 me.setRawValue(me.rawValue);
213 // No content generated via template, it's all added components
214 getSubTplMarkup: function() {
219 afterRender: function() {
223 if (me.ddReorder && !me.dragGroup && !me.dropGroup){
224 me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
227 if (me.draggable || me.dragGroup){
228 me.dragZone = Ext.create('Ext.view.DragZone', {
230 ddGroup: me.dragGroup,
231 dragText: '{0} Item{1}'
234 if (me.droppable || me.dropGroup){
235 me.dropZone = Ext.create('Ext.view.DropZone', {
237 ddGroup: me.dropGroup,
238 handleNodeDrop: function(data, dropRecord, position) {
239 var view = this.view,
240 store = view.getStore(),
241 records = data.records,
244 // remove the Models from the source Store
245 data.view.store.remove(records);
247 index = store.indexOf(dropRecord);
248 if (position === 'after') {
251 store.insert(index, records);
252 view.getSelectionModel().select(records);
258 onSelectionChange: function() {
263 * Clears any values currently selected.
265 clearValue: function() {
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.
274 getSubmitValue: function() {
276 delimiter = me.delimiter,
278 return Ext.isString(delimiter) ? val.join(delimiter) : val;
282 getRawValue: function() {
284 boundList = me.boundList;
286 me.rawValue = Ext.Array.map(boundList.getSelectionModel().getSelection(), function(model) {
287 return model.get(me.valueField);
294 setRawValue: function(value) {
296 boundList = me.boundList,
299 value = Ext.Array.from(value);
304 Ext.Array.forEach(value, function(val) {
306 model = me.store.findRecord(me.valueField, val, undef, undef, true, true);
311 boundList.getSelectionModel().select(models, false, true);
318 valueToRaw: function(value) {
322 // compare array values
323 isEqual: function(v1, v2) {
324 var fromArray = Ext.Array.from,
331 if (len !== v2.length) {
335 for(i = 0; i < len; i++) {
336 if (v2[i] !== v1[i]) {
344 getErrors : function(value) {
346 format = Ext.String.format,
347 errors = me.callParent(arguments),
350 value = Ext.Array.from(value || me.getValue());
351 numSelected = value.length;
353 if (!me.allowBlank && numSelected < 1) {
354 errors.push(me.blankText);
356 if (numSelected < this.minSelections) {
357 errors.push(format(me.minSelectionsText, me.minSelections));
359 if (numSelected > this.maxSelections) {
360 errors.push(format(me.maxSelectionsText, me.maxSelections));
366 onDisable: function() {
372 me.boundList.disable();
376 onEnable: function() {
382 me.boundList.enable();
386 setReadOnly: function(readOnly) {
387 this.readOnly = readOnly;
388 this.updateReadOnly();
392 * @private Lock or unlock the BoundList's selection model to match the current disabled/readonly state
394 updateReadOnly: function() {
396 boundList = me.boundList,
397 readOnly = me.readOnly || me.disabled;
399 boundList.getSelectionModel().setLocked(readOnly);
403 onDestroy: function(){
404 Ext.destroyMembers(this, 'panel', 'boundList', 'dragZone', 'dropZone');