/** * A filter that can be applied to an `Ext.util.Collection` or other data container such * an `Ext.data.Store`. A `Filter` can be simply a filter on a `property` and `value` pair * or a filter function with custom logic. * * Normally filters are added to stores or collections but they can be created directly: * * var ageFilter = new Ext.util.Filter({ * property: 'age', * value: 42, * operator: '<' * }); * * var longNameFilter = new Ext.util.Filter({ * filterFn: function(item) { * return item.name.length > 4; * } * }); */Ext.define('Ext.util.Filter', { extend: 'Ext.util.BasicFilter', config: { /** * @cfg {String} property * The property to filter on. Required unless a {@link #filterFn} is passed. */ property: null, /** * @cfg {String} root * This property is used to descend items to check for meaningful properties on * which to filter. For a `Ext.data.Model` for example this would be `'data'`. */ root: null, /** * @cfg {RegExp/Mixed} value * The value you want to match against. Required unless a {@link #filterFn} is passed. * * Can be a regular expression which will be used as a matcher or any other value * such as an object or an array of objects. This value is compared using the configured * {@link #operator}. */ value: null, /** * @cfg {Function} filterFn * A custom filter function which is passed each item. This function must return * `true` to accept an item or `false` to reject it. */ filterFn: 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, /** * @cfg {Boolean} disableOnEmpty * `true` to not have this filter participate in the filtering process when the * {@link #value} of this the filter is empty according to {@link Ext#isEmpty}. * * @since 5.1.0 */ disableOnEmpty: false, /** * @cfg {String} operator * The operator to use to compare the {@link #cfg!property} to this Filter's * {@link #cfg!value}. * * Possible values are: * * * `<` * * `<=` * * `=` * * `>=` * * `>` * * `!=` * * `in` * * `notin` * * `like` * * `/=` * * The `in` and `notin` operator expects this filter's {@link #cfg-value} to be * an array and matches values that are present in that array. * * The `like` operator matches values that contain this filter's {@link #cfg-value} * as a substring. * * The `/=` operator uses the {@link #cfg-value} as the source for a `RegExp` and * tests whether the candidate value matches the regular expression. */ operator: null, /** * @cfg {Function} [convert] * A function to do any conversion on the value before comparison. For example, * something that returns the date only part of a date. * @cfg {Object} convert.value The value to convert. * @cfg {Object} convert.return The converted value. * @private */ convert: null }, /** * @cfg {Object} [scope] * The context (`this` property) in which the filtering function is called. Defaults * to this Filter object. */ scope: null, // Needed for scope above. If `scope` were a "config" it would be merged and lose its // identity. $configStrict: false, statics: { /** * Creates a single filter function which encapsulates the passed Filter array or * Collection. * @param {Ext.util.Filter[]/Ext.util.Collection} filters The filters from 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) { if (!filters) { return Ext.returnTrue; } return function(candidate) { var items = filters.isCollection ? filters.items : filters, length = items.length, match = true, i, filter; for (i = 0; match && i < length; i++) { filter = items[i]; // Skip disabled filters if (!filter.getDisabled()) { match = filter.filter(candidate); } } return match; }; }, /** * Checks if two filters have the same properties (Property, Operator and Value). * * @param {Ext.util.Filter} filter1 The first filter to be compared * @param {Ext.util.Filter} filter2 The second filter to be compared * @return {Boolean} `true` if they have the same properties. * @since 6.2.0 */ isEqual: function(filter1, filter2) { if (filter1.getProperty() !== filter2.getProperty()) { return false; } if (filter1.getOperator() !== filter2.getOperator()) { return false; } if (filter1.getValue() === filter2.getValue()) { return true; } if (Ext.isArray(filter1) && Ext.isArray(filter2)) { return Ext.Array.equals(filter1, filter2); } return false; }, /** * Checks whether the filter will produce a meaningful value. Since filters * may be used in conjunction with data binding, this is a sanity check to * check whether the resulting filter will be able to match. * * @param {Object} cfg The filter config object * @return {Boolean/String} `true` if the filter will produce a valid value * * @private */ isInvalid: function(cfg) { if (!cfg.filterFn) { // If we don't have a filterFn, we must have a property if (!cfg.property) { return 'A Filter requires either a property or a filterFn to be set'; } if (!cfg.hasOwnProperty('value') && !cfg.operator) { return 'A Filter requires either a property and value, or a filterFn to be set'; } } return false; } }, //<debug> constructor: function(config) { var warn = Ext.util.Filter.isInvalid(config); if (warn) { Ext.log.warn(warn); } this.callParent([ config ]); }, //</debug> preventConvert: { 'in': 1, notin: 1 }, filter: function(item) { var me = this, filterFn = me._filterFn || me.getFilterFn(), convert = me.getConvert(), value = me._value; me._filterValue = value; me.isDateValue = Ext.isDate(value); if (me.isDateValue) { me.dateValue = value.getTime(); } if (convert && !me.preventConvert[me.getOperator()]) { me._filterValue = convert.call(me.scope || me, value); } return filterFn.call(me.scope || me, item); }, getId: function() { var me = this, id = me._id; if (!id) { id = me.getProperty(); if (!id) { id = Ext.id(null, 'ext-filter-'); } me._id = id; } return id; }, getFilterFn: function() { var me = this, filterFn = me._filterFn, operator; if (!filterFn) { operator = me.getOperator(); if (operator) { filterFn = me.operatorFns[operator]; } else { // This part is broken our into its own method so the function expression // contained there does not get hoisted and created on each call this // method. filterFn = me.createRegexFilter(); } me._filterFn = filterFn; // Mark as generated by default. This becomes important when proxies encode // filters. See proxy.Server#encodeFilters(). me.generatedFilterFn = true; } return filterFn; }, /** * @private * Creates a filter function for the configured value/anyMatch/caseSensitive options * for this Filter. */ createRegexFilter: function() { var me = this, anyMatch = !!me.getAnyMatch(), exact = !!me.getExactMatch(), value = me.getValue(), matcher = Ext.String.createRegex(value, !anyMatch, // startsWith !anyMatch && exact, // endsWith !me.getCaseSensitive()); return function(item) { var val = me.getPropertyValue(item); return matcher ? matcher.test(val) : (val == null); }; }, /** * Returns the property of interest from the given item, based on the configured `root` * and `property` configs. * @param {Object} item The item. * @return {Object} The property of the object. * @private */ getPropertyValue: function(item) { var root = this._root, value = (root == null) ? item : item[root]; return value[this._property]; }, /** * Returns this filter's state. * @return {Object} */ getState: function() { var config = this.getInitialConfig(), result = {}, name; for (name in config) { // We only want the instance properties in this case, not inherited ones, // so we need hasOwnProperty to filter out our class values. if (config.hasOwnProperty(name)) { result[name] = config[name]; } } delete result.root; result.value = this.getValue(); return result; }, getScope: function() { return this.scope; }, /** * Returns this filter's serialized state. This is used when transmitting this filter * to a server. * @return {Object} */ serialize: function() { var result = this.getState(), serializer = this.getSerializer(), serialized; delete result.id; delete result.serializer; if (serializer) { serialized = serializer.call(this, result); if (serialized) { result = serialized; } } return result; }, serializeTo: function(out) { var me = this, primitive, serialized; // Filters with a custom filterFn cannot be serialized. But since #getFilterFn() // always returns a filterFn, we need to check if it's been generated by default. // If so, we know that the filter cannot have a custom filterFn defined, and it // is therefore okay to serialize. me.getFilterFn(); if (me.generatedFilterFn) { out.push(serialized = me.serialize()); primitive = me.primitiveRe.test(typeof serialized); } return !primitive; }, updateOperator: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateConvert: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateProperty: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateAnyMatch: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateExactMatch: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateCaseSensitive: function() { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); }, updateValue: function(value) { // Need to clear any generated local filter fn and increment generation this.onConfigMutation(); if (this.getDisableOnEmpty()) { this.setDisabled(Ext.isEmpty(value)); } }, updateFilterFn: function(filterFn) { delete this.generatedFilterFn; }, onConfigMutation: function() { // Developers may use this to see if a filter has changed in ways that must cause // a reevaluation of filtering this.generation++; if (this.generatedFilterFn) { this._filterFn = null; } }, updateDisableOnEmpty: function(disableOnEmpty) { // Only poke disabled if true because otherwise we'll smash the disabled // config that may also be getting set. if (disableOnEmpty) { this.setDisabled(Ext.isEmpty(this.getValue())); } }, privates: { primitiveRe: /string|number|boolean/, getCandidateValue: function(candidate, v, preventCoerce) { var me = this, convert = me._convert, result = me.getPropertyValue(candidate); if (convert) { result = convert.call(me.scope || me, result); } else if (!preventCoerce) { result = Ext.coerce(result, v); } return result; } }}, function(Filter) { var prototype = Filter.prototype, operatorFns = (prototype.operatorFns = { "<": function(candidate) { var v = this._filterValue; return this.getCandidateValue(candidate, v) < v; }, "<=": function(candidate) { var v = this._filterValue; return this.getCandidateValue(candidate, v) <= v; }, "=": function(candidate) { var me = this, v = me._filterValue; candidate = me.getCandidateValue(candidate, v); if (me.isDateValue && candidate instanceof Date) { candidate = candidate.getTime(); v = me.dateValue; } return candidate == v; // eslint-disable-line eqeqeq }, "===": function(candidate) { var me = this, v = me._filterValue; candidate = me.getCandidateValue(candidate, v, true); if (me.isDateValue && candidate instanceof Date) { candidate = candidate.getTime(); v = me.dateValue; } return candidate === v; }, ">=": function(candidate) { var v = this._filterValue; return this.getCandidateValue(candidate, v) >= v; }, ">": function(candidate) { var v = this._filterValue; return this.getCandidateValue(candidate, v) > v; }, "!=": function(candidate) { var me = this, v = me._filterValue; candidate = me.getCandidateValue(candidate, v); if (me.isDateValue && candidate instanceof Date) { candidate = candidate.getTime(); v = me.dateValue; } return candidate != v; // eslint-disable-line eqeqeq }, "!==": function(candidate) { var me = this, v = me._filterValue; candidate = me.getCandidateValue(candidate, v, true); if (me.isDateValue && candidate instanceof Date) { candidate = candidate.getTime(); v = me.dateValue; } return candidate !== v; }, "in": function(candidate) { var v = this._filterValue; return Ext.Array.contains(v, this.getCandidateValue(candidate, v)); }, notin: function(candidate) { var v = this._filterValue; return !Ext.Array.contains(v, this.getCandidateValue(candidate, v)); }, like: function(candidate) { var v = this._filterValue; // eslint-disable-next-line max-len return v && this.getCandidateValue(candidate, v).toLowerCase().indexOf(v.toLowerCase()) > -1; }, "/=": function(candidate) { var me = this, v = me._filterValue; candidate = me.getCandidateValue(candidate, v); // Only compile a RegExp when the source string changes if (v !== me.lastRegExpSource) { me.lastRegExpSource = v; try { me.regex = new RegExp(v, 'i'); } catch (e) { me.regex = null; } } return me.regex ? me.regex.test(candidate) : false; } }); // Operator type '==' is the same as operator type '=' operatorFns['=='] = operatorFns['=']; operatorFns.gt = operatorFns['>']; operatorFns.ge = operatorFns['>=']; operatorFns.lt = operatorFns['<']; operatorFns.le = operatorFns['<=']; operatorFns.eq = operatorFns['=']; operatorFns.ne = operatorFns['!='];});