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