The source code

    /*!
     * Ext JS Library 3.3.1
     * Copyright(c) 2006-2010 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    Ext.namespace('Ext.ux.grid');

    /**
     * @class Ext.ux.grid.GridFilters
     * @extends Ext.util.Observable
     * GridFilter is a plugin (ptype='gridfilters') for grids that
     * allow for a slightly more robust representation of filtering than what is
     * provided by the default store.
     * Filtering is adjusted by the user using the grid's column header menu
     * (this menu can be disabled through configuration). Through this menu users
     * can configure, enable, and disable filters for each column.
     * Features:
     *
     * Filtering implementations :
     *
     * Default filtering for Strings, Numeric Ranges, Date Ranges, Lists (which can
     * be backed by a Ext.data.Store), and Boolean. Additional custom filter types
     * and menus are easily created by extending Ext.ux.grid.filter.Filter.
     *
     * Graphical indicators :
     *
     * Columns that are filtered have {@link #filterCls a configurable css class}
     * applied to the column headers.
     *
     * Paging :
     *
     * If specified as a plugin to the grid's configured PagingToolbar, the current page
     * will be reset to page 1 whenever you update the filters.
     *
     * Automatic Reconfiguration :
     *
     * Filters automatically reconfigure when the grid 'reconfigure' event fires.
     *
     * Stateful :
     * Filter information will be persisted across page loads by specifying a
     * stateId in the Grid configuration.
     *
     * The filter collection binds to the
     * {@link Ext.grid.GridPanel#beforestaterestore beforestaterestore}
     * and {@link Ext.grid.GridPanel#beforestatesave beforestatesave}
     * events in order to be stateful.
     *
     * Grid Changes :
     *
     * A filters property is added to the grid pointing to
     * this plugin.
     * A filterupdate event is added to the grid and is
     * fired upon onStateChange completion.
     *
     * Server side code examples :
     *
     * PHP - (Thanks VinylFox)
     * Ruby on Rails - (Thanks Zyclops)
     * Ruby on Rails - (Thanks Rotomaul)
     * Python - (Thanks Matt)
     * Grails - (Thanks Mike)
     *
     *
     * Example usage:
     *
    var store = new Ext.data.GroupingStore({
        ...
    });

    var filters = new Ext.ux.grid.GridFilters({
        autoReload: false, //don't reload automatically
        local: true, //only filter locally
        // filters may be configured through the plugin,
        // or in the column definition within the column model configuration
        filters: [{
            type: 'numeric',
            dataIndex: 'id'
        }, {
            type: 'string',
            dataIndex: 'name'
        }, {
            type: 'numeric',
            dataIndex: 'price'
        }, {
            type: 'date',
            dataIndex: 'dateAdded'
        }, {
            type: 'list',
            dataIndex: 'size',
            options: ['extra small', 'small', 'medium', 'large', 'extra large'],
            phpMode: true
        }, {
            type: 'boolean',
            dataIndex: 'visible'
        }]
    });
    var cm = new Ext.grid.ColumnModel([{
        ...
    }]);

    var grid = new Ext.grid.GridPanel({
         ds: store,
         cm: cm,
         view: new Ext.grid.GroupingView(),
         plugins: [filters],
         height: 400,
         width: 700,
         bbar: new Ext.PagingToolbar({
             store: store,
             pageSize: 15,
             plugins: [filters] //reset page to page 1 if filters change
         })
     });

    store.load({params: {start: 0, limit: 15}});

    // a filters property is added to the grid
    grid.filters
     *
     */
    Ext.ux.grid.GridFilters = Ext.extend(Ext.util.Observable, {
        /**
         * @cfg {Boolean} autoReload
         * Defaults to true, reloading the datasource when a filter change happens.
         * Set this to false to prevent the datastore from being reloaded if there
         * are changes to the filters.  See {@link updateBuffer}.
         */
        autoReload : true,
        /**
         * @cfg {Boolean} encode
         * Specify true for {@link #buildQuery} to use Ext.util.JSON.encode to
         * encode the filter query parameter sent with a remote request.
         * Defaults to false.
         */
        /**
         * @cfg {Array} filters
         * An Array of filters config objects. Refer to each filter type class for
         * configuration details specific to each filter type. Filters for Strings,
         * Numeric Ranges, Date Ranges, Lists, and Boolean are the standard filters
         * available.
         */
        /**
         * @cfg {String} filterCls
         * The css class to be applied to column headers with active filters.
         * Defaults to 'ux-filterd-column'.
         */
        filterCls : 'ux-filtered-column',
        /**
         * @cfg {Boolean} local
         * true to use Ext.data.Store filter functions (local filtering)
         * instead of the default (false) server side filtering.
         */
        local : false,
        /**
         * @cfg {String} menuFilterText
         * defaults to 'Filters'.
         */
        menuFilterText : 'Filters',
        /**
         * @cfg {String} paramPrefix
         * The url parameter prefix for the filters.
         * Defaults to 'filter'.
         */
        paramPrefix : 'filter',
        /**
         * @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,
        /**
         * @cfg {Integer} updateBuffer
         * Number of milliseconds to defer store updates since the last filter change.
         */
        updateBuffer : 500,

        /** @private */
        constructor : function (config) {
            config = config || {};
            this.deferredUpdate = new Ext.util.DelayedTask(this.reload, this);
            this.filters = new Ext.util.MixedCollection();
            this.filters.getKey = function (o) {
                return o ? o.dataIndex : null;
            };
            this.addFilters(config.filters);
            delete config.filters;
            Ext.apply(this, config);
        },

        /** @private */
        init : function (grid) {
            if (grid instanceof Ext.grid.GridPanel) {
                this.grid = grid;

                this.bindStore(this.grid.getStore(), true);
                // assumes no filters were passed in the constructor, so try and use ones from the colModel
                if(this.filters.getCount() == 0){
                    this.addFilters(this.grid.getColumnModel());
                }

                this.grid.filters = this;

                this.grid.addEvents({'filterupdate': true});

                grid.on({
                    scope: this,
                    beforestaterestore: this.applyState,
                    beforestatesave: this.saveState,
                    beforedestroy: this.destroy,
                    reconfigure: this.onReconfigure
                });

                if (grid.rendered){
                    this.onRender();
                } else {
                    grid.on({
                        scope: this,
                        single: true,
                        render: this.onRender
                    });
                }

            } else if (grid instanceof Ext.PagingToolbar) {
                this.toolbar = grid;
            }
        },

        /**
         * @private
         * Handler for the grid's beforestaterestore event (fires before the state of the
         * grid is restored).
         * @param {Object} grid The grid object
         * @param {Object} state The hash of state values returned from the StateProvider.
         */
        applyState : function (grid, state) {
            var key, filter;
            this.applyingState = true;
            this.clearFilters();
            if (state.filters) {
                for (key in state.filters) {
                    filter = this.filters.get(key);
                    if (filter) {
                        filter.setValue(state.filters[key]);
                        filter.setActive(true);
                    }
                }
            }
            this.deferredUpdate.cancel();
            if (this.local) {
                this.reload();
            }
            delete this.applyingState;
            delete state.filters;
        },

        /**
         * Saves the state of all active filters
         * @param {Object} grid
         * @param {Object} state
         * @return {Boolean}
         */
        saveState : function (grid, state) {
            var filters = {};
            this.filters.each(function (filter) {
                if (filter.active) {
                    filters[filter.dataIndex] = filter.getValue();
                }
            });
            return (state.filters = filters);
        },

        /**
         * @private
         * Handler called when the grid is rendered
         */
        onRender : function () {
            this.grid.getView().on('refresh', this.onRefresh, this);
            this.createMenu();
        },

        /**
         * @private
         * Handler called by the grid 'beforedestroy' event
         */
        destroy : function () {
            this.removeAll();
            this.purgeListeners();

            if(this.filterMenu){
                Ext.menu.MenuMgr.unregister(this.filterMenu);
                this.filterMenu.destroy();
                 this.filterMenu = this.menu.menu = null;
            }
        },

        /**
         * Remove all filters, permanently destroying them.
         */
        removeAll : function () {
            if(this.filters){
                Ext.destroy.apply(Ext, this.filters.items);
                // remove all items from the collection
                this.filters.clear();
            }
        },

        /**
         * Changes the data store bound to this view and refreshes it.
         * @param {Store} store The store to bind to this view
         */
        bindStore : function(store, initial){
            if(!initial && this.store){
                if (this.local) {
                    store.un('load', this.onLoad, this);
                } else {
                    store.un('beforeload', this.onBeforeLoad, this);
                }
            }
            if(store){
                if (this.local) {
                    store.on('load', this.onLoad, this);
                } else {
                    store.on('beforeload', this.onBeforeLoad, this);
                }
            }
            this.store = store;
        },

        /**
         * @private
         * Handler called when the grid reconfigure event fires
         */
        onReconfigure : function () {
            this.bindStore(this.grid.getStore());
            this.store.clearFilter();
            this.removeAll();
            this.addFilters(this.grid.getColumnModel());
            this.updateColumnHeadings();
        },

        createMenu : function () {
            var view = this.grid.getView(),
                hmenu = view.hmenu;

            if (this.showMenu && hmenu) {

                this.sep  = hmenu.addSeparator();
                this.filterMenu = new Ext.menu.Menu({
                    id: this.grid.id + '-filters-menu'
                });
                this.menu = hmenu.add({
                    checked: false,
                    itemId: 'filters',
                    text: this.menuFilterText,
                    menu: this.filterMenu
                });

                this.menu.on({
                    scope: this,
                    checkchange: this.onCheckChange,
                    beforecheckchange: this.onBeforeCheck
                });
                hmenu.on('beforeshow', this.onMenu, this);
            }
            this.updateColumnHeadings();
        },

        /**
         * @private
         * Get the filter menu from the filters MixedCollection based on the clicked header
         */
        getMenuFilter : function () {
            var view = this.grid.getView();
            if (!view || view.hdCtxIndex === undefined) {
                return null;
            }
            return this.filters.get(
                view.cm.config[view.hdCtxIndex].dataIndex
            );
        },

        /**
         * @private
         * Handler called by the grid's hmenu beforeshow event
         */
        onMenu : function (filterMenu) {
            var filter = this.getMenuFilter();

            if (filter) {
    /*
    TODO: lazy rendering
                if (!filter.menu) {
                    filter.menu = filter.createMenu();
                }
    */
                this.menu.menu = filter.menu;
                this.menu.setChecked(filter.active, false);
                // disable the menu if filter.disabled explicitly set to true
                this.menu.setDisabled(filter.disabled === true);
            }

            this.menu.setVisible(filter !== undefined);
            this.sep.setVisible(filter !== undefined);
        },

        /** @private */
        onCheckChange : function (item, value) {
            this.getMenuFilter().setActive(value);
        },

        /** @private */
        onBeforeCheck : function (check, value) {
            return !value || this.getMenuFilter().isActivatable();
        },

        /**
         * @private
         * Handler for all events on filters.
         * @param {String} event Event name
         * @param {Object} filter Standard signature of the event before the event is fired
         */
        onStateChange : function (event, filter) {
            if (event === 'serialize') {
                return;
            }

            if (filter == this.getMenuFilter()) {
                this.menu.setChecked(filter.active, false);
            }

            if ((this.autoReload || this.local) && !this.applyingState) {
                this.deferredUpdate.delay(this.updateBuffer);
            }
            this.updateColumnHeadings();

            if (!this.applyingState) {
                this.grid.saveState();
            }
            this.grid.fireEvent('filterupdate', this, filter);
        },

        /**
         * @private
         * Handler for store's beforeload event when configured for remote filtering
         * @param {Object} store
         * @param {Object} options
         */
        onBeforeLoad : function (store, options) {
            options.params = options.params || {};
            this.cleanParams(options.params);
            var params = this.buildQuery(this.getFilterData());
            Ext.apply(options.params, params);
        },

        /**
         * @private
         * Handler for store's load event when configured for local filtering
         * @param {Object} store
         * @param {Object} options
         */
        onLoad : function (store, options) {
            store.filterBy(this.getRecordFilter());
        },

        /**
         * @private
         * Handler called when the grid's view is refreshed
         */
        onRefresh : function () {
            this.updateColumnHeadings();
        },

        /**
         * Update the styles for the header row based on the active filters
         */
        updateColumnHeadings : function () {
            var view = this.grid.getView(),
                i, len, filter;
            if (view.mainHd) {
                for (i = 0, len = view.cm.config.length; i < len; i++) {
                    filter = this.getFilter(view.cm.config[i].dataIndex);
                    Ext.fly(view.getHeaderCell(i))[filter && filter.active ? 'addClass' : 'removeClass'](this.filterCls);
                }
            }
        },

        /** @private */
        reload : function () {
            if (this.local) {
                this.grid.store.clearFilter(true);
                this.grid.store.filterBy(this.getRecordFilter());
            } else {
                var start,
                    store = this.grid.store;
                this.deferredUpdate.cancel();
                if (this.toolbar) {
                    start = store.paramNames.start;
                    if (store.lastOptions && store.lastOptions.params && store.lastOptions.params[start]) {
                        store.lastOptions.params[start] = 0;
                    }
                }
                store.reload();
            }
        },

        /**
         * Method factory that generates a record validator for the filters active at the time
         * of invokation.
         * @private
         */
        getRecordFilter : function () {
            var f = [], len, i;
            this.filters.each(function (filter) {
                if (filter.active) {
                    f.push(filter);
                }
            });

            len = f.length;
            return function (record) {
                for (i = 0; i < len; i++) {
                    if (!f[i].validateRecord(record)) {
                        return false;
                    }
                }
                return true;
            };
        },

        /**
         * Adds a filter to the collection and observes it for state change.
         * @param {Object/Ext.ux.grid.filter.Filter} config A filter configuration or a filter object.
         * @return {Ext.ux.grid.filter.Filter} The existing or newly created filter object.
         */
        addFilter : function (config) {
            var Cls = this.getFilterClass(config.type),
                filter = config.menu ? config : (new Cls(config));
            this.filters.add(filter);

            Ext.util.Observable.capture(filter, this.onStateChange, this);
            return filter;
        },

        /**
         * Adds filters to the collection.
         * @param {Array/Ext.grid.ColumnModel} filters Either an Array of
         * filter configuration objects or an Ext.grid.ColumnModel.  The columns
         * of a passed Ext.grid.ColumnModel will be examined for a filter
         * property and, if present, will be used as the filter configuration object.
         */
        addFilters : function (filters) {
            if (filters) {
                var i, len, filter, cm = false, dI;
                if (filters instanceof Ext.grid.ColumnModel) {
                    filters = filters.config;
                    cm = true;
                }
                for (i = 0, len = filters.length; i < len; i++) {
                    filter = false;
                    if (cm) {
                        dI = filters[i].dataIndex;
                        filter = filters[i].filter || filters[i].filterable;
                        if (filter){
                            filter = (filter === true) ? {} : filter;
                            Ext.apply(filter, {dataIndex:dI});
                            // filter type is specified in order of preference:
                            //     filter type specified in config
                            //     type specified in store's field's type config
                            filter.type = filter.type || this.store.fields.get(dI).type.type;
                        }
                    } else {
                        filter = filters[i];
                    }
                    // if filter config found add filter for the column
                    if (filter) {
                        this.addFilter(filter);
                    }
                }
            }
        },

        /**
         * Returns a filter for the given dataIndex, if one exists.
         * @param {String} dataIndex The dataIndex of the desired filter object.
         * @return {Ext.ux.grid.filter.Filter}
         */
        getFilter : function (dataIndex) {
            return this.filters.get(dataIndex);
        },

        /**
         * Turns all filters off. This does not clear the configuration information
         * (see {@link #removeAll}).
         */
        clearFilters : function () {
            this.filters.each(function (filter) {
                filter.setActive(false);
            });
        },

        /**
         * Returns an Array of the currently active filters.
         * @return {Array} filters Array of the currently active filters.
         */
        getFilterData : function () {
            var filters = [], i, len;

            this.filters.each(function (f) {
                if (f.active) {
                    var d = [].concat(f.serialize());
                    for (i = 0, len = d.length; i < len; i++) {
                        filters.push({
                            field: f.dataIndex,
                            data: d[i]
                        });
                    }
                }
            });
            return filters;
        },

        /**
         * Function to take the active filters data and build it into a query.
         * The format of the query depends on the {@link #encode}
         * configuration:
         *
         *
         * false : Default
         *
         * Flatten into query string of the form (assuming {@link #paramPrefix}='filters':
         *
    filters[0][field]="someDataIndex"&
    filters[0][data][comparison]="someValue1"&
    filters[0][data][type]="someValue2"&
    filters[0][data][value]="someValue3"&
         *
         *
         * true :
         *
         * JSON encode the filter data
         *
    filters[0][field]="someDataIndex"&
    filters[0][data][comparison]="someValue1"&
    filters[0][data][type]="someValue2"&
    filters[0][data][value]="someValue3"&
         *
         *
         *
         * Override this method to customize the format of the filter query for remote requests.
         * @param {Array} filters A collection of objects representing active filters and their configuration.
         *    Each element will take the form of {field: dataIndex, data: filterConf}. dataIndex is not assured
         *    to be unique as any one filter may be a composite of more basic filters for the same dataIndex.
         * @return {Object} Query keys and values
         */
        buildQuery : function (filters) {
            var p = {}, i, f, root, dataPrefix, key, tmp,
                len = filters.length;

            if (!this.encode){
                for (i = 0; i < len; i++) {
                    f = filters[i];
                    root = [this.paramPrefix, '[', i, ']'].join('');
                    p[root + '[field]'] = f.field;

                    dataPrefix = root + '[data]';
                    for (key in f.data) {
                        p[[dataPrefix, '[', key, ']'].join('')] = f.data[key];
                    }
                }
            } else {
                tmp = [];
                for (i = 0; i < len; i++) {
                    f = filters[i];
                    tmp.push(Ext.apply(
                        {},
                        {field: f.field},
                        f.data
                    ));
                }
                // only build if there is active filter
                if (tmp.length > 0){
                    p[this.paramPrefix] = Ext.util.JSON.encode(tmp);
                }
            }
            return p;
        },

        /**
         * Removes filter related query parameters from the provided object.
         * @param {Object} p Query parameters that may contain filter related fields.
         */
        cleanParams : function (p) {
            // if encoding just delete the property
            if (this.encode) {
                delete p[this.paramPrefix];
            // otherwise scrub the object of filter data
            } else {
                var regex, key;
                regex = new RegExp('^' + this.paramPrefix + '\[[0-9]+\]');
                for (key in p) {
                    if (regex.test(key)) {
                        delete p[key];
                    }
                }
            }
        },

        /**
         * Function for locating filter classes, overwrite this with your favorite
         * loader to provide dynamic filter loading.
         * @param {String} type The type of filter to load ('Filter' is automatically
         * appended to the passed type; eg, 'string' becomes 'StringFilter').
         * @return {Class} The Ext.ux.grid.filter.Class
         */
        getFilterClass : function (type) {
            // map the supported Ext.data.Field type values into a supported filter
            switch(type) {
                case 'auto':
                  type = 'string';
                  break;
                case 'int':
                case 'float':
                  type = 'numeric';
                  break;
                case 'bool':
                  type = 'boolean';
                  break;
            }
            return Ext.ux.grid.filter[type.substr(0, 1).toUpperCase() + type.substr(1) + 'Filter'];
        }
    });

    // register ptype
    Ext.preg('gridfilters', Ext.ux.grid.GridFilters);