text: /**
   * 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,
  
      init: function (grid) {
          var me = this,
              store, headerCt;
  
          //<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;
  
          if (me.headerMenuListeners) {
              Ext.destroy(me.headerMenuListeners);
              me.headerMenuListeners = null;
          }
  
          me.headerMenuListeners = 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;
  
          Ext.destroy(me.headerCtListeners, me.gridListeners, me.headerMenuListeners);
  
          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();
                  }
              }
  
          }
      }
  });
  