- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / test / data / dromaeo / lib / yui-selector.js
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  */
14
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() {};
23
24 var Y = YAHOO.util;
25
26 var reNth = /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/;
27
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     },
44
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     },
55
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     },
75
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         },
86
87         'nth-child': function(node, val) {
88             return getNth(node, val);
89         },
90
91         'nth-last-child': function(node, val) {
92             return getNth(node, val, null, true);
93         },
94
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         },
106
107         'last-child': function(node) {
108             var children = getChildren(node.parentNode);
109             return children[children.length - 1] === node;
110         },
111
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         },
125
126         'only-of-type': function(node) {
127             return getChildren(node.parentNode, node.tagName.toLowerCase()).length === 1;
128         },
129
130         'empty': function(node) {
131             return node.childNodes.length === 0;
132         },
133
134         'not': function(node, simple) {
135             return !Selector.test(node, simple);
136         },
137
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     },
146
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;
159
160         if (!node) {
161             return false;
162         }
163
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     },
175
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 || [];
187
188         var node,
189             result = [],
190             tokens = tokenize(selector);
191
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     },
207
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     }
222 };
223
224 var query = function(selector, root, firstOnly, deDupe) {
225     var result =  (firstOnly) ? null : [];
226     if (!selector) {
227         return result;
228     }
229
230     var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
231
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     }
241
242     if (root && !root.nodeName) { // assume ID
243         root = Selector.document.getElementById(root);
244         if (!root) {
245             return result;
246         }
247     }
248
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     }
260
261     // use id shortcut when possible
262     if (id) {
263         node = Selector.document.getElementById(id);
264
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     }
277
278     if (root && !nodes.length) {
279         nodes = root.getElementsByTagName(token.tag);
280     }
281
282     if (nodes.length) {
283         result = rFilter(nodes, token, firstOnly, deDupe); 
284     }
285
286     clearParentCache();
287     return result;
288 };
289
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 }();
312
313 var rFilter = function(nodes, token, firstOnly, deDupe) {
314     var result = firstOnly ? null : [];
315
316     for (var i = 0, len = nodes.length; i < len; i++) {
317         if (! rTestNode(nodes[i], '', token, deDupe)) {
318             continue;
319         }
320
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         }
331
332         result[result.length] = nodes[i];
333     }
334
335     return result;
336 };
337
338 var rTestNode = function(node, selector, token, deDupe) {
339     token = token || tokenize(selector).pop() || {};
340
341     if (!node.tagName ||
342         (token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) ||
343         (deDupe && node._found) ) {
344         return false;
345     }
346
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     }
360
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     }
369
370     return (token.previous && token.previous.combinator !== ',') ?
371             combinators[token.previous.combinator](node, token) :
372             true;
373 };
374
375
376 var foundCache = [];
377 var parentCache = [];
378 var regexCache = {};
379
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 = [];
389 };
390
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 }();
401
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];
408 };
409
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     },
419
420     '>': function(node, token) {
421         return rTestNode(node.parentNode, null, token.previous);
422     },
423
424     '+': function(node, token) {
425         var sib = node.previousSibling;
426         while (sib && sib.nodeType !== 1) {
427             sib = sib.previousSibling;
428         }
429
430         if (sib && rTestNode(sib, null, token.previous)) {
431             return true; 
432         }
433         return false;
434     },
435
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         }
444
445         return false;
446     }
447 };
448
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;
461
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 }();
475
476 /*
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 
481 */
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 = [];
490
491     var siblings = getChildren(node.parentNode, tag);
492
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     }
501
502     if (a === 0) { // just the first
503         if (reverse) {
504             b = siblings.length - b + 1; 
505         }
506
507         if (siblings[b - 1] === node) {
508             return true;
509         } else {
510             return false;
511         }
512
513     } else if (a < 0) {
514         reverse = !!reverse;
515         a = Math.abs(a);
516     }
517
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;
532 };
533
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     }
540 };
541
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;
549 };
550
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*/
557 };
558
559 /**
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
569
570     selector = replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
571
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.
575
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] || [];
593
594                     // capture ID for fast path to element
595                     if (re === 'attributes' && match[1] === 'id') {
596                         token.id = match[3];
597                     }
598
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);
609
610                     token = { // prep next token
611                         previous: token
612                     };
613                 }
614             }
615         }
616     } while (found);
617
618     return tokens;
619 };
620
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;
633 };
634
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     }
647
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;
654 };
655
656 Selector = new Selector();
657 Selector.patterns = patterns;
658 Y.Selector = Selector;
659
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';
663 }
664
665 })();
666 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"});