]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/ComponentQuery.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / ComponentQuery.js
CommitLineData
6527f429
DM
1/**\r
2 * Provides searching of Components within Ext.ComponentManager (globally) or a specific\r
3 * Ext.container.Container on the document with a similar syntax to a CSS selector.\r
4 * Returns Array of matching Components, or empty Array.\r
5 *\r
6 * ## Basic Component lookup\r
7 *\r
8 * Components can be retrieved by using their {@link Ext.Component xtype}:\r
9 *\r
10 * - `component`\r
11 * - `gridpanel`\r
12 *\r
13 * Matching by `xtype` matches inherited types, so in the following code, the previous field\r
14 * *of any type which inherits from `TextField`* will be found:\r
15 *\r
16 * prevField = myField.previousNode('textfield');\r
17 *\r
18 * To match only the exact type, pass the "shallow" flag by adding `(true)` to xtype\r
19 * (See Component's {@link Ext.Component#isXType isXType} method):\r
20 *\r
21 * prevTextField = myField.previousNode('textfield(true)');\r
22 *\r
23 * You can search Components by their `id` or `itemId` property, prefixed with a #:\r
24 *\r
25 * #myContainer\r
26 *\r
27 * Component `xtype` and `id` or `itemId` can be used together to avoid possible\r
28 * id collisions between Components of different types:\r
29 *\r
30 * panel#myPanel\r
31 *\r
32 * When Component's `id` or `xtype` contains dots, you can escape them in your selector:\r
33 *\r
34 * my\.panel#myPanel\r
35 *\r
36 * Keep in mind that JavaScript treats the backslash character in a special way, so you\r
37 * need to escape it, too, in the actual code:\r
38 *\r
39 * var myPanel = Ext.ComponentQuery.query('my\\.panel#myPanel');\r
40 *\r
41 * ## Traversing Component tree\r
42 *\r
43 * Components can be found by their relation to other Components. There are several\r
44 * relationship operators, mostly taken from CSS selectors:\r
45 *\r
46 * - **`E F`** All descendant Components of E that match F\r
47 * - **`E > F`** All direct children Components of E that match F\r
48 * - **`E ^ F`** All parent Components of E that match F\r
49 *\r
50 * Expressions between relationship operators are matched left to right, i.e. leftmost\r
51 * selector is applied first, then if one or more matches are found, relationship operator\r
52 * itself is applied, then next selector expression, etc. It is possible to combine\r
53 * relationship operators in complex selectors:\r
54 *\r
55 * window[title="Input form"] textfield[name=login] ^ form > button[action=submit]\r
56 *\r
57 * That selector can be read this way: Find a window with title "Input form", in that\r
58 * window find a TextField with name "login" at any depth (including subpanels and/or\r
59 * FieldSets), then find an `Ext.form.Panel` that is a parent of the TextField, and in\r
60 * that form find a direct child that is a button with custom property `action` set to\r
61 * value "submit".\r
62 *\r
63 * Whitespace on both sides of `^` and `>` operators is non-significant, i.e. can be\r
64 * omitted, but usually is used for clarity.\r
65 *\r
66 * ## Searching by Component attributes\r
67 *\r
68 * Components can be searched by their object property values (attributes). To do that,\r
69 * use attribute matching expression in square brackets:\r
70 *\r
71 * - `component[disabled]` - matches any Component that has `disabled` property with\r
72 * any truthy (non-empty, not `false`) value.\r
73 * - `panel[title="Test"]` - matches any Component that has `title` property set to\r
74 * "Test". Note that if the value does not contain spaces, the quotes are optional.\r
75 *\r
76 * Attributes can use any of the following operators to compare values:\r
77 * `=`, `!=`, `^=`, `$=`, `*=`, `%=`, `|=` and `~=`.\r
78 *\r
79 * Prefixing the attribute name with an at sign `@` means that the property must be\r
80 * the object's `ownProperty`, not a property from the prototype chain.\r
81 *\r
82 * Specifications like `[propName]` check that the property is a truthy value. To check\r
83 * that the object has an `ownProperty` of a certain name, regardless of the value use\r
84 * the form `[?propName]`.\r
85 *\r
86 * The specified value is coerced to match the type of the property found in the\r
87 * candidate Component using {@link Ext#coerce}.\r
88 *\r
89 * If you need to find Components by their `itemId` property, use the `#id` form; it will\r
90 * do the same as `[itemId=id]` but is easier to read.\r
91 *\r
92 * If you need to include a metacharacter like (, ), [, ], etc., in the query, escape it\r
93 * by prefixing it with a backslash:\r
94 *\r
95 * var component = Ext.ComponentQuery.query('[myProperty=\\[foo\\]]');\r
96 *\r
97 * ## Attribute matching operators\r
98 *\r
99 * The '=' operator will return the results that **exactly** match the\r
100 * specified object property (attribute):\r
101 *\r
102 * Ext.ComponentQuery.query('panel[cls=my-cls]');\r
103 *\r
104 * Will match the following Component:\r
105 *\r
106 * Ext.create('Ext.window.Window', {\r
107 * cls: 'my-cls'\r
108 * });\r
109 *\r
110 * But will not match the following Component, because 'my-cls' is one value\r
111 * among others:\r
112 *\r
113 * Ext.create('Ext.panel.Panel', {\r
114 * cls: 'foo-cls my-cls bar-cls'\r
115 * });\r
116 *\r
117 * You can use the '~=' operator instead, it will return Components with\r
118 * the property that **exactly** matches one of the whitespace-separated\r
119 * values. This is also true for properties that only have *one* value:\r
120 *\r
121 * Ext.ComponentQuery.query('panel[cls~=my-cls]');\r
122 *\r
123 * Will match both Components:\r
124 *\r
125 * Ext.create('Ext.panel.Panel', {\r
126 * cls: 'foo-cls my-cls bar-cls'\r
127 * });\r
128 * \r
129 * Ext.create('Ext.window.Window', {\r
130 * cls: 'my-cls'\r
131 * });\r
132 *\r
133 * Generally, '=' operator is more suited for object properties other than\r
134 * CSS classes, while '~=' operator will work best with properties that\r
135 * hold lists of whitespace-separated CSS classes.\r
136 *\r
137 * The '^=' operator will return Components with specified attribute that\r
138 * start with the passed value:\r
139 *\r
140 * Ext.ComponentQuery.query('panel[title^=Sales]');\r
141 *\r
142 * Will match the following Component:\r
143 *\r
144 * Ext.create('Ext.panel.Panel', {\r
145 * title: 'Sales estimate for Q4'\r
146 * });\r
147 *\r
148 * The '$=' operator will return Components with specified properties that\r
149 * end with the passed value:\r
150 *\r
151 * Ext.ComponentQuery.query('field[fieldLabel$=name]');\r
152 *\r
153 * Will match the following Component:\r
154 *\r
155 * Ext.create('Ext.form.field.Text', {\r
156 * fieldLabel: 'Enter your name'\r
157 * });\r
158 *\r
159 * The '/=' operator will return Components with specified properties that\r
160 * match the passed regular expression:\r
161 *\r
162 * Ext.ComponentQuery.query('button[action/="edit|save"]');\r
163 *\r
164 * Will match the following Components with a custom `action` property:\r
165 *\r
166 * Ext.create('Ext.button.Button', {\r
167 * action: 'edit'\r
168 * });\r
169 *\r
170 * Ext.create('Ext.button.Button', {\r
171 * action: 'save'\r
172 * });\r
173 *\r
174 * When you need to use meta characters like [], (), etc. in your query, make sure\r
175 * to escape them with back slashes:\r
176 *\r
177 * Ext.ComponentQuery.query('panel[title="^Sales for Q\\[1-4\\]"]');\r
178 *\r
179 * The following test will find panels with their `ownProperty` collapsed being equal to\r
180 * `false`. It will **not** match a collapsed property from the prototype chain.\r
181 *\r
182 * Ext.ComponentQuery.query('panel[@collapsed=false]');\r
183 *\r
184 * Member expressions from candidate Components may be tested. If the expression returns\r
185 * a *truthy* value, the candidate Component will be included in the query:\r
186 *\r
187 * var disabledFields = myFormPanel.query("{isDisabled()}");\r
188 *\r
189 * Such expressions are executed in Component's context, and the above expression is\r
190 * similar to running this snippet for every Component in your application:\r
191 *\r
192 * if (component.isDisabled()) {\r
193 * matches.push(component);\r
194 * }\r
195 *\r
196 * It is important to use only methods that are available in **every** Component instance\r
197 * to avoid run time exceptions. If you need to match your Components with a custom\r
198 * condition formula, you can augment `Ext.Component` to provide custom matcher that\r
199 * will return `false` by default, and override it in your custom classes:\r
200 * \r
201 * Ext.define('My.Component', {\r
202 * override: 'Ext.Component',\r
203 * myMatcher: function() { return false; }\r
204 * });\r
205 *\r
206 * Ext.define('My.Panel', {\r
207 * extend: 'Ext.panel.Panel',\r
208 * requires: ['My.Component'], // Ensure that Component override is applied\r
209 * myMatcher: function(selector) {\r
210 * return selector === 'myPanel';\r
211 * }\r
212 * });\r
213 *\r
214 * After that you can use a selector with your custom matcher to find all instances\r
215 * of `My.Panel`:\r
216 *\r
217 * Ext.ComponentQuery.query("{myMatcher('myPanel')}");\r
218 *\r
219 * However if you really need to use a custom matcher, you may find it easier to implement\r
220 * a custom Pseudo class instead (see below).\r
221 *\r
222 * ## Conditional matching\r
223 *\r
224 * Attribute matchers can be combined to select only Components that match **all**\r
225 * conditions (logical AND operator):\r
226 *\r
227 * Ext.ComponentQuery.query('panel[cls~=my-cls][floating=true][title$="sales data"]');\r
228 *\r
229 * E.g., the query above will match only a Panel-descended Component that has 'my-cls'\r
230 * CSS class *and* is floating *and* with a title that ends with "sales data".\r
231 *\r
232 * Expressions separated with commas will match any Component that satisfies\r
233 * *either* expression (logical OR operator):\r
234 *\r
235 * Ext.ComponentQuery.query('field[fieldLabel^=User], field[fieldLabel*=password]');\r
236 *\r
237 * E.g., the query above will match any field with field label starting with "User",\r
238 * *or* any field that has "password" in its label.\r
239 *\r
240 * If you need to include a comma in an attribute matching expression, escape it with a\r
241 * backslash:\r
242 *\r
243 * Ext.ComponentQuery.query('field[fieldLabel^="User\\, foo"], field[fieldLabel*=password]');\r
244 *\r
245 * ## Pseudo classes\r
246 *\r
247 * Pseudo classes may be used to filter results in the same way as in\r
248 * {@link Ext.dom.Query}. There are five default pseudo classes:\r
249 *\r
250 * * `not` Negates a selector.\r
251 * * `first` Filters out all except the first matching item for a selector.\r
252 * * `last` Filters out all except the last matching item for a selector.\r
253 * * `focusable` Filters out all except Components which are currently able to recieve\r
254 * focus.\r
255 * * `nth-child` Filters Components by ordinal position in the selection.\r
256 * * `scrollable` Filters out all except Components which are scrollable.\r
257 *\r
258 * These pseudo classes can be used with other matchers or without them:\r
259 *\r
260 * // Select first direct child button in any panel\r
261 * Ext.ComponentQuery.query('panel > button:first');\r
262 *\r
263 * // Select last field in Profile form\r
264 * Ext.ComponentQuery.query('form[title=Profile] field:last');\r
265 *\r
266 * // Find first focusable Component in a panel and focus it\r
267 * panel.down(':focusable').focus();\r
268 *\r
269 * // Select any field that is not hidden in a form\r
270 * form.query('field:not(hiddenfield)');\r
271 *\r
272 * // Find last scrollable Component and reset its scroll positions.\r
273 * tabpanel.down(':scrollable[hideMode=display]:last').getScrollable().scrollTo(0, 0);\r
274 *\r
275 * Pseudo class `nth-child` can be used to find any child Component by its\r
276 * position relative to its siblings. This class' handler takes one argument\r
277 * that specifies the selection formula as `Xn` or `Xn+Y`:\r
278 *\r
279 * // Find every odd field in a form\r
280 * form.query('field:nth-child(2n+1)'); // or use shortcut: :nth-child(odd)\r
281 *\r
282 * // Find every even field in a form\r
283 * form.query('field:nth-child(2n)'); // or use shortcut: :nth-child(even)\r
284 *\r
285 * // Find every 3rd field in a form\r
286 * form.query('field:nth-child(3n)');\r
287 *\r
288 * Pseudo classes can be combined to further filter the results, e.g., in the\r
289 * form example above we can modify the query to exclude hidden fields:\r
290 *\r
291 * // Find every 3rd non-hidden field in a form\r
292 * form.query('field:not(hiddenfield):nth-child(3n)');\r
293 *\r
294 * Note that when combining pseudo classes, whitespace is significant, i.e.\r
295 * there should be no spaces between pseudo classes. This is a common mistake;\r
296 * if you accidentally type a space between `field` and `:not`, the query\r
297 * will not return any result because it will mean "find *field's children\r
298 * Components* that are not hidden fields...".\r
299 *\r
300 * ## Custom pseudo classes\r
301 *\r
302 * It is possible to define your own custom pseudo classes. In fact, a\r
303 * pseudo class is just a property in `Ext.ComponentQuery.pseudos` object\r
304 * that defines pseudo class name (property name) and pseudo class handler\r
305 * (property value):\r
306 *\r
307 * // Function receives array and returns a filtered array.\r
308 * Ext.ComponentQuery.pseudos.invalid = function(items) {\r
309 * var i = 0, l = items.length, c, result = [];\r
310 * for (; i < l; i++) {\r
311 * if (!(c = items[i]).isValid()) {\r
312 * result.push(c);\r
313 * }\r
314 * }\r
315 * return result;\r
316 * };\r
317 * \r
318 * var invalidFields = myFormPanel.query('field:invalid');\r
319 * if (invalidFields.length) {\r
320 * invalidFields[0].getEl().scrollIntoView(myFormPanel.body);\r
321 * for (var i = 0, l = invalidFields.length; i < l; i++) {\r
322 * invalidFields[i].getEl().frame("red");\r
323 * }\r
324 * }\r
325 *\r
326 * Pseudo class handlers can be even more flexible, with a selector\r
327 * argument used to define the logic:\r
328 *\r
329 * // Handler receives array of itmes and selector in parentheses\r
330 * Ext.ComponentQuery.pseudos.titleRegex = function(components, selector) {\r
331 * var i = 0, l = components.length, c, result = [], regex = new RegExp(selector);\r
332 * for (; i < l; i++) {\r
333 * c = components[i];\r
334 * if (c.title && regex.test(c.title)) {\r
335 * result.push(c);\r
336 * }\r
337 * }\r
338 * return result;\r
339 * }\r
340 *\r
341 * var salesTabs = tabPanel.query('panel:titleRegex("sales\\s+for\\s+201[123]")');\r
342 *\r
343 * Be careful when using custom pseudo classes with MVC Controllers: when\r
344 * you use a pseudo class in Controller's `control` or `listen` component\r
345 * selectors, the pseudo class' handler function will be called very often\r
346 * and may slow down your application significantly. A good rule of thumb\r
347 * is to always specify Component xtype with the pseudo class so that the\r
348 * handlers are only called on Components that you need, and try to make\r
349 * the condition checks as cheap in terms of execution time as possible.\r
350 * Note how in the example above, handler function checks that Component\r
351 * *has* a title first, before running regex test on it.\r
352 *\r
353 * ## Query examples\r
354 *\r
355 * Queries return an array of Components. Here are some example queries:\r
356 *\r
357 * // retrieve all Ext.Panels in the document by xtype\r
358 * var panelsArray = Ext.ComponentQuery.query('panel');\r
359 *\r
360 * // retrieve all Ext.Panels within the container with an id myCt\r
361 * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');\r
362 *\r
363 * // retrieve all direct children which are Ext.Panels within myCt\r
364 * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');\r
365 *\r
366 * // retrieve all grids or trees\r
367 * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');\r
368 *\r
369 * // Focus first Component\r
370 * myFormPanel.child(':focusable').focus();\r
371 *\r
372 * // Retrieve every odd text field in a form\r
373 * myFormPanel.query('textfield:nth-child(odd)');\r
374 *\r
375 * // Retrieve every even field in a form, excluding hidden fields\r
376 * myFormPanel.query('field:not(hiddenfield):nth-child(even)');\r
377 *\r
378 * // Retrieve every scrollable in a tabpanel\r
379 * tabpanel.query(':scrollable');\r
380 *\r
381 * For easy access to queries based from a particular Container see the\r
382 * {@link Ext.container.Container#query}, {@link Ext.container.Container#down} and\r
383 * {@link Ext.container.Container#child} methods. Also see\r
384 * {@link Ext.Component#up}.\r
385 */\r
386Ext.define('Ext.ComponentQuery', {\r
387 singleton: true,\r
388 requires: [\r
389 'Ext.ComponentManager',\r
390 'Ext.util.Operators',\r
391 'Ext.util.LruCache'\r
392 ]\r
393}, function() {\r
394\r
395 var cq = this,\r
396 queryOperators = Ext.util.Operators,\r
397 nthRe = /(\d*)n\+?(\d*)/,\r
398 nthRe2 = /\D/,\r
399 stripLeadingSpaceRe = /^(\s)+/,\r
400 unescapeRe = /\\(.)/g,\r
401 regexCache = new Ext.util.LruCache({\r
402 maxSize: 100\r
403 }),\r
404\r
405 // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied\r
406 // as a member on each item in the passed array.\r
407 filterFnPattern = [\r
408 'var r = [],',\r
409 'i = 0,',\r
410 'it = items,',\r
411 'l = it.length,',\r
412 'c;',\r
413 'for (; i < l; i++) {',\r
414 'c = it[i];',\r
415 'if (c.{0}) {',\r
416 'r.push(c);',\r
417 '}',\r
418 '}',\r
419 'return r;'\r
420 ].join(''),\r
421\r
422 filterItems = function(items, operation) {\r
423 // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]\r
424 // The operation's method loops over each item in the candidate array and\r
425 // returns an array of items which match its criteria\r
426 return operation.method.apply(this, [ items ].concat(operation.args));\r
427 },\r
428\r
429 getItems = function(items, mode) {\r
430 var result = [],\r
431 i = 0,\r
432 length = items.length,\r
433 candidate,\r
434 deep = mode !== '>';\r
435 \r
436 for (; i < length; i++) {\r
437 candidate = items[i];\r
438 if (candidate.getRefItems) {\r
439 result = result.concat(candidate.getRefItems(deep));\r
440 }\r
441 }\r
442 return result;\r
443 },\r
444\r
445 getAncestors = function(items) {\r
446 var result = [],\r
447 i = 0,\r
448 length = items.length,\r
449 candidate;\r
450 for (; i < length; i++) {\r
451 candidate = items[i];\r
452 while (!!(candidate = candidate.getRefOwner())) {\r
453 result.push(candidate);\r
454 }\r
455 }\r
456 return result;\r
457 },\r
458\r
459 // Filters the passed candidate array and returns only items which match the passed xtype\r
460 filterByXType = function(items, xtype, shallow) {\r
461 if (xtype === '*') {\r
462 return items.slice();\r
463 }\r
464 else {\r
465 var result = [],\r
466 i = 0,\r
467 length = items.length,\r
468 candidate;\r
469 for (; i < length; i++) {\r
470 candidate = items[i];\r
471 if (candidate.isXType(xtype, shallow)) {\r
472 result.push(candidate);\r
473 }\r
474 }\r
475 return result;\r
476 }\r
477 },\r
478\r
479 // Filters the passed candidate array and returns only items which have the specified property match\r
480 filterByAttribute = function(items, property, operator, compareTo) {\r
481 var result = [],\r
482 i = 0,\r
483 length = items.length,\r
484 mustBeOwnProperty,\r
485 presenceOnly,\r
486 candidate, propValue,\r
487 j, propLen,\r
488 config;\r
489\r
490 // Prefixing property name with an @ means that the property must be in the candidate, not in its prototype\r
491 if (property.charAt(0) === '@') {\r
492 mustBeOwnProperty = true;\r
493 property = property.substr(1);\r
494 }\r
495 if (property.charAt(0) === '?') {\r
496 mustBeOwnProperty = true;\r
497 presenceOnly = true;\r
498 property = property.substr(1);\r
499 }\r
500\r
501 for (; i < length; i++) {\r
502 candidate = items[i];\r
503\r
504 // If the candidate is a product of the Ext class system, then\r
505 // use the configurator to call getters to access the property.\r
506 // CQ can be used to filter raw Objects.\r
507 config = candidate.getConfigurator && candidate.self.$config.configs[property];\r
508 if (config) {\r
509 propValue = candidate[config.names.get]();\r
510 } else if (mustBeOwnProperty && !candidate.hasOwnProperty(property)) {\r
511 continue;\r
512 } else {\r
513 propValue = candidate[property];\r
514 }\r
515\r
516 if (presenceOnly) {\r
517 result.push(candidate);\r
518 }\r
519 // implies property is an array, and we must compare value against each element.\r
520 else if (operator === '~=') {\r
521 if (propValue) {\r
522 //We need an array\r
523 if (!Ext.isArray(propValue)) {\r
524 propValue = propValue.split(' ');\r
525 }\r
526\r
527 for (j = 0, propLen = propValue.length; j < propLen; j++) {\r
528 if (queryOperators[operator](Ext.coerce(propValue[j], compareTo), compareTo)) {\r
529 result.push(candidate);\r
530 break;\r
531 }\r
532 }\r
533 }\r
534 }\r
535 else if (operator === '/=') {\r
536 if (propValue != null && compareTo.test(propValue)) {\r
537 result.push(candidate);\r
538 }\r
539 } else if (!compareTo ? !!candidate[property] : queryOperators[operator](Ext.coerce(propValue, compareTo), compareTo)) {\r
540 result.push(candidate);\r
541 }\r
542 }\r
543 return result;\r
544 },\r
545\r
546 // Filters the passed candidate array and returns only items which have the specified itemId or id\r
547 filterById = function(items, id) {\r
548 var result = [],\r
549 i = 0,\r
550 length = items.length,\r
551 candidate;\r
552 for (; i < length; i++) {\r
553 candidate = items[i];\r
554 if (candidate.getItemId() === id) {\r
555 result.push(candidate);\r
556 }\r
557 }\r
558 return result;\r
559 },\r
560\r
561 // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in\r
562 filterByPseudo = function(items, name, value) {\r
563 return cq.pseudos[name](items, value);\r
564 },\r
565\r
566 // Determines leading mode\r
567 // > for direct child, and ^ to switch to ownerCt axis\r
568 modeRe = /^(\s?([>\^])\s?|\s|$)/,\r
569\r
570 // Matches a token with possibly (true|false) appended for the "shallow" parameter\r
571 tokenRe = /^(#)?((?:\\\.|[\w\-])+|\*)(?:\((true|false)\))?/,\r
572\r
573 matchers = [{\r
574 // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter\r
575 re: /^\.((?:\\\.|[\w\-])+)(?:\((true|false)\))?/,\r
576 method: filterByXType,\r
577 argTransform: function(args) {\r
578 //<debug>\r
579 var selector = args[0];\r
580 Ext.log.warn('"'+ selector +'" ComponentQuery selector style is deprecated,' +\r
581 ' use "' + selector.replace(/^\./, '') + '" without the leading dot instead');\r
582 //</debug>\r
583 \r
584 if (args[1] !== undefined) {\r
585 args[1] = args[1].replace(unescapeRe, '$1');\r
586 }\r
587 \r
588 return args.slice(1);\r
589 }\r
590 }, {\r
591 // Allow [@attribute] to check truthy ownProperty\r
592 // Allow [?attribute] to check for presence of ownProperty\r
593 // Allow [$attribute]\r
594 // Checks for @|?|$ -> word/hyphen chars -> any special attribute selector characters before\r
595 // the '=', etc. It strips out whitespace.\r
596 // For example:\r
597 // [attribute=value], [attribute^=value], [attribute$=value], [attribute*=value],\r
598 // [attribute~=value], [attribute%=value], [attribute!=value]\r
599 re: /^(?:\[((?:[@?$])?[\w\-]*)\s*(?:([\^$*~%!\/]?=)\s*(['"])?((?:\\\]|.)*?)\3)?(?!\\)\])/,\r
600 method: filterByAttribute,\r
601 argTransform: function(args) {\r
602 var selector = args[0],\r
603 property = args[1],\r
604 operator = args[2],\r
605 //quote = args[3],\r
606 compareTo = args[4],\r
607 compareRe;\r
608 \r
609 // Unescape the attribute value matcher first\r
610 if (compareTo !== undefined) {\r
611 compareTo = compareTo.replace(unescapeRe, '$1');\r
612\r
613 //<debug>\r
614 var format = Ext.String.format,\r
615 msg = "ComponentQuery selector '{0}' has an unescaped ({1}) character at the {2} " +\r
616 "of the attribute value pattern. Usually that indicates an error " +\r
617 "where the opening quote is not followed by the closing quote. " +\r
618 "If you need to match a ({1}) character at the {2} of the attribute " +\r
619 "value, escape the quote character in your pattern: (\\{1})",\r
620 match;\r
621 \r
622 if (match = /^(['"]).*?[^'"]$/.exec(compareTo)) { // jshint ignore:line\r
623 Ext.log.warn(format(msg, selector, match[1], 'beginning'));\r
624 }\r
625 else if (match = /^[^'"].*?(['"])$/.exec(compareTo)) { // jshint ignore:line\r
626 Ext.log.warn(format(msg, selector, match[1], 'end'));\r
627 }\r
628 //</debug>\r
629 }\r
630 \r
631 if (operator === '/=') {\r
632 compareRe = regexCache.get(compareTo);\r
633 if (compareRe) {\r
634 compareTo = compareRe;\r
635 } else {\r
636 compareTo = regexCache.add(compareTo, new RegExp(compareTo));\r
637 }\r
638 }\r
639 \r
640 return [property, operator, compareTo];\r
641 }\r
642 }, {\r
643 // checks for #cmpItemId\r
644 re: /^#((?:\\\.|[\w\-])+)/,\r
645 method: filterById\r
646 }, {\r
647 // checks for :<pseudo_class>(<selector>)\r
648 re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,\r
649 method: filterByPseudo,\r
650 argTransform: function(args) {\r
651 if (args[2] !== undefined) {\r
652 args[2] = args[2].replace(unescapeRe, '$1');\r
653 }\r
654 \r
655 return args.slice(1);\r
656 }\r
657 }, {\r
658 // checks for {<member_expression>}\r
659 re: /^(?:\{([^\}]+)\})/,\r
660 method: filterFnPattern\r
661 }];\r
662\r
663 // Internal class Ext.ComponentQuery.Query\r
664 cq.Query = Ext.extend(Object, {\r
665 constructor: function(cfg) {\r
666 cfg = cfg || {};\r
667 Ext.apply(this, cfg);\r
668 },\r
669 \r
670 // Executes this Query upon the selected root.\r
671 // The root provides the initial source of candidate Component matches which are progressively\r
672 // filtered by iterating through this Query's operations cache.\r
673 // If no root is provided, all registered Components are searched via the ComponentManager.\r
674 // root may be a Container who's descendant Components are filtered\r
675 // root may be a Component with an implementation of getRefItems which provides some nested Components such as the\r
676 // docked items within a Panel.\r
677 // root may be an array of candidate Components to filter using this Query.\r
678 execute: function(root) {\r
679 var operations = this.operations,\r
680 result = [],\r
681 op, i, len;\r
682 \r
683 for (i = 0, len = operations.length; i < len; i++) {\r
684 op = operations[i];\r
685 \r
686 result = result.concat(this._execute(root, op));\r
687 }\r
688 \r
689 return result;\r
690 },\r
691 \r
692 _execute: function(root, operations) {\r
693 var i = 0,\r
694 length = operations.length,\r
695 operation,\r
696 workingItems;\r
697\r
698 // no root, use all Components in the document\r
699 if (!root) {\r
700 workingItems = Ext.ComponentManager.getAll();\r
701 }\r
702 // Root is an iterable object like an Array, or system Collection, eg HtmlCollection\r
703 else if (Ext.isIterable(root)) {\r
704 workingItems = root;\r
705 }\r
706 // Root is a MixedCollection\r
707 else if (root.isMixedCollection) {\r
708 workingItems = root.items;\r
709 }\r
710\r
711 // We are going to loop over our operations and take care of them\r
712 // one by one.\r
713 for (; i < length; i++) {\r
714 operation = operations[i];\r
715\r
716 // The mode operation requires some custom handling.\r
717 // All other operations essentially filter down our current\r
718 // working items, while mode replaces our current working\r
719 // items by getting children from each one of our current\r
720 // working items. The type of mode determines the type of\r
721 // children we get. (e.g. > only gets direct children)\r
722 if (operation.mode === '^') {\r
723 workingItems = getAncestors(workingItems || [root]);\r
724 }\r
725 else if (operation.mode) {\r
726 workingItems = getItems(workingItems || [root], operation.mode);\r
727 }\r
728 else {\r
729 workingItems = filterItems(workingItems || getItems([root]), operation);\r
730 }\r
731\r
732 // If this is the last operation, it means our current working\r
733 // items are the final matched items. Thus return them!\r
734 if (i === length -1) {\r
735 return workingItems;\r
736 }\r
737 }\r
738 return [];\r
739 },\r
740\r
741 is: function(component) {\r
742 var operations = this.operations,\r
743 result = false,\r
744 len = operations.length,\r
745 op, i;\r
746 \r
747 if (len === 0) {\r
748 return true;\r
749 }\r
750 \r
751 for (i = 0; i < len; i++) {\r
752 op = operations[i];\r
753 \r
754 result = this._is(component, op);\r
755 \r
756 if (result) {\r
757 return result;\r
758 }\r
759 }\r
760 \r
761 return false;\r
762 },\r
763 \r
764 _is: function(component, operations) {\r
765 var len = operations.length,\r
766 active = [component],\r
767 operation, i, j, mode, items, item;\r
768 \r
769 // Loop backwards, since we're going up the hierarchy\r
770 for (i = len - 1; i >= 0; --i) {\r
771 operation = operations[i];\r
772 mode = operation.mode;\r
773 // Traversing hierarchy\r
774 if (mode) {\r
775 if (mode === '^') {\r
776 active = getItems(active, ' ');\r
777 } else if (mode === '>') {\r
778 items = [];\r
779 for (j = 0, len = active.length; j < len; ++j) {\r
780 item = active[j].getRefOwner();\r
781 if (item) {\r
782 items.push(item);\r
783 }\r
784 }\r
785 active = items;\r
786 } else {\r
787 active = getAncestors(active);\r
788 }\r
789 \r
790 // After traversing the hierarchy, if we have no items, jump out\r
791 if (active.length === 0) {\r
792 return false;\r
793 }\r
794 \r
795 } else {\r
796 active = filterItems(active, operation);\r
797 if (active.length === 0) {\r
798 return false;\r
799 }\r
800 }\r
801 }\r
802 return true;\r
803 },\r
804 \r
805 getMatches: function(components, operations) {\r
806 var len = operations.length,\r
807 i;\r
808 \r
809 for (i = 0; i < len; ++i) {\r
810 components = filterItems(components, operations[i]);\r
811 // Performance enhancement, if we have nothing, we can\r
812 // never add anything new, so jump out\r
813 if (components.length === 0) {\r
814 break;\r
815 } \r
816 }\r
817 return components;\r
818 },\r
819\r
820 isMultiMatch: function() {\r
821 return this.operations.length > 1;\r
822 }\r
823 });\r
824\r
825 Ext.apply(cq, {\r
826\r
827 /**\r
828 * @private\r
829 * Cache of selectors and matching ComponentQuery.Query objects\r
830 */\r
831 cache: new Ext.util.LruCache({\r
832 maxSize: 100\r
833 }),\r
834\r
835 /**\r
836 * @private\r
837 * Cache of pseudo class filter functions\r
838 */\r
839 pseudos: {\r
840 not: function(components, selector){\r
841 var i = 0,\r
842 length = components.length,\r
843 results = [],\r
844 index = -1,\r
845 component;\r
846\r
847 for(; i < length; ++i) {\r
848 component = components[i];\r
849 if (!cq.is(component, selector)) {\r
850 results[++index] = component;\r
851 }\r
852 }\r
853 return results;\r
854 },\r
855 first: function(components) {\r
856 var ret = [];\r
857\r
858 if (components.length > 0) {\r
859 ret.push(components[0]);\r
860 }\r
861 return ret;\r
862 },\r
863 last: function(components) {\r
864 var len = components.length,\r
865 ret = [];\r
866\r
867 if (len > 0) {\r
868 ret.push(components[len - 1]);\r
869 }\r
870 return ret;\r
871 },\r
872 focusable: function(cmps) {\r
873 var len = cmps.length,\r
874 results = [],\r
875 i = 0,\r
876 c;\r
877\r
878 for (; i < len; i++) {\r
879 c = cmps[i];\r
880\r
881 if (c.isFocusable && c.isFocusable()) {\r
882 results.push(c);\r
883 }\r
884 }\r
885\r
886 return results;\r
887 },\r
888 "nth-child" : function(c, a) {\r
889 var result = [],\r
890 m = nthRe.exec(a === "even" && "2n" || a === "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a),\r
891 f = (m[1] || 1) - 0, len = m[2] - 0,\r
892 i, n, nodeIndex;\r
893\r
894 for (i = 0; n = c[i]; i++) { // jshint ignore:line\r
895 nodeIndex = i + 1;\r
896 if (f === 1) {\r
897 if (len === 0 || nodeIndex === len) {\r
898 result.push(n);\r
899 }\r
900 } else if ((nodeIndex + len) % f === 0){\r
901 result.push(n);\r
902 }\r
903 }\r
904\r
905 return result;\r
906 },\r
907 scrollable: function(cmps) {\r
908 var len = cmps.length,\r
909 results = [],\r
910 i = 0,\r
911 c;\r
912\r
913 for (; i < len; i++) {\r
914 c = cmps[i];\r
915\r
916 // Note that modern toolkit prefixes with an underscore.\r
917 if (c.scrollable || c._scrollable) {\r
918 results.push(c);\r
919 }\r
920 }\r
921\r
922 return results;\r
923 }\r
924 },\r
925\r
926 /**\r
927 * Returns an array of matched Components from within the passed root object.\r
928 *\r
929 * This method filters returned Components in a similar way to how CSS selector based DOM\r
930 * queries work using a textual selector string.\r
931 *\r
932 * See class summary for details.\r
933 *\r
934 * @param {String} selector The selector string to filter returned Components.\r
935 * @param {Ext.container.Container} [root] The Container within which to perform the query.\r
936 * If omitted, all Components within the document are included in the search.\r
937 *\r
938 * This parameter may also be an array of Components to filter according to the selector.\r
939 * @return {Ext.Component[]} The matched Components.\r
940 *\r
941 * @member Ext.ComponentQuery\r
942 */\r
943 query: function(selector, root) {\r
944 // An empty query will match every Component\r
945 if (!selector) {\r
946 return Ext.ComponentManager.all.getArray();\r
947 }\r
948 \r
949 var results = [],\r
950 noDupResults = [], \r
951 dupMatcher = {}, \r
952 query = cq.cache.get(selector),\r
953 resultsLn, cmp, i;\r
954\r
955 if (!query) {\r
956 query = cq.cache.add(selector, cq.parse(selector));\r
957 }\r
958 \r
959 results = query.execute(root);\r
960 \r
961 // multiple selectors, potential to find duplicates\r
962 // lets filter them out.\r
963 if (query.isMultiMatch()) {\r
964 resultsLn = results.length;\r
965 for (i = 0; i < resultsLn; i++) {\r
966 cmp = results[i];\r
967 if (!dupMatcher[cmp.id]) {\r
968 noDupResults.push(cmp);\r
969 dupMatcher[cmp.id] = true;\r
970 }\r
971 }\r
972 results = noDupResults;\r
973 }\r
974 return results;\r
975 },\r
976\r
977 /**\r
978 * Traverses the tree rooted at the passed root in pre-order mode, calling the passed function on the nodes at each level.\r
979 * That is the function is called upon each node **before** being called on its children).\r
980 *\r
981 * For an object to be queryable, it must implement the `getRefItems` method which returns all\r
982 * immediate child items.\r
983 *\r
984 * This method is used at each level down the cascade. Currently {@link Ext.Component Component}s\r
985 * and {@link Ext.data.TreeModel TreeModel}s are queryable.\r
986 *\r
987 * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery on them.\r
988 *\r
989 * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before calling the function.\r
990 * An empty string matches any node.\r
991 * @param {String} root The root queryable object to start from.\r
992 * @param {Function} fn The function to call. Return `false` to abort the traverse.\r
993 * @param {Object} fn.node The node being visited.\r
994 * @param {Object} [scope] The context (`this` reference) in which the function is executed.\r
995 * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list to pass down extra data known to the caller\r
996 * **after** the node being visited.\r
997 */\r
998 visitPreOrder: function(selector, root, fn, scope, extraArgs) {\r
999 cq._visit(true, selector, root, fn, scope, extraArgs);\r
1000 },\r
1001\r
1002 /**\r
1003 * Traverses the tree rooted at the passed root in post-order mode, calling the passed function on the nodes at each level.\r
1004 * That is the function is called upon each node **after** being called on its children).\r
1005 *\r
1006 * For an object to be queryable, it must implement the `getRefItems` method which returns all\r
1007 * immediate child items.\r
1008 *\r
1009 * This method is used at each level down the cascade. Currently {@link Ext.Component Component}s\r
1010 * and {@link Ext.data.TreeModel TreeModel}s are queryable.\r
1011 *\r
1012 * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery on them.\r
1013 *\r
1014 * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before calling the function.\r
1015 * An empty string matches any node.\r
1016 * @param {String} root The root queryable object to start from.\r
1017 * @param {Function} fn The function to call. Return `false` to abort the traverse.\r
1018 * @param {Object} fn.node The node being visited.\r
1019 * @param {Object} [scope] The context (`this` reference) in which the function is executed.\r
1020 * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list to pass down extra data known to the caller\r
1021 * **after** the node being visited.\r
1022 */\r
1023 visitPostOrder: function(selector, root, fn, scope, extraArgs) {\r
1024 cq._visit(false, selector, root, fn, scope, extraArgs);\r
1025 },\r
1026\r
1027 /**\r
1028 * @private\r
1029 * Visit implementation which handles both preOrder and postOrder modes.\r
1030 */\r
1031 _visit: function(preOrder, selector, root, fn, scope, extraArgs) {\r
1032 var query = cq.cache.get(selector),\r
1033 callArgs = [root],\r
1034 children,\r
1035 len = 0,\r
1036 i, rootMatch;\r
1037\r
1038 if (!query) {\r
1039 query = cq.cache.add(selector, cq.parse(selector));\r
1040 }\r
1041\r
1042 rootMatch = query.is(root);\r
1043\r
1044 if (root.getRefItems) {\r
1045 children = root.getRefItems();\r
1046 len = children.length;\r
1047 }\r
1048\r
1049 // append optional extraArgs\r
1050 if (extraArgs) {\r
1051 Ext.Array.push(callArgs, extraArgs);\r
1052 }\r
1053 if (preOrder) {\r
1054 if (rootMatch) {\r
1055 if (fn.apply(scope || root, callArgs) === false) {\r
1056 return false;\r
1057 }\r
1058 }\r
1059 }\r
1060 for (i = 0; i < len; i++) {\r
1061 if (cq._visit.call(cq, preOrder, selector, children[i], fn, scope, extraArgs) === false) {\r
1062 return false;\r
1063 }\r
1064 }\r
1065 if (!preOrder) {\r
1066 if (rootMatch) {\r
1067 if (fn.apply(scope || root, callArgs) === false) {\r
1068 return false;\r
1069 }\r
1070 }\r
1071 }\r
1072 },\r
1073\r
1074 /**\r
1075 * Tests whether the passed Component matches the selector string.\r
1076 * An empty selector will always match.\r
1077 *\r
1078 * @param {Ext.Component} component The Component to test\r
1079 * @param {String} selector The selector string to test against.\r
1080 * @return {Boolean} True if the Component matches the selector.\r
1081 * @member Ext.ComponentQuery\r
1082 */\r
1083 is: function(component, selector) {\r
1084 if (!selector) {\r
1085 return true;\r
1086 }\r
1087 \r
1088 var query = cq.cache.get(selector);\r
1089 if (!query) {\r
1090 query = cq.cache.add(selector, cq.parse(selector));\r
1091 }\r
1092 \r
1093 return query.is(component);\r
1094 },\r
1095\r
1096 parse: function(selector) {\r
1097 var operations = [],\r
1098 selectors, sel, i, len;\r
1099 \r
1100 selectors = Ext.splitAndUnescape(selector, ',');\r
1101 \r
1102 for (i = 0, len = selectors.length; i < len; i++) {\r
1103 // Trim the whitespace as the parser expects it.\r
1104 sel = Ext.String.trim(selectors[i]);\r
1105 \r
1106 // Usually, a dangling comma at the end of a selector means a typo.\r
1107 // In that case, the last sel value will be an empty string; the parser\r
1108 // will silently ignore it which is not good, so we throw an error here.\r
1109 //<debug>\r
1110 if (sel === '') {\r
1111 Ext.raise('Invalid ComponentQuery selector: ""');\r
1112 }\r
1113 //</debug>\r
1114 \r
1115 operations.push(cq._parse(sel));\r
1116 }\r
1117\r
1118 // Now that we have all our operations in an array, we are going\r
1119 // to create a new Query using these operations.\r
1120 return new cq.Query({\r
1121 operations: operations\r
1122 });\r
1123 },\r
1124 \r
1125 _parse: function(selector) {\r
1126 var operations = [],\r
1127 trim = Ext.String.trim,\r
1128 length = matchers.length,\r
1129 lastSelector,\r
1130 tokenMatch,\r
1131 token,\r
1132 matchedChar,\r
1133 modeMatch,\r
1134 selectorMatch,\r
1135 transform,\r
1136 i, matcher, method, args;\r
1137\r
1138 // We are going to parse the beginning of the selector over and\r
1139 // over again, slicing off the selector any portions we converted into an\r
1140 // operation, until it is an empty string.\r
1141 while (selector && lastSelector !== selector) {\r
1142 lastSelector = selector;\r
1143\r
1144 // First we check if we are dealing with a token like #, * or an xtype\r
1145 tokenMatch = selector.match(tokenRe);\r
1146\r
1147 if (tokenMatch) {\r
1148 matchedChar = tokenMatch[1];\r
1149 token = trim(tokenMatch[2]).replace(unescapeRe, '$1');\r
1150\r
1151 // If the token is prefixed with a # we push a filterById operation to our stack\r
1152 if (matchedChar === '#') {\r
1153 operations.push({\r
1154 method: filterById,\r
1155 args: [token]\r
1156 });\r
1157 }\r
1158 // If the token is a * or an xtype string, we push a filterByXType\r
1159 // operation to the stack.\r
1160 else {\r
1161 operations.push({\r
1162 method: filterByXType,\r
1163 args: [token, Boolean(tokenMatch[3])]\r
1164 });\r
1165 }\r
1166\r
1167 // Now we slice of the part we just converted into an operation\r
1168 selector = selector.replace(tokenMatch[0], '').replace(stripLeadingSpaceRe, '$1');\r
1169 }\r
1170\r
1171 // If the next part of the query is not a space or > or ^, it means we\r
1172 // are going to check for more things that our current selection\r
1173 // has to comply to.\r
1174 while (!(modeMatch = selector.match(modeRe))) {\r
1175 // Lets loop over each type of matcher and execute it\r
1176 // on our current selector.\r
1177 for (i = 0; selector && i < length; i++) {\r
1178 matcher = matchers[i];\r
1179 selectorMatch = selector.match(matcher.re);\r
1180 method = matcher.method;\r
1181 transform = matcher.argTransform;\r
1182\r
1183 // If we have a match, add an operation with the method\r
1184 // associated with this matcher, and pass the regular\r
1185 // expression matches are arguments to the operation.\r
1186 if (selectorMatch) {\r
1187 // Transform function will do unescaping and additional checks\r
1188 if (transform) {\r
1189 args = transform(selectorMatch);\r
1190 }\r
1191 else {\r
1192 args = selectorMatch.slice(1);\r
1193 }\r
1194 \r
1195 operations.push({\r
1196 method: Ext.isString(matcher.method)\r
1197 // Turn a string method into a function by formatting the string with our selector matche expression\r
1198 // A new method is created for different match expressions, eg {id=='textfield-1024'}\r
1199 // Every expression may be different in different selectors.\r
1200 ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))\r
1201 : matcher.method,\r
1202 args: args\r
1203 });\r
1204 \r
1205 selector = selector.replace(selectorMatch[0], '').replace(stripLeadingSpaceRe, '$1');\r
1206 break; // Break on match\r
1207 }\r
1208 // Exhausted all matches: It's an error\r
1209 if (i === (length - 1)) {\r
1210 Ext.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');\r
1211 }\r
1212 }\r
1213 }\r
1214\r
1215 // Now we are going to check for a mode change. This means a space\r
1216 // or a > to determine if we are going to select all the children\r
1217 // of the currently matched items, or a ^ if we are going to use the\r
1218 // ownerCt axis as the candidate source.\r
1219 if (modeMatch[1]) { // Assignment, and test for truthiness!\r
1220 operations.push({\r
1221 mode: modeMatch[2]||modeMatch[1]\r
1222 });\r
1223 \r
1224 // When we have consumed the mode character, clean up leading spaces\r
1225 selector = selector.replace(modeMatch[0], '').replace(stripLeadingSpaceRe, '');\r
1226 }\r
1227 }\r
1228 \r
1229 return operations;\r
1230 }\r
1231 });\r
1232\r
1233 /**\r
1234 * Same as {@link Ext.ComponentQuery#query}.\r
1235 * @param {String} selector The selector string to filter returned Components.\r
1236 * @param {Ext.container.Container} [root] The Container within which to perform the query.\r
1237 * If omitted, all Components within the document are included in the search.\r
1238 *\r
1239 * This parameter may also be an array of Components to filter according to the selector.\r
1240 * @return {Ext.Component[]} The matched Components.\r
1241 * @method all\r
1242 * @member Ext\r
1243 */\r
1244 Ext.all = function () {\r
1245 return cq.query.apply(cq, arguments);\r
1246 };\r
1247\r
1248 /**\r
1249 * Returns the first match to the given component query.\r
1250 * See {@link Ext.ComponentQuery#query}.\r
1251 * @param {String} selector The selector string to filter returned Component.\r
1252 * @param {Ext.container.Container} [root] The Container within which to perform the query.\r
1253 * If omitted, all Components within the document are included in the search.\r
1254 *\r
1255 * This parameter may also be an array of Components to filter according to the selector.\r
1256 * @return {Ext.Component} The first matched Component or `null`.\r
1257 * @method first\r
1258 * @member Ext\r
1259 */\r
1260 Ext.first = function () {\r
1261 var matches = cq.query.apply(cq, arguments);\r
1262 return (matches && matches[0]) || null;\r
1263 }\r
1264});\r