/** * A control that allows selection of multiple items in a list */ Ext.define('Ext.ux.form.MultiSelect', { extend: 'Ext.form.FieldContainer', mixins: { bindable: 'Ext.util.Bindable', field: 'Ext.form.field.Field' }, alternateClassName: 'Ext.ux.Multiselect', alias: ['widget.multiselectfield', 'widget.multiselect'], requires: ['Ext.panel.Panel', 'Ext.view.BoundList', 'Ext.layout.container.Fit'], uses: ['Ext.view.DragZone', 'Ext.view.DropZone'], layout: 'fit', /** * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone. */ /** * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone. */ /** * @cfg {String} [title=""] A title for the underlying panel. */ /** * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable. */ 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=false] True if the list should only allow append drops when drag/drop is enabled. * This is useful for lists which are sorted. */ appendOnly: false, /** * @cfg {String} [displayField="text"] Name of the desired display field in the dataset. */ displayField: 'text', /** * @cfg {String} [valueField="text"] Name of the desired value field in the dataset. */ /** * @cfg {Boolean} [allowBlank=true] False to require at least one item in the list to be selected, true to allow no * selection. */ allowBlank: true, /** * @cfg {Number} [minSelections=0] Minimum number of selections allowed. */ minSelections: 0, /** * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed. */ maxSelections: Number.MAX_VALUE, /** * @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items. */ blankText: 'This field is required', /** * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"] * Validation message displayed when {@link #minSelections} is not met. * The {0} token will be replaced by the value of {@link #minSelections}. */ minSelectionsText: 'Minimum {0} item(s) required', /** * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"] * Validation message displayed when {@link #maxSelections} is not met * The {0} token will be replaced by the value of {@link #maxSelections}. */ maxSelectionsText: 'Minimum {0} item(s) required', /** * @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting} * the field as part of a form. 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: *
*/ ignoreSelectChange: 0, /** * @cfg {Object} listConfig * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor. * Any configuration that is valid for BoundList can be included. */ 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.items = me.setupItems(); me.callParent(); me.initField(); me.addEvents('drop'); }, setupItems: function() { var me = this; me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({ deferInitialRefresh: false, border: false, multiSelect: true, store: me.store, displayField: me.displayField, disabled: me.disabled }, me.listConfig)); me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me); return { border: true, layout: 'fit', title: me.title, tbar: me.tbar, items: me.boundList }; }, onSelectChange: function(selModel, selections){ if (!this.ignoreSelectChange) { this.setValue(selections); } }, getSelected: function(){ return this.boundList.getSelectionModel().getSelection(); }, // compare array values isEqual: function(v1, v2) { var fromArray = Ext.Array.from, i = 0, len; v1 = fromArray(v1); v2 = fromArray(v2); len = v1.length; if (len !== v2.length) { return false; } for(; i < len; i++) { if (v2[i] !== v1[i]) { return false; } } return true; }, afterRender: function(){ var me = this; me.callParent(); if (me.selectOnRender) { ++me.ignoreSelectChange; me.boundList.getSelectionModel().select(me.getRecordsForValue(me.value)); --me.ignoreSelectChange; delete me.toSelect; } 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); me.fireEvent('drop', me, records); } }); } }, isValid : function() { var me = this, disabled = me.disabled, validate = me.forceValidation || !disabled; return validate ? me.validateValue(me.value) : disabled; }, validateValue: function(value) { var me = this, errors = me.getErrors(value), isValid = Ext.isEmpty(errors); if (!me.preventMark) { if (isValid) { me.clearInvalid(); } else { me.markInvalid(errors); } } return isValid; }, markInvalid : function(errors) { // Save the message and fire the 'invalid' event var me = this, oldMsg = me.getActiveError(); me.setActiveErrors(Ext.Array.from(errors)); if (oldMsg !== me.getActiveError()) { me.updateLayout(); } }, /** * Clear any invalid styles/messages for this field. * * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true` * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set. */ clearInvalid : function() { // Clear the message and fire the 'valid' event var me = this, hadError = me.hasActiveError(); me.unsetActiveError(); if (hadError) { me.updateLayout(); } }, getSubmitData: function() { var me = this, data = null, val; if (!me.disabled && me.submitValue && !me.isFileUpload()) { val = me.getSubmitValue(); if (val !== null) { data = {}; data[me.getName()] = val; } } return data; }, /** * Returns the value that would be included in a standard form submit for this field. * * @return {String} The value to be submitted, or null. */ getSubmitValue: function() { var me = this, delimiter = me.delimiter, val = me.getValue(); return Ext.isString(delimiter) ? val.join(delimiter) : val; }, getValue: function(){ return this.value; }, getRecordsForValue: function(value){ var me = this, records = [], all = me.store.getRange(), valueField = me.valueField, i = 0, allLen = all.length, rec, j, valueLen; for (valueLen = value.length; i < valueLen; ++i) { for (j = 0; j < allLen; ++j) { rec = all[j]; if (rec.get(valueField) == value[i]) { records.push(rec); } } } return records; }, setupValue: function(value){ var delimiter = this.delimiter, valueField = this.valueField, i = 0, out, len, item; if (Ext.isDefined(value)) { if (delimiter && Ext.isString(value)) { value = value.split(delimiter); } else if (!Ext.isArray(value)) { value = [value]; } for (len = value.length; i < len; ++i) { item = value[i]; if (item && item.isModel) { value[i] = item.get(valueField); } } out = Ext.Array.unique(value); } else { out = []; } return out; }, setValue: function(value){ var me = this, selModel = me.boundList.getSelectionModel(); // Store not loaded yet - we cannot set the value if (!me.store.getCount()) { me.store.on({ load: Ext.Function.bind(me.setValue, me, [value]), single: true }); return; } value = me.setupValue(value); me.mixins.field.setValue.call(me, value); if (me.rendered) { ++me.ignoreSelectChange; selModel.deselectAll(); selModel.select(me.getRecordsForValue(value)); --me.ignoreSelectChange; } else { me.selectOnRender = true; } }, clearValue: function(){ this.setValue([]); }, onEnable: function(){ var list = this.boundList; this.callParent(); if (list) { list.enable(); } }, onDisable: function(){ var list = this.boundList; this.callParent(); if (list) { list.disable(); } }, getErrors : function(value) { var me = this, format = Ext.String.format, errors = [], numSelected; value = Ext.Array.from(value || me.getValue()); numSelected = value.length; if (!me.allowBlank && numSelected < 1) { errors.push(me.blankText); } if (numSelected < me.minSelections) { errors.push(format(me.minSelectionsText, me.minSelections)); } if (numSelected > me.maxSelections) { errors.push(format(me.maxSelectionsText, me.maxSelections)); } return errors; }, onDestroy: function(){ var me = this; me.bindStore(null); Ext.destroy(me.dragZone, me.dropZone); me.callParent(); }, onBindStore: function(store){ var boundList = this.boundList; if (boundList) { boundList.bindStore(store); } } });