/** * Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the * context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching * on their records. Example usage: * * //set up a fictional MixedCollection containing a few people to filter on * var allNames = new Ext.util.MixedCollection(); * allNames.addAll([ * {id: 1, name: 'Ed', age: 25}, * {id: 2, name: 'Jamie', age: 37}, * {id: 3, name: 'Abe', age: 32}, * {id: 4, name: 'Aaron', age: 26}, * {id: 5, name: 'David', age: 32} * ]); * * var ageFilter = new Ext.util.Filter({ * property: 'age', * value : 32 * }); * * var longNameFilter = new Ext.util.Filter({ * filterFn: function(item) { * return item.name.length > 4; * } * }); * * //a new MixedCollection with the 3 names longer than 4 characters * var longNames = allNames.filter(longNameFilter); * * //a new MixedCollection with the 2 people of age 32: * var youngFolk = allNames.filter(ageFilter); * */ Ext.define('Ext.util.Filter', { /** * @cfg {String} property * The property to filter on. Required unless a {@link #filterFn} is passed */ /** * @cfg {Mixed} value * The value to filter on. Required unless a {@link #filterFn} is passed. */ /** * @cfg {Function} filterFn * A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should return * `true` to accept each item or `false` to reject it. */ /** * @cfg {String} [id] * An identifier by which this Filter is indexed in a {@link Ext.data.Store#property-filters Store's filters collection} * * Identified Filters may be individually removed from a Store's filter set by using {@link Ext.data.Store#removeFilter}. * * Anonymous Filters may be removed en masse by passing `null` to {@link Ext.data.Store#removeFilter}. */ id: null, /** * @cfg {Boolean} anyMatch * True to allow any match - no regex start/end line anchors will be added. */ anyMatch: false, /** * @cfg {Boolean} exactMatch * True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true. */ exactMatch: false, /** * @cfg {Boolean} caseSensitive * True to make the regex case sensitive (adds 'i' switch to regex). */ caseSensitive: false, /** * @property {Boolean} disabled * Setting this property to `true` disables this individual Filter so that it no longer contributes to a {@link Ext.data.Store#property-filters Store's filter set} * * When disabled, the next time the store is filtered, the Filter plays no part in filtering and records eliminated by it may rejoin the dataset. * */ disabled: false, /** * @cfg {String} [operator] * The operator to use to compare the {@link #cfg-property} to this Filter's {@link #cfg-value} * * Possible values are: * * < * * <= * * = * * >= * * > * * != */ operator: null, /** * @cfg {String} root * Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data' to * make the filter pull the {@link #property} out of the data object of each item */ statics: { /** * Creates a single filter function which encapsulates the passed Filter array. * @param {Ext.util.Filter[]} filters The filter set for which to create a filter function * @return {Function} a function, which when passed a candidate object returns `true` if * the candidate passes all the specified Filters. */ createFilterFn: function(filters) { return filters && filters.length ? function(candidate) { var isMatch = true, length = filters.length, i, filter; for (i = 0; isMatch && i < length; i++) { filter = filters[i]; // Disabling a filter stops it from contributing to the overall filter function. if (!filter.disabled) { isMatch = isMatch && filter.filterFn.call(filter.scope || filter, candidate); } } return isMatch; } : function() { return true; }; } }, operatorFns: { "<": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) < this.value; }, "<=": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) <= this.value; }, "=": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) == this.value; }, ">=": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) >= this.value; }, ">": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) > this.value; }, "!=": function(candidate) { return Ext.coerce(this.getRoot(candidate)[this.property], this.value) != this.value; } }, /** * Creates new Filter. * @param {Object} [config] Config object */ constructor: function(config) { var me = this; me.initialConfig = config; Ext.apply(me, config); //we're aliasing filter to filterFn mostly for API cleanliness reasons, despite the fact it dirties the code here. //Ext.util.Sorter takes a sorterFn property but allows .sort to be called - we do the same here me.filter = me.filter || me.filterFn; if (me.filter === undefined) { me.setValue(config.value); } }, /** * Changes the value that this filter tests its configured (@link #cfg-property} with. * @param {Mixed} value The new value to compare the property with. */ setValue: function(value) { var me = this; me.value = value; if (me.property === undefined || me.value === undefined) { // Commented this out temporarily because it stops us using string ids in models. TODO: Remove this once // Model has been updated to allow string ids // Ext.Error.raise("A Filter requires either a property or a filterFn to be set"); } else { me.filter = me.createFilterFn(); } me.filterFn = me.filter; }, /** * Changes the filtering function which this Filter uses to choose items to include. * * This replaces any configured {@link #cfg-filterFn} and overrides any {@link #cfg-property} and {@link #cfg-value) settings. * @param {Function} filterFn A function which returns `true` or `false` to either include or exclude the passed object. * @param {Object} filterFn.value The value for consideration to be included or excluded. * */ setFilterFn: function(filterFn) { this.filterFn = this.filter = filterFn; }, /** * @private * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter */ createFilterFn: function() { var me = this, matcher = me.createValueMatcher(), property = me.property; if (me.operator) { return me.operatorFns[me.operator]; } else { return function(item) { var value = me.getRoot(item)[property]; return matcher === null ? value === null : matcher.test(value); }; } }, /** * @private * Returns the root property of the given item, based on the configured {@link #root} property * @param {Object} item The item * @return {Object} The root property of the object */ getRoot: function(item) { var root = this.root; return root === undefined ? item : item[root]; }, /** * @private * Returns a regular expression based on the given value and matching options */ createValueMatcher : function() { var me = this, value = me.value, anyMatch = me.anyMatch, exactMatch = me.exactMatch, caseSensitive = me.caseSensitive, escapeRe = Ext.String.escapeRegex; if (value === null) { return value; } if (!value.exec) { // not a regex value = String(value); if (anyMatch === true) { value = escapeRe(value); } else { value = '^' + escapeRe(value); if (exactMatch === true) { value += '$'; } } value = new RegExp(value, caseSensitive ? '' : 'i'); } return value; }, serialize: function() { var me = this, result = Ext.apply({}, me.initialConfig); result.value = me.value; return result; } }, function() { // Operator type '==' is the same as operator type '=' this.prototype.operatorFns['=='] = this.prototype.operatorFns['=']; });