2 Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
8 * The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements.
10 * @title Selector Utility
11 * @namespace YAHOO.util
12 * @requires yahoo, dom
17 * Provides helper methods for collecting and filtering DOM elements.
18 * @namespace YAHOO.util
22 var Selector = function() {};
26 var reNth = /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/;
28 Selector.prototype = {
30 * Default document for use queries
33 * @default window.document
35 document: window.document,
37 * Mapping of attributes to aliases, normally to work around HTMLAttributes
38 * that conflict with JS reserved words.
39 * @property attrAliases
46 * Mapping of shorthand tokens to corresponding attribute selector
51 //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
52 '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
53 '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
57 * List of operators and corresponding boolean functions.
58 * These functions are passed the attribute and the current node's value of the attribute.
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
67 return (s + attr + s).indexOf((s + val + s)) > -1;
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
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.
83 'root': function(node) {
84 return node === node.ownerDocument.documentElement;
87 'nth-child': function(node, val) {
88 return getNth(node, val);
91 'nth-last-child': function(node, val) {
92 return getNth(node, val, null, true);
95 'nth-of-type': function(node, val) {
96 return getNth(node, val, node.tagName);
99 'nth-last-of-type': function(node, val) {
100 return getNth(node, val, node.tagName, true);
103 'first-child': function(node) {
104 return getChildren(node.parentNode)[0] === node;
107 'last-child': function(node) {
108 var children = getChildren(node.parentNode);
109 return children[children.length - 1] === node;
112 'first-of-type': function(node, val) {
113 return getChildren(node.parentNode, node.tagName.toLowerCase())[0];
116 'last-of-type': function(node, val) {
117 var children = getChildren(node.parentNode, node.tagName.toLowerCase());
118 return children[children.length - 1];
121 'only-child': function(node) {
122 var children = getChildren(node.parentNode);
123 return children.length === 1 && children[0] === node;
126 'only-of-type': function(node) {
127 return getChildren(node.parentNode, node.tagName.toLowerCase()).length === 1;
130 'empty': function(node) {
131 return node.childNodes.length === 0;
134 'not': function(node, simple) {
135 return !Selector.test(node, simple);
138 'contains': function(node, str) {
139 var text = node.innerText || node.textContent || '';
140 return text.indexOf(str) > -1;
142 'checked': function(node) {
143 return node.checked === true;
148 * Test if the supplied node matches the supplied selector.
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.
157 test: function(node, selector) {
158 node = Selector.document.getElementById(node) || node;
164 var groups = selector ? selector.split(',') : [];
166 for (var i = 0, len = groups.length; i < len; ++i) {
167 if ( rTestNode(node, groups[i]) ) { // passes if ANY group matches
173 return rTestNode(node, selector);
177 * Filters a set of nodes based on a given CSS selector.
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.
185 filter: function(nodes, selector) {
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
203 result = rFilter(nodes, tokenize(selector)[0]);
209 * Retrieves a set of nodes based on a given CSS selector.
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.
218 query: function(selector, root, firstOnly) {
219 var result = query(selector, root, firstOnly);
224 var query = function(selector, root, firstOnly, deDupe) {
225 var result = (firstOnly) ? null : [];
230 var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
232 if (groups.length > 1) {
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);
242 if (root && !root.nodeName) { // assume ID
243 root = Selector.document.getElementById(root);
249 root = root || Selector.document;
250 var tokens = tokenize(selector);
251 var idToken = tokens[getIdTokenIndex(tokens)],
255 token = tokens.pop() || {};
258 id = getId(idToken.attributes);
261 // use id shortcut when possible
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
270 root = node; // start from here
278 if (root && !nodes.length) {
279 nodes = root.getElementsByTagName(token.tag);
283 result = rFilter(nodes, token, firstOnly, deDupe);
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);
295 } else if ( document.documentElement.compareDocumentPosition ) { // gecko
296 return function(needle, haystack) {
297 return !!(haystack.compareDocumentPosition(needle) & 16);
299 } else { // Safari < 3
300 return function(needle, haystack) {
301 var parent = needle.parentNode;
303 if (needle === parent) {
306 parent = parent.parentNode;
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)) {
325 if (nodes[i]._found) {
328 nodes[i]._found = true;
329 foundCache[foundCache.length] = nodes[i];
332 result[result.length] = nodes[i];
338 var rTestNode = function(node, selector, token, deDupe) {
339 token = token || tokenize(selector).pop() || {};
342 (token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) ||
343 (deDupe && node._found) ) {
347 if (token.attributes.length) {
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) {
354 if ( Selector.operators[token.attributes[i][1]] &&
355 !Selector.operators[token.attributes[i][1]](attribute, token.attributes[i][2])) {
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])) {
370 return (token.previous && token.previous.combinator !== ',') ?
371 combinators[token.previous.combinator](node, token) :
377 var parentCache = [];
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;
385 foundCache[i].removeAttribute('_found');
391 var clearParentCache = function() {
392 if (!document.documentElement.children) { // caching children lookups for gecko
394 for (var i = 0, len = parentCache.length; i < len; ++i) {
395 delete parentCache[i]._children;
399 } else return function() {}; // do nothing
402 var getRegExp = function(str, flags) {
404 if (!regexCache[str + flags]) {
405 regexCache[str + flags] = new RegExp(str, flags);
407 return regexCache[str + flags];
411 ' ': function(node, token) {
412 while (node = node.parentNode) {
413 if (rTestNode(node, '', token.previous)) {
420 '>': function(node, token) {
421 return rTestNode(node.parentNode, null, token.previous);
424 '+': function(node, token) {
425 var sib = node.previousSibling;
426 while (sib && sib.nodeType !== 1) {
427 sib = sib.previousSibling;
430 if (sib && rTestNode(sib, null, token.previous)) {
436 '~': function(node, token) {
437 var sib = node.previousSibling;
439 if (sib.nodeType === 1 && rTestNode(sib, null, token.previous)) {
442 sib = sib.previousSibling;
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 || [];
455 return function(node, tag) {
456 if (node._children) {
457 return node._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];
469 node._children = children;
470 parentCache[parentCache.length] = node;
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();
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_
491 var siblings = getChildren(node.parentNode, tag);
494 a = 2; // always every other
497 b = (oddeven === 'odd') ? 1 : 0;
498 } else if ( isNaN(a) ) {
499 a = (n) ? 1 : 0; // start from the first or no repeat
502 if (a === 0) { // just the first
504 b = siblings.length - b + 1;
507 if (siblings[b - 1] === node) {
519 for (var i = b - 1, len = siblings.length; i < len; i += a) {
520 if ( i >= 0 && siblings[i] === node ) {
525 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
526 if ( i < len && siblings[i] === node ) {
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] === '=') {
542 var getIdTokenIndex = function(tokens) {
543 for (var i = 0, len = tokens.length; i < len; ++i) {
544 if (getId(tokens[i].attributes)) {
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.
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
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.
578 'form:first-child[type=button]:not(button)[lang|=en]'
581 found = false; // reset after full pass
582 for (var re in patterns) {
583 if (!YAHOO.lang.hasOwnProperty(patterns, re)) {
586 if (re != 'tag' && re != 'combinator') { // only one allowed
587 token[re] = token[re] || [];
589 if (match = patterns[re].exec(selector)) { // note assignment
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') {
599 token[re].push(match.slice(1));
600 } else { // single selector (tag, combinator)
601 token[re] = match[1];
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() : '*';
610 token = { // prep next token
621 var fixAttributes = function(attr) {
622 var aliases = Selector.attrAliases;
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]];
628 if (!attr[i][1]) { // use exists operator
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 "#"
639 selector = selector.replace(patterns.attributes, 'REPLACED_ATTRIBUTE');
641 for (var re in shorthand) {
642 if (!YAHOO.lang.hasOwnProperty(shorthand, re)) {
645 selector = selector.replace(getRegExp(re, 'gi'), shorthand[re]);
649 for (var i = 0, len = attrs.length; i < len; ++i) {
650 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
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';
666 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"});