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 YAHOO.log('filter: scanning input for HTMLElements/IDs', 'info', 'Selector');
244 for (var i = 0, len = nodes.length; i < len; ++i) {
245 if (!nodes[i].tagName) { // tagName limits to HTMLElements
246 node = Y.Selector.document.getElementById(nodes[i]);
247 if (node) { // skip IDs that return null
250 YAHOO.log('filter: skipping invalid node', 'warn', 'Selector');
255 result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
256 YAHOO.log('filter: returning:' + result.length, 'info', 'Selector');
260 _filter: function(nodes, token, firstOnly, deDupe) {
261 var result = firstOnly ? null : [],
262 foundCache = Y.Selector._foundCache;
264 for (var i = 0, len = nodes.length; i < len; i++) {
265 if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
273 if (nodes[i]._found) {
276 nodes[i]._found = true;
277 foundCache[foundCache.length] = nodes[i];
280 result[result.length] = nodes[i];
287 * Retrieves a set of nodes based on a given CSS selector.
290 * @param {string} selector The CSS Selector to test the node against.
291 * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
292 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
293 * @return {Array} An array of nodes that match the given selector.
296 query: function(selector, root, firstOnly) {
297 var result = Y.Selector._query(selector, root, firstOnly);
298 YAHOO.log('query: returning ' + result, 'info', 'Selector');
303 _query: function(selector, root, firstOnly, deDupe) {
304 var result = (firstOnly) ? null : [],
311 var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
313 if (groups.length > 1) {
315 for (var i = 0, len = groups.length; i < len; ++i) {
316 found = arguments.callee(groups[i], root, firstOnly, true);
317 result = firstOnly ? found : result.concat(found);
319 Y.Selector._clearFoundCache();
323 if (root && !root.nodeName) { // assume ID
324 root = Y.Selector.document.getElementById(root);
326 YAHOO.log('invalid root node provided', 'warn', 'Selector');
331 root = root || Y.Selector.document;
333 if (root.nodeName !== '#document') { // prepend with root selector
334 Y.Dom.generateId(root); // TODO: cleanup after?
335 selector = root.tagName + '#' + root.id + ' ' + selector;
337 root = root.ownerDocument;
340 var tokens = Y.Selector._tokenize(selector);
341 var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
344 token = tokens.pop() || {};
347 id = Y.Selector._getId(idToken.attributes);
350 // use id shortcut when possible
352 node = node || Y.Selector.document.getElementById(id);
354 if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
355 if ( Y.Selector._test(node, null, idToken) ) {
356 if (idToken === token) {
357 nodes = [node]; // simple selector
358 } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
359 root = node; // start from here
367 if (root && !nodes.length) {
368 nodes = root.getElementsByTagName(token.tag);
372 result = Y.Selector._filter(nodes, token, firstOnly, deDupe);
379 _clearFoundCache: function() {
380 var foundCache = Y.Selector._foundCache;
381 YAHOO.log('getBySelector: clearing found cache of ' + foundCache.length + ' elements');
382 for (var i = 0, len = foundCache.length; i < len; ++i) {
383 try { // IE no like delete
384 delete foundCache[i]._found;
386 foundCache[i].removeAttribute('_found');
390 YAHOO.log('getBySelector: done clearing foundCache');
394 _getRegExp: function(str, flags) {
395 var regexCache = Y.Selector._regexCache;
397 if (!regexCache[str + flags]) {
398 regexCache[str + flags] = new RegExp(str, flags);
400 return regexCache[str + flags];
403 _getChildren: function() {
404 if (document.documentElement.children) { // document for capability test
405 return function(node, tag) {
406 return (tag) ? node.children.tags(tag) : node.children || [];
409 return function(node, tag) {
410 if (node._children) {
411 return node._children;
414 childNodes = node.childNodes;
416 for (var i = 0, len = childNodes.length; i < len; ++i) {
417 if (childNodes[i].tagName) {
418 if (!tag || childNodes[i].tagName === tag) {
419 children[children.length] = childNodes[i];
423 node._children = children;
430 ' ': function(node, token) {
431 while ( (node = node.parentNode) ) {
432 if (Y.Selector._test(node, '', token.previous)) {
439 '>': function(node, token) {
440 return Y.Selector._test(node.parentNode, null, token.previous);
443 '+': function(node, token) {
444 var sib = node.previousSibling;
445 while (sib && sib.nodeType !== 1) {
446 sib = sib.previousSibling;
449 if (sib && Y.Selector._test(sib, null, token.previous)) {
455 '~': function(node, token) {
456 var sib = node.previousSibling;
458 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
461 sib = sib.previousSibling;
470 an+b = get every _a_th node starting at the _b_th
471 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
472 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
473 an+0 = get every _a_th element, "0" may be omitted
475 _getNth: function(node, expr, tag, reverse) {
476 Y.Selector._re.nth.test(expr);
477 var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
478 n = RegExp.$2, // "n"
479 oddeven = RegExp.$3, // "odd" or "even"
480 b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
484 var siblings = Y.Selector._getChildren(node.parentNode, tag);
487 a = 2; // always every other
490 b = (oddeven === 'odd') ? 1 : 0;
491 } else if ( isNaN(a) ) {
492 a = (n) ? 1 : 0; // start from the first or no repeat
495 if (a === 0) { // just the first
497 b = siblings.length - b + 1;
500 if (siblings[b - 1] === node) {
512 for (var i = b - 1, len = siblings.length; i < len; i += a) {
513 if ( i >= 0 && siblings[i] === node ) {
518 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
519 if ( i < len && siblings[i] === node ) {
527 _getId: function(attr) {
528 for (var i = 0, len = attr.length; i < len; ++i) {
529 if (attr[i][0] == 'id' && attr[i][1] === '=') {
535 _getIdTokenIndex: function(tokens) {
536 for (var i = 0, len = tokens.length; i < len; ++i) {
537 if (Y.Selector._getId(tokens[i].attributes)) {
545 tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
546 attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
547 pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
548 combinator: /^\s*([>+~]|\s)\s*/
552 Break selector into token units per simple selector.
553 Combinator is attached to left-hand selector.
555 _tokenize: function(selector) {
556 var token = {}, // one token per simple selector (left selector holds combinator)
557 tokens = [], // array of tokens
558 id, // unique id for the simple selector (if found)
559 found = false, // whether or not any matches were found this pass
560 patterns = Y.Selector._patterns,
561 match; // the regex match
563 selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
566 Search for selector patterns, store, and strip them from the selector string
567 until no patterns match (invalid selector) or we run out of chars.
569 Multiple attributes and pseudos are allowed, in any order.
571 'form:first-child[type=button]:not(button)[lang|=en]'
574 found = false; // reset after full pass
575 for (var re in patterns) {
576 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
577 if (re != 'tag' && re != 'combinator') { // only one allowed
578 token[re] = token[re] || [];
580 if ( (match = patterns[re].exec(selector)) ) { // note assignment
582 if (re != 'tag' && re != 'combinator') { // only one allowed
583 // capture ID for fast path to element
584 if (re === 'attributes' && match[1] === 'id') {
588 token[re].push(match.slice(1));
589 } else { // single selector (tag, combinator)
590 token[re] = match[1];
592 selector = selector.replace(match[0], ''); // strip current match from selector
593 if (re === 'combinator' || !selector.length) { // next token or done
594 token.attributes = Y.Selector._fixAttributes(token.attributes);
595 token.pseudos = token.pseudos || [];
596 token.tag = token.tag ? token.tag.toUpperCase() : '*';
599 token = { // prep next token
612 _fixAttributes: function(attr) {
613 var aliases = Y.Selector.attrAliases;
615 for (var i = 0, len = attr.length; i < len; ++i) {
616 if (aliases[attr[i][0]]) { // convert reserved words, etc
617 attr[i][0] = aliases[attr[i][0]];
619 if (!attr[i][1]) { // use exists operator
626 _replaceShorthand: function(selector) {
627 var shorthand = Y.Selector.shorthand;
629 //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
630 var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
632 selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
634 for (var re in shorthand) {
635 if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
636 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
641 for (var i = 0, len = attrs.length; i < len; ++i) {
642 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
649 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
650 Y.Selector.attrAliases['class'] = 'className';
651 Y.Selector.attrAliases['for'] = 'htmlFor';
655 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.7.0", build: "1799"});