/** * @class Ext.ComponentQuery * @extends Object * * 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. * * Components can be retrieved by using their {@link Ext.Component xtype} with an optional . prefix <ul> <li>component or .component</li> <li>gridpanel or .gridpanel</li> </ul> * * An itemId or id must be prefixed with a # <ul> <li>#myContainer</li> </ul> * * * Attributes must be wrapped in brackets <ul> <li>component[autoScroll]</li> <li>panel[title="Test"]</li> </ul> * * Member expressions from candidate Components may be tested. If the expression returns a <i>truthy</i> value, * the candidate Component will be included in the query:<pre><code> var disabledFields = myFormPanel.query("{isDisabled()}"); </code></pre> * * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:<code><pre> // 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"); } } </pre></code> * <p> * Default pseudos include:<br /> * - not * </p> * * Queries return an array of components. * Here are some example queries. <pre><code> // 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 gridpanels and listviews var gridsAndLists = Ext.ComponentQuery.query('gridpanel, listview'); </code></pre> 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}. * @singleton */ Ext.define('Ext.ComponentQuery', { singleton: true, uses: ['Ext.ComponentManager'] }, function() { var cq = this, // 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.ownerCt || candidate.floatParent))) { 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 EA = Ext.Array, result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), 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, value) { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (!value ? !!candidate[property] : (String(candidate[property]) === value)) { 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] 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 }]; /** * @class Ext.ComponentQuery.Query * @extends Object * @private */ cq.Query = Ext.extend(Object, { constructor: function(cfg) { cfg = cfg || {}; Ext.apply(this, cfg); }, /** * @private * 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 a candidate Array else if (Ext.isArray(root)) { workingItems = root; } // 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; } }, /** * <p>Returns an array of matched Components from within the passed root object.</p> * <p>This method filters returned Components in a similar way to how CSS selector based DOM * queries work using a textual selector string.</p> * <p>See class summary for details.</p> * @param selector The selector string to filter returned Components * @param root <p>The Container within which to perform the query. If omitted, all Components * within the document are included in the search.</p> * <p>This parameter may also be an array of Components to filter according to the selector.</p> * @returns {Array} The matched Components. * @member Ext.ComponentQuery * @method query */ 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]; if (!query) { this.cache[selector] = query = 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 component The Component to test * @param selector The selector string to test against. * @return {Boolean} True if the Component matches the selector. * @member Ext.ComponentQuery * @method query */ is: function(component, selector) { if (!selector) { return true; } var query = this.cache[selector]; if (!query) { this.cache[selector] = query = this.parse(selector); } return query.is(component); }, 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 } //<debug> // Exhausted all matches: It's an error if (i === (length - 1)) { Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"'); } //</debug> } } // 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 }); } }); });