/**
* This class is a grid {@link Ext.AbstractPlugin plugin} that adds a simple and flexible
* presentation for {@link Ext.data.AbstractStore#filters store filters}.
*
* Filters can be modified by the end-user using the grid's column header menu. Through
* this menu users can configure, enable, and disable filters for each column.
*
* # Example Usage
*
* @example
* var shows = Ext.create('Ext.data.Store', {
* fields: ['id','show'],
* data: [
* {id: 0, show: 'Battlestar Galactica'},
* {id: 1, show: 'Doctor Who'},
* {id: 2, show: 'Farscape'},
* {id: 3, show: 'Firefly'},
* {id: 4, show: 'Star Trek'},
* {id: 5, show: 'Star Wars: Christmas Special'}
* ]
* });
*
* Ext.create('Ext.grid.Panel', {
* renderTo: Ext.getBody(),
* title: 'Sci-Fi Television',
* height: 250,
* width: 250,
* store: shows,
* plugins: {
* gridfilters: true
* },
* columns: [{
* dataIndex: 'id',
* text: 'ID',
* width: 50
* },{
* dataIndex: 'show',
* text: 'Show',
* flex: 1,
* filter: {
* // required configs
* type: 'string',
* // optional configs
* value: 'star', // setting a value makes the filter active.
* itemDefaults: {
* // any Ext.form.field.Text configs accepted
* }
* }
* }]
* });
*
* # Features
*
* ## Filtering implementations
*
* Currently provided filter types are:
*
* * `{@link Ext.grid.filters.filter.Boolean boolean}`
* * `{@link Ext.grid.filters.filter.Date date}`
* * `{@link Ext.grid.filters.filter.List list}`
* * `{@link Ext.grid.filters.filter.Number number}`
* * `{@link Ext.grid.filters.filter.String string}`
*
* **Note:** You can find inline examples for each filter on its specific filter page.
*
* ## Graphical Indicators
*
* Columns that are filtered have {@link #filterCls CSS class} applied to their column
* headers. This style can be managed using that CSS class or by setting these Sass
* variables in your theme or application:
*
* $grid-filters-column-filtered-font-style: italic !default;
*
* $grid-filters-column-filtered-font-weight: bold !default;
*
* ## Stateful
*
* Filter information will be persisted across page loads by specifying a `stateId`
* in the Grid configuration. In actuality this state is saved by the `store`, but this
* plugin ensures that saved filters are properly identified and reclaimed on subsequent
* visits to the page.
*
* ## Grid Changes
*
* - A `filters` property is added to the Grid using this plugin.
*
* # Upgrading From Ext.ux.grid.FilterFeature
*
* The biggest change for developers converting from the user extension is most likely the
* conversion to standard {@link Ext.data.AbstractStore#filters store filters}. In the
* process, the "like" and "in" operators are now supported by `{@link Ext.util.Filter}`.
* These filters and all other filters added to the store will be sent in the standard
* way (using the "filters" parameter by default).
*
* Since this plugin now uses actual store filters, the `onBeforeLoad` listener and all
* helper methods that were used to clean and build the params have been removed. The store
* will send the filters managed by this plugin along in its normal request.
*/
Ext.define('Ext.grid.filters.Filters', {
extend: 'Ext.plugin.Abstract',
alias: 'plugin.gridfilters',
mixins: [
'Ext.util.StoreHolder'
],
requires: [
'Ext.grid.filters.filter.*'
],
id: 'gridfilters',
/**
* @property {Object} defaultFilterTypes
* This property maps {@link Ext.data.Model#cfg-fields field type} to the
* appropriate grid filter type.
* @private
*/
defaultFilterTypes: {
'boolean': 'boolean',
'int': 'number',
date: 'date',
number: 'number'
},
/**
* @property {String} [filterCls="x-grid-filters-filtered-column"]
* The CSS applied to column headers with active filters.
*/
filterCls: Ext.baseCSSPrefix + 'grid-filters-filtered-column',
/**
* @cfg {String} [menuFilterText]
* The text for the filters menu.
* @locale
*/
menuFilterText: 'Filters',
/**
* @cfg {Boolean} showMenu
* Defaults to true, including a filter submenu in the default header menu.
*/
showMenu: true,
/**
* @cfg {String} stateId
* Name of the value to be used to store state information.
*/
stateId: undefined,
/**
* @property {Object} headerMenuListeners
* Name of the object to be used to store listeners.
* @private
*/
init: function(grid) {
var me = this,
store, headerCt;
// Initialize headerMenuListeners property by a new empty object
// to prevent caching the headerMenuListeners properties
// while creating another grid with filters
me.headerMenuListeners = {};
//<debug>
Ext.Assert.falsey(me.grid);
//</debug>
me.grid = grid;
grid.filters = me;
if (me.grid.normalGrid) {
me.isLocked = true;
}
grid.clearFilters = me.clearFilters.bind(me);
store = grid.store;
headerCt = grid.headerCt;
me.headerCtListeners = headerCt.on({
destroyable: true,
scope: me,
add: me.onAdd,
menucreate: me.onMenuCreate
});
// if menu is already created before filter is initialized
if (headerCt.menu && !headerCt.menu.destroyed) {
this.onMenuCreate(headerCt, headerCt.menu);
}
me.gridListeners = grid.on({
destroyable: true,
scope: me,
reconfigure: me.onReconfigure
});
if (store.isEmptyStore) {
return;
}
me.bindStore(store);
me.initColumns();
},
/**
* Creates the Filter objects for the current configuration.
* Reconfigure and on add handlers.
* @private
*/
initColumns: function() {
var grid = this.grid,
store = grid.getStore(),
columns = grid.columnManager.getColumns(),
len = columns.length,
i, column,
filter, filterCollection;
// We start with filters defined on any columns.
for (i = 0; i < len; i++) {
column = columns[i];
filter = column.filter;
if (filter && !filter.isGridFilter) {
if (!filterCollection) {
filterCollection = store.getFilters();
filterCollection.beginUpdate();
}
this.createColumnFilter(column);
}
}
if (filterCollection) {
filterCollection.endUpdate();
}
},
createColumnFilter: function(column) {
var me = this,
columnFilter = column.filter,
filter = {
column: column,
grid: me.grid,
owner: me
},
field, model, type;
if (Ext.isString(columnFilter)) {
filter.type = columnFilter;
}
else {
Ext.apply(filter, columnFilter);
}
if (!filter.type) {
model = me.store.getModel();
// If no filter type given, first try to get it from the data field.
field = model && model.getField(column.dataIndex);
type = field && field.type;
filter.type = (type && me.defaultFilterTypes[type]) ||
column.defaultFilterType || 'string';
}
column.filter = Ext.Factory.gridFilter(filter);
if (!column.menuDisabled) {
column.requiresMenu = true;
}
},
onAdd: function(headerCt, column, index) {
var filter = column.filter;
if (filter && !filter.isGridFilter) {
this.createColumnFilter(column);
}
},
/**
* @private
* Handle creation of the grid's header menu.
*/
onMenuCreate: function(headerCt, menu) {
var me = this,
headerCtId = headerCt.getId();
if (me.headerMenuListeners[headerCtId]) {
Ext.destroy(me.headerMenuListeners[headerCtId]);
delete me.headerMenuListeners[headerCtId];
}
me.headerMenuListeners[headerCtId] = menu.on({
beforeshow: me.onMenuBeforeShow,
destroyable: true,
scope: me
});
},
/**
* @private
* Handle showing of the grid's header menu. Sets up the filter item and menu
* appropriate for the target column.
*/
onMenuBeforeShow: function(menu) {
var me = this,
menuItem, filter, parentTable, parentTableId;
if (me.showMenu) {
// In the case of a locked grid, we need to cache the 'Filters' menuItem for each grid
// since there's only one Filters instance. Both grids/menus can't share the same
// menuItem!
if (!me.filterMenuItem) {
me.filterMenuItem = {};
}
// Don't get the owner panel if in a locking grid since we need to get the unique
// filterMenuItem key. Instead, get a ref to the parent, i.e., lockedGrid, normalGrid,
// etc.
parentTable = menu.up('tablepanel');
parentTableId = parentTable.id;
menuItem = me.filterMenuItem[parentTableId];
if (!menuItem || menuItem.destroyed) {
menuItem = me.createMenuItem(menu, parentTableId);
}
// Save a ref to the root "Filters" menu item, column filters make use of it.
me.activeFilterMenuItem = menuItem;
filter = me.getMenuFilter(parentTable.headerCt);
if (filter) {
filter.showMenu(menuItem);
}
menuItem.setVisible(!!filter);
if (me.sep) {
me.sep.setVisible(!!filter);
}
}
},
createMenuItem: function(menu, parentTableId) {
var me = this,
item;
// only add separator if there are other menu items
if (menu.items.length) {
me.sep = menu.add('-');
}
item = menu.add({
checked: false,
itemId: 'filters',
text: me.menuFilterText,
listeners: {
scope: me,
checkchange: me.onCheckChange
}
});
return (me.filterMenuItem[parentTableId] = item);
},
destroy: function() {
var me = this,
filterMenuItem = me.filterMenuItem,
item, headers, i;
Ext.destroy(me.headerCtListeners, me.gridListeners);
if (me.headerMenuListeners) {
headers = Ext.Object.getKeys(me.headerMenuListeners);
for (i = 0; i < headers.length; i++) {
Ext.destroy(me.headerMenuListeners[headers[i]]);
}
}
me.bindStore(null);
me.sep = Ext.destroy(me.sep);
for (item in filterMenuItem) {
filterMenuItem[item].destroy();
}
this.callParent();
},
onUnbindStore: function(store) {
if (store && !store.destroyed) {
store.getFilters().un('remove', this.onFilterRemove, this);
}
},
onBindStore: function(store, initial, propName) {
var me = this;
me.local = !store.getRemoteFilter();
store.getFilters().on('remove', me.onFilterRemove, me);
if (me.grid.stateful && store.initialConfig.statefulFilters !== false) {
store.statefulFilters = true;
}
},
onFilterRemove: function(filterCollection, list) {
// We need to know when a store filter has been removed by an operation of the gridfilters
// UI, i.e., store.clearFilter(). The preventFilterRemoval flag lets us know whether or not
// this listener has been reached by a filter operation (preventFilterRemoval === true)
// or by something outside of the UI (preventFilterRemoval === undefined).
var len = list.items.length,
columnManager = this.grid.columnManager,
i, item, header, filter;
for (i = 0; i < len; i++) {
item = list.items[i];
header = columnManager.getHeaderByDataIndex(item.getProperty());
if (header) {
// First, we need to make sure there is indeed a filter and that its menu
// has been created. If not, there's no point in continuing.
//
// Also, even though the store may be filtered by this dataIndex, it doesn't
// necessarily mean that it was created via the gridfilters API. To be sure,
// we need to check the prefix, as this is the only way we can be sure
// of its provenance (note that we can't check `operator`).
//
// Note that we need to do an indexOf check on the string because TriFilters
// will contain extra characters specifying its type.
//
// TODO: Should we support updating the gridfilters if one or more of its filters
// have been removed directly by the bound store?
filter = header.filter;
if (!filter || !filter.menu ||
item.getId().indexOf(filter.getBaseIdPrefix()) === -1) {
continue;
}
if (!filter.preventFilterRemoval) {
// This is only called on the filter if called
// from outside of the gridfilters UI.
filter.onFilterRemove(item.getOperator());
}
}
}
},
/**
* @private
* Get the filter menu from the filters MixedCollection based on the clicked header.
*/
getMenuFilter: function(headerCt) {
return headerCt.getMenu().activeHeader.filter;
},
/**
* @private
*
*/
onCheckChange: function(item, value) {
// Locking grids must lookup the correct grid.
var parentTable = this.isLocked ? item.up('tablepanel') : this.grid,
filter = this.getMenuFilter(parentTable.headerCt);
filter.setActive(value);
},
getHeaders: function() {
return this.grid.view.headerCt.columnManager.getColumns();
},
/**
* Checks the plugin's grid for statefulness.
* @return {Boolean}
*/
isStateful: function() {
return this.grid.stateful;
},
/**
* Adds a filter to the collection and creates a store filter if has a `value` property.
* @param {Object/Object[]/Ext.util.Filter/Ext.util.Filter[]} filters A filter
* configuration or a filter object.
*/
addFilter: function(filters) {
var me = this,
grid = me.grid,
store = me.store,
hasNewColumns = false,
suppressNextFilter = true,
dataIndex, column, i, len, filter, columnFilter;
if (!Ext.isArray(filters)) {
filters = [filters];
}
for (i = 0, len = filters.length; i < len; i++) {
filter = filters[i];
dataIndex = filter.dataIndex;
column = grid.columnManager.getHeaderByDataIndex(dataIndex);
// We only create filters that map to an existing column.
if (column) {
hasNewColumns = true;
// Don't suppress active filters.
if (filter.value) {
suppressNextFilter = false;
}
columnFilter = column.filter;
// If already a gridfilter, let's destroy it and recreate another
// from the new config.
if (columnFilter && columnFilter.isGridFilter) {
columnFilter.deactivate();
columnFilter.destroy();
if (me.activeFilterMenuItem) {
me.activeFilterMenuItem.menu = null;
}
}
column.filter = filter;
}
}
// Batch initialize all column filters.
if (hasNewColumns) {
store.suppressNextFilter = suppressNextFilter;
me.initColumns();
store.suppressNextFilter = false;
}
},
/**
* Adds filters to the collection.
* @param {Array} filters An Array of filter configuration objects.
*/
addFilters: function(filters) {
if (filters) {
this.addFilter(filters);
}
},
/**
* Turns all filters off. This does not clear the configuration information.
*/
clearFilters: function() {
var grid = this.grid,
columns = grid.columnManager.getColumns(),
store = grid.store,
column, filter, i, len, filterCollection;
// We start with filters defined on any columns.
for (i = 0, len = columns.length; i < len; i++) {
column = columns[i];
filter = column.filter;
if (filter && filter.isGridFilter) {
if (!filterCollection) {
filterCollection = store.getFilters();
filterCollection.beginUpdate();
}
filter.setActive(false);
}
}
if (filterCollection) {
filterCollection.endUpdate();
}
},
onReconfigure: function(grid, store, columns, oldStore) {
var me = this,
filterMenuItem = me.filterMenuItem,
changed = oldStore !== store,
key;
// The Filters item's menu should have already been destroyed by the time we get here but
// we still need to null out the menu reference.
if (columns) {
for (key in filterMenuItem) {
filterMenuItem[key].setMenu(null);
}
}
if (store) {
if (oldStore && !oldStore.destroyed && changed) {
me.resetFilters(oldStore);
}
if (changed) {
me.bindStore(store);
me.applyFilters(store);
}
}
me.initColumns();
},
privates: {
applyFilters: function(store) {
var columns = this.grid.columnManager.getColumns(),
len = columns.length,
i, column, filter, filterCollection;
// We start with filters defined on any columns.
for (i = 0; i < len; i++) {
column = columns[i];
filter = column.filter;
if (filter && filter.isGridFilter) {
if (!filterCollection) {
filterCollection = store.getFilters();
filterCollection.beginUpdate();
}
if (filter.active) {
filter.activate();
}
}
}
if (filterCollection) {
filterCollection.endUpdate();
}
},
resetFilters: function(store) {
var filters = store.getFilters(),
i, updating, filter;
if (filters) {
for (i = filters.getCount() - 1; i >= 0; --i) {
filter = filters.getAt(i);
if (filter.isGridFilter) {
if (!updating) {
filters.beginUpdate();
}
filters.remove(filter);
updating = true;
}
}
if (updating) {
filters.endUpdate();
}
}
}
}
});