Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / ComponentQuery.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.ComponentQuery
17  * @extends Object
18  * @singleton
19  *
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.
22  *
23  * Components can be retrieved by using their {@link Ext.Component xtype} with an optional . prefix
24  *
25  * - `component` or `.component`
26  * - `gridpanel` or `.gridpanel`
27  *
28  * An itemId or id must be prefixed with a #
29  *
30  * - `#myContainer`
31  *
32  * Attributes must be wrapped in brackets
33  *
34  * - `component[autoScroll]`
35  * - `panel[title="Test"]`
36  *
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:
39  *
40  *     var disabledFields = myFormPanel.query("{isDisabled()}");
41  *
42  * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
43  *
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()) {
49  *                 result.push(c);
50  *             }
51  *         }
52  *         return result;
53  *     };
54  *      
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");
60  *         }
61  *     }
62  *
63  * Default pseudos include:
64  *
65  * - not
66  *
67  * Queries return an array of components.
68  * Here are some example queries.
69  *
70  *     // retrieve all Ext.Panels in the document by xtype
71  *     var panelsArray = Ext.ComponentQuery.query('panel');
72  *
73  *     // retrieve all Ext.Panels within the container with an id myCt
74  *     var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
75  *
76  *     // retrieve all direct children which are Ext.Panels within myCt
77  *     var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
78  *
79  *     // retrieve all grids and trees
80  *     var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
81  *
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}.
85  */
86 Ext.define('Ext.ComponentQuery', {
87     singleton: true,
88     uses: ['Ext.ComponentManager']
89 }, function() {
90
91     var cq = this,
92
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.
95         filterFnPattern = [
96             'var r = [],',
97                 'i = 0,',
98                 'it = items,',
99                 'l = it.length,',
100                 'c;',
101             'for (; i < l; i++) {',
102                 'c = it[i];',
103                 'if (c.{0}) {',
104                    'r.push(c);',
105                 '}',
106             '}',
107             'return r;'
108         ].join(''),
109
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));
115         },
116
117         getItems = function(items, mode) {
118             var result = [],
119                 i = 0,
120                 length = items.length,
121                 candidate,
122                 deep = mode !== '>';
123                 
124             for (; i < length; i++) {
125                 candidate = items[i];
126                 if (candidate.getRefItems) {
127                     result = result.concat(candidate.getRefItems(deep));
128                 }
129             }
130             return result;
131         },
132
133         getAncestors = function(items) {
134             var result = [],
135                 i = 0,
136                 length = items.length,
137                 candidate;
138             for (; i < length; i++) {
139                 candidate = items[i];
140                 while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
141                     result.push(candidate);
142                 }
143             }
144             return result;
145         },
146
147         // Filters the passed candidate array and returns only items which match the passed xtype
148         filterByXType = function(items, xtype, shallow) {
149             if (xtype === '*') {
150                 return items.slice();
151             }
152             else {
153                 var result = [],
154                     i = 0,
155                     length = items.length,
156                     candidate;
157                 for (; i < length; i++) {
158                     candidate = items[i];
159                     if (candidate.isXType(xtype, shallow)) {
160                         result.push(candidate);
161                     }
162                 }
163                 return result;
164             }
165         },
166
167         // Filters the passed candidate array and returns only items which have the passed className
168         filterByClassName = function(items, className) {
169             var EA = Ext.Array,
170                 result = [],
171                 i = 0,
172                 length = items.length,
173                 candidate;
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);
178                 }
179             }
180             return result;
181         },
182
183         // Filters the passed candidate array and returns only items which have the specified property match
184         filterByAttribute = function(items, property, operator, value) {
185             var result = [],
186                 i = 0,
187                 length = items.length,
188                 candidate;
189             for (; i < length; i++) {
190                 candidate = items[i];
191                 if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
192                     result.push(candidate);
193                 }
194             }
195             return result;
196         },
197
198         // Filters the passed candidate array and returns only items which have the specified itemId or id
199         filterById = function(items, id) {
200             var result = [],
201                 i = 0,
202                 length = items.length,
203                 candidate;
204             for (; i < length; i++) {
205                 candidate = items[i];
206                 if (candidate.getItemId() === id) {
207                     result.push(candidate);
208                 }
209             }
210             return result;
211         },
212
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);
216         },
217
218         // Determines leading mode
219         // > for direct child, and ^ to switch to ownerCt axis
220         modeRe = /^(\s?([>\^])\s?|\s|$)/,
221
222         // Matches a token with possibly (true|false) appended for the "shallow" parameter
223         tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
224
225         matchers = [{
226             // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
227             re: /^\.([\w\-]+)(?:\((true|false)\))?/,
228             method: filterByXType
229         },{
230             // checks for [attribute=value]
231             re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
232             method: filterByAttribute
233         }, {
234             // checks for #cmpItemId
235             re: /^#([\w\-]+)/,
236             method: filterById
237         }, {
238             // checks for :<pseudo_class>(<selector>)
239             re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
240             method: filterByPseudo
241         }, {
242             // checks for {<member_expression>}
243             re: /^(?:\{([^\}]+)\})/,
244             method: filterFnPattern
245         }];
246
247     /**
248      * @class Ext.ComponentQuery.Query
249      * @extends Object
250      * @private
251      */
252     cq.Query = Ext.extend(Object, {
253         constructor: function(cfg) {
254             cfg = cfg || {};
255             Ext.apply(this, cfg);
256         },
257
258         /**
259          * @private
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.
268          */
269         execute : function(root) {
270             var operations = this.operations,
271                 i = 0,
272                 length = operations.length,
273                 operation,
274                 workingItems;
275
276             // no root, use all Components in the document
277             if (!root) {
278                 workingItems = Ext.ComponentManager.all.getArray();
279             }
280             // Root is a candidate Array
281             else if (Ext.isArray(root)) {
282                 workingItems = root;
283             }
284
285             // We are going to loop over our operations and take care of them
286             // one by one.
287             for (; i < length; i++) {
288                 operation = operations[i];
289
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]);
298                 }
299                 else if (operation.mode) {
300                     workingItems = getItems(workingItems || [root], operation.mode);
301                 }
302                 else {
303                     workingItems = filterItems(workingItems || getItems([root]), operation);
304                 }
305
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) {
309                     return workingItems;
310                 }
311             }
312             return [];
313         },
314
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],
320                 ln, i;
321
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) {
327                             return false;
328                         }
329                     }
330                 }
331                 return true;
332             }
333             return false;
334         }
335     });
336
337     Ext.apply(this, {
338
339         // private cache of selectors and matching ComponentQuery.Query objects
340         cache: {},
341
342         // private cache of pseudo class filter functions
343         pseudos: {
344             not: function(components, selector){
345                 var CQ = Ext.ComponentQuery,
346                     i = 0,
347                     length = components.length,
348                     results = [],
349                     index = -1,
350                     component;
351                 
352                 for(; i < length; ++i) {
353                     component = components[i];
354                     if (!CQ.is(component, selector)) {
355                         results[++index] = component;
356                     }
357                 }
358                 return results;
359             }
360         },
361
362         /**
363          * Returns an array of matched Components from within the passed root object.
364          *
365          * This method filters returned Components in a similar way to how CSS selector based DOM
366          * queries work using a textual selector string.
367          *
368          * See class summary for details.
369          *
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.
373          * 
374          * This parameter may also be an array of Components to filter according to the selector.</p>
375          * @returns {[Ext.Component]} The matched Components.
376          * 
377          * @member Ext.ComponentQuery
378          */
379         query: function(selector, root) {
380             var selectors = selector.split(','),
381                 length = selectors.length,
382                 i = 0,
383                 results = [],
384                 noDupResults = [], 
385                 dupMatcher = {}, 
386                 query, resultsLn, cmp;
387
388             for (; i < length; i++) {
389                 selector = Ext.String.trim(selectors[i]);
390                 query = this.cache[selector];
391                 if (!query) {
392                     this.cache[selector] = query = this.parse(selector);
393                 }
394                 results = results.concat(query.execute(root));
395             }
396
397             // multiple selectors, potential to find duplicates
398             // lets filter them out.
399             if (length > 1) {
400                 resultsLn = results.length;
401                 for (i = 0; i < resultsLn; i++) {
402                     cmp = results[i];
403                     if (!dupMatcher[cmp.id]) {
404                         noDupResults.push(cmp);
405                         dupMatcher[cmp.id] = true;
406                     }
407                 }
408                 results = noDupResults;
409             }
410             return results;
411         },
412
413         /**
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
419          */
420         is: function(component, selector) {
421             if (!selector) {
422                 return true;
423             }
424             var query = this.cache[selector];
425             if (!query) {
426                 this.cache[selector] = query = this.parse(selector);
427             }
428             return query.is(component);
429         },
430
431         parse: function(selector) {
432             var operations = [],
433                 length = matchers.length,
434                 lastSelector,
435                 tokenMatch,
436                 matchedChar,
437                 modeMatch,
438                 selectorMatch,
439                 i, matcher, method;
440
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;
446
447                 // First we check if we are dealing with a token like #, * or an xtype
448                 tokenMatch = selector.match(tokenRe);
449
450                 if (tokenMatch) {
451                     matchedChar = tokenMatch[1];
452
453                     // If the token is prefixed with a # we push a filterById operation to our stack
454                     if (matchedChar === '#') {
455                         operations.push({
456                             method: filterById,
457                             args: [Ext.String.trim(tokenMatch[2])]
458                         });
459                     }
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 === '.') {
463                         operations.push({
464                             method: filterByClassName,
465                             args: [Ext.String.trim(tokenMatch[2])]
466                         });
467                     }
468                     // If the token is a * or an xtype string, we push a filterByXType
469                     // operation to the stack.
470                     else {
471                         operations.push({
472                             method: filterByXType,
473                             args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
474                         });
475                     }
476
477                     // Now we slice of the part we just converted into an operation
478                     selector = selector.replace(tokenMatch[0], '');
479                 }
480
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
483                 // has to comply to.
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;
491
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.
495                         if (selectorMatch) {
496                             operations.push({
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))))
502                                     : matcher.method,
503                                 args: selectorMatch.slice(1)
504                             });
505                             selector = selector.replace(selectorMatch[0], '');
506                             break; // Break on match
507                         }
508                         //<debug>
509                         // Exhausted all matches: It's an error
510                         if (i === (length - 1)) {
511                             Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
512                         }
513                         //</debug>
514                     }
515                 }
516
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!
522                     operations.push({
523                         mode: modeMatch[2]||modeMatch[1]
524                     });
525                     selector = selector.replace(modeMatch[0], '');
526                 }
527             }
528
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
533             });
534         }
535     });
536 });