/**
* @class Ext.ux.form.MultiSelect
* @extends Ext.form.field.Base
* A control that allows selection and form submission of multiple list items.
*
* @history
* 2008-06-19 bpm Original code contributed by Toby Stuart (with contributions from Robert Williams)
* 2008-06-19 bpm Docs and demo code clean up
*
* @constructor
* Create a new MultiSelect
* @param {Object} config Configuration options
* @xtype multiselect
*/
Ext.define('Ext.ux.form.MultiSelect', {
extend: 'Ext.form.field.Base',
alternateClassName: 'Ext.ux.Multiselect',
alias: ['widget.multiselect', 'widget.multiselectfield'],
uses: [
'Ext.view.BoundList',
'Ext.form.FieldSet',
'Ext.ux.layout.component.form.MultiSelect',
'Ext.view.DragZone',
'Ext.view.DropZone'
],
/**
* @cfg {String} listTitle An optional title to be displayed at the top of the selection list.
*/
/**
* @cfg {String/Array} dragGroup The ddgroup name(s) for the MultiSelect DragZone (defaults to undefined).
*/
/**
* @cfg {String/Array} dropGroup The ddgroup name(s) for the MultiSelect DropZone (defaults to undefined).
*/
/**
* @cfg {Boolean} ddReorder Whether the items in the MultiSelect list are drag/drop reorderable (defaults to false).
*/
ddReorder: false,
/**
* @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
* This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
* to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
*/
/**
* @cfg {String} appendOnly True if the list should only allow append drops when drag/drop is enabled
* (use for lists which are sorted, defaults to false).
*/
appendOnly: false,
/**
* @cfg {String} displayField Name of the desired display field in the dataset (defaults to 'text').
*/
displayField: 'text',
/**
* @cfg {String} valueField Name of the desired value field in the dataset (defaults to the
* value of {@link #displayField}).
*/
/**
* @cfg {Boolean} allowBlank False to require at least one item in the list to be selected, true to allow no
* selection (defaults to true).
*/
allowBlank: true,
/**
* @cfg {Number} minSelections Minimum number of selections allowed (defaults to 0).
*/
minSelections: 0,
/**
* @cfg {Number} maxSelections Maximum number of selections allowed (defaults to Number.MAX_VALUE).
*/
maxSelections: Number.MAX_VALUE,
/**
* @cfg {String} blankText Default text displayed when the control contains no items (defaults to 'This field is required')
*/
blankText: 'This field is required',
/**
* @cfg {String} minSelectionsText Validation message displayed when {@link #minSelections} is not met (defaults to 'Minimum {0}
* item(s) required'). The {0} token will be replaced by the value of {@link #minSelections}.
*/
minSelectionsText: 'Minimum {0} item(s) required',
/**
* @cfg {String} maxSelectionsText Validation message displayed when {@link #maxSelections} is not met (defaults to 'Maximum {0}
* item(s) allowed'). The {0} token will be replaced by the value of {@link #maxSelections}.
*/
maxSelectionsText: 'Maximum {0} item(s) allowed',
/**
* @cfg {String} delimiter The string used to delimit the selected values when {@link #getSubmitValue submitting}
* the field as part of a form. Defaults to ','. If you wish to have the selected values submitted as separate
* parameters rather than a single delimited parameter, set this to null.
*/
delimiter: ',',
/**
* @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to undefined).
* Acceptable values for this property are:
*
* - any {@link Ext.data.Store Store} subclass
* - an Array : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
*
* - 1-dimensional array : (e.g., ['Foo','Bar'])
* A 1-dimensional array will automatically be expanded (each array item will be the combo
* {@link #valueField value} and {@link #displayField text})
* - 2-dimensional array : (e.g., [['f','Foo'],['b','Bar']])
* For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
* {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
*
*/
componentLayout: 'multiselectfield',
fieldBodyCls: Ext.baseCSSPrefix + 'form-multiselect-body',
// private
initComponent: function(){
var me = this;
me.bindStore(me.store, true);
if (me.store.autoCreated) {
me.valueField = me.displayField = 'field1';
if (!me.store.expanded) {
me.displayField = 'field2';
}
}
if (!Ext.isDefined(me.valueField)) {
me.valueField = me.displayField;
}
me.callParent();
},
bindStore: function(store, initial) {
var me = this,
oldStore = me.store,
boundList = me.boundList;
if (oldStore && !initial && oldStore !== store && oldStore.autoDestroy) {
oldStore.destroy();
}
me.store = store ? Ext.data.StoreManager.lookup(store) : null;
if (boundList) {
boundList.bindStore(store || null);
}
},
// private
onRender: function(ct, position) {
var me = this,
panel, boundList, selModel;
me.callParent(arguments);
boundList = me.boundList = Ext.create('Ext.view.BoundList', {
multiSelect: true,
store: me.store,
displayField: me.displayField,
border: false
});
selModel = boundList.getSelectionModel();
me.mon(selModel, {
selectionChange: me.onSelectionChange,
scope: me
});
panel = me.panel = Ext.create('Ext.panel.Panel', {
title: me.listTitle,
tbar: me.tbar,
items: [boundList],
renderTo: me.bodyEl,
layout: 'fit'
});
// Must set upward link after first render
panel.ownerCt = me;
// Set selection to current value
me.setRawValue(me.rawValue);
},
// No content generated via template, it's all added components
getSubTplMarkup: function() {
return '';
},
// private
afterRender: function() {
var me = this;
me.callParent();
if (me.ddReorder && !me.dragGroup && !me.dropGroup){
me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
}
if (me.draggable || me.dragGroup){
me.dragZone = Ext.create('Ext.view.DragZone', {
view: me.boundList,
ddGroup: me.dragGroup,
dragText: '{0} Item{1}'
});
}
if (me.droppable || me.dropGroup){
me.dropZone = Ext.create('Ext.view.DropZone', {
view: me.boundList,
ddGroup: me.dropGroup,
handleNodeDrop: function(data, dropRecord, position) {
var view = this.view,
store = view.getStore(),
records = data.records,
index;
// remove the Models from the source Store
data.view.store.remove(records);
index = store.indexOf(dropRecord);
if (position === 'after') {
index++;
}
store.insert(index, records);
view.getSelectionModel().select(records);
}
});
}
},
onSelectionChange: function() {
this.checkChange();
},
/**
* Clears any values currently selected.
*/
clearValue: function() {
this.setValue([]);
},
/**
* Return the value(s) to be submitted for this field. The returned value depends on the {@link #delimiter}
* config: If it is set to a String value (like the default ',') then this will return the selected values
* joined by the delimiter. If it is set to null then the values will be returned as an Array.
*/
getSubmitValue: function() {
var me = this,
delimiter = me.delimiter,
val = me.getValue();
return Ext.isString(delimiter) ? val.join(delimiter) : val;
},
// inherit docs
getRawValue: function() {
var me = this,
boundList = me.boundList;
if (boundList) {
me.rawValue = Ext.Array.map(boundList.getSelectionModel().getSelection(), function(model) {
return model.get(me.valueField);
});
}
return me.rawValue;
},
// inherit docs
setRawValue: function(value) {
var me = this,
boundList = me.boundList,
models;
value = Ext.Array.from(value);
me.rawValue = value;
if (boundList) {
models = [];
Ext.Array.forEach(value, function(val) {
var undef,
model = me.store.findRecord(me.valueField, val, undef, undef, true, true);
if (model) {
models.push(model);
}
});
boundList.getSelectionModel().select(models, false, true);
}
return value;
},
// no conversion
valueToRaw: function(value) {
return value;
},
// compare array values
isEqual: function(v1, v2) {
var fromArray = Ext.Array.from,
i, len;
v1 = fromArray(v1);
v2 = fromArray(v2);
len = v1.length;
if (len !== v2.length) {
return false;
}
for(i = 0; i < len; i++) {
if (v2[i] !== v1[i]) {
return false;
}
}
return true;
},
getErrors : function(value) {
var me = this,
format = Ext.String.format,
errors = me.callParent(arguments),
numSelected;
value = Ext.Array.from(value || me.getValue());
numSelected = value.length;
if (!me.allowBlank && numSelected < 1) {
errors.push(me.blankText);
}
if (numSelected < this.minSelections) {
errors.push(format(me.minSelectionsText, me.minSelections));
}
if (numSelected > this.maxSelections) {
errors.push(format(me.maxSelectionsText, me.maxSelections));
}
return errors;
},
onDisable: function() {
this.callParent();
this.disabled = true;
this.updateReadOnly();
},
onEnable: function() {
this.callParent();
this.disabled = false;
this.updateReadOnly();
},
setReadOnly: function(readOnly) {
this.readOnly = readOnly;
this.updateReadOnly();
},
/**
* @private Lock or unlock the BoundList's selection model to match the current disabled/readonly state
*/
updateReadOnly: function() {
var me = this,
boundList = me.boundList,
readOnly = me.readOnly || me.disabled;
if (boundList) {
boundList.getSelectionModel().setLocked(readOnly);
}
},
onDestroy: function(){
Ext.destroyMembers(this, 'panel', 'boundList', 'dragZone', 'dropZone');
this.callParent();
}
});