Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1198 lines
47 KiB

/**
* Provides searching of Components within Ext.ComponentManager (globally) or a specific
* Ext.container.Container on the document with a similar syntax to a CSS selector.
* Returns Array of matching Components, or empty Array.
*
* ## Basic Component lookup
*
* Components can be retrieved by using their {@link Ext.Component xtype}:
*
* - `component`
* - `gridpanel`
*
* Matching by `xtype` matches inherited types, so in the following code, the previous field
* *of any type which inherits from `TextField`* will be found:
*
* prevField = myField.previousNode('textfield');
*
* To match only the exact type, pass the "shallow" flag by adding `(true)` to xtype
* (See Component's {@link Ext.Component#isXType isXType} method):
*
* prevTextField = myField.previousNode('textfield(true)');
*
* You can search Components by their `id` or `itemId` property, prefixed with a #:
*
* #myContainer
*
* Component `xtype` and `id` or `itemId` can be used together to avoid possible
* id collisions between Components of different types:
*
* panel#myPanel
*
* When Component's `id` or `xtype` contains dots, you can escape them in your selector:
*
* my\.panel#myPanel
*
* Keep in mind that JavaScript treats the backslash character in a special way, so you
* need to escape it, too, in the actual code:
*
* var myPanel = Ext.ComponentQuery.query('my\\.panel#myPanel');
*
* ## Traversing Component tree
*
* Components can be found by their relation to other Components. There are several
* relationship operators, mostly taken from CSS selectors:
*
* - **`E F`** All descendant Components of E that match F
* - **`E > F`** All direct children Components of E that match F
* - **`E ^ F`** All parent Components of E that match F
*
* Expressions between relationship operators are matched left to right, i.e. leftmost
* selector is applied first, then if one or more matches are found, relationship operator
* itself is applied, then next selector expression, etc. It is possible to combine
* relationship operators in complex selectors:
*
* window[title="Input form"] textfield[name=login] ^ form > button[action=submit]
*
* That selector can be read this way: Find a window with title "Input form", in that
* window find a TextField with name "login" at any depth (including subpanels and/or
* FieldSets), then find an `Ext.form.Panel` that is a parent of the TextField, and in
* that form find a direct child that is a button with custom property `action` set to
* value "submit".
*
* Whitespace on both sides of `^` and `>` operators is non-significant, i.e. can be
* omitted, but usually is used for clarity.
*
* ## Searching by Component attributes
*
* Components can be searched by their object property values (attributes). To do that,
* use attribute matching expression in square brackets:
*
* - `component[disabled]` - matches any Component that has `disabled` property with
* any truthy (non-empty, not `false`) value.
* - `panel[title="Test"]` - matches any Component that has `title` property set to
* "Test". Note that if the value does not contain spaces, the quotes are optional.
*
* Attributes can use any of the following operators to compare values:
* `=`, `!=`, `^=`, `$=`, `*=`, `%=`, `|=` and `~=`.
*
* Prefixing the attribute name with an at sign `@` means that the property must be
* the object's `ownProperty`, not a property from the prototype chain.
*
* Specifications like `[propName]` check that the property is a truthy value. To check
* that the object has an `ownProperty` of a certain name, regardless of the value use
* the form `[?propName]`.
*
* The specified value is coerced to match the type of the property found in the
* candidate Component using {@link Ext#coerce}.
*
* If you need to find Components by their `itemId` property, use the `#id` form; it will
* do the same as `[itemId=id]` but is easier to read.
*
* If you need to include a metacharacter like (, ), [, ], etc., in the query, escape it
* by prefixing it with a backslash:
*
* var component = Ext.ComponentQuery.query('[myProperty=\\[foo\\]]');
*
* ## Attribute matching operators
*
* The '=' operator will return the results that **exactly** match the
* specified object property (attribute):
*
* Ext.ComponentQuery.query('panel[cls=my-cls]');
*
* Will match the following Component:
*
* Ext.create('Ext.window.Window', {
* cls: 'my-cls'
* });
*
* But will not match the following Component, because 'my-cls' is one value
* among others:
*
* Ext.create('Ext.panel.Panel', {
* cls: 'foo-cls my-cls bar-cls'
* });
*
* You can use the '~=' operator instead, it will return Components with
* the property that **exactly** matches one of the whitespace-separated
* values. This is also true for properties that only have *one* value:
*
* Ext.ComponentQuery.query('panel[cls~=my-cls]');
*
* Will match both Components:
*
* Ext.create('Ext.panel.Panel', {
* cls: 'foo-cls my-cls bar-cls'
* });
*
* Ext.create('Ext.window.Window', {
* cls: 'my-cls'
* });
*
* Generally, '=' operator is more suited for object properties other than
* CSS classes, while '~=' operator will work best with properties that
* hold lists of whitespace-separated CSS classes.
*
* The '^=' operator will return Components with specified attribute that
* start with the passed value:
*
* Ext.ComponentQuery.query('panel[title^=Sales]');
*
* Will match the following Component:
*
* Ext.create('Ext.panel.Panel', {
* title: 'Sales estimate for Q4'
* });
*
* The '$=' operator will return Components with specified properties that
* end with the passed value:
*
* Ext.ComponentQuery.query('field[fieldLabel$=name]');
*
* Will match the following Component:
*
* Ext.create('Ext.form.field.Text', {
* fieldLabel: 'Enter your name'
* });
*
* The '/=' operator will return Components with specified properties that
* match the passed regular expression:
*
* Ext.ComponentQuery.query('button[action/="edit|save"]');
*
* Will match the following Components with a custom `action` property:
*
* Ext.create('Ext.button.Button', {
* action: 'edit'
* });
*
* Ext.create('Ext.button.Button', {
* action: 'save'
* });
*
* When you need to use meta characters like [], (), etc. in your query, make sure
* to escape them with back slashes:
*
* Ext.ComponentQuery.query('panel[title="^Sales for Q\\[1-4\\]"]');
*
* The following test will find panels with their `ownProperty` collapsed being equal to
* `false`. It will **not** match a collapsed property from the prototype chain.
*
* Ext.ComponentQuery.query('panel[@collapsed=false]');
*
* Member expressions from candidate Components may be tested. If the expression returns
* a *truthy* value, the candidate Component will be included in the query:
*
* var disabledFields = myFormPanel.query("{isDisabled()}");
*
* Such expressions are executed in Component's context, and the above expression is
* similar to running this snippet for every Component in your application:
*
* if (component.isDisabled()) {
* matches.push(component);
* }
*
* It is important to use only methods that are available in **every** Component instance
* to avoid run time exceptions. If you need to match your Components with a custom
* condition formula, you can augment `Ext.Component` to provide custom matcher that
* will return `false` by default, and override it in your custom classes:
*
* Ext.define('My.Component', {
* override: 'Ext.Component',
* myMatcher: function() { return false; }
* });
*
* Ext.define('My.Panel', {
* extend: 'Ext.panel.Panel',
* requires: ['My.Component'], // Ensure that Component override is applied
* myMatcher: function(selector) {
* return selector === 'myPanel';
* }
* });
*
* After that you can use a selector with your custom matcher to find all instances
* of `My.Panel`:
*
* Ext.ComponentQuery.query("{myMatcher('myPanel')}");
*
* However if you really need to use a custom matcher, you may find it easier to implement
* a custom Pseudo class instead (see below).
*
* ## Conditional matching
*
* Attribute matchers can be combined to select only Components that match **all**
* conditions (logical AND operator):
*
* Ext.ComponentQuery.query('panel[cls~=my-cls][floating=true][title$="sales data"]');
*
* E.g., the query above will match only a Panel-descended Component that has 'my-cls'
* CSS class *and* is floating *and* with a title that ends with "sales data".
*
* Expressions separated with commas will match any Component that satisfies
* *either* expression (logical OR operator):
*
* Ext.ComponentQuery.query('field[fieldLabel^=User], field[fieldLabel*=password]');
*
* E.g., the query above will match any field with field label starting with "User",
* *or* any field that has "password" in its label.
*
* If you need to include a comma in an attribute matching expression, escape it with a
* backslash:
*
* Ext.ComponentQuery.query('field[fieldLabel^="User\\, foo"], field[fieldLabel*=password]');
*
* ## Pseudo classes
*
* Pseudo classes may be used to filter results in the same way as in
* {@link Ext.dom.Query}. There are five default pseudo classes:
*
* * `not` Negates a selector.
* * `first` Filters out all except the first matching item for a selector.
* * `last` Filters out all except the last matching item for a selector.
* * `focusable` Filters out all except Components which are currently able to recieve
* focus.
* * `nth-child` Filters Components by ordinal position in the selection.
*
* These pseudo classes can be used with other matchers or without them:
*
* // Select first direct child button in any panel
* Ext.ComponentQuery.query('panel > button:first');
*
* // Select last field in Profile form
* Ext.ComponentQuery.query('form[title=Profile] field:last');
*
* // Find first focusable Component in a panel and focus it
* panel.down(':focusable').focus();
*
* // Select any field that is not hidden in a form
* form.query('field:not(hiddenfield)');
*
* Pseudo class `nth-child` can be used to find any child Component by its
* position relative to its siblings. This class' handler takes one argument
* that specifies the selection formula as `Xn` or `Xn+Y`:
*
* // Find every odd field in a form
* form.query('field:nth-child(2n+1)'); // or use shortcut: :nth-child(odd)
*
* // Find every even field in a form
* form.query('field:nth-child(2n)'); // or use shortcut: :nth-child(even)
*
* // Find every 3rd field in a form
* form.query('field:nth-child(3n)');
*
* Pseudo classes can be combined to further filter the results, e.g., in the
* form example above we can modify the query to exclude hidden fields:
*
* // Find every 3rd non-hidden field in a form
* form.query('field:not(hiddenfield):nth-child(3n)');
*
* Note that when combining pseudo classes, whitespace is significant, i.e.
* there should be no spaces between pseudo classes. This is a common mistake;
* if you accidentally type a space between `field` and `:not`, the query
* will not return any result because it will mean "find *field's children
* Components* that are not hidden fields...".
*
* ## Custom pseudo classes
*
* It is possible to define your own custom pseudo classes. In fact, a
* pseudo class is just a property in `Ext.ComponentQuery.pseudos` object
* that defines pseudo class name (property name) and pseudo class handler
* (property value):
*
* // Function receives array and returns a filtered array.
* Ext.ComponentQuery.pseudos.invalid = function(items) {
* var i = 0, l = items.length, c, result = [];
* for (; i < l; i++) {
* if (!(c = items[i]).isValid()) {
* result.push(c);
* }
* }
* return result;
* };
*
* var invalidFields = myFormPanel.query('field:invalid');
* if (invalidFields.length) {
* invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
* for (var i = 0, l = invalidFields.length; i < l; i++) {
* invalidFields[i].getEl().frame("red");
* }
* }
*
* Pseudo class handlers can be even more flexible, with a selector
* argument used to define the logic:
*
* // Handler receives array of itmes and selector in parentheses
* Ext.ComponentQuery.pseudos.titleRegex = function(components, selector) {
* var i = 0, l = components.length, c, result = [], regex = new RegExp(selector);
* for (; i < l; i++) {
* c = components[i];
* if (c.title && regex.test(c.title)) {
* result.push(c);
* }
* }
* return result;
* }
*
* var salesTabs = tabPanel.query('panel:titleRegex("sales\\s+for\\s+201[123]")');
*
* Be careful when using custom pseudo classes with MVC Controllers: when
* you use a pseudo class in Controller's `control` or `listen` component
* selectors, the pseudo class' handler function will be called very often
* and may slow down your application significantly. A good rule of thumb
* is to always specify Component xtype with the pseudo class so that the
* handlers are only called on Components that you need, and try to make
* the condition checks as cheap in terms of execution time as possible.
* Note how in the example above, handler function checks that Component
* *has* a title first, before running regex test on it.
*
* ## Query examples
*
* Queries return an array of Components. Here are some example queries:
*
* // retrieve all Ext.Panels in the document by xtype
* var panelsArray = Ext.ComponentQuery.query('panel');
*
* // retrieve all Ext.Panels within the container with an id myCt
* var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
*
* // retrieve all direct children which are Ext.Panels within myCt
* var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
*
* // retrieve all grids or trees
* var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
*
* // Focus first Component
* myFormPanel.child(':focusable').focus();
*
* // Retrieve every odd text field in a form
* myFormPanel.query('textfield:nth-child(odd)');
*
* // Retrieve every even field in a form, excluding hidden fields
* myFormPanel.query('field:not(hiddenfield):nth-child(even)');
*
* For easy access to queries based from a particular Container see the
* {@link Ext.container.Container#query}, {@link Ext.container.Container#down} and
* {@link Ext.container.Container#child} methods. Also see
* {@link Ext.Component#up}.
*/
Ext.define('Ext.ComponentQuery', {
singleton: true,
requires: [
'Ext.ComponentManager',
'Ext.util.Operators',
'Ext.util.LruCache'
]
}, function() {
var cq = this,
queryOperators = Ext.util.Operators,
nthRe = /(\d*)n\+?(\d*)/,
nthRe2 = /\D/,
stripLeadingSpaceRe = /^(\s)+/,
unescapeRe = /\\(.)/g,
regexCache = new Ext.util.LruCache({
maxSize: 100
}),
// A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
// as a member on each item in the passed array.
filterFnPattern = [
'var r = [],',
'i = 0,',
'it = items,',
'l = it.length,',
'c;',
'for (; i < l; i++) {',
'c = it[i];',
'if (c.{0}) {',
'r.push(c);',
'}',
'}',
'return r;'
].join(''),
filterItems = function(items, operation) {
// Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
// The operation's method loops over each item in the candidate array and
// returns an array of items which match its criteria
return operation.method.apply(this, [ items ].concat(operation.args));
},
getItems = function(items, mode) {
var result = [],
i = 0,
length = items.length,
candidate,
deep = mode !== '>';
for (; i < length; i++) {
candidate = items[i];
if (candidate.getRefItems) {
result = result.concat(candidate.getRefItems(deep));
}
}
return result;
},
getAncestors = function(items) {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
while (!!(candidate = candidate.getRefOwner())) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which match the passed xtype
filterByXType = function(items, xtype, shallow) {
if (xtype === '*') {
return items.slice();
}
else {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
if (candidate.isXType(xtype, shallow)) {
result.push(candidate);
}
}
return result;
}
},
// Filters the passed candidate array and returns only items which have the specified property match
filterByAttribute = function(items, property, operator, compareTo) {
var result = [],
i = 0,
length = items.length,
mustBeOwnProperty,
presenceOnly,
candidate, propValue,
j, propLen,
config;
// Prefixing property name with an @ means that the property must be in the candidate, not in its prototype
if (property.charAt(0) === '@') {
mustBeOwnProperty = true;
property = property.substr(1);
}
if (property.charAt(0) === '?') {
mustBeOwnProperty = true;
presenceOnly = true;
property = property.substr(1);
}
for (; i < length; i++) {
candidate = items[i];
config = candidate.self.$config.configs[property];
if (config) {
propValue = candidate[config.names.get]();
} else if (mustBeOwnProperty && !candidate.hasOwnProperty(property)) {
continue;
} else {
propValue = candidate[property];
}
if (presenceOnly) {
result.push(candidate);
}
// implies property is an array, and we must compare value against each element.
else if (operator === '~=') {
if (propValue) {
//We need an array
if (!Ext.isArray(propValue)) {
propValue = propValue.split(' ');
}
for (j = 0, propLen = propValue.length; j < propLen; j++) {
if (queryOperators[operator](Ext.coerce(propValue[j], compareTo), compareTo)) {
result.push(candidate);
break;
}
}
}
}
else if (operator === '/=') {
if (propValue != null && compareTo.test(propValue)) {
result.push(candidate);
}
} else if (!compareTo ? !!candidate[property] : queryOperators[operator](Ext.coerce(propValue, compareTo), compareTo)) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which have the specified itemId or id
filterById = function(items, id) {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
if (candidate.getItemId() === id) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
filterByPseudo = function(items, name, value) {
return cq.pseudos[name](items, value);
},
// Determines leading mode
// > for direct child, and ^ to switch to ownerCt axis
modeRe = /^(\s?([>\^])\s?|\s|$)/,
// Matches a token with possibly (true|false) appended for the "shallow" parameter
tokenRe = /^(#)?((?:\\\.|[\w\-])+|\*)(?:\((true|false)\))?/,
matchers = [{
// Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
re: /^\.((?:\\\.|[\w\-])+)(?:\((true|false)\))?/,
method: filterByXType,
argTransform: function(args) {
//<debug>
var selector = args[0];
Ext.log.warn('"'+ selector +'" ComponentQuery selector style is deprecated,' +
' use "' + selector.replace(/^\./, '') + '" without the leading dot instead');
//</debug>
if (args[1] !== undefined) {
args[1] = args[1].replace(unescapeRe, '$1');
}
return args.slice(1);
}
}, {
// Allow [@attribute] to check truthy ownProperty
// Allow [?attribute] to check for presence of ownProperty
// Allow [$attribute]
// Checks for @|?|$ -> word/hyphen chars -> any special attribute selector characters before
// the '=', etc. It strips out whitespace.
// For example:
// [attribute=value], [attribute^=value], [attribute$=value], [attribute*=value],
// [attribute~=value], [attribute%=value], [attribute!=value]
re: /^(?:\[((?:[@?$])?[\w\-]*)\s*(?:([\^$*~%!\/]?=)\s*(['"])?((?:\\\]|.)*?)\3)?(?!\\)\])/,
method: filterByAttribute,
argTransform: function(args) {
var selector = args[0],
property = args[1],
operator = args[2],
//quote = args[3],
compareTo = args[4],
compareRe;
// Unescape the attribute value matcher first
if (compareTo !== undefined) {
compareTo = compareTo.replace(unescapeRe, '$1');
//<debug>
var format = Ext.String.format,
msg = "ComponentQuery selector '{0}' has an unescaped ({1}) character at the {2} " +
"of the attribute value pattern. Usually that indicates an error " +
"where the opening quote is not followed by the closing quote. " +
"If you need to match a ({1}) character at the {2} of the attribute " +
"value, escape the quote character in your pattern: (\\{1})",
match;
if (match = /^(['"]).*?[^'"]$/.exec(compareTo)) { // jshint ignore:line
Ext.log.warn(format(msg, selector, match[1], 'beginning'));
}
else if (match = /^[^'"].*?(['"])$/.exec(compareTo)) { // jshint ignore:line
Ext.log.warn(format(msg, selector, match[1], 'end'));
}
//</debug>
}
if (operator === '/=') {
compareRe = regexCache.get(compareTo);
if (compareRe) {
compareTo = compareRe;
} else {
compareTo = regexCache.add(compareTo, new RegExp(compareTo));
}
}
return [property, operator, compareTo];
}
}, {
// checks for #cmpItemId
re: /^#((?:\\\.|[\w\-])+)/,
method: filterById
}, {
// checks for :<pseudo_class>(<selector>)
re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
method: filterByPseudo,
argTransform: function(args) {
if (args[2] !== undefined) {
args[2] = args[2].replace(unescapeRe, '$1');
}
return args.slice(1);
}
}, {
// checks for {<member_expression>}
re: /^(?:\{([^\}]+)\})/,
method: filterFnPattern
}];
// Internal class Ext.ComponentQuery.Query
cq.Query = Ext.extend(Object, {
constructor: function(cfg) {
cfg = cfg || {};
Ext.apply(this, cfg);
},
// Executes this Query upon the selected root.
// The root provides the initial source of candidate Component matches which are progressively
// filtered by iterating through this Query's operations cache.
// If no root is provided, all registered Components are searched via the ComponentManager.
// root may be a Container who's descendant Components are filtered
// root may be a Component with an implementation of getRefItems which provides some nested Components such as the
// docked items within a Panel.
// root may be an array of candidate Components to filter using this Query.
execute: function(root) {
var operations = this.operations,
result = [],
op, i, len;
for (i = 0, len = operations.length; i < len; i++) {
op = operations[i];
result = result.concat(this._execute(root, op));
}
return result;
},
_execute: function(root, operations) {
var i = 0,
length = operations.length,
operation,
workingItems;
// no root, use all Components in the document
if (!root) {
workingItems = Ext.ComponentManager.getAll();
}
// Root is an iterable object like an Array, or system Collection, eg HtmlCollection
else if (Ext.isIterable(root)) {
workingItems = root;
}
// Root is a MixedCollection
else if (root.isMixedCollection) {
workingItems = root.items;
}
// We are going to loop over our operations and take care of them
// one by one.
for (; i < length; i++) {
operation = operations[i];
// The mode operation requires some custom handling.
// All other operations essentially filter down our current
// working items, while mode replaces our current working
// items by getting children from each one of our current
// working items. The type of mode determines the type of
// children we get. (e.g. > only gets direct children)
if (operation.mode === '^') {
workingItems = getAncestors(workingItems || [root]);
}
else if (operation.mode) {
workingItems = getItems(workingItems || [root], operation.mode);
}
else {
workingItems = filterItems(workingItems || getItems([root]), operation);
}
// If this is the last operation, it means our current working
// items are the final matched items. Thus return them!
if (i === length -1) {
return workingItems;
}
}
return [];
},
is: function(component) {
var operations = this.operations,
result = false,
len = operations.length,
op, i;
if (len === 0) {
return true;
}
for (i = 0; i < len; i++) {
op = operations[i];
result = this._is(component, op);
if (result) {
return result;
}
}
return false;
},
_is: function(component, operations) {
var len = operations.length,
active = [component],
operation, i, j, mode, items, item;
// Loop backwards, since we're going up the hierarchy
for (i = len - 1; i >= 0; --i) {
operation = operations[i];
mode = operation.mode;
// Traversing hierarchy
if (mode) {
if (mode === '^') {
active = getItems(active, ' ');
} else if (mode === '>') {
items = [];
for (j = 0, len = active.length; j < len; ++j) {
item = active[j].getRefOwner();
if (item) {
items.push(item);
}
}
active = items;
} else {
active = getAncestors(active);
}
// After traversing the hierarchy, if we have no items, jump out
if (active.length === 0) {
return false;
}
} else {
active = filterItems(active, operation);
if (active.length === 0) {
return false;
}
}
}
return true;
},
getMatches: function(components, operations) {
var len = operations.length,
i;
for (i = 0; i < len; ++i) {
components = filterItems(components, operations[i]);
// Performance enhancement, if we have nothing, we can
// never add anything new, so jump out
if (components.length === 0) {
break;
}
}
return components;
},
isMultiMatch: function() {
return this.operations.length > 1;
}
});
Ext.apply(cq, {
// private cache of selectors and matching ComponentQuery.Query objects
cache: new Ext.util.LruCache({
maxSize: 100
}),
// private cache of pseudo class filter functions
pseudos: {
not: function(components, selector){
var i = 0,
length = components.length,
results = [],
index = -1,
component;
for(; i < length; ++i) {
component = components[i];
if (!cq.is(component, selector)) {
results[++index] = component;
}
}
return results;
},
first: function(components) {
var ret = [];
if (components.length > 0) {
ret.push(components[0]);
}
return ret;
},
last: function(components) {
var len = components.length,
ret = [];
if (len > 0) {
ret.push(components[len - 1]);
}
return ret;
},
focusable: function(cmps) {
var len = cmps.length,
results = [],
i = 0,
c;
for (; i < len; i++) {
c = cmps[i];
if (c.isFocusable && c.isFocusable()) {
results.push(c);
}
}
return results;
},
"nth-child" : function(c, a) {
var result = [],
m = nthRe.exec(a === "even" && "2n" || a === "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a),
f = (m[1] || 1) - 0, len = m[2] - 0,
i, n, nodeIndex;
for (i = 0; n = c[i]; i++) { // jshint ignore:line
nodeIndex = i + 1;
if (f === 1) {
if (len === 0 || nodeIndex === len) {
result.push(n);
}
} else if ((nodeIndex + len) % f === 0){
result.push(n);
}
}
return result;
}
},
/**
* Returns an array of matched Components from within the passed root object.
*
* This method filters returned Components in a similar way to how CSS selector based DOM
* queries work using a textual selector string.
*
* See class summary for details.
*
* @param {String} selector The selector string to filter returned Components
* @param {Ext.container.Container} [root] The Container within which to perform the query.
* If omitted, all Components within the document are included in the search.
*
* This parameter may also be an array of Components to filter according to the selector.
* @return {Ext.Component[]} The matched Components.
*
* @member Ext.ComponentQuery
*/
query: function(selector, root) {
// An empty query will match every Component
if (!selector) {
return Ext.ComponentManager.all.getArray();
}
var results = [],
noDupResults = [],
dupMatcher = {},
query = cq.cache.get(selector),
resultsLn, cmp, i;
if (!query) {
query = cq.cache.add(selector, cq.parse(selector));
}
results = query.execute(root);
// multiple selectors, potential to find duplicates
// lets filter them out.
if (query.isMultiMatch()) {
resultsLn = results.length;
for (i = 0; i < resultsLn; i++) {
cmp = results[i];
if (!dupMatcher[cmp.id]) {
noDupResults.push(cmp);
dupMatcher[cmp.id] = true;
}
}
results = noDupResults;
}
return results;
},
/**
* Traverses the tree rooted at the passed root in pre-order mode, calling the passed function on the nodes at each level.
* That is the function is called upon each node **before** being called on its children).
*
* For an object to be queryable, it must implement the `getRefItems` method which returns all
* immediate child items.
*
* This method is used at each level down the cascade. Currently {@link Ext.Component Component}s
* and {@link Ext.data.TreeModel TreeModel}s are queryable.
*
* If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery on them.
*
* @param {Object} selector A ComponentQuery selector used to filter candidate nodes before calling the function.
* An empty string matches any node.
* @param {String} root The root queryable object to start from.
* @param {Function} fn The function to call. Return `false` to abort the traverse.
* @param {Object} fn.node The node being visited.
* @param {Object} [scope] The context (`this` reference) in which the function is executed.
* @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list to pass down extra data known to the caller
* **after** the node being visited.
*/
visitPreOrder: function(selector, root, fn, scope, extraArgs) {
cq._visit(true, selector, root, fn, scope, extraArgs);
},
/**
* Traverses the tree rooted at the passed root in post-order mode, calling the passed function on the nodes at each level.
* That is the function is called upon each node **after** being called on its children).
*
* For an object to be queryable, it must implement the `getRefItems` method which returns all
* immediate child items.
*
* This method is used at each level down the cascade. Currently {@link Ext.Component Component}s
* and {@link Ext.data.TreeModel TreeModel}s are queryable.
*
* If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery on them.
*
* @param {Object} selector A ComponentQuery selector used to filter candidate nodes before calling the function.
* An empty string matches any node.
* @param {String} root The root queryable object to start from.
* @param {Function} fn The function to call. Return `false` to abort the traverse.
* @param {Object} fn.node The node being visited.
* @param {Object} [scope] The context (`this` reference) in which the function is executed.
* @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list to pass down extra data known to the caller
* **after** the node being visited.
*/
visitPostOrder: function(selector, root, fn, scope, extraArgs) {
cq._visit(false, selector, root, fn, scope, extraArgs);
},
// @private
// visit implementation which handles both preOrder and postOrder modes.
_visit: function(preOrder, selector, root, fn, scope, extraArgs) {
var query = cq.cache.get(selector),
callArgs = [root],
children,
len = 0,
i, rootMatch;
if (!query) {
query = cq.cache.add(selector, cq.parse(selector));
}
rootMatch = query.is(root);
if (root.getRefItems) {
children = root.getRefItems();
len = children.length;
}
// append optional extraArgs
if (extraArgs) {
Ext.Array.push(callArgs, extraArgs);
}
if (preOrder) {
if (rootMatch) {
if (fn.apply(scope || root, callArgs) === false) {
return false;
}
}
}
for (i = 0; i < len; i++) {
if (cq._visit.call(cq, preOrder, selector, children[i], fn, scope, extraArgs) === false) {
return false;
}
}
if (!preOrder) {
if (rootMatch) {
if (fn.apply(scope || root, callArgs) === false) {
return false;
}
}
}
},
/**
* Tests whether the passed Component matches the selector string.
* An empty selector will always match.
*
* @param {Ext.Component} component The Component to test
* @param {String} selector The selector string to test against.
* @return {Boolean} True if the Component matches the selector.
* @member Ext.ComponentQuery
*/
is: function(component, selector) {
if (!selector) {
return true;
}
var query = cq.cache.get(selector);
if (!query) {
query = cq.cache.add(selector, cq.parse(selector));
}
return query.is(component);
},
parse: function(selector) {
var operations = [],
selectors, sel, i, len;
selectors = Ext.splitAndUnescape(selector, ',');
for (i = 0, len = selectors.length; i < len; i++) {
// Trim the whitespace as the parser expects it.
sel = Ext.String.trim(selectors[i]);
// Usually, a dangling comma at the end of a selector means a typo.
// In that case, the last sel value will be an empty string; the parser
// will silently ignore it which is not good, so we throw an error here.
//<debug>
if (sel === '') {
Ext.Error.raise('Invalid ComponentQuery selector: ""');
}
//</debug>
operations.push(cq._parse(sel));
}
// Now that we have all our operations in an array, we are going
// to create a new Query using these operations.
return new cq.Query({
operations: operations
});
},
_parse: function(selector) {
var operations = [],
trim = Ext.String.trim,
length = matchers.length,
lastSelector,
tokenMatch,
token,
matchedChar,
modeMatch,
selectorMatch,
transform,
i, matcher, method, args;
// We are going to parse the beginning of the selector over and
// over again, slicing off the selector any portions we converted into an
// operation, until it is an empty string.
while (selector && lastSelector !== selector) {
lastSelector = selector;
// First we check if we are dealing with a token like #, * or an xtype
tokenMatch = selector.match(tokenRe);
if (tokenMatch) {
matchedChar = tokenMatch[1];
token = trim(tokenMatch[2]).replace(unescapeRe, '$1');
// If the token is prefixed with a # we push a filterById operation to our stack
if (matchedChar === '#') {
operations.push({
method: filterById,
args: [token]
});
}
// If the token is a * or an xtype string, we push a filterByXType
// operation to the stack.
else {
operations.push({
method: filterByXType,
args: [token, Boolean(tokenMatch[3])]
});
}
// Now we slice of the part we just converted into an operation
selector = selector.replace(tokenMatch[0], '').replace(stripLeadingSpaceRe, '$1');
}
// If the next part of the query is not a space or > or ^, it means we
// are going to check for more things that our current selection
// has to comply to.
while (!(modeMatch = selector.match(modeRe))) {
// Lets loop over each type of matcher and execute it
// on our current selector.
for (i = 0; selector && i < length; i++) {
matcher = matchers[i];
selectorMatch = selector.match(matcher.re);
method = matcher.method;
transform = matcher.argTransform;
// If we have a match, add an operation with the method
// associated with this matcher, and pass the regular
// expression matches are arguments to the operation.
if (selectorMatch) {
// Transform function will do unescaping and additional checks
if (transform) {
args = transform(selectorMatch);
}
else {
args = selectorMatch.slice(1);
}
operations.push({
method: Ext.isString(matcher.method)
// Turn a string method into a function by formatting the string with our selector matche expression
// A new method is created for different match expressions, eg {id=='textfield-1024'}
// Every expression may be different in different selectors.
? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
: matcher.method,
args: args
});
selector = selector.replace(selectorMatch[0], '').replace(stripLeadingSpaceRe, '$1');
break; // Break on match
}
// Exhausted all matches: It's an error
if (i === (length - 1)) {
Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
}
}
}
// Now we are going to check for a mode change. This means a space
// or a > to determine if we are going to select all the children
// of the currently matched items, or a ^ if we are going to use the
// ownerCt axis as the candidate source.
if (modeMatch[1]) { // Assignment, and test for truthiness!
operations.push({
mode: modeMatch[2]||modeMatch[1]
});
// When we have consumed the mode character, clean up leading spaces
selector = selector.replace(modeMatch[0], '').replace(stripLeadingSpaceRe, '');
}
}
return operations;
}
});
});