MDL-26502 check browser - fix for Symbian (backported)
[moodle.git] / lib / yui / selector / selector-beta-debug.js
blob344b35d62981424d7933b4dc3ad88c70493e7006
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             YAHOO.log('filter: scanning input for HTMLElements/IDs', 'info', 'Selector');
194             for (var i = 0, len = nodes.length; i < len; ++i) {
195                 if (!nodes[i].tagName) { // tagName limits to HTMLElements 
196                     node = Selector.document.getElementById(nodes[i]);
197                     if (node) { // skip IDs that return null 
198                         nodes[i] = node;
199                     } else {
200                         YAHOO.log('filter: skipping invalid node', 'warn', 'Selector');
201                     }
202                 }
203             }
204         }
205         result = rFilter(nodes, tokenize(selector)[0]);
206         clearParentCache();
207         YAHOO.log('filter: returning:' + result.length, 'info', 'Selector');
208         return result;
209     },
211     /**
212      * Retrieves a set of nodes based on a given CSS selector. 
213      * @method query
214      *
215      * @param {string} selector The CSS Selector to test the node against.
216      * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
217      * @param {Boolean} firstOnly optional Whether or not to return only the first match.
218      * @return {Array} An array of nodes that match the given selector.
219      * @static
220      */
221     query: function(selector, root, firstOnly) {
222         var result = query(selector, root, firstOnly);
223         YAHOO.log('query: returning ' + result, 'info', 'Selector');
224         return result;
225     }
228 var query = function(selector, root, firstOnly, deDupe) {
229     var result =  (firstOnly) ? null : [];
230     if (!selector) {
231         return result;
232     }
234     var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
236     if (groups.length > 1) {
237         var found;
238         for (var i = 0, len = groups.length; i < len; ++i) {
239             found = arguments.callee(groups[i], root, firstOnly, true);
240             result = firstOnly ? found : result.concat(found); 
241         }
242         clearFoundCache();
243         return result;
244     }
246     if (root && !root.nodeName) { // assume ID
247         root = Selector.document.getElementById(root);
248         if (!root) {
249             YAHOO.log('invalid root node provided', 'warn', 'Selector');
250             return result;
251         }
252     }
254     root = root || Selector.document;
255     var tokens = tokenize(selector);
256     var idToken = tokens[getIdTokenIndex(tokens)],
257         nodes = [],
258         node,
259         id,
260         token = tokens.pop() || {};
261         
262     if (idToken) {
263         id = getId(idToken.attributes);
264     }
266     // use id shortcut when possible
267     if (id) {
268         node = Selector.document.getElementById(id);
270         if (node && (root.nodeName == '#document' || contains(node, root))) {
271             if ( rTestNode(node, null, idToken) ) {
272                 if (idToken === token) {
273                     nodes = [node]; // simple selector
274                 } else {
275                     root = node; // start from here
276                 }
277             }
278         } else {
279             return result;
280         }
281     }
283     if (root && !nodes.length) {
284         nodes = root.getElementsByTagName(token.tag);
285     }
287     if (nodes.length) {
288         result = rFilter(nodes, token, firstOnly, deDupe); 
289     }
291     clearParentCache();
292     return result;
295 var contains = function() {
296     if (document.documentElement.contains && !YAHOO.env.ua.webkit < 422)  { // IE & Opera, Safari < 3 contains is broken
297         return function(needle, haystack) {
298             return haystack.contains(needle);
299         };
300     } else if ( document.documentElement.compareDocumentPosition ) { // gecko
301         return function(needle, haystack) {
302             return !!(haystack.compareDocumentPosition(needle) & 16);
303         };
304     } else  { // Safari < 3
305         return function(needle, haystack) {
306             var parent = needle.parentNode;
307             while (parent) {
308                 if (needle === parent) {
309                     return true;
310                 }
311                 parent = parent.parentNode;
312             } 
313             return false;
314         }; 
315     }
316 }();
318 var rFilter = function(nodes, token, firstOnly, deDupe) {
319     var result = firstOnly ? null : [];
321     for (var i = 0, len = nodes.length; i < len; i++) {
322         if (! rTestNode(nodes[i], '', token, deDupe)) {
323             continue;
324         }
326         if (firstOnly) {
327             return nodes[i];
328         }
329         if (deDupe) {
330             if (nodes[i]._found) {
331                 continue;
332             }
333             nodes[i]._found = true;
334             foundCache[foundCache.length] = nodes[i];
335         }
337         result[result.length] = nodes[i];
338     }
340     return result;
343 var rTestNode = function(node, selector, token, deDupe) {
344     token = token || tokenize(selector).pop() || {};
346     if (!node.tagName ||
347         (token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) ||
348         (deDupe && node._found) ) {
349         return false;
350     }
352     if (token.attributes.length) {
353         var attribute;
354         for (var i = 0, len = token.attributes.length; i < len; ++i) {
355             attribute = node.getAttribute(token.attributes[i][0], 2);
356             if (attribute === null || attribute === undefined) {
357                 return false;
358             }
359             if ( Selector.operators[token.attributes[i][1]] &&
360                     !Selector.operators[token.attributes[i][1]](attribute, token.attributes[i][2])) {
361                 return false;
362             }
363         }
364     }
366     if (token.pseudos.length) {
367         for (var i = 0, len = token.pseudos.length; i < len; ++i) {
368             if (Selector.pseudos[token.pseudos[i][0]] &&
369                     !Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
370                 return false;
371             }
372         }
373     }
375     return (token.previous && token.previous.combinator !== ',') ?
376             combinators[token.previous.combinator](node, token) :
377             true;
381 var foundCache = [];
382 var parentCache = [];
383 var regexCache = {};
385 var clearFoundCache = function() {
386     YAHOO.log('getBySelector: clearing found cache of ' + foundCache.length + ' elements');
387     for (var i = 0, len = foundCache.length; i < len; ++i) {
388         try { // IE no like delete
389             delete foundCache[i]._found;
390         } catch(e) {
391             foundCache[i].removeAttribute('_found');
392         }
393     }
394     foundCache = [];
395     YAHOO.log('getBySelector: done clearing foundCache');
398 var clearParentCache = function() {
399     if (!document.documentElement.children) { // caching children lookups for gecko
400         return function() {
401             for (var i = 0, len = parentCache.length; i < len; ++i) {
402                 delete parentCache[i]._children;
403             }
404             parentCache = [];
405         };
406     } else return function() {}; // do nothing
407 }();
409 var getRegExp = function(str, flags) {
410     flags = flags || '';
411     if (!regexCache[str + flags]) {
412         regexCache[str + flags] = new RegExp(str, flags);
413     }
414     return regexCache[str + flags];
417 var combinators = {
418     ' ': function(node, token) {
419         while (node = node.parentNode) {
420             if (rTestNode(node, '', token.previous)) {
421                 return true;
422             }
423         }  
424         return false;
425     },
427     '>': function(node, token) {
428         return rTestNode(node.parentNode, null, token.previous);
429     },
431     '+': function(node, token) {
432         var sib = node.previousSibling;
433         while (sib && sib.nodeType !== 1) {
434             sib = sib.previousSibling;
435         }
437         if (sib && rTestNode(sib, null, token.previous)) {
438             return true; 
439         }
440         return false;
441     },
443     '~': function(node, token) {
444         var sib = node.previousSibling;
445         while (sib) {
446             if (sib.nodeType === 1 && rTestNode(sib, null, token.previous)) {
447                 return true;
448             }
449             sib = sib.previousSibling;
450         }
452         return false;
453     }
456 var getChildren = function() {
457     if (document.documentElement.children) { // document for capability test
458         return function(node, tag) {
459             return (tag) ? node.children.tags(tag) : node.children || [];
460         };
461     } else {
462         return function(node, tag) {
463             if (node._children) {
464                 return node._children;
465             }
466             var children = [],
467                 childNodes = node.childNodes;
469             for (var i = 0, len = childNodes.length; i < len; ++i) {
470                 if (childNodes[i].tagName) {
471                     if (!tag || childNodes[i].tagName.toLowerCase() === tag) {
472                         children[children.length] = childNodes[i];
473                     }
474                 }
475             }
476             node._children = children;
477             parentCache[parentCache.length] = node;
478             return children;
479         };
480     }
481 }();
484     an+b = get every _a_th node starting at the _b_th
485     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
486     1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
487     an+0 = get every _a_th element, "0" may be omitted 
489 var getNth = function(node, expr, tag, reverse) {
490     if (tag) tag = tag.toLowerCase();
491     reNth.test(expr);
492     var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
493         n = RegExp.$2, // "n"
494         oddeven = RegExp.$3, // "odd" or "even"
495         b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
496         result = [];
498     var siblings = getChildren(node.parentNode, tag);
500     if (oddeven) {
501         a = 2; // always every other
502         op = '+';
503         n = 'n';
504         b = (oddeven === 'odd') ? 1 : 0;
505     } else if ( isNaN(a) ) {
506         a = (n) ? 1 : 0; // start from the first or no repeat
507     }
509     if (a === 0) { // just the first
510         if (reverse) {
511             b = siblings.length - b + 1; 
512         }
514         if (siblings[b - 1] === node) {
515             return true;
516         } else {
517             return false;
518         }
520     } else if (a < 0) {
521         reverse = !!reverse;
522         a = Math.abs(a);
523     }
525     if (!reverse) {
526         for (var i = b - 1, len = siblings.length; i < len; i += a) {
527             if ( i >= 0 && siblings[i] === node ) {
528                 return true;
529             }
530         }
531     } else {
532         for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
533             if ( i < len && siblings[i] === node ) {
534                 return true;
535             }
536         }
537     }
538     return false;
541 var getId = function(attr) {
542     for (var i = 0, len = attr.length; i < len; ++i) {
543         if (attr[i][0] == 'id' && attr[i][1] === '=') {
544             return attr[i][2];
545         }
546     }
549 var getIdTokenIndex = function(tokens) {
550     for (var i = 0, len = tokens.length; i < len; ++i) {
551         if (getId(tokens[i].attributes)) {
552             return i;
553         }
554     }
555     return -1;
558 var patterns = {
559     tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
560     attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
561     //attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^'"\]]*)['"]?\]*/i,
562     pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
563     combinator: /^\s*([>+~]|\s)\s*/
567     Break selector into token units per simple selector.
568     Combinator is attached to left-hand selector.
569  */
570 var tokenize = function(selector) {
571     var token = {},     // one token per simple selector (left selector holds combinator)
572         tokens = [],    // array of tokens
573         id,             // unique id for the simple selector (if found)
574         found = false,  // whether or not any matches were found this pass
575         match;          // the regex match
577     selector = replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
579     /*
580         Search for selector patterns, store, and strip them from the selector string
581         until no patterns match (invalid selector) or we run out of chars.
583         Multiple attributes and pseudos are allowed, in any order.
584         for example:
585             'form:first-child[type=button]:not(button)[lang|=en]'
586     */
587     do {
588         found = false; // reset after full pass
589         for (var re in patterns) {
590                 if (!YAHOO.lang.hasOwnProperty(patterns, re)) {
591                     continue;
592                 }
593                 if (re != 'tag' && re != 'combinator') { // only one allowed
594                     token[re] = token[re] || [];
595                 }
596             if (match = patterns[re].exec(selector)) { // note assignment
597                 found = true;
598                 if (re != 'tag' && re != 'combinator') { // only one allowed
599                     //token[re] = token[re] || [];
601                     // capture ID for fast path to element
602                     if (re === 'attributes' && match[1] === 'id') {
603                         token.id = match[3];
604                     }
606                     token[re].push(match.slice(1));
607                 } else { // single selector (tag, combinator)
608                     token[re] = match[1];
609                 }
610                 selector = selector.replace(match[0], ''); // strip current match from selector
611                 if (re === 'combinator' || !selector.length) { // next token or done
612                     token.attributes = fixAttributes(token.attributes);
613                     token.pseudos = token.pseudos || [];
614                     token.tag = token.tag ? token.tag.toUpperCase() : '*';
615                     tokens.push(token);
617                     token = { // prep next token
618                         previous: token
619                     };
620                 }
621             }
622         }
623     } while (found);
625     return tokens;
628 var fixAttributes = function(attr) {
629     var aliases = Selector.attrAliases;
630     attr = attr || [];
631     for (var i = 0, len = attr.length; i < len; ++i) {
632         if (aliases[attr[i][0]]) { // convert reserved words, etc
633             attr[i][0] = aliases[attr[i][0]];
634         }
635         if (!attr[i][1]) { // use exists operator
636             attr[i][1] = '';
637         }
638     }
639     return attr;
642 var replaceShorthand = function(selector) {
643     var shorthand = Selector.shorthand;
644     var attrs = selector.match(patterns.attributes); // pull attributes to avoid false pos on "." and "#"
645     if (attrs) {
646         selector = selector.replace(patterns.attributes, 'REPLACED_ATTRIBUTE');
647     }
648     for (var re in shorthand) {
649         if (!YAHOO.lang.hasOwnProperty(shorthand, re)) {
650             continue;
651         }
652         selector = selector.replace(getRegExp(re, 'gi'), shorthand[re]);
653     }
655     if (attrs) {
656         for (var i = 0, len = attrs.length; i < len; ++i) {
657             selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
658         }
659     }
660     return selector;
663 Selector = new Selector();
664 Selector.patterns = patterns;
665 Y.Selector = Selector;
667 if (YAHOO.env.ua.ie) { // rewrite class for IE (others use getAttribute('class')
668     Y.Selector.attrAliases['class'] = 'className';
669     Y.Selector.attrAliases['for'] = 'htmlFor';
672 })();
673 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"});