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 = Y.Selector._query(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.documentElement.children.tags) { // document for capability test
405 return function(node, tag) {
406 return (tag) ? node.children.tags(tag) : node.children || [];
409 return function(node, tag) {
411 childNodes = node.childNodes;
413 for (var i = 0, len = childNodes.length; i < len; ++i) {
414 if (childNodes[i].tagName) {
415 if (!tag || childNodes[i].tagName === tag) {
416 children.push(childNodes[i]);
426 ' ': function(node, token) {
427 while ( (node = node.parentNode) ) {
428 if (Y.Selector._test(node, '', token.previous)) {
435 '>': function(node, token) {
436 return Y.Selector._test(node.parentNode, null, token.previous);
439 '+': function(node, token) {
440 var sib = node.previousSibling;
441 while (sib && sib.nodeType !== 1) {
442 sib = sib.previousSibling;
445 if (sib && Y.Selector._test(sib, null, token.previous)) {
451 '~': function(node, token) {
452 var sib = node.previousSibling;
454 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
457 sib = sib.previousSibling;
466 an+b = get every _a_th node starting at the _b_th
467 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
468 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
469 an+0 = get every _a_th element, "0" may be omitted
471 _getNth: function(node, expr, tag, reverse) {
472 Y.Selector._re.nth.test(expr);
473 var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
474 n = RegExp.$2, // "n"
475 oddeven = RegExp.$3, // "odd" or "even"
476 b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
480 var siblings = Y.Selector._getChildren(node.parentNode, tag);
483 a = 2; // always every other
486 b = (oddeven === 'odd') ? 1 : 0;
487 } else if ( isNaN(a) ) {
488 a = (n) ? 1 : 0; // start from the first or no repeat
491 if (a === 0) { // just the first
493 b = siblings.length - b + 1;
496 if (siblings[b - 1] === node) {
508 for (var i = b - 1, len = siblings.length; i < len; i += a) {
509 if ( i >= 0 && siblings[i] === node ) {
514 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
515 if ( i < len && siblings[i] === node ) {
523 _getId: function(attr) {
524 for (var i = 0, len = attr.length; i < len; ++i) {
525 if (attr[i][0] == 'id' && attr[i][1] === '=') {
531 _getIdTokenIndex: function(tokens) {
532 for (var i = 0, len = tokens.length; i < len; ++i) {
533 if (Y.Selector._getId(tokens[i].attributes)) {
541 tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
542 attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
543 pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
544 combinator: /^\s*([>+~]|\s)\s*/
548 Break selector into token units per simple selector.
549 Combinator is attached to left-hand selector.
551 _tokenize: function(selector) {
552 var token = {}, // one token per simple selector (left selector holds combinator)
553 tokens = [], // array of tokens
554 id, // unique id for the simple selector (if found)
555 found = false, // whether or not any matches were found this pass
556 patterns = Y.Selector._patterns,
557 match; // the regex match
559 selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
562 Search for selector patterns, store, and strip them from the selector string
563 until no patterns match (invalid selector) or we run out of chars.
565 Multiple attributes and pseudos are allowed, in any order.
567 'form:first-child[type=button]:not(button)[lang|=en]'
570 found = false; // reset after full pass
571 for (var re in patterns) {
572 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
573 if (re != 'tag' && re != 'combinator') { // only one allowed
574 token[re] = token[re] || [];
576 if ( (match = patterns[re].exec(selector)) ) { // note assignment
578 if (re != 'tag' && re != 'combinator') { // only one allowed
579 // capture ID for fast path to element
580 if (re === 'attributes' && match[1] === 'id') {
584 token[re].push(match.slice(1));
585 } else { // single selector (tag, combinator)
586 token[re] = match[1];
588 selector = selector.replace(match[0], ''); // strip current match from selector
589 if (re === 'combinator' || !selector.length) { // next token or done
590 token.attributes = Y.Selector._fixAttributes(token.attributes);
591 token.pseudos = token.pseudos || [];
592 token.tag = token.tag ? token.tag.toUpperCase() : '*';
595 token = { // prep next token
608 _fixAttributes: function(attr) {
609 var aliases = Y.Selector.attrAliases;
611 for (var i = 0, len = attr.length; i < len; ++i) {
612 if (aliases[attr[i][0]]) { // convert reserved words, etc
613 attr[i][0] = aliases[attr[i][0]];
615 if (!attr[i][1]) { // use exists operator
622 _replaceShorthand: function(selector) {
623 var shorthand = Y.Selector.shorthand;
625 //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
626 var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
628 selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
630 for (var re in shorthand) {
631 if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
632 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
637 for (var i = 0, len = attrs.length; i < len; ++i) {
638 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
645 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
646 Y.Selector.attrAliases['class'] = 'className';
647 Y.Selector.attrAliases['for'] = 'htmlFor';
651 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});