2 Copyright (c) 2009, 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
19 * Provides helper methods for collecting and filtering DOM elements.
20 * @namespace YAHOO.util
30 nth: /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/,
36 * Default document for use queries
39 * @default window.document
41 document: window.document,
43 * Mapping of attributes to aliases, normally to work around HTMLAttributes
44 * that conflict with JS reserved words.
45 * @property attrAliases
52 * Mapping of shorthand tokens to corresponding attribute selector
57 //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
58 '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
59 '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
63 * List of operators and corresponding boolean functions.
64 * These functions are passed the attribute and the current node's value of the attribute.
69 '=': function(attr, val) { return attr === val; }, // Equality
70 '!=': function(attr, val) { return attr !== val; }, // Inequality
71 '~=': function(attr, val) { // Match one of space seperated words
73 return (s + attr + s).indexOf((s + val + s)) > -1;
75 '|=': function(attr, val) { return attr === val || attr.slice(0, val.length + 1) === val + '-'; }, // Matches value followed by optional hyphen
76 '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
77 '$=': function(attr, val) { return attr.slice(-val.length) === val; }, // Match ends with value
78 '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring
79 '': function(attr, val) { return attr; } // Just test for existence of attribute
83 * List of pseudo-classes and corresponding boolean functions.
84 * These functions are called with the current node, and any value that was parsed with the pseudo regex.
89 'root': function(node) {
90 return node === node.ownerDocument.documentElement;
93 'nth-child': function(node, val) {
94 return Y.Selector._getNth(node, val);
97 'nth-last-child': function(node, val) {
98 return Y.Selector._getNth(node, val, null, true);
101 'nth-of-type': function(node, val) {
102 return Y.Selector._getNth(node, val, node.tagName);
105 'nth-last-of-type': function(node, val) {
106 return Y.Selector._getNth(node, val, node.tagName, true);
109 'first-child': function(node) {
110 return Y.Selector._getChildren(node.parentNode)[0] === node;
113 'last-child': function(node) {
114 var children = Y.Selector._getChildren(node.parentNode);
115 return children[children.length - 1] === node;
118 'first-of-type': function(node, val) {
119 return Y.Selector._getChildren(node.parentNode, node.tagName)[0];
122 'last-of-type': function(node, val) {
123 var children = Y.Selector._getChildren(node.parentNode, node.tagName);
124 return children[children.length - 1];
127 'only-child': function(node) {
128 var children = Y.Selector._getChildren(node.parentNode);
129 return children.length === 1 && children[0] === node;
132 'only-of-type': function(node) {
133 return Y.Selector._getChildren(node.parentNode, node.tagName).length === 1;
136 'empty': function(node) {
137 return node.childNodes.length === 0;
140 'not': function(node, simple) {
141 return !Y.Selector.test(node, simple);
144 'contains': function(node, str) {
145 var text = node.innerText || node.textContent || '';
146 return text.indexOf(str) > -1;
148 'checked': function(node) {
149 return node.checked === true;
154 * Test if the supplied node matches the supplied selector.
157 * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
158 * @param {string} selector The CSS Selector to test the node against.
159 * @return{boolean} Whether or not the node matches the selector.
163 test: function(node, selector) {
164 node = Y.Selector.document.getElementById(node) || node;
170 var groups = selector ? selector.split(',') : [];
172 for (var i = 0, len = groups.length; i < len; ++i) {
173 if ( Y.Selector._test(node, groups[i]) ) { // passes if ANY group matches
179 return Y.Selector._test(node, selector);
182 _test: function(node, selector, token, deDupe) {
183 token = token || Y.Selector._tokenize(selector).pop() || {};
186 (token.tag !== '*' && node.tagName !== token.tag) ||
187 (deDupe && node._found) ) {
191 if (token.attributes.length) {
194 re_urls = Y.Selector._re.urls;
196 if (!node.attributes || !node.attributes.length) {
199 for (var i = 0, attr; attr = token.attributes[i++];) {
200 ieFlag = (re_urls.test(attr[0])) ? 2 : 0;
201 val = node.getAttribute(attr[0], ieFlag);
202 if (val === null || val === undefined) {
205 if ( Y.Selector.operators[attr[1]] &&
206 !Y.Selector.operators[attr[1]](val, attr[2])) {
212 if (token.pseudos.length) {
213 for (var i = 0, len = token.pseudos.length; i < len; ++i) {
214 if (Y.Selector.pseudos[token.pseudos[i][0]] &&
215 !Y.Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
221 return (token.previous && token.previous.combinator !== ',') ?
222 Y.Selector._combinators[token.previous.combinator](node, token) :
227 * Filters a set of nodes based on a given CSS selector.
230 * @param {array} nodes A set of nodes/ids to filter.
231 * @param {string} selector The selector used to test each node.
232 * @return{array} An array of nodes from the supplied array that match the given selector.
235 filter: function(nodes, selector) {
240 tokens = Y.Selector._tokenize(selector);
242 if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes
243 for (var i = 0, len = nodes.length; i < len; ++i) {
244 if (!nodes[i].tagName) { // tagName limits to HTMLElements
245 node = Y.Selector.document.getElementById(nodes[i]);
246 if (node) { // skip IDs that return null
253 result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
257 _filter: function(nodes, token, firstOnly, deDupe) {
258 var result = firstOnly ? null : [],
259 foundCache = Y.Selector._foundCache;
261 for (var i = 0, len = nodes.length; i < len; i++) {
262 if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
270 if (nodes[i]._found) {
273 nodes[i]._found = true;
274 foundCache[foundCache.length] = nodes[i];
277 result[result.length] = nodes[i];
284 * Retrieves a set of nodes based on a given CSS selector.
287 * @param {string} selector The CSS Selector to test the node against.
288 * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
289 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
290 * @return {Array} An array of nodes that match the given selector.
293 query: function(selector, root, firstOnly) {
294 var result = Y.Selector._query(selector, root, firstOnly);
299 _query: function(selector, root, firstOnly, deDupe) {
300 var result = (firstOnly) ? null : [],
307 var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
309 if (groups.length > 1) {
311 for (var i = 0, len = groups.length; i < len; ++i) {
312 found = arguments.callee(groups[i], root, firstOnly, true);
313 result = firstOnly ? found : result.concat(found);
315 Y.Selector._clearFoundCache();
319 if (root && !root.nodeName) { // assume ID
320 root = Y.Selector.document.getElementById(root);
326 root = root || Y.Selector.document;
328 if (root.nodeName !== '#document') { // prepend with root selector
329 Y.Dom.generateId(root); // TODO: cleanup after?
330 selector = root.tagName + '#' + root.id + ' ' + selector;
332 root = root.ownerDocument;
335 var tokens = Y.Selector._tokenize(selector);
336 var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
339 token = tokens.pop() || {};
342 id = Y.Selector._getId(idToken.attributes);
345 // use id shortcut when possible
347 node = node || Y.Selector.document.getElementById(id);
349 if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
350 if ( Y.Selector._test(node, null, idToken) ) {
351 if (idToken === token) {
352 nodes = [node]; // simple selector
353 } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
354 root = node; // start from here
362 if (root && !nodes.length) {
363 nodes = root.getElementsByTagName(token.tag);
367 result = Y.Selector._filter(nodes, token, firstOnly, deDupe);
374 _clearFoundCache: function() {
375 var foundCache = Y.Selector._foundCache;
376 for (var i = 0, len = foundCache.length; i < len; ++i) {
377 try { // IE no like delete
378 delete foundCache[i]._found;
380 foundCache[i].removeAttribute('_found');
387 _getRegExp: function(str, flags) {
388 var regexCache = Y.Selector._regexCache;
390 if (!regexCache[str + flags]) {
391 regexCache[str + flags] = new RegExp(str, flags);
393 return regexCache[str + flags];
396 _getChildren: function() {
397 if (document.documentElement.children) { // document for capability test
398 return function(node, tag) {
399 return (tag) ? node.children.tags(tag) : node.children || [];
402 return function(node, tag) {
403 if (node._children) {
404 return node._children;
407 childNodes = node.childNodes;
409 for (var i = 0, len = childNodes.length; i < len; ++i) {
410 if (childNodes[i].tagName) {
411 if (!tag || childNodes[i].tagName === tag) {
412 children[children.length] = childNodes[i];
416 node._children = children;
423 ' ': function(node, token) {
424 while ( (node = node.parentNode) ) {
425 if (Y.Selector._test(node, '', token.previous)) {
432 '>': function(node, token) {
433 return Y.Selector._test(node.parentNode, null, token.previous);
436 '+': function(node, token) {
437 var sib = node.previousSibling;
438 while (sib && sib.nodeType !== 1) {
439 sib = sib.previousSibling;
442 if (sib && Y.Selector._test(sib, null, token.previous)) {
448 '~': function(node, token) {
449 var sib = node.previousSibling;
451 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
454 sib = sib.previousSibling;
463 an+b = get every _a_th node starting at the _b_th
464 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
465 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
466 an+0 = get every _a_th element, "0" may be omitted
468 _getNth: function(node, expr, tag, reverse) {
469 Y.Selector._re.nth.test(expr);
470 var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
471 n = RegExp.$2, // "n"
472 oddeven = RegExp.$3, // "odd" or "even"
473 b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
477 var siblings = Y.Selector._getChildren(node.parentNode, tag);
480 a = 2; // always every other
483 b = (oddeven === 'odd') ? 1 : 0;
484 } else if ( isNaN(a) ) {
485 a = (n) ? 1 : 0; // start from the first or no repeat
488 if (a === 0) { // just the first
490 b = siblings.length - b + 1;
493 if (siblings[b - 1] === node) {
505 for (var i = b - 1, len = siblings.length; i < len; i += a) {
506 if ( i >= 0 && siblings[i] === node ) {
511 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
512 if ( i < len && siblings[i] === node ) {
520 _getId: function(attr) {
521 for (var i = 0, len = attr.length; i < len; ++i) {
522 if (attr[i][0] == 'id' && attr[i][1] === '=') {
528 _getIdTokenIndex: function(tokens) {
529 for (var i = 0, len = tokens.length; i < len; ++i) {
530 if (Y.Selector._getId(tokens[i].attributes)) {
538 tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
539 attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
540 pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
541 combinator: /^\s*([>+~]|\s)\s*/
545 Break selector into token units per simple selector.
546 Combinator is attached to left-hand selector.
548 _tokenize: function(selector) {
549 var token = {}, // one token per simple selector (left selector holds combinator)
550 tokens = [], // array of tokens
551 id, // unique id for the simple selector (if found)
552 found = false, // whether or not any matches were found this pass
553 patterns = Y.Selector._patterns,
554 match; // the regex match
556 selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
559 Search for selector patterns, store, and strip them from the selector string
560 until no patterns match (invalid selector) or we run out of chars.
562 Multiple attributes and pseudos are allowed, in any order.
564 'form:first-child[type=button]:not(button)[lang|=en]'
567 found = false; // reset after full pass
568 for (var re in patterns) {
569 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
570 if (re != 'tag' && re != 'combinator') { // only one allowed
571 token[re] = token[re] || [];
573 if ( (match = patterns[re].exec(selector)) ) { // note assignment
575 if (re != 'tag' && re != 'combinator') { // only one allowed
576 // capture ID for fast path to element
577 if (re === 'attributes' && match[1] === 'id') {
581 token[re].push(match.slice(1));
582 } else { // single selector (tag, combinator)
583 token[re] = match[1];
585 selector = selector.replace(match[0], ''); // strip current match from selector
586 if (re === 'combinator' || !selector.length) { // next token or done
587 token.attributes = Y.Selector._fixAttributes(token.attributes);
588 token.pseudos = token.pseudos || [];
589 token.tag = token.tag ? token.tag.toUpperCase() : '*';
592 token = { // prep next token
605 _fixAttributes: function(attr) {
606 var aliases = Y.Selector.attrAliases;
608 for (var i = 0, len = attr.length; i < len; ++i) {
609 if (aliases[attr[i][0]]) { // convert reserved words, etc
610 attr[i][0] = aliases[attr[i][0]];
612 if (!attr[i][1]) { // use exists operator
619 _replaceShorthand: function(selector) {
620 var shorthand = Y.Selector.shorthand;
622 //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
623 var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
625 selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
627 for (var re in shorthand) {
628 if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
629 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
634 for (var i = 0, len = attrs.length; i < len; ++i) {
635 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
642 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
643 Y.Selector.attrAliases['class'] = 'className';
644 Y.Selector.attrAliases['for'] = 'htmlFor';
648 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.7.0", build: "1799"});