3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.ComponentQuery
20 * Provides searching of Components within Ext.ComponentManager (globally) or a specific
21 * Ext.container.Container on the document with a similar syntax to a CSS selector.
23 * Components can be retrieved by using their {@link Ext.Component xtype} with an optional . prefix
25 * - `component` or `.component`
26 * - `gridpanel` or `.gridpanel`
28 * An itemId or id must be prefixed with a #
32 * Attributes must be wrapped in brackets
34 * - `component[autoScroll]`
35 * - `panel[title="Test"]`
37 * Member expressions from candidate Components may be tested. If the expression returns a *truthy* value,
38 * the candidate Component will be included in the query:
40 * var disabledFields = myFormPanel.query("{isDisabled()}");
42 * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
44 * // Function receives array and returns a filtered array.
45 * Ext.ComponentQuery.pseudos.invalid = function(items) {
46 * var i = 0, l = items.length, c, result = [];
47 * for (; i < l; i++) {
48 * if (!(c = items[i]).isValid()) {
55 * var invalidFields = myFormPanel.query('field:invalid');
56 * if (invalidFields.length) {
57 * invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
58 * for (var i = 0, l = invalidFields.length; i < l; i++) {
59 * invalidFields[i].getEl().frame("red");
63 * Default pseudos include:
67 * Queries return an array of components.
68 * Here are some example queries.
70 * // retrieve all Ext.Panels in the document by xtype
71 * var panelsArray = Ext.ComponentQuery.query('panel');
73 * // retrieve all Ext.Panels within the container with an id myCt
74 * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
76 * // retrieve all direct children which are Ext.Panels within myCt
77 * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
79 * // retrieve all grids and trees
80 * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
82 * For easy access to queries based from a particular Container see the {@link Ext.container.Container#query},
83 * {@link Ext.container.Container#down} and {@link Ext.container.Container#child} methods. Also see
84 * {@link Ext.Component#up}.
86 Ext.define('Ext.ComponentQuery', {
88 uses: ['Ext.ComponentManager']
93 // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
94 // as a member on each item in the passed array.
101 'for (; i < l; i++) {',
110 filterItems = function(items, operation) {
111 // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
112 // The operation's method loops over each item in the candidate array and
113 // returns an array of items which match its criteria
114 return operation.method.apply(this, [ items ].concat(operation.args));
117 getItems = function(items, mode) {
120 length = items.length,
124 for (; i < length; i++) {
125 candidate = items[i];
126 if (candidate.getRefItems) {
127 result = result.concat(candidate.getRefItems(deep));
133 getAncestors = function(items) {
136 length = items.length,
138 for (; i < length; i++) {
139 candidate = items[i];
140 while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
141 result.push(candidate);
147 // Filters the passed candidate array and returns only items which match the passed xtype
148 filterByXType = function(items, xtype, shallow) {
150 return items.slice();
155 length = items.length,
157 for (; i < length; i++) {
158 candidate = items[i];
159 if (candidate.isXType(xtype, shallow)) {
160 result.push(candidate);
167 // Filters the passed candidate array and returns only items which have the passed className
168 filterByClassName = function(items, className) {
172 length = items.length,
174 for (; i < length; i++) {
175 candidate = items[i];
176 if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) {
177 result.push(candidate);
183 // Filters the passed candidate array and returns only items which have the specified property match
184 filterByAttribute = function(items, property, operator, value) {
187 length = items.length,
189 for (; i < length; i++) {
190 candidate = items[i];
191 if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
192 result.push(candidate);
198 // Filters the passed candidate array and returns only items which have the specified itemId or id
199 filterById = function(items, id) {
202 length = items.length,
204 for (; i < length; i++) {
205 candidate = items[i];
206 if (candidate.getItemId() === id) {
207 result.push(candidate);
213 // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
214 filterByPseudo = function(items, name, value) {
215 return cq.pseudos[name](items, value);
218 // Determines leading mode
219 // > for direct child, and ^ to switch to ownerCt axis
220 modeRe = /^(\s?([>\^])\s?|\s|$)/,
222 // Matches a token with possibly (true|false) appended for the "shallow" parameter
223 tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
226 // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
227 re: /^\.([\w\-]+)(?:\((true|false)\))?/,
228 method: filterByXType
230 // checks for [attribute=value]
231 re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
232 method: filterByAttribute
234 // checks for #cmpItemId
238 // checks for :<pseudo_class>(<selector>)
239 re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
240 method: filterByPseudo
242 // checks for {<member_expression>}
243 re: /^(?:\{([^\}]+)\})/,
244 method: filterFnPattern
248 * @class Ext.ComponentQuery.Query
252 cq.Query = Ext.extend(Object, {
253 constructor: function(cfg) {
255 Ext.apply(this, cfg);
260 * Executes this Query upon the selected root.
261 * The root provides the initial source of candidate Component matches which are progressively
262 * filtered by iterating through this Query's operations cache.
263 * If no root is provided, all registered Components are searched via the ComponentManager.
264 * root may be a Container who's descendant Components are filtered
265 * root may be a Component with an implementation of getRefItems which provides some nested Components such as the
266 * docked items within a Panel.
267 * root may be an array of candidate Components to filter using this Query.
269 execute : function(root) {
270 var operations = this.operations,
272 length = operations.length,
276 // no root, use all Components in the document
278 workingItems = Ext.ComponentManager.all.getArray();
280 // Root is a candidate Array
281 else if (Ext.isArray(root)) {
285 // We are going to loop over our operations and take care of them
287 for (; i < length; i++) {
288 operation = operations[i];
290 // The mode operation requires some custom handling.
291 // All other operations essentially filter down our current
292 // working items, while mode replaces our current working
293 // items by getting children from each one of our current
294 // working items. The type of mode determines the type of
295 // children we get. (e.g. > only gets direct children)
296 if (operation.mode === '^') {
297 workingItems = getAncestors(workingItems || [root]);
299 else if (operation.mode) {
300 workingItems = getItems(workingItems || [root], operation.mode);
303 workingItems = filterItems(workingItems || getItems([root]), operation);
306 // If this is the last operation, it means our current working
307 // items are the final matched items. Thus return them!
308 if (i === length -1) {
315 is: function(component) {
316 var operations = this.operations,
317 components = Ext.isArray(component) ? component : [component],
318 originalLength = components.length,
319 lastOperation = operations[operations.length-1],
322 components = filterItems(components, lastOperation);
323 if (components.length === originalLength) {
324 if (operations.length > 1) {
325 for (i = 0, ln = components.length; i < ln; i++) {
326 if (Ext.Array.indexOf(this.execute(), components[i]) === -1) {
339 // private cache of selectors and matching ComponentQuery.Query objects
342 // private cache of pseudo class filter functions
344 not: function(components, selector){
345 var CQ = Ext.ComponentQuery,
347 length = components.length,
352 for(; i < length; ++i) {
353 component = components[i];
354 if (!CQ.is(component, selector)) {
355 results[++index] = component;
363 * Returns an array of matched Components from within the passed root object.
365 * This method filters returned Components in a similar way to how CSS selector based DOM
366 * queries work using a textual selector string.
368 * See class summary for details.
370 * @param {String} selector The selector string to filter returned Components
371 * @param {Ext.container.Container} root The Container within which to perform the query.
372 * If omitted, all Components within the document are included in the search.
374 * This parameter may also be an array of Components to filter according to the selector.</p>
375 * @returns {[Ext.Component]} The matched Components.
377 * @member Ext.ComponentQuery
379 query: function(selector, root) {
380 var selectors = selector.split(','),
381 length = selectors.length,
386 query, resultsLn, cmp;
388 for (; i < length; i++) {
389 selector = Ext.String.trim(selectors[i]);
390 query = this.cache[selector];
392 this.cache[selector] = query = this.parse(selector);
394 results = results.concat(query.execute(root));
397 // multiple selectors, potential to find duplicates
398 // lets filter them out.
400 resultsLn = results.length;
401 for (i = 0; i < resultsLn; i++) {
403 if (!dupMatcher[cmp.id]) {
404 noDupResults.push(cmp);
405 dupMatcher[cmp.id] = true;
408 results = noDupResults;
414 * Tests whether the passed Component matches the selector string.
415 * @param {Ext.Component} component The Component to test
416 * @param {String} selector The selector string to test against.
417 * @return {Boolean} True if the Component matches the selector.
418 * @member Ext.ComponentQuery
420 is: function(component, selector) {
424 var query = this.cache[selector];
426 this.cache[selector] = query = this.parse(selector);
428 return query.is(component);
431 parse: function(selector) {
433 length = matchers.length,
441 // We are going to parse the beginning of the selector over and
442 // over again, slicing off the selector any portions we converted into an
443 // operation, until it is an empty string.
444 while (selector && lastSelector !== selector) {
445 lastSelector = selector;
447 // First we check if we are dealing with a token like #, * or an xtype
448 tokenMatch = selector.match(tokenRe);
451 matchedChar = tokenMatch[1];
453 // If the token is prefixed with a # we push a filterById operation to our stack
454 if (matchedChar === '#') {
457 args: [Ext.String.trim(tokenMatch[2])]
460 // If the token is prefixed with a . we push a filterByClassName operation to our stack
461 // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix
462 else if (matchedChar === '.') {
464 method: filterByClassName,
465 args: [Ext.String.trim(tokenMatch[2])]
468 // If the token is a * or an xtype string, we push a filterByXType
469 // operation to the stack.
472 method: filterByXType,
473 args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
477 // Now we slice of the part we just converted into an operation
478 selector = selector.replace(tokenMatch[0], '');
481 // If the next part of the query is not a space or > or ^, it means we
482 // are going to check for more things that our current selection
484 while (!(modeMatch = selector.match(modeRe))) {
485 // Lets loop over each type of matcher and execute it
486 // on our current selector.
487 for (i = 0; selector && i < length; i++) {
488 matcher = matchers[i];
489 selectorMatch = selector.match(matcher.re);
490 method = matcher.method;
492 // If we have a match, add an operation with the method
493 // associated with this matcher, and pass the regular
494 // expression matches are arguments to the operation.
497 method: Ext.isString(matcher.method)
498 // Turn a string method into a function by formatting the string with our selector matche expression
499 // A new method is created for different match expressions, eg {id=='textfield-1024'}
500 // Every expression may be different in different selectors.
501 ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
503 args: selectorMatch.slice(1)
505 selector = selector.replace(selectorMatch[0], '');
506 break; // Break on match
509 // Exhausted all matches: It's an error
510 if (i === (length - 1)) {
511 Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
517 // Now we are going to check for a mode change. This means a space
518 // or a > to determine if we are going to select all the children
519 // of the currently matched items, or a ^ if we are going to use the
520 // ownerCt axis as the candidate source.
521 if (modeMatch[1]) { // Assignment, and test for truthiness!
523 mode: modeMatch[2]||modeMatch[1]
525 selector = selector.replace(modeMatch[0], '');
529 // Now that we have all our operations in an array, we are going
530 // to create a new Query using these operations.
531 return new cq.Query({
532 operations: operations