/** * リストから複数のアイテムを選択できるようにするコントロールです。 */ Ext.define('Ext.ux.form.MultiSelect', { extend: 'Ext.form.FieldContainer', mixins: [ 'Ext.util.StoreHolder', '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: 'anchor', /** * @cfg {String} [dragGroup=""] MultiSelect DragZoneのddgropu名。 */ /** * @cfg {String} [dropGroup=""] MultiSelect DropZoneのddgroup名。 */ /** * @cfg {String} [title=""] 基本的なパネルのタイトル。 */ /** * @cfg {Boolean} [ddReorder=false] MultiSelectリストのアイテムが、ドラッグアンドドロップで順番を変えることができるかどうか。 */ ddReorder: false, /** * @cfg {Object/Array} tbar コントロールの選択リストの最上部に挿入するオプションのツールバー。これは、ツールバーに追加する{@link Ext.toolbar.Toolbar}オブジェクト、ツールバーコンフィグ、またはbuttons/buttonコンフィグの配列のいずれかです。{@link Ext.panel.Panel#tbar}を参照してください。 */ /** * @cfg {String} [appendOnly=false] ドラッグアンドドロップが有効なときに、リストがドロップによる追加のみ許可する場合は`true`。これはソートされたリストで役に立ちます。 */ appendOnly: false, /** * @cfg {String} [displayField="text"] 望ましいデータセットの表示フィールドの名前。 */ displayField: 'text', /** * @cfg {String} [valueField="text"] データセットの目標値フィールドの名前。 */ /** * @cfg {Boolean} [allowBlank=true] `false`にすると、リストから選択される1つ以上のアイテムを要求します。`true`にした場合は、アイテムの選択ができません。 */ allowBlank: true, /** * @cfg {Number} [minSelections=0] 選択可能な最小数。 */ minSelections: 0, /** * @cfg {Number} [maxSelections=Number.MAX_VALUE] 選択可能な最大数。 */ maxSelections: Number.MAX_VALUE, /** * @cfg {String} [blankText="This field is required"] コントロールがアイテムを含まないときに表示されるデフォルトのテキスト。 */ blankText: 'This field is required', /** * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"] {@link #minSelections}に適合しなかったときに表示されるバリデーションメッセージ。{0}トークンが{@link #minSelections}の値により置き換えられます。 */ minSelectionsText: 'Minimum {0} item(s) required', /** * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"] Validation message displayed when {@link #maxSelections}に適合しなかったときに表示されるバリデーションメッセージ。{0}トークンが{@link #maxSelections}の値により置き換えられます。 */ maxSelectionsText: 'Maximum {0} item(s) required', /** * @cfg {String} [delimiter=","] フィールドをフォームの一部として{@link #getSubmitValue 送信}するときに、選択した値の範囲を決めるために使用する文字列。単一の区切りパラメータとしてではなく別々のパラメータとして選択した値を送信する場合は、このコンフィグに`null`をセットします。 */ delimiter: ',', /** * @cfg {String} [dragText="{0} Item{1}"] アイテムのドラッグ中に表示するテキスト。{0}がアイテム数により置き換えられます。アイテムが複数ある場合、{1}が複数形に置き換えられます。 */ dragText: '{0} Item{1}', /** * @cfg {Ext.data.Store/Array} store このMultiSelectと結びつくデータソースです(デフォルト値は`undefined`)。このプロパティに設定できる値は以下です。<div class="mdetail-params"><ul> <li><b>すべての{@link Ext.data.Store ストア}サブクラス</b></li> <li><b>配列</b> :配列は内部的に{@link Ext.data.ArrayStore}に変換されます。<div class="mdetail-params"><ul> <li><b>1次元配列</b> :(例 <tt>['Foo','Bar']</tt>)<div class="sub-desc"> 1次元配列が自動的に拡張されます(各配列アイテムはコンボボックスの{@link #valueField 値}と{@link #displayField テキスト}となります)</div></li> <li><b>2次元配列</b> :(例 <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc"> 多次元配列の場合、各アイテムのインデックス0の値がコンボボックスの{@link #valueField #valueField}、インデックス1の値が{@link #displayField #displayField}とみなされます。</div></li></ul></div></li></ul></div> */ ignoreSelectChange: 0, /** * @cfg {Object} listConfig * {@link Ext.view.BoundList}のコンストラクタに渡されるオプションのコンフィグプロパティのセット。BoundListで有効などんなコンフィグも含むことができます。 */ //TODO - doc me.addEvents('drop'); 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(); }, setupItems: function() { var me = this; me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({ anchor: 'none 100%', border: 1, multiSelect: true, store: me.store, displayField: me.displayField, disabled: me.disabled }, me.listConfig)); me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me); // Only need to wrap the BoundList in a Panel if we have a title. if (!me.title) { return me.boundList; } // Wrap to add a title me.boundList.border = false; return { border: true, anchor: 'none 100%', layout: 'anchor', 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, records; me.callParent(); if (me.selectOnRender) { records = me.getRecordsForValue(me.value); if (records.length) { ++me.ignoreSelectChange; me.boundList.getSelectionModel().select(records); --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: me.dragText }); } 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(); } }, /** * このフィールドの不正なスタイルまたはメッセージを消去します。 * * __注意_:__ このメソッドの使用により、値のバリデーションに_パス_しなかった場合にフィールドの{@link #validate}や{@link #isValid}メソッドから`true`が返されるわけではありません。したがって、単にフィールドのエラーをクリアしても、{@link Ext.form.action.Submit#clientValidation}オプションをセットして送信したフォームの送信を可能にするとは限りません。 */ 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; }, /** * このフィールドに送信する標準的なフォームが含む値を返します。 * * @return {String} 送信される値、または`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 = me.store; // Store not loaded yet - we cannot set the value if (!store.getCount()) { 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(); if (value.length) { 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); } } });