Данная статья расскажет как можно расширить существующий Ext.toolbar.Paging добавив в него функционал для фильтрации Ext.grid.Panel по выбранным, или выбранным колонкам. Поддерживает и удаленная и локальная сортировка. Имплементацию данного функционала мы сделаем дважды, первый раз в виде наследника Ext.toolbar.Paging, а второй в виде плагина для таблицы.
Имплементация 1
Доступны следующие настройки
{ filterItemWidth: 300, filterOnType: true, filterSplitButtonGlyph: "xf0b0@FontAwesome", filterSplitButtonText: "Filter" }
Не забудьте установить свойство remoteFilter в store, если вам необходимо фильтровать все данные, а не только содержимое текущей страницы.
Ext.define('PagingToolbarWithSearch', { extend: 'Ext.toolbar.Paging', xtype: 'pagingtoolbarwithsearch', filterItemWidth: 300, filterOnType: true, filterSplitButtonGlyph: 'xf0b0@FontAwesome', filterSplitButtonText: "Filter", getPagingItems: function () { var me = this, inputListeners = { scope: me, blur: me.onPagingBlur }; inputListeners[Ext.supports.SpecialKeyDownRepeat ? 'keydown' : 'keypress'] = me.onPagingKeyDown; return [{ itemId: 'first', tooltip: me.firstText, overflowText: me.firstText, iconCls: Ext.baseCSSPrefix + 'tbar-page-first', disabled: true, handler: me.moveFirst, scope: me }, { itemId: 'prev', tooltip: me.prevText, overflowText: me.prevText, iconCls: Ext.baseCSSPrefix + 'tbar-page-prev', disabled: true, handler: me.movePrevious, scope: me }, '-', me.beforePageText, { xtype: 'numberfield', itemId: 'inputItem', name: 'inputItem', cls: Ext.baseCSSPrefix + 'tbar-page-number', allowDecimals: false, minValue: 1, hideTrigger: true, enableKeyEvents: true, keyNavEnabled: false, selectOnFocus: true, submitValue: false, // mark it as not a field so the form will not catch it when getting fields isFormField: false, width: me.inputItemWidth, margin: '-1 2 3 2', listeners: inputListeners }, { xtype: 'tbtext', itemId: 'afterTextItem', html: Ext.String.format(me.afterPageText, 1) }, '-', { itemId: 'next', tooltip: me.nextText, overflowText: me.nextText, iconCls: Ext.baseCSSPrefix + 'tbar-page-next', disabled: true, handler: me.moveNext, scope: me }, { itemId: 'last', tooltip: me.lastText, overflowText: me.lastText, iconCls: Ext.baseCSSPrefix + 'tbar-page-last', disabled: true, handler: me.moveLast, scope: me }, '-', { itemId: 'refresh', tooltip: me.refreshText, overflowText: me.refreshText, iconCls: Ext.baseCSSPrefix + 'tbar-loading', disabled: me.store.isLoading(), handler: me.doRefresh, scope: me }, '-', this.getFilterSplitButton(), this.getFilterTextField() ]; }, getFilterSplitButton: function () { if (!this.filterSplitButton) { this.filterSplitButton = Ext.create('Ext.button.Split', { text: this.filterSplitButtonText, handler: this.doFilter, glyph: this.filterSplitButtonGlyph, scope: this }); } return this.filterSplitButton; }, getFilterTextField: function () { if (!this.filterTextField) { this.filterTextField = Ext.create('Ext.form.field.Text', { submitValue: false, isFormField: false, width: this.filterItemWidth, margin: '-1 2 3 2', enableKeyEvents: true }); if (this.filterOnType) { this.filterTextField.on('change', this.doFilter, this); } else { this.filterTextField.on('specialkey', function () { if (e.getKey() == e.ENTER) { this.doFilter(); } }, this); } } return this.filterTextField; }, beforeRender: function () { this.callParent(arguments); this.updateBarInfo(); this.updateSearchColumnsMenu(); }, getSelectAllColumnsMenuItem: function () { if (!this.selectAllColumnsMenuItem) { this.selectAllColumnsMenuItem = Ext.create('Ext.menu.CheckItem', { text: "All Columns", checked: true, listeners: { checkchange: { fn: function (menuCheckItem, checked) { this.getFilterColumnsMenu().items.each(function (item) { if (item.dataIndex) { item.setChecked(checked); } }); this.doFilter(); }, scope: this } } }); } return this.selectAllColumnsMenuItem; }, getFilterColumnsMenu: function () { if (!this.filterColumnsMenu) { this.filterColumnsMenu = Ext.create('Ext.menu.Menu', { items: [ this.getSelectAllColumnsMenuItem(), { xtype: 'menuseparator' } ] }); } return this.filterColumnsMenu; }, updateSearchColumnsMenu: function () { var columns = this.up('grid').getColumns(); Ext.Array.each(columns, function (column) { this.getFilterColumnsMenu().add({ xtype: 'menucheckitem', text: column.text, dataIndex: column.dataIndex, checked: true, listeners: { checkchange: { fn: function (menuCheckItem, checked) { if (checked) { var selectAllColumnsMenuItemCheck = true; this.getFilterColumnsMenu().items.each(function (item) { if (item.dataIndex && item.checked == false) { selectAllColumnsMenuItemCheck = false; return false; } }); this.getSelectAllColumnsMenuItem().setChecked(selectAllColumnsMenuItemCheck, true); } else { this.getSelectAllColumnsMenuItem().setChecked(false, true); } this.doFilter(); }, scope: this } } }); }, this); this.getFilterSplitButton().setMenu(this.getFilterColumnsMenu()); }, doFilter: function () { var searchColumnIndexes = [], searchValue = this.getFilterTextField().getValue();; this.getFilterSplitButton().getMenu().items.each(function (item) { if (item.dataIndex && item.checked) { searchColumnIndexes.push(item.dataIndex); } }, this); if (this.store.remoteFilter) { this.remoteFilter(searchColumnIndexes, searchValue) } else { this.localFilter(searchColumnIndexes, searchValue); } }, localFilter: function (searchColumnIndexes, searchValue) { this.store.removeFilter(this.filter); this.filter = new Ext.util.Filter({ filterFn: function (record) { if (searchColumnIndexes.length === 0 || Ext.isEmpty(searchValue)) { return true; } var found = false; Ext.Array.each(searchColumnIndexes, function (dataIndex) { if (record.get(dataIndex) && record.get(dataIndex).indexOf(searchValue) != -1) { found = true; return false; } }, this); return found; } }); this.store.addFilter(this.filter); }, remoteFilter: function (searchColumnIndexes, searchValue) { var remoteFilters = []; Ext.Array.each(searchColumnIndexes, function (columnIndex) { remoteFilters.push({ property: columnIndex, value: searchValue }); }); this.store.clearFilter(); this.store.filter(remoteFilters); } });
Fiddle:
Имплементация 2
Конечно у таблицы, есть другие элементы GUI, поэтому я реализовал её с поддержкой простейшей разметки. В следующем примере у меня есть кнопка перезагрузки данных и два элемента фильтра: кнопка меню и текстовое поле фильтра будут размещаться между разделителями.
initComponent: function () { this.items = [ this.getReloadButton(), '-', '**FILTER_SPLIT_BUTTON**', '**FILTER_TEXT_FIELD**', '-' ]; this.callParent(); },
Ext.define('App.ux.grid.toolbar.plugin.Filter', { extend: 'Ext.plugin.Abstract', alias: 'plugin.filterplugin', filterItemWidth: 100, filterOnType: true, filterSplitButtonGlyph: 'xf0b0@FontAwesome', filterSplitButtonText: "Filter", filterSplitButtonPlaceHolder: '**FILTER_SPLIT_BUTTON**', filterTextFieldPlaceHolder: '**FILTER_TEXT_FIELD**', init: function (toolbar) { this.placeFields(); toolbar.on('afterrender', this.updateSearchColumnsMenu, this); }, placeFields: function () { this.replacePlaceHolderByCmp( this.filterSplitButtonPlaceHolder, this.getFilterSplitButton() ); this.replacePlaceHolderByCmp( this.filterTextFieldPlaceHolder, this.getFilterTextField() ); }, replacePlaceHolderByCmp: function (placeHolder, cmp) { var placeHolderIndex = this.getIndexByPlaceHolder(placeHolder), placeHolderCmp = this.getPlaceHolderCmp(placeHolder); this.getCmp().remove(placeHolderCmp); this.getCmp().insert(placeHolderIndex, cmp); }, getIndexByPlaceHolder: function (placeHolder) { return this.getCmp().items.findIndex('text', placeHolder); }, getPlaceHolderCmp: function (placeHolder) { var cmp = this.getCmp().down('[text="' + placeHolder + '"]'); return cmp; }, getFilterSplitButton: function () { if (!this.filterSplitButton) { this.filterSplitButton = Ext.create('Ext.button.Split', { text: this.filterSplitButtonText, handler: this.doFilter, glyph: this.filterSplitButtonGlyph, scope: this }); } return this.filterSplitButton; }, getFilterTextField: function () { if (!this.filterTextField) { this.filterTextField = Ext.create('Ext.form.field.Text', { submitValue: false, isFormField: false, width: this.filterItemWidth, margin: '-1 2 3 2', enableKeyEvents: true }); if (this.filterOnType) { this.filterTextField.on('change', this.doFilter, this); } else { this.filterTextField.on('specialkey', function () { if (e.getKey() == e.ENTER) { this.doFilter(); } }, this); } } return this.filterTextField; }, getSelectAllColumnsMenuItem: function () { if (!this.selectAllColumnsMenuItem) { this.selectAllColumnsMenuItem = Ext.create('Ext.menu.CheckItem', { text: "All Columns", checked: true, listeners: { checkchange: { fn: function (menuCheckItem, checked) { this.getFilterColumnsMenu().items.each(function (item) { if (item.dataIndex) { item.setChecked(checked); } }); this.doFilter(); }, scope: this } } }); } return this.selectAllColumnsMenuItem; }, getFilterColumnsMenu: function () { if (!this.filterColumnsMenu) { this.filterColumnsMenu = Ext.create('Ext.menu.Menu', { items: [ this.getSelectAllColumnsMenuItem(), { xtype: 'menuseparator' } ] }); } return this.filterColumnsMenu; }, updateSearchColumnsMenu: function () { var columns = this.getCmp().up('grid').getColumns(); Ext.Array.each(columns, function (column) { this.getFilterColumnsMenu().add({ xtype: 'menucheckitem', text: column.text, dataIndex: column.dataIndex, checked: true, listeners: { checkchange: { fn: function (menuCheckItem, checked) { if (checked) { var selectAllColumnsMenuItemCheck = true; this.getFilterColumnsMenu().items.each(function (item) { if (item.dataIndex && item.checked == false) { selectAllColumnsMenuItemCheck = false; return false; } }); this.getSelectAllColumnsMenuItem().setChecked(selectAllColumnsMenuItemCheck, true); } else { this.getSelectAllColumnsMenuItem().setChecked(false, true); } this.doFilter(); }, scope: this } } }); }, this); this.getFilterSplitButton().setMenu(this.getFilterColumnsMenu()); }, doFilter: function () { var searchColumnIndexes = [], searchValue = this.getFilterTextField().getValue();; this.getFilterSplitButton().getMenu().items.each(function (item) { if (item.dataIndex && item.checked) { searchColumnIndexes.push(item.dataIndex); } }, this); if (this.getStore().remoteFilter) { this.remoteFilter(searchColumnIndexes, searchValue) } else { this.localFilter(searchColumnIndexes, searchValue); } }, localFilter: function (searchColumnIndexes, searchValue) { this.getStore().removeFilter(this.filter); this.filter = new Ext.util.Filter({ filterFn: function (record) { if (searchColumnIndexes.length === 0 || Ext.isEmpty(searchValue)) { return true; } var found = false; Ext.Array.each(searchColumnIndexes, function (dataIndex) { if (record.get(dataIndex) && record.get(dataIndex).indexOf(searchValue) != -1) { found = true; return false; } }, this); return found; } }); this.getStore().addFilter(this.filter); }, remoteFilter: function (searchColumnIndexes, searchValue) { var remoteFilters = []; Ext.Array.each(searchColumnIndexes, function (columnIndex) { remoteFilters.push({ property: columnIndex, value: searchValue }); }); this.getStore().clearFilter(); this.getStore().filter(remoteFilters); }, getStore: function () { if (!this.store) { this.store = this.getCmp().up('grid').getStore(); } return this.store; } });
Fiddle: