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 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
200 YAHOO.log('filter: skipping invalid node', 'warn', 'Selector');
205 result = rFilter(nodes, tokenize(selector)[0]);
207 YAHOO.log('filter: returning:' + result.length, 'info', 'Selector');
212 * Retrieves a set of nodes based on a given CSS selector.
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.
221 query: function(selector, root, firstOnly) {
222 var result = query(selector, root, firstOnly);
223 YAHOO.log('query: returning ' + result, 'info', 'Selector');
228 var query = function(selector, root, firstOnly, deDupe) {
229 var result = (firstOnly) ? null : [];
234 var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
236 if (groups.length > 1) {
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);
246 if (root && !root.nodeName) { // assume ID
247 root = Selector.document.getElementById(root);
249 YAHOO.log('invalid root node provided', 'warn', 'Selector');
254 root = root || Selector.document;
255 var tokens = tokenize(selector);
256 var idToken = tokens[getIdTokenIndex(tokens)],
260 token = tokens.pop() || {};
263 id = getId(idToken.attributes);
266 // use id shortcut when possible
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
275 root = node; // start from here
283 if (root && !nodes.length) {
284 nodes = root.getElementsByTagName(token.tag);
288 result = rFilter(nodes, token, firstOnly, deDupe);
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);
300 } else if ( document.documentElement.compareDocumentPosition ) { // gecko
301 return function(needle, haystack) {
302 return !!(haystack.compareDocumentPosition(needle) & 16);
304 } else { // Safari < 3
305 return function(needle, haystack) {
306 var parent = needle.parentNode;
308 if (needle === parent) {
311 parent = parent.parentNode;
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)) {
330 if (nodes[i]._found) {
333 nodes[i]._found = true;
334 foundCache[foundCache.length] = nodes[i];
337 result[result.length] = nodes[i];
343 var rTestNode = function(node, selector, token, deDupe) {
344 token = token || tokenize(selector).pop() || {};
347 (token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) ||
348 (deDupe && node._found) ) {
352 if (token.attributes.length) {
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) {
359 if ( Selector.operators[token.attributes[i][1]] &&
360 !Selector.operators[token.attributes[i][1]](attribute, token.attributes[i][2])) {
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])) {
375 return (token.previous && token.previous.combinator !== ',') ?
376 combinators[token.previous.combinator](node, token) :
382 var parentCache = [];
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;
391 foundCache[i].removeAttribute('_found');
395 YAHOO.log('getBySelector: done clearing foundCache');
398 var clearParentCache = function() {
399 if (!document.documentElement.children) { // caching children lookups for gecko
401 for (var i = 0, len = parentCache.length; i < len; ++i) {
402 delete parentCache[i]._children;
406 } else return function() {}; // do nothing
409 var getRegExp = function(str, flags) {
411 if (!regexCache[str + flags]) {
412 regexCache[str + flags] = new RegExp(str, flags);
414 return regexCache[str + flags];
418 ' ': function(node, token) {
419 while (node = node.parentNode) {
420 if (rTestNode(node, '', token.previous)) {
427 '>': function(node, token) {
428 return rTestNode(node.parentNode, null, token.previous);
431 '+': function(node, token) {
432 var sib = node.previousSibling;
433 while (sib && sib.nodeType !== 1) {
434 sib = sib.previousSibling;
437 if (sib && rTestNode(sib, null, token.previous)) {
443 '~': function(node, token) {
444 var sib = node.previousSibling;
446 if (sib.nodeType === 1 && rTestNode(sib, null, token.previous)) {
449 sib = sib.previousSibling;
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 || [];
462 return function(node, tag) {
463 if (node._children) {
464 return node._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];
476 node._children = children;
477 parentCache[parentCache.length] = node;
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();
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_
498 var siblings = getChildren(node.parentNode, tag);
501 a = 2; // always every other
504 b = (oddeven === 'odd') ? 1 : 0;
505 } else if ( isNaN(a) ) {
506 a = (n) ? 1 : 0; // start from the first or no repeat
509 if (a === 0) { // just the first
511 b = siblings.length - b + 1;
514 if (siblings[b - 1] === node) {
526 for (var i = b - 1, len = siblings.length; i < len; i += a) {
527 if ( i >= 0 && siblings[i] === node ) {
532 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
533 if ( i < len && siblings[i] === node ) {
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] === '=') {
549 var getIdTokenIndex = function(tokens) {
550 for (var i = 0, len = tokens.length; i < len; ++i) {
551 if (getId(tokens[i].attributes)) {
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.
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
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.
585 'form:first-child[type=button]:not(button)[lang|=en]'
588 found = false; // reset after full pass
589 for (var re in patterns) {
590 if (!YAHOO.lang.hasOwnProperty(patterns, re)) {
593 if (re != 'tag' && re != 'combinator') { // only one allowed
594 token[re] = token[re] || [];
596 if (match = patterns[re].exec(selector)) { // note assignment
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') {
606 token[re].push(match.slice(1));
607 } else { // single selector (tag, combinator)
608 token[re] = match[1];
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() : '*';
617 token = { // prep next token
628 var fixAttributes = function(attr) {
629 var aliases = Selector.attrAliases;
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]];
635 if (!attr[i][1]) { // use exists operator
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 "#"
646 selector = selector.replace(patterns.attributes, 'REPLACED_ATTRIBUTE');
648 for (var re in shorthand) {
649 if (!YAHOO.lang.hasOwnProperty(shorthand, re)) {
652 selector = selector.replace(getRegExp(re, 'gi'), shorthand[re]);
656 for (var i = 0, len = attrs.length; i < len; ++i) {
657 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
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';
673 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"});