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.
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
11 * Create a new MultiSelect
12 * @param {Object} config Configuration options
15 Ext.define('Ext.ux.form.MultiSelect', {
16 extend: 'Ext.form.field.Base',
17 alternateClassName: 'Ext.ux.Multiselect',
18 alias: ['widget.multiselect', 'widget.multiselectfield'],
22 'Ext.ux.layout.component.form.MultiSelect',
28 * @cfg {String} listTitle An optional title to be displayed at the top of the selection list.
32 * @cfg {String/Array} dragGroup The ddgroup name(s) for the MultiSelect DragZone (defaults to undefined).
36 * @cfg {String/Array} dropGroup The ddgroup name(s) for the MultiSelect DropZone (defaults to undefined).
40 * @cfg {Boolean} ddReorder Whether the items in the MultiSelect list are drag/drop reorderable (defaults to false).
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}.
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).
57 * @cfg {String} displayField Name of the desired display field in the dataset (defaults to 'text').
62 * @cfg {String} valueField Name of the desired value field in the dataset (defaults to the
63 * value of {@link #displayField}).
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).
73 * @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
78 * @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
80 maxSelections: Number.MAX_VALUE,
83 * @cfg {String} blankText Default text displayed when the control contains no items (defaults to 'This field is required')
85 blankText: 'This field is required',
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}.
91 minSelectionsText: 'Minimum {0} item(s) required',
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}.
97 maxSelectionsText: 'Maximum {0} item(s) allowed',
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>.
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>
122 componentLayout: 'multiselectfield',
124 fieldBodyCls: Ext.baseCSSPrefix + 'form-multiselect-body',
128 initComponent: function(){
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';
139 if (!Ext.isDefined(me.valueField)) {
140 me.valueField = me.displayField;
146 bindStore: function(store, initial) {
149 boundList = me.boundList;
151 if (oldStore && !initial && oldStore !== store && oldStore.autoDestroy) {
155 me.store = store ? Ext.data.StoreManager.lookup(store) : null;
157 boundList.bindStore(store || null);
163 onRender: function(ct, position) {
165 panel, boundList, selModel;
167 me.callParent(arguments);
169 boundList = me.boundList = Ext.create('Ext.view.BoundList', {
172 displayField: me.displayField,
176 selModel = boundList.getSelectionModel();
178 selectionChange: me.onSelectionChange,
182 panel = me.panel = Ext.create('Ext.panel.Panel', {
190 // Must set upward link after first render
193 // Set selection to current value
194 me.setRawValue(me.rawValue);
197 // No content generated via template, it's all added components
198 getSubTplMarkup: function() {
203 afterRender: function() {
207 if (me.ddReorder && !me.dragGroup && !me.dropGroup){
208 me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
211 if (me.draggable || me.dragGroup){
212 me.dragZone = Ext.create('Ext.view.DragZone', {
214 ddGroup: me.dragGroup,
215 dragText: '{0} Item{1}'
218 if (me.droppable || me.dropGroup){
219 me.dropZone = Ext.create('Ext.view.DropZone', {
221 ddGroup: me.dropGroup,
222 handleNodeDrop: function(data, dropRecord, position) {
223 var view = this.view,
224 store = view.getStore(),
225 records = data.records,
228 // remove the Models from the source Store
229 data.view.store.remove(records);
231 index = store.indexOf(dropRecord);
232 if (position === 'after') {
235 store.insert(index, records);
236 view.getSelectionModel().select(records);
242 onSelectionChange: function() {
247 * Clears any values currently selected.
249 clearValue: function() {
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.
258 getSubmitValue: function() {
260 delimiter = me.delimiter,
262 return Ext.isString(delimiter) ? val.join(delimiter) : val;
266 getRawValue: function() {
268 boundList = me.boundList;
270 me.rawValue = Ext.Array.map(boundList.getSelectionModel().getSelection(), function(model) {
271 return model.get(me.valueField);
278 setRawValue: function(value) {
280 boundList = me.boundList,
283 value = Ext.Array.from(value);
288 Ext.Array.forEach(value, function(val) {
290 model = me.store.findRecord(me.valueField, val, undef, undef, true, true);
295 boundList.getSelectionModel().select(models, false, true);
302 valueToRaw: function(value) {
306 // compare array values
307 isEqual: function(v1, v2) {
308 var fromArray = Ext.Array.from,
315 if (len !== v2.length) {
319 for(i = 0; i < len; i++) {
320 if (v2[i] !== v1[i]) {
328 getErrors : function(value) {
330 format = Ext.String.format,
331 errors = me.callParent(arguments),
334 value = Ext.Array.from(value || me.getValue());
335 numSelected = value.length;
337 if (!me.allowBlank && numSelected < 1) {
338 errors.push(me.blankText);
340 if (numSelected < this.minSelections) {
341 errors.push(format(me.minSelectionsText, me.minSelections));
343 if (numSelected > this.maxSelections) {
344 errors.push(format(me.maxSelectionsText, me.maxSelections));
350 onDisable: function() {
352 this.disabled = true;
353 this.updateReadOnly();
356 onEnable: function() {
358 this.disabled = false;
359 this.updateReadOnly();
362 setReadOnly: function(readOnly) {
363 this.readOnly = readOnly;
364 this.updateReadOnly();
368 * @private Lock or unlock the BoundList's selection model to match the current disabled/readonly state
370 updateReadOnly: function() {
372 boundList = me.boundList,
373 readOnly = me.readOnly || me.disabled;
375 boundList.getSelectionModel().setLocked(readOnly);
379 onDestroy: function(){
380 Ext.destroyMembers(this, 'panel', 'boundList', 'dragZone', 'dropZone');