3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.ux.grid.FiltersFeature
17 * @extends Ext.grid.Feature
19 FiltersFeature is a grid {@link Ext.grid.Feature feature} that allows for a slightly more
20 robust representation of filtering than what is provided by the default store.
22 Filtering is adjusted by the user using the grid's column header menu (this menu can be
23 disabled through configuration). Through this menu users can configure, enable, and
24 disable filters for each column.
28 ##Filtering implementations:##
30 Default filtering for Strings, Numeric Ranges, Date Ranges, Lists (which can be backed by a
31 {@link Ext.data.Store}), and Boolean. Additional custom filter types and menus are easily
32 created by extending {@link Ext.ux.grid.filter.Filter}.
34 ##Graphical Indicators:##
36 Columns that are filtered have {@link #filterCls a configurable css class} applied to the column headers.
38 ##Automatic Reconfiguration:##
40 Filters automatically reconfigure when the grid 'reconfigure' event fires.
44 Filter information will be persisted across page loads by specifying a `stateId`
45 in the Grid configuration.
47 The filter collection binds to the {@link Ext.grid.Panel#beforestaterestore beforestaterestore}
48 and {@link Ext.grid.Panel#beforestatesave beforestatesave} events in order to be stateful.
50 ##GridPanel Changes:##
52 - A `filters` property is added to the GridPanel using this feature.
53 - A `filterupdate` event is added to the GridPanel and is fired upon onStateChange completion.
55 ##Server side code examples:##
57 - [PHP](http://www.vinylfox.com/extjs/grid-filter-php-backend-code.php) - (Thanks VinylFox)</li>
58 - [Ruby on Rails](http://extjs.com/forum/showthread.php?p=77326#post77326) - (Thanks Zyclops)</li>
59 - [Ruby on Rails](http://extjs.com/forum/showthread.php?p=176596#post176596) - (Thanks Rotomaul)</li>
60 - [Python](http://www.debatablybeta.com/posts/using-extjss-grid-filtering-with-django/) - (Thanks Matt)</li>
61 - [Grails](http://mcantrell.wordpress.com/2008/08/22/extjs-grids-and-grails/) - (Thanks Mike)</li>
65 var store = Ext.create('Ext.data.Store', {
72 autoReload: false, //don't reload automatically
73 local: true, //only filter locally
74 // filters may be configured through the plugin,
75 // or in the column definition within the headers configuration
87 dataIndex: 'dateAdded'
91 options: ['extra small', 'small', 'medium', 'large', 'extra large'],
99 var grid = Ext.create('Ext.grid.Panel', {
102 filters: [filtersCfg],
105 bbar: Ext.create('Ext.PagingToolbar', {
110 // a filters property is added to the GridPanel
115 Ext.define('Ext.ux.grid.FiltersFeature', {
116 extend: 'Ext.grid.feature.Feature',
117 alias: 'feature.filters',
119 'Ext.ux.grid.menu.ListMenu',
120 'Ext.ux.grid.menu.RangeMenu',
121 'Ext.ux.grid.filter.BooleanFilter',
122 'Ext.ux.grid.filter.DateFilter',
123 'Ext.ux.grid.filter.ListFilter',
124 'Ext.ux.grid.filter.NumericFilter',
125 'Ext.ux.grid.filter.StringFilter'
129 * @cfg {Boolean} autoReload
130 * Defaults to true, reloading the datasource when a filter change happens.
131 * Set this to false to prevent the datastore from being reloaded if there
132 * are changes to the filters. See <code>{@link updateBuffer}</code>.
136 * @cfg {Boolean} encode
137 * Specify true for {@link #buildQuery} to use Ext.util.JSON.encode to
138 * encode the filter query parameter sent with a remote request.
142 * @cfg {Array} filters
143 * An Array of filters config objects. Refer to each filter type class for
144 * configuration details specific to each filter type. Filters for Strings,
145 * Numeric Ranges, Date Ranges, Lists, and Boolean are the standard filters
149 * @cfg {String} filterCls
150 * The css class to be applied to column headers with active filters.
151 * Defaults to <tt>'ux-filterd-column'</tt>.
153 filterCls : 'ux-filtered-column',
155 * @cfg {Boolean} local
156 * <tt>true</tt> to use Ext.data.Store filter functions (local filtering)
157 * instead of the default (<tt>false</tt>) server side filtering.
161 * @cfg {String} menuFilterText
162 * defaults to <tt>'Filters'</tt>.
164 menuFilterText : 'Filters',
166 * @cfg {String} paramPrefix
167 * The url parameter prefix for the filters.
168 * Defaults to <tt>'filter'</tt>.
170 paramPrefix : 'filter',
172 * @cfg {Boolean} showMenu
173 * Defaults to true, including a filter submenu in the default header menu.
177 * @cfg {String} stateId
178 * Name of the value to be used to store state information.
182 * @cfg {Integer} updateBuffer
183 * Number of milliseconds to defer store updates since the last filter change.
187 // doesn't handle grid body events
188 hasFeatureEvent: false,
192 constructor : function (config) {
195 config = config || {};
196 Ext.apply(me, config);
198 me.deferredUpdate = Ext.create('Ext.util.DelayedTask', me.reload, me);
201 me.filters = me.createFiltersCollection();
202 me.filterConfigs = config.filters;
205 attachEvents: function() {
208 headerCt = view.headerCt,
209 grid = me.getGridPanel();
211 me.bindStore(view.getStore(), true);
213 // Listen for header menu being created
214 headerCt.on('menucreate', me.onMenuCreate, me);
216 view.on('refresh', me.onRefresh, me);
219 beforestaterestore: me.applyState,
220 beforestatesave: me.saveState,
221 beforedestroy: me.destroy
224 // Add event and filters shortcut on grid panel
226 grid.addEvents('filterupdate');
229 createFiltersCollection: function () {
230 return Ext.create('Ext.util.MixedCollection', false, function (o) {
231 return o ? o.dataIndex : null;
236 * @private Create the Filter objects for the current configuration, destroying any existing ones first.
238 createFilters: function() {
240 hadFilters = me.filters.getCount(),
241 grid = me.getGridPanel(),
242 filters = me.createFiltersCollection(),
243 model = grid.store.model,
244 fields = model.prototype.fields,
251 me.saveState(null, state);
254 function add (dataIndex, config, filterable) {
255 if (dataIndex && (filterable || config)) {
256 field = fields.get(dataIndex);
258 dataIndex: dataIndex,
259 type: (field && field.type && field.type.type) || 'auto'
262 if (Ext.isObject(config)) {
263 Ext.apply(filter, config);
266 filters.replace(filter);
270 // We start with filters from our config and then merge on filters from the columns
271 // in the grid. The Grid columns take precedence.
272 Ext.Array.each(me.filterConfigs, function (fc) {
273 add(fc.dataIndex, fc);
276 Ext.Array.each(grid.columns, function (column) {
277 if (column.filterable === false) {
278 filters.removeAtKey(column.dataIndex);
280 add(column.dataIndex, column.filter, column.filterable);
285 me.addFilters(filters.items);
288 me.applyState(null, state);
293 * @private Handle creation of the grid's header menu. Initializes the filters and listens
294 * for the menu being shown.
296 onMenuCreate: function(headerCt, menu) {
299 menu.on('beforeshow', me.onMenuBeforeShow, me);
303 * @private Handle showing of the grid's header menu. Sets up the filter item and menu
304 * appropriate for the target column.
306 onMenuBeforeShow: function(menu) {
311 menuItem = me.menuItem;
312 if (!menuItem || menuItem.isDestroyed) {
313 me.createMenuItem(menu);
314 menuItem = me.menuItem;
317 filter = me.getMenuFilter();
320 menuItem.menu = filter.menu;
321 menuItem.setChecked(filter.active);
322 // disable the menu if filter.disabled explicitly set to true
323 menuItem.setDisabled(filter.disabled === true);
325 menuItem.setVisible(!!filter);
326 this.sep.setVisible(!!filter);
331 createMenuItem: function(menu) {
333 me.sep = menu.add('-');
334 me.menuItem = menu.add({
337 text: me.menuFilterText,
340 checkchange: me.onCheckChange,
341 beforecheckchange: me.onBeforeCheck
346 getGridPanel: function() {
347 return this.view.up('gridpanel');
352 * Handler for the grid's beforestaterestore event (fires before the state of the
354 * @param {Object} grid The grid object
355 * @param {Object} state The hash of state values returned from the StateProvider.
357 applyState : function (grid, state) {
359 this.applyingState = true;
362 for (key in state.filters) {
363 filter = this.filters.get(key);
365 filter.setValue(state.filters[key]);
366 filter.setActive(true);
370 this.deferredUpdate.cancel();
374 delete this.applyingState;
375 delete state.filters;
379 * Saves the state of all active filters
380 * @param {Object} grid
381 * @param {Object} state
384 saveState : function (grid, state) {
386 this.filters.each(function (filter) {
388 filters[filter.dataIndex] = filter.getValue();
391 return (state.filters = filters);
396 * Handler called by the grid 'beforedestroy' event
398 destroy : function () {
400 Ext.destroyMembers(me, 'menuItem', 'sep');
406 * Remove all filters, permanently destroying them.
408 removeAll : function () {
410 Ext.destroy.apply(Ext, this.filters.items);
411 // remove all items from the collection
412 this.filters.clear();
418 * Changes the data store bound to this view and refreshes it.
419 * @param {Store} store The store to bind to this view
421 bindStore : function(store, initial){
422 if(!initial && this.store){
424 store.un('load', this.onLoad, this);
426 store.un('beforeload', this.onBeforeLoad, this);
431 store.on('load', this.onLoad, this);
433 store.on('beforeload', this.onBeforeLoad, this);
442 * Get the filter menu from the filters MixedCollection based on the clicked header
444 getMenuFilter : function () {
445 var header = this.view.headerCt.getMenu().activeHeader;
446 return header ? this.filters.get(header.dataIndex) : null;
450 onCheckChange : function (item, value) {
451 this.getMenuFilter().setActive(value);
455 onBeforeCheck : function (check, value) {
456 return !value || this.getMenuFilter().isActivatable();
461 * Handler for all events on filters.
462 * @param {String} event Event name
463 * @param {Object} filter Standard signature of the event before the event is fired
465 onStateChange : function (event, filter) {
466 if (event !== 'serialize') {
468 grid = me.getGridPanel();
470 if (filter == me.getMenuFilter()) {
471 me.menuItem.setChecked(filter.active, false);
474 if ((me.autoReload || me.local) && !me.applyingState) {
475 me.deferredUpdate.delay(me.updateBuffer);
477 me.updateColumnHeadings();
479 if (!me.applyingState) {
482 grid.fireEvent('filterupdate', me, filter);
488 * Handler for store's beforeload event when configured for remote filtering
489 * @param {Object} store
490 * @param {Object} options
492 onBeforeLoad : function (store, options) {
493 options.params = options.params || {};
494 this.cleanParams(options.params);
495 var params = this.buildQuery(this.getFilterData());
496 Ext.apply(options.params, params);
501 * Handler for store's load event when configured for local filtering
502 * @param {Object} store
503 * @param {Object} options
505 onLoad : function (store, options) {
506 store.filterBy(this.getRecordFilter());
511 * Handler called when the grid's view is refreshed
513 onRefresh : function () {
514 this.updateColumnHeadings();
518 * Update the styles for the header row based on the active filters
520 updateColumnHeadings : function () {
522 headerCt = me.view.headerCt;
524 headerCt.items.each(function(header) {
525 var filter = me.getFilter(header.dataIndex);
526 header[filter && filter.active ? 'addCls' : 'removeCls'](me.filterCls);
532 reload : function () {
534 store = me.view.getStore(),
538 store.clearFilter(true);
539 store.filterBy(me.getRecordFilter());
541 me.deferredUpdate.cancel();
547 * Method factory that generates a record validator for the filters active at the time
551 getRecordFilter : function () {
553 this.filters.each(function (filter) {
560 return function (record) {
561 for (i = 0; i < len; i++) {
562 if (!f[i].validateRecord(record)) {
571 * Adds a filter to the collection and observes it for state change.
572 * @param {Object/Ext.ux.grid.filter.Filter} config A filter configuration or a filter object.
573 * @return {Ext.ux.grid.filter.Filter} The existing or newly created filter object.
575 addFilter : function (config) {
576 var Cls = this.getFilterClass(config.type),
577 filter = config.menu ? config : (new Cls(config));
578 this.filters.add(filter);
580 Ext.util.Observable.capture(filter, this.onStateChange, this);
585 * Adds filters to the collection.
586 * @param {Array} filters An Array of filter configuration objects.
588 addFilters : function (filters) {
591 for (i = 0, len = filters.length; i < len; i++) {
593 // if filter config found add filter for the column
595 this.addFilter(filter);
602 * Returns a filter for the given dataIndex, if one exists.
603 * @param {String} dataIndex The dataIndex of the desired filter object.
604 * @return {Ext.ux.grid.filter.Filter}
606 getFilter : function (dataIndex) {
607 return this.filters.get(dataIndex);
611 * Turns all filters off. This does not clear the configuration information
612 * (see {@link #removeAll}).
614 clearFilters : function () {
615 this.filters.each(function (filter) {
616 filter.setActive(false);
621 * Returns an Array of the currently active filters.
622 * @return {Array} filters Array of the currently active filters.
624 getFilterData : function () {
625 var filters = [], i, len;
627 this.filters.each(function (f) {
629 var d = [].concat(f.serialize());
630 for (i = 0, len = d.length; i < len; i++) {
642 * Function to take the active filters data and build it into a query.
643 * The format of the query depends on the <code>{@link #encode}</code>
645 * <div class="mdetail-params"><ul>
647 * <li><b><tt>false</tt></b> : <i>Default</i>
648 * <div class="sub-desc">
649 * Flatten into query string of the form (assuming <code>{@link #paramPrefix}='filters'</code>:
651 filters[0][field]="someDataIndex"&
652 filters[0][data][comparison]="someValue1"&
653 filters[0][data][type]="someValue2"&
654 filters[0][data][value]="someValue3"&
657 * <li><b><tt>true</tt></b> :
658 * <div class="sub-desc">
659 * JSON encode the filter data
661 filters[0][field]="someDataIndex"&
662 filters[0][data][comparison]="someValue1"&
663 filters[0][data][type]="someValue2"&
664 filters[0][data][value]="someValue3"&
668 * Override this method to customize the format of the filter query for remote requests.
669 * @param {Array} filters A collection of objects representing active filters and their configuration.
670 * Each element will take the form of {field: dataIndex, data: filterConf}. dataIndex is not assured
671 * to be unique as any one filter may be a composite of more basic filters for the same dataIndex.
672 * @return {Object} Query keys and values
674 buildQuery : function (filters) {
675 var p = {}, i, f, root, dataPrefix, key, tmp,
676 len = filters.length;
679 for (i = 0; i < len; i++) {
681 root = [this.paramPrefix, '[', i, ']'].join('');
682 p[root + '[field]'] = f.field;
684 dataPrefix = root + '[data]';
685 for (key in f.data) {
686 p[[dataPrefix, '[', key, ']'].join('')] = f.data[key];
691 for (i = 0; i < len; i++) {
699 // only build if there is active filter
701 p[this.paramPrefix] = Ext.JSON.encode(tmp);
708 * Removes filter related query parameters from the provided object.
709 * @param {Object} p Query parameters that may contain filter related fields.
711 cleanParams : function (p) {
712 // if encoding just delete the property
714 delete p[this.paramPrefix];
715 // otherwise scrub the object of filter data
718 regex = new RegExp('^' + this.paramPrefix + '\[[0-9]+\]');
720 if (regex.test(key)) {
728 * Function for locating filter classes, overwrite this with your favorite
729 * loader to provide dynamic filter loading.
730 * @param {String} type The type of filter to load ('Filter' is automatically
731 * appended to the passed type; eg, 'string' becomes 'StringFilter').
732 * @return {Class} The Ext.ux.grid.filter.Class
734 getFilterClass : function (type) {
735 // map the supported Ext.data.Field type values into a supported filter
748 return Ext.ClassManager.getByAlias('gridfilter.' + type);