tweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloud
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.
1197 lines
47 KiB
1197 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; |
|
} |
|
}); |
|
});
|
|
|