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 = Y.Selector._query(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.documentElement.children.tags) { // document for capability test
398 return function(node, tag) {
399 return (tag) ? node.children.tags(tag) : node.children || [];
402 return function(node, tag) {
404 childNodes = node.childNodes;
406 for (var i = 0, len = childNodes.length; i < len; ++i) {
407 if (childNodes[i].tagName) {
408 if (!tag || childNodes[i].tagName === tag) {
409 children.push(childNodes[i]);
419 ' ': function(node, token) {
420 while ( (node = node.parentNode) ) {
421 if (Y.Selector._test(node, '', token.previous)) {
428 '>': function(node, token) {
429 return Y.Selector._test(node.parentNode, null, token.previous);
432 '+': function(node, token) {
433 var sib = node.previousSibling;
434 while (sib && sib.nodeType !== 1) {
435 sib = sib.previousSibling;
438 if (sib && Y.Selector._test(sib, null, token.previous)) {
444 '~': function(node, token) {
445 var sib = node.previousSibling;
447 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
450 sib = sib.previousSibling;
459 an+b = get every _a_th node starting at the _b_th
460 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
461 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
462 an+0 = get every _a_th element, "0" may be omitted
464 _getNth: function(node, expr, tag, reverse) {
465 Y.Selector._re.nth.test(expr);
466 var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
467 n = RegExp.$2, // "n"
468 oddeven = RegExp.$3, // "odd" or "even"
469 b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
473 var siblings = Y.Selector._getChildren(node.parentNode, tag);
476 a = 2; // always every other
479 b = (oddeven === 'odd') ? 1 : 0;
480 } else if ( isNaN(a) ) {
481 a = (n) ? 1 : 0; // start from the first or no repeat
484 if (a === 0) { // just the first
486 b = siblings.length - b + 1;
489 if (siblings[b - 1] === node) {
501 for (var i = b - 1, len = siblings.length; i < len; i += a) {
502 if ( i >= 0 && siblings[i] === node ) {
507 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
508 if ( i < len && siblings[i] === node ) {
516 _getId: function(attr) {
517 for (var i = 0, len = attr.length; i < len; ++i) {
518 if (attr[i][0] == 'id' && attr[i][1] === '=') {
524 _getIdTokenIndex: function(tokens) {
525 for (var i = 0, len = tokens.length; i < len; ++i) {
526 if (Y.Selector._getId(tokens[i].attributes)) {
534 tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
535 attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
536 pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
537 combinator: /^\s*([>+~]|\s)\s*/
541 Break selector into token units per simple selector.
542 Combinator is attached to left-hand selector.
544 _tokenize: function(selector) {
545 var token = {}, // one token per simple selector (left selector holds combinator)
546 tokens = [], // array of tokens
547 id, // unique id for the simple selector (if found)
548 found = false, // whether or not any matches were found this pass
549 patterns = Y.Selector._patterns,
550 match; // the regex match
552 selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
555 Search for selector patterns, store, and strip them from the selector string
556 until no patterns match (invalid selector) or we run out of chars.
558 Multiple attributes and pseudos are allowed, in any order.
560 'form:first-child[type=button]:not(button)[lang|=en]'
563 found = false; // reset after full pass
564 for (var re in patterns) {
565 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
566 if (re != 'tag' && re != 'combinator') { // only one allowed
567 token[re] = token[re] || [];
569 if ( (match = patterns[re].exec(selector)) ) { // note assignment
571 if (re != 'tag' && re != 'combinator') { // only one allowed
572 // capture ID for fast path to element
573 if (re === 'attributes' && match[1] === 'id') {
577 token[re].push(match.slice(1));
578 } else { // single selector (tag, combinator)
579 token[re] = match[1];
581 selector = selector.replace(match[0], ''); // strip current match from selector
582 if (re === 'combinator' || !selector.length) { // next token or done
583 token.attributes = Y.Selector._fixAttributes(token.attributes);
584 token.pseudos = token.pseudos || [];
585 token.tag = token.tag ? token.tag.toUpperCase() : '*';
588 token = { // prep next token
601 _fixAttributes: function(attr) {
602 var aliases = Y.Selector.attrAliases;
604 for (var i = 0, len = attr.length; i < len; ++i) {
605 if (aliases[attr[i][0]]) { // convert reserved words, etc
606 attr[i][0] = aliases[attr[i][0]];
608 if (!attr[i][1]) { // use exists operator
615 _replaceShorthand: function(selector) {
616 var shorthand = Y.Selector.shorthand;
618 //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
619 var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
621 selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
623 for (var re in shorthand) {
624 if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
625 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
630 for (var i = 0, len = attrs.length; i < len; ++i) {
631 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
638 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
639 Y.Selector.attrAliases['class'] = 'className';
640 Y.Selector.attrAliases['for'] = 'htmlFor';
644 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});