/** * @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: *
*/ 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(); } });