]> git.proxmox.com Git - sencha-touch.git/blob - src/src/ComponentQuery.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / ComponentQuery.js
1 /**
2 * @class Ext.ComponentQuery
3 * @extends Object
4 * @singleton
5 *
6 * Provides searching of Components within {@link Ext.ComponentManager} (globally) or a specific
7 * {@link Ext.Container} on the document with a similar syntax to a CSS selector.
8 *
9 * Components can be retrieved by using their {@link Ext.Component xtype} with an optional '.' prefix
10 *
11 * - `component` or `.component`
12 * - `gridpanel` or `.gridpanel`
13 *
14 * An itemId or id must be prefixed with a #
15 *
16 * - `#myContainer`
17 *
18 * Attributes must be wrapped in brackets
19 *
20 * - `component[autoScroll]`
21 * - `panel[title="Test"]`
22 *
23 * Attributes can use the '=' or '~=' operators to do the pattern matching.
24 *
25 * The <strong>'='</strong> operator will return the results that <strong>exactly</strong> match:
26 *
27 * Ext.Component.query('panel[cls=my-cls]')
28 *
29 * Will match the following Component:
30 *
31 * Ext.create('Ext.Panel', {
32 * cls : 'my-cls'
33 * });
34 *
35 * The <strong>'~='</strong> operator will return results that <strong>exactly</strong> matches one of the whitespace-separated values:
36 *
37 * Ext.Component.query('panel[cls~=my-cls]')
38 *
39 * Will match the follow Component:
40 *
41 * Ext.create('My.Panel', {
42 * cls : 'foo-cls my-cls bar-cls'
43 * });
44 *
45 * This is because it <strong>exactly</strong> matched the 'my-cls' within the cls config.
46 *
47 * Member expressions from candidate Components may be tested. If the expression returns a *truthy* value,
48 * the candidate Component will be included in the query:
49 *
50 * var disabledFields = myFormPanel.query("{isDisabled()}");
51 *
52 * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
53 *
54 * // Function receives array and returns a filtered array.
55 * Ext.ComponentQuery.pseudos.invalid = function(items) {
56 * var i = 0, l = items.length, c, result = [];
57 * for (; i < l; i++) {
58 * if (!(c = items[i]).isValid()) {
59 * result.push(c);
60 * }
61 * }
62 * return result;
63 * };
64 *
65 * var invalidFields = myFormPanel.query('field:invalid');
66 * if (invalidFields.length) {
67 * invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
68 * for (var i = 0, l = invalidFields.length; i < l; i++) {
69 * invalidFields[i].getEl().frame("red");
70 * }
71 * }
72 *
73 * Default pseudos include:
74 *
75 * - not
76 *
77 * Queries return an array of components.
78 * Here are some example queries.
79 *
80 * // retrieve all Ext.Panels in the document by xtype
81 * var panelsArray = Ext.ComponentQuery.query('panel');
82 *
83 * // retrieve all Ext.Panels within the container with an id myCt
84 * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
85 *
86 * // retrieve all direct children which are Ext.Panels within myCt
87 * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
88 *
89 * // retrieve all grids and trees
90 * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
91 *
92 * For easy access to queries based from a particular Container see the {@link Ext.Container#query},
93 * {@link Ext.Container#down} and {@link Ext.Container#child} methods. Also see
94 * {@link Ext.Component#up}.
95 */
96 Ext.define('Ext.ComponentQuery', {
97 singleton: true,
98 uses: ['Ext.ComponentManager']
99 }, function() {
100
101 var cq = this,
102
103 // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
104 // as a member on each item in the passed array.
105 filterFnPattern = [
106 'var r = [],',
107 'i = 0,',
108 'it = items,',
109 'l = it.length,',
110 'c;',
111 'for (; i < l; i++) {',
112 'c = it[i];',
113 'if (c.{0}) {',
114 'r.push(c);',
115 '}',
116 '}',
117 'return r;'
118 ].join(''),
119
120 filterItems = function(items, operation) {
121 // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
122 // The operation's method loops over each item in the candidate array and
123 // returns an array of items which match its criteria
124 return operation.method.apply(this, [ items ].concat(operation.args));
125 },
126
127 getItems = function(items, mode) {
128 var result = [],
129 i = 0,
130 length = items.length,
131 candidate,
132 deep = mode !== '>';
133
134 for (; i < length; i++) {
135 candidate = items[i];
136 if (candidate.getRefItems) {
137 result = result.concat(candidate.getRefItems(deep));
138 }
139 }
140 return result;
141 },
142
143 getAncestors = function(items) {
144 var result = [],
145 i = 0,
146 length = items.length,
147 candidate;
148 for (; i < length; i++) {
149 candidate = items[i];
150 while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
151 result.push(candidate);
152 }
153 }
154 return result;
155 },
156
157 // Filters the passed candidate array and returns only items which match the passed xtype
158 filterByXType = function(items, xtype, shallow) {
159 if (xtype === '*') {
160 return items.slice();
161 }
162 else {
163 var result = [],
164 i = 0,
165 length = items.length,
166 candidate;
167 for (; i < length; i++) {
168 candidate = items[i];
169 if (candidate.isXType(xtype, shallow)) {
170 result.push(candidate);
171 }
172 }
173 return result;
174 }
175 },
176
177 // Filters the passed candidate array and returns only items which have the passed className
178 filterByClassName = function(items, className) {
179 var EA = Ext.Array,
180 result = [],
181 i = 0,
182 length = items.length,
183 candidate;
184 for (; i < length; i++) {
185 candidate = items[i];
186 if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) {
187 result.push(candidate);
188 }
189 }
190 return result;
191 },
192
193 // Filters the passed candidate array and returns only items which have the specified property match
194 filterByAttribute = function(items, property, operator, value) {
195 var result = [],
196 i = 0,
197 length = items.length,
198 candidate, getter, getValue;
199 for (; i < length; i++) {
200 candidate = items[i];
201 getter = Ext.Class.getConfigNameMap(property).get;
202 if (operator === '~=') {
203 getValue = null;
204
205 if (candidate[getter]) {
206 getValue = candidate[getter]();
207 } else if (candidate.config && candidate.config[property]) {
208 getValue = String(candidate.config[property]);
209 } else if (candidate[property]) {
210 getValue = String(candidate[property]);
211 }
212
213 if (getValue) {
214 //normalize to an array
215 if (!Ext.isArray(getValue)) {
216 getValue = getValue.split(' ');
217 }
218
219 var v = 0,
220 vLen = getValue.length,
221 val;
222
223 for (; v < vLen; v++) {
224 /**
225 * getValue[v] could still be whitespaced-separated, this normalizes it. This is an example:
226 *
227 * {
228 * html : 'Imprint',
229 * cls : 'overlay-footer-item overlay-footer-imprint'
230 * }
231 */
232 val = String(getValue[v]).split(' ');
233
234 if (Ext.Array.indexOf(val, value) !== -1) {
235 result.push(candidate);
236 }
237 }
238 }
239 } else if (candidate[getter]) {
240 getValue = candidate[getter]();
241 if (!value ? !!getValue : (String(getValue) === value)) {
242 result.push(candidate);
243 }
244 }
245 else if (candidate.config && candidate.config[property]) {
246 if (!value ? !!candidate.config[property] : (String(candidate.config[property]) === value)) {
247 result.push(candidate);
248 }
249 }
250 else if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
251 result.push(candidate);
252 }
253 }
254 return result;
255 },
256
257 // Filters the passed candidate array and returns only items which have the specified itemId or id
258 filterById = function(items, id) {
259 var result = [],
260 i = 0,
261 length = items.length,
262 candidate;
263 for (; i < length; i++) {
264 candidate = items[i];
265 if (candidate.getId() === id || candidate.getItemId() === id) {
266 result.push(candidate);
267 }
268 }
269 return result;
270 },
271
272 // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
273 filterByPseudo = function(items, name, value) {
274 return cq.pseudos[name](items, value);
275 },
276
277 // Determines leading mode
278 // > for direct child, and ^ to switch to ownerCt axis
279 modeRe = /^(\s?([>\^])\s?|\s|$)/,
280
281 // Matches a token with possibly (true|false) appended for the "shallow" parameter
282 tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
283
284 matchers = [{
285 // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
286 re: /^\.([\w\-]+)(?:\((true|false)\))?/,
287 method: filterByXType
288 },{
289 // checks for [attribute=value]
290 re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
291 method: filterByAttribute
292 }, {
293 // checks for #cmpItemId
294 re: /^#([\w\-]+)/,
295 method: filterById
296 }, {
297 // checks for :<pseudo_class>(<selector>)
298 re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
299 method: filterByPseudo
300 }, {
301 // checks for {<member_expression>}
302 re: /^(?:\{([^\}]+)\})/,
303 method: filterFnPattern
304 }];
305
306 cq.Query = Ext.extend(Object, {
307 constructor: function(cfg) {
308 cfg = cfg || {};
309 Ext.apply(this, cfg);
310 },
311
312 /**
313 * @private
314 * Executes this Query upon the selected root.
315 * The root provides the initial source of candidate Component matches which are progressively
316 * filtered by iterating through this Query's operations cache.
317 * If no root is provided, all registered Components are searched via the ComponentManager.
318 * root may be a Container who's descendant Components are filtered
319 * root may be a Component with an implementation of getRefItems which provides some nested Components such as the
320 * docked items within a Panel.
321 * root may be an array of candidate Components to filter using this Query.
322 */
323 execute : function(root) {
324 var operations = this.operations,
325 i = 0,
326 length = operations.length,
327 operation,
328 workingItems;
329
330 // no root, use all Components in the document
331 if (!root) {
332 workingItems = Ext.ComponentManager.all.getArray();
333 }
334 // Root is a candidate Array
335 else if (Ext.isArray(root)) {
336 workingItems = root;
337 }
338
339 // We are going to loop over our operations and take care of them
340 // one by one.
341 for (; i < length; i++) {
342 operation = operations[i];
343
344 // The mode operation requires some custom handling.
345 // All other operations essentially filter down our current
346 // working items, while mode replaces our current working
347 // items by getting children from each one of our current
348 // working items. The type of mode determines the type of
349 // children we get. (e.g. > only gets direct children)
350 if (operation.mode === '^') {
351 workingItems = getAncestors(workingItems || [root]);
352 }
353 else if (operation.mode) {
354 workingItems = getItems(workingItems || [root], operation.mode);
355 }
356 else {
357 workingItems = filterItems(workingItems || getItems([root]), operation);
358 }
359
360 // If this is the last operation, it means our current working
361 // items are the final matched items. Thus return them!
362 if (i === length -1) {
363 return workingItems;
364 }
365 }
366 return [];
367 },
368
369 is: function(component) {
370 var operations = this.operations,
371 components = Ext.isArray(component) ? component : [component],
372 originalLength = components.length,
373 lastOperation = operations[operations.length-1],
374 ln, i;
375
376 components = filterItems(components, lastOperation);
377 if (components.length === originalLength) {
378 if (operations.length > 1) {
379 for (i = 0, ln = components.length; i < ln; i++) {
380 if (Ext.Array.indexOf(this.execute(), components[i]) === -1) {
381 return false;
382 }
383 }
384 }
385 return true;
386 }
387 return false;
388 }
389 });
390
391 Ext.apply(this, {
392
393 // private cache of selectors and matching ComponentQuery.Query objects
394 cache: {},
395
396 // private cache of pseudo class filter functions
397 pseudos: {
398 not: function(components, selector){
399 var CQ = Ext.ComponentQuery,
400 i = 0,
401 length = components.length,
402 results = [],
403 index = -1,
404 component;
405
406 for(; i < length; ++i) {
407 component = components[i];
408 if (!CQ.is(component, selector)) {
409 results[++index] = component;
410 }
411 }
412 return results;
413 }
414 },
415
416 /**
417 * Returns an array of matched Components from within the passed root object.
418 *
419 * This method filters returned Components in a similar way to how CSS selector based DOM
420 * queries work using a textual selector string.
421 *
422 * See class summary for details.
423 *
424 * @param {String} selector The selector string to filter returned Components
425 * @param {Ext.Container} root The Container within which to perform the query.
426 * If omitted, all Components within the document are included in the search.
427 *
428 * This parameter may also be an array of Components to filter according to the selector.
429 * @return {Ext.Component[]} The matched Components.
430 *
431 * @member Ext.ComponentQuery
432 */
433 query: function(selector, root) {
434 var selectors = selector.split(','),
435 length = selectors.length,
436 i = 0,
437 results = [],
438 noDupResults = [],
439 dupMatcher = {},
440 query, resultsLn, cmp;
441
442 for (; i < length; i++) {
443 selector = Ext.String.trim(selectors[i]);
444 query = this.parse(selector);
445 // query = this.cache[selector];
446 // if (!query) {
447 // this.cache[selector] = query = this.parse(selector);
448 // }
449 results = results.concat(query.execute(root));
450 }
451
452 // multiple selectors, potential to find duplicates
453 // lets filter them out.
454 if (length > 1) {
455 resultsLn = results.length;
456 for (i = 0; i < resultsLn; i++) {
457 cmp = results[i];
458 if (!dupMatcher[cmp.id]) {
459 noDupResults.push(cmp);
460 dupMatcher[cmp.id] = true;
461 }
462 }
463 results = noDupResults;
464 }
465 return results;
466 },
467
468 /**
469 * Tests whether the passed Component matches the selector string.
470 * @param {Ext.Component} component The Component to test.
471 * @param {String} selector The selector string to test against.
472 * @return {Boolean} `true` if the Component matches the selector.
473 * @member Ext.ComponentQuery
474 */
475 is: function(component, selector) {
476 if (!selector) {
477 return true;
478 }
479 var query = this.cache[selector];
480 if (!query) {
481 this.cache[selector] = query = this.parse(selector);
482 }
483 return query.is(component);
484 },
485
486 parse: function(selector) {
487 var operations = [],
488 length = matchers.length,
489 lastSelector,
490 tokenMatch,
491 matchedChar,
492 modeMatch,
493 selectorMatch,
494 i, matcher, method;
495
496 // We are going to parse the beginning of the selector over and
497 // over again, slicing off the selector any portions we converted into an
498 // operation, until it is an empty string.
499 while (selector && lastSelector !== selector) {
500 lastSelector = selector;
501
502 // First we check if we are dealing with a token like #, * or an xtype
503 tokenMatch = selector.match(tokenRe);
504
505 if (tokenMatch) {
506 matchedChar = tokenMatch[1];
507
508 // If the token is prefixed with a # we push a filterById operation to our stack
509 if (matchedChar === '#') {
510 operations.push({
511 method: filterById,
512 args: [Ext.String.trim(tokenMatch[2])]
513 });
514 }
515 // If the token is prefixed with a . we push a filterByClassName operation to our stack
516 // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix
517 else if (matchedChar === '.') {
518 operations.push({
519 method: filterByClassName,
520 args: [Ext.String.trim(tokenMatch[2])]
521 });
522 }
523 // If the token is a * or an xtype string, we push a filterByXType
524 // operation to the stack.
525 else {
526 operations.push({
527 method: filterByXType,
528 args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
529 });
530 }
531
532 // Now we slice of the part we just converted into an operation
533 selector = selector.replace(tokenMatch[0], '');
534 }
535
536 // If the next part of the query is not a space or > or ^, it means we
537 // are going to check for more things that our current selection
538 // has to comply to.
539 while (!(modeMatch = selector.match(modeRe))) {
540 // Lets loop over each type of matcher and execute it
541 // on our current selector.
542 for (i = 0; selector && i < length; i++) {
543 matcher = matchers[i];
544 selectorMatch = selector.match(matcher.re);
545 method = matcher.method;
546
547 // If we have a match, add an operation with the method
548 // associated with this matcher, and pass the regular
549 // expression matches are arguments to the operation.
550 if (selectorMatch) {
551 operations.push({
552 method: Ext.isString(matcher.method)
553 // Turn a string method into a function by formatting the string with our selector matche expression
554 // A new method is created for different match expressions, eg {id=='textfield-1024'}
555 // Every expression may be different in different selectors.
556 ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
557 : matcher.method,
558 args: selectorMatch.slice(1)
559 });
560 selector = selector.replace(selectorMatch[0], '');
561 break; // Break on match
562 }
563 //<debug>
564 // Exhausted all matches: It's an error
565 if (i === (length - 1)) {
566 Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
567 }
568 //</debug>
569 }
570 }
571
572 // Now we are going to check for a mode change. This means a space
573 // or a > to determine if we are going to select all the children
574 // of the currently matched items, or a ^ if we are going to use the
575 // ownerCt axis as the candidate source.
576 if (modeMatch[1]) { // Assignment, and test for truthiness!
577 operations.push({
578 mode: modeMatch[2]||modeMatch[1]
579 });
580 selector = selector.replace(modeMatch[0], '');
581 }
582 }
583
584 // Now that we have all our operations in an array, we are going
585 // to create a new Query using these operations.
586 return new cq.Query({
587 operations: operations
588 });
589 }
590 });
591 });