/** * 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 AbstractComponent's {@link Ext.AbstractComponent#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 * * ## 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[autoScroll]` - matches any Component that has `autoScroll` 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 operators in {@link Ext.dom.Query DomQuery}'s * {@link Ext.dom.Query#operators operators} to compare values. * * 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 `#id` form; it will * do the same but is easier to read. * * ## 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 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. * * ## 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.dom.Query' ] }, function() { var cq = this, domQueryOperators = Ext.dom.Query.operators, nthRe = /(\d*)n\+?(\d*)/, nthRe2 = /\D/, // 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 passed className filterByClassName = function(items, className) { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (candidate.hasCls(className)) { 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; // 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]; // Check candidate hasOwnProperty is propName prefixed with a bang. if (!mustBeOwnProperty || candidate.hasOwnProperty(property)) { // pull out property value to test 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 (domQueryOperators[operator](Ext.coerce(propValue[j], compareTo), compareTo)) { result.push(candidate); break; } } } } else if (!compareTo ? !!candidate[property] : domQueryOperators[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 }, { // checks for [attribute=value], [attribute^=value], [attribute$=value], [attribute*=value], [attribute~=value], [attribute%=value], [attribute!=value] // Allow [@attribute] to check truthy ownProperty // Allow [?attribute] to check for presence of ownProperty re: /^(?:\[((?:@|\?)?[\w\-\$]*[^\^\$\*~%!])\s?(?:(=|.=)\s?['"]?(.*?)["']?)?\])/, method: filterByAttribute }, { // checks for #cmpItemId re: /^#([\w\-]+)/, method: filterById }, { // checks for :<pseudo_class>(<selector>) re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/, method: filterByPseudo }, { // 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, i = 0, length = operations.length, operation, workingItems; // no root, use all Components in the document if (!root) { workingItems = Ext.ComponentManager.all.getArray(); } // 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, components = Ext.isArray(component) ? component : [component], originalLength = components.length, lastOperation = operations[operations.length-1], ln, i; components = filterItems(components, lastOperation); if (components.length === originalLength) { if (operations.length > 1) { for (i = 0, ln = components.length; i < ln; i++) { if (Ext.Array.indexOf(this.execute(), components[i]) === -1) { return false; } } } return true; } return false; } }); Ext.apply(this, { // private cache of selectors and matching ComponentQuery.Query objects cache: {}, // private cache of pseudo class filter functions pseudos: { not: function(components, selector){ var CQ = Ext.ComponentQuery, 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 this is a generally focusable Component (has a focusEl, is rendered, enabled and visible) // then it is currently focusable if focus management is enabled or if it is an input field, a button or a menu item if (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, l = m[2] - 0, i, n, nodeIndex; for (i = 0; n = c[i]; i++) { nodeIndex = i + 1; if (f == 1) { if (l == 0 || nodeIndex == l) { result.push(n); } } else if ((nodeIndex + l) % 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. * @returns {Ext.Component[]} The matched Components. * * @member Ext.ComponentQuery */ query: function(selector, root) { var selectors = selector.split(','), length = selectors.length, i = 0, results = [], noDupResults = [], dupMatcher = {}, query, resultsLn, cmp; for (; i < length; i++) { selector = Ext.String.trim(selectors[i]); query = this.cache[selector] || (this.cache[selector] = this.parse(selector)); results = results.concat(query.execute(root)); } // multiple selectors, potential to find duplicates // lets filter them out. if (length > 1) { 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; }, /** * Tests whether the passed Component matches the selector string. * @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 selectors = selector.split(','), length = selectors.length, i = 0, query; for (; i < length; i++) { selector = Ext.String.trim(selectors[i]); query = this.cache[selector] || (this.cache[selector] = this.parse(selector)); if (query.is(component)) { return true; } } return false; }, parse: function(selector) { var operations = [], length = matchers.length, lastSelector, tokenMatch, matchedChar, modeMatch, selectorMatch, i, matcher, method; // 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]; // If the token is prefixed with a # we push a filterById operation to our stack if (matchedChar === '#') { operations.push({ method: filterById, args: [Ext.String.trim(tokenMatch[2])] }); } // If the token is prefixed with a . we push a filterByClassName operation to our stack // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix else if (matchedChar === '.') { operations.push({ method: filterByClassName, args: [Ext.String.trim(tokenMatch[2])] }); } // If the token is a * or an xtype string, we push a filterByXType // operation to the stack. else { operations.push({ method: filterByXType, args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])] }); } // Now we slice of the part we just converted into an operation selector = selector.replace(tokenMatch[0], ''); } // 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; // 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) { 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: selectorMatch.slice(1) }); selector = selector.replace(selectorMatch[0], ''); 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] }); selector = selector.replace(modeMatch[0], ''); } } // 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 }); } }); });