]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/yui/build/selector/selector.js
Release 6.2.0beta4
[Github/sugarcrm.git] / include / javascript / yui / build / selector / selector.js
1 /*
2 Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
5 version: 2.8.0r4
6 */
7 /**
8  * The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements.
9  * @module selector
10  * @title Selector Utility
11  * @namespace YAHOO.util
12  * @requires yahoo, dom
13  */
14
15 (function() {
16 var Y = YAHOO.util;
17
18 /**
19  * Provides helper methods for collecting and filtering DOM elements.
20  * @namespace YAHOO.util
21  * @class Selector
22  * @static
23  */
24
25 Y.Selector = {
26     _foundCache: [],
27     _regexCache: {},
28
29     _re: {
30         nth: /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/,
31         attr: /(\[.*\])/g,
32         urls: /^(?:href|src)/
33     },
34
35     /**
36      * Default document for use queries 
37      * @property document
38      * @type object
39      * @default window.document
40      */
41     document: window.document,
42     /**
43      * Mapping of attributes to aliases, normally to work around HTMLAttributes
44      * that conflict with JS reserved words.
45      * @property attrAliases
46      * @type object
47      */
48     attrAliases: {
49     },
50
51     /**
52      * Mapping of shorthand tokens to corresponding attribute selector 
53      * @property shorthand
54      * @type object
55      */
56     shorthand: {
57         //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
58         '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
59         '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
60     },
61
62     /**
63      * List of operators and corresponding boolean functions. 
64      * These functions are passed the attribute and the current node's value of the attribute.
65      * @property operators
66      * @type object
67      */
68     operators: {
69         '=': function(attr, val) { return attr === val; }, // Equality
70         '!=': function(attr, val) { return attr !== val; }, // Inequality
71         '~=': function(attr, val) { // Match one of space seperated words 
72             var s = ' ';
73             return (s + attr + s).indexOf((s + val + s)) > -1;
74         },
75         '|=': function(attr, val) { return attr === val || attr.slice(0, val.length + 1) === val + '-'; }, // Matches value followed by optional hyphen
76         '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
77         '$=': function(attr, val) { return attr.slice(-val.length) === val; }, // Match ends with value
78         '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring 
79         '': function(attr, val) { return attr; } // Just test for existence of attribute
80     },
81
82     /**
83      * List of pseudo-classes and corresponding boolean functions. 
84      * These functions are called with the current node, and any value that was parsed with the pseudo regex.
85      * @property pseudos
86      * @type object
87      */
88     pseudos: {
89         'root': function(node) {
90             return node === node.ownerDocument.documentElement;
91         },
92
93         'nth-child': function(node, val) {
94             return Y.Selector._getNth(node, val);
95         },
96
97         'nth-last-child': function(node, val) {
98             return Y.Selector._getNth(node, val, null, true);
99         },
100
101         'nth-of-type': function(node, val) {
102             return Y.Selector._getNth(node, val, node.tagName);
103         },
104          
105         'nth-last-of-type': function(node, val) {
106             return Y.Selector._getNth(node, val, node.tagName, true);
107         },
108          
109         'first-child': function(node) {
110             return Y.Selector._getChildren(node.parentNode)[0] === node;
111         },
112
113         'last-child': function(node) {
114             var children = Y.Selector._getChildren(node.parentNode);
115             return children[children.length - 1] === node;
116         },
117
118         'first-of-type': function(node, val) {
119             return Y.Selector._getChildren(node.parentNode, node.tagName)[0];
120         },
121          
122         'last-of-type': function(node, val) {
123             var children = Y.Selector._getChildren(node.parentNode, node.tagName);
124             return children[children.length - 1];
125         },
126          
127         'only-child': function(node) {
128             var children = Y.Selector._getChildren(node.parentNode);
129             return children.length === 1 && children[0] === node;
130         },
131
132         'only-of-type': function(node) {
133             return Y.Selector._getChildren(node.parentNode, node.tagName).length === 1;
134         },
135
136         'empty': function(node) {
137             return node.childNodes.length === 0;
138         },
139
140         'not': function(node, simple) {
141             return !Y.Selector.test(node, simple);
142         },
143
144         'contains': function(node, str) {
145             var text = node.innerText || node.textContent || '';
146             return text.indexOf(str) > -1;
147         },
148         'checked': function(node) {
149             return node.checked === true;
150         }
151     },
152
153     /**
154      * Test if the supplied node matches the supplied selector.
155      * @method test
156      *
157      * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
158      * @param {string} selector The CSS Selector to test the node against.
159      * @return{boolean} Whether or not the node matches the selector.
160      * @static
161     
162      */
163     test: function(node, selector) {
164         node = Y.Selector.document.getElementById(node) || node;
165
166         if (!node) {
167             return false;
168         }
169
170         var groups = selector ? selector.split(',') : [];
171         if (groups.length) {
172             for (var i = 0, len = groups.length; i < len; ++i) {
173                 if ( Y.Selector._test(node, groups[i]) ) { // passes if ANY group matches
174                     return true;
175                 }
176             }
177             return false;
178         }
179         return Y.Selector._test(node, selector);
180     },
181
182     _test: function(node, selector, token, deDupe) {
183         token = token || Y.Selector._tokenize(selector).pop() || {};
184
185         if (!node.tagName ||
186             (token.tag !== '*' && node.tagName !== token.tag) ||
187             (deDupe && node._found) ) {
188             return false;
189         }
190
191         if (token.attributes.length) {
192             var val,
193                 ieFlag,
194                 re_urls = Y.Selector._re.urls;
195
196             if (!node.attributes || !node.attributes.length) {
197                 return false;
198             }
199             for (var i = 0, attr; attr = token.attributes[i++];) {
200                 ieFlag = (re_urls.test(attr[0])) ? 2 : 0;
201                 val = node.getAttribute(attr[0], ieFlag);
202                 if (val === null || val === undefined) {
203                     return false;
204                 }
205                 if ( Y.Selector.operators[attr[1]] &&
206                         !Y.Selector.operators[attr[1]](val, attr[2])) {
207                     return false;
208                 }
209             }
210         }
211
212         if (token.pseudos.length) {
213             for (var i = 0, len = token.pseudos.length; i < len; ++i) {
214                 if (Y.Selector.pseudos[token.pseudos[i][0]] &&
215                         !Y.Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
216                     return false;
217                 }
218             }
219         }
220
221         return (token.previous && token.previous.combinator !== ',') ?
222                 Y.Selector._combinators[token.previous.combinator](node, token) :
223                 true;
224     },
225
226     /**
227      * Filters a set of nodes based on a given CSS selector. 
228      * @method filter
229      *
230      * @param {array} nodes A set of nodes/ids to filter. 
231      * @param {string} selector The selector used to test each node.
232      * @return{array} An array of nodes from the supplied array that match the given selector.
233      * @static
234      */
235     filter: function(nodes, selector) {
236         nodes = nodes || [];
237
238         var node,
239             result = [],
240             tokens = Y.Selector._tokenize(selector);
241
242         if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes
243             for (var i = 0, len = nodes.length; i < len; ++i) {
244                 if (!nodes[i].tagName) { // tagName limits to HTMLElements 
245                     node = Y.Selector.document.getElementById(nodes[i]);
246                     if (node) { // skip IDs that return null 
247                         nodes[i] = node;
248                     } else {
249                     }
250                 }
251             }
252         }
253         result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
254         return result;
255     },
256
257     _filter: function(nodes, token, firstOnly, deDupe) {
258         var result = firstOnly ? null : [],
259             foundCache = Y.Selector._foundCache;
260
261         for (var i = 0, len = nodes.length; i < len; i++) {
262             if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
263                 continue;
264             }
265
266             if (firstOnly) {
267                 return nodes[i];
268             }
269             if (deDupe) {
270                 if (nodes[i]._found) {
271                     continue;
272                 }
273                 nodes[i]._found = true;
274                 foundCache[foundCache.length] = nodes[i];
275             }
276
277             result[result.length] = nodes[i];
278         }
279
280         return result;
281     },
282
283     /**
284      * Retrieves a set of nodes based on a given CSS selector. 
285      * @method query
286      *
287      * @param {string} selector The CSS Selector to test the node against.
288      * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
289      * @param {Boolean} firstOnly optional Whether or not to return only the first match.
290      * @return {Array} An array of nodes that match the given selector.
291      * @static
292      */
293     query: function(selector, root, firstOnly) {
294         var result = Y.Selector._query(selector, root, firstOnly);
295         return result;
296     },
297
298
299     _query: function(selector, root, firstOnly, deDupe) {
300         var result =  (firstOnly) ? null : [],
301             node;
302
303         if (!selector) {
304             return result;
305         }
306
307         var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
308
309         if (groups.length > 1) {
310             var found;
311             for (var i = 0, len = groups.length; i < len; ++i) {
312                 found = Y.Selector._query(groups[i], root, firstOnly, true);
313                 result = firstOnly ? found : result.concat(found); 
314             }
315             Y.Selector._clearFoundCache();
316             return result;
317         }
318
319         if (root && !root.nodeName) { // assume ID
320             root = Y.Selector.document.getElementById(root);
321             if (!root) {
322                 return result;
323             }
324         }
325
326         root = root || Y.Selector.document;
327
328         if (root.nodeName !== '#document') { // prepend with root selector
329             Y.Dom.generateId(root); // TODO: cleanup after?
330             selector = root.tagName + '#' + root.id + ' ' + selector;
331             node = root;
332             root = root.ownerDocument;
333         }
334
335         var tokens = Y.Selector._tokenize(selector);
336         var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
337             nodes = [],
338             id,
339             token = tokens.pop() || {};
340             
341         if (idToken) {
342             id = Y.Selector._getId(idToken.attributes);
343         }
344
345         // use id shortcut when possible
346         if (id) {
347             node = node || Y.Selector.document.getElementById(id);
348
349             if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
350                 if ( Y.Selector._test(node, null, idToken) ) {
351                     if (idToken === token) {
352                         nodes = [node]; // simple selector
353                     } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
354                         root = node; // start from here
355                     }
356                 }
357             } else {
358                 return result;
359             }
360         }
361
362         if (root && !nodes.length) {
363             nodes = root.getElementsByTagName(token.tag);
364         }
365
366         if (nodes.length) {
367             result = Y.Selector._filter(nodes, token, firstOnly, deDupe); 
368         }
369
370         return result;
371     },
372
373
374     _clearFoundCache: function() {
375         var foundCache = Y.Selector._foundCache;
376         for (var i = 0, len = foundCache.length; i < len; ++i) {
377             try { // IE no like delete
378                 delete foundCache[i]._found;
379             } catch(e) {
380                 foundCache[i].removeAttribute('_found');
381             }
382         }
383         foundCache = [];
384     },
385
386
387     _getRegExp: function(str, flags) {
388         var regexCache = Y.Selector._regexCache;
389         flags = flags || '';
390         if (!regexCache[str + flags]) {
391             regexCache[str + flags] = new RegExp(str, flags);
392         }
393         return regexCache[str + flags];
394     },
395
396     _getChildren: function() {
397         if (document.documentElement.children && document.documentElement.children.tags) { // document for capability test
398             return function(node, tag) {
399                 return (tag) ? node.children.tags(tag) : node.children || [];
400             };
401         } else {
402             return function(node, tag) {
403                 var children = [],
404                     childNodes = node.childNodes;
405
406                 for (var i = 0, len = childNodes.length; i < len; ++i) {
407                     if (childNodes[i].tagName) {
408                         if (!tag || childNodes[i].tagName === tag) {
409                             children.push(childNodes[i]);
410                         }
411                     }
412                 }
413                 return children;
414             };
415         }
416     }(),
417
418     _combinators: {
419         ' ': function(node, token) {
420             while ( (node = node.parentNode) ) {
421                 if (Y.Selector._test(node, '', token.previous)) {
422                     return true;
423                 }
424             }  
425             return false;
426         },
427
428         '>': function(node, token) {
429             return Y.Selector._test(node.parentNode, null, token.previous);
430         },
431
432         '+': function(node, token) {
433             var sib = node.previousSibling;
434             while (sib && sib.nodeType !== 1) {
435                 sib = sib.previousSibling;
436             }
437
438             if (sib && Y.Selector._test(sib, null, token.previous)) {
439                 return true; 
440             }
441             return false;
442         },
443
444         '~': function(node, token) {
445             var sib = node.previousSibling;
446             while (sib) {
447                 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
448                     return true;
449                 }
450                 sib = sib.previousSibling;
451             }
452
453             return false;
454         }
455     },
456
457
458     /*
459         an+b = get every _a_th node starting at the _b_th
460         0n+b = no repeat ("0" and "n" may both be omitted (together) , e.g. "0n+1" or "1", not "0+1"), return only the _b_th element
461         1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
462         an+0 = get every _a_th element, "0" may be omitted 
463     */
464     _getNth: function(node, expr, tag, reverse) {
465         Y.Selector._re.nth.test(expr);
466         var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
467             n = RegExp.$2, // "n"
468             oddeven = RegExp.$3, // "odd" or "even"
469             b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
470             result = [],
471             op;
472
473         var siblings = Y.Selector._getChildren(node.parentNode, tag);
474
475         if (oddeven) {
476             a = 2; // always every other
477             op = '+';
478             n = 'n';
479             b = (oddeven === 'odd') ? 1 : 0;
480         } else if ( isNaN(a) ) {
481             a = (n) ? 1 : 0; // start from the first or no repeat
482         }
483
484         if (a === 0) { // just the first
485             if (reverse) {
486                 b = siblings.length - b + 1; 
487             }
488
489             if (siblings[b - 1] === node) {
490                 return true;
491             } else {
492                 return false;
493             }
494
495         } else if (a < 0) {
496             reverse = !!reverse;
497             a = Math.abs(a);
498         }
499
500         if (!reverse) {
501             for (var i = b - 1, len = siblings.length; i < len; i += a) {
502                 if ( i >= 0 && siblings[i] === node ) {
503                     return true;
504                 }
505             }
506         } else {
507             for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
508                 if ( i < len && siblings[i] === node ) {
509                     return true;
510                 }
511             }
512         }
513         return false;
514     },
515
516     _getId: function(attr) {
517         for (var i = 0, len = attr.length; i < len; ++i) {
518             if (attr[i][0] == 'id' && attr[i][1] === '=') {
519                 return attr[i][2];
520             }
521         }
522     },
523
524     _getIdTokenIndex: function(tokens) {
525         for (var i = 0, len = tokens.length; i < len; ++i) {
526             if (Y.Selector._getId(tokens[i].attributes)) {
527                 return i;
528             }
529         }
530         return -1;
531     },
532
533     _patterns: {
534         tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
535         attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
536         pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
537         combinator: /^\s*([>+~]|\s)\s*/
538     },
539
540     /**
541         Break selector into token units per simple selector.
542         Combinator is attached to left-hand selector.
543      */
544     _tokenize: function(selector) {
545         var token = {},     // one token per simple selector (left selector holds combinator)
546             tokens = [],    // array of tokens
547             id,             // unique id for the simple selector (if found)
548             found = false,  // whether or not any matches were found this pass
549             patterns = Y.Selector._patterns,
550             match;          // the regex match
551
552         selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
553
554         /*
555             Search for selector patterns, store, and strip them from the selector string
556             until no patterns match (invalid selector) or we run out of chars.
557
558             Multiple attributes and pseudos are allowed, in any order.
559             for example:
560                 'form:first-child[type=button]:not(button)[lang|=en]'
561         */
562         do {
563             found = false; // reset after full pass
564             for (var re in patterns) {
565                 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
566                     if (re != 'tag' && re != 'combinator') { // only one allowed
567                         token[re] = token[re] || [];
568                     }
569                     if ( (match = patterns[re].exec(selector)) ) { // note assignment
570                         found = true;
571                         if (re != 'tag' && re != 'combinator') { // only one allowed
572                             // capture ID for fast path to element
573                             if (re === 'attributes' && match[1] === 'id') {
574                                 token.id = match[3];
575                             }
576
577                             token[re].push(match.slice(1));
578                         } else { // single selector (tag, combinator)
579                             token[re] = match[1];
580                         }
581                         selector = selector.replace(match[0], ''); // strip current match from selector
582                         if (re === 'combinator' || !selector.length) { // next token or done
583                             token.attributes = Y.Selector._fixAttributes(token.attributes);
584                             token.pseudos = token.pseudos || [];
585                             token.tag = token.tag ? token.tag.toUpperCase() : '*';
586                             tokens.push(token);
587
588                             token = { // prep next token
589                                 previous: token
590                             };
591                         }
592                     }
593                 }
594             }
595         } while (found);
596
597         return tokens;
598     },
599
600
601     _fixAttributes: function(attr) {
602         var aliases = Y.Selector.attrAliases;
603         attr = attr || [];
604         for (var i = 0, len = attr.length; i < len; ++i) {
605             if (aliases[attr[i][0]]) { // convert reserved words, etc
606                 attr[i][0] = aliases[attr[i][0]];
607             }
608             if (!attr[i][1]) { // use exists operator
609                 attr[i][1] = '';
610             }
611         }
612         return attr;
613     },
614
615     _replaceShorthand: function(selector) {
616         var shorthand = Y.Selector.shorthand;
617
618         //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
619         var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
620         if (attrs) {
621             selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
622         }
623         for (var re in shorthand) {
624             if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
625                 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
626             }
627         }
628
629         if (attrs) {
630             for (var i = 0, len = attrs.length; i < len; ++i) {
631                 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
632             }
633         }
634         return selector;
635     }
636 };
637
638 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
639     Y.Selector.attrAliases['class'] = 'className';
640     Y.Selector.attrAliases['for'] = 'htmlFor';
641 }
642
643 })();
644 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});