2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('selector-css2', function(Y) {
11 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
13 * @submodule selector-css2
18 * Provides helper methods for collecting and filtering DOM elements.
21 var PARENT_NODE = 'parentNode',
23 ATTRIBUTES = 'attributes',
24 COMBINATOR = 'combinator',
27 Selector = Y.Selector,
30 _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/, // TODO: move?
32 _children: function(node, tag) {
33 var ret = node.children,
39 if (node.children && tag && node.children.tags) {
40 children = node.children.tags(tag);
41 } else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
42 childNodes = ret || node.childNodes;
44 for (i = 0; (child = childNodes[i++]);) {
46 if (!tag || tag === child.tagName) {
58 attr: /(\[[^\]]*\])/g,
59 pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\)))*/i
63 * Mapping of shorthand tokens to corresponding attribute selector
68 '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
69 '\\.(-?[_a-z]+[-\\w]*)': '[className~=$1]'
73 * List of operators and corresponding boolean functions.
74 * These functions are passed the attribute and the current node's value of the attribute.
79 '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
81 //'=': '^{val}$', // equality
82 '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
83 '|=': '^{val}-?' // optional hyphen-delimited
87 'first-child': function(node) {
88 return Y.Selector._children(node[PARENT_NODE])[0] === node;
92 _bruteQuery: function(selector, root, firstOnly) {
95 tokens = Selector._tokenize(selector),
96 token = tokens[tokens.length - 1],
97 rootDoc = Y.DOM._getDoc(root),
104 // if we have an initial ID, set to root when in document
106 if (tokens[0] && rootDoc === root &&
107 (id = tokens[0].id) &&
108 rootDoc.getElementById(id)) {
109 root = rootDoc.getElementById(id);
116 className = token.className;
117 tagName = token.tagName || '*';
119 if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
120 // try ID first, unless no root.all && root not in document
121 // (root.all works off document, but not getElementById)
122 // TODO: move to allById?
123 if (id && (root.all || (root.nodeType === 9 || Y.DOM.inDoc(root)))) {
124 nodes = Y.DOM.allById(id, root);
126 } else if (className) {
127 nodes = root.getElementsByClassName(className);
128 } else { // default to tagName
129 nodes = root.getElementsByTagName(tagName);
132 } else { // brute getElementsByTagName('*')
133 child = root.firstChild;
135 if (child.tagName) { // only collect HTMLElements
138 child = child.nextSilbing || child.firstChild;
142 ret = Selector._filterNodes(nodes, tokens, firstOnly);
149 _filterNodes: function(nodes, tokens, firstOnly) {
157 getters = Y.Selector.getters,
163 //FUNCTION = 'function',
169 for (i = 0; (tmpNode = node = nodes[i++]);) {
174 while (tmpNode && tmpNode.tagName) {
179 while ((test = tests[--j])) {
181 if (getters[test[0]]) {
182 value = getters[test[0]](tmpNode, test[0]);
184 value = tmpNode[test[0]];
185 // use getAttribute for non-standard attributes
186 if (value === undefined && tmpNode.getAttribute) {
187 value = tmpNode.getAttribute(test[0]);
191 if ((operator === '=' && value !== test[2]) || // fast path for equality
192 (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
193 operator.test && !operator.test(value)) || // regex test
194 (!operator.test && // protect against RegExp as function (webkit)
195 typeof operator === 'function' && !operator(tmpNode, test[0]))) { // function test
197 // skip non element nodes or non-matching tags
198 if ((tmpNode = tmpNode[path])) {
201 (token.tagName && token.tagName !== tmpNode.tagName))
203 tmpNode = tmpNode[path];
211 n--; // move to next token
212 // now that we've passed the test, move up the tree by combinator
213 if (!pass && (combinator = token.combinator)) {
214 path = combinator.axis;
215 tmpNode = tmpNode[path];
217 // skip non element nodes
218 while (tmpNode && !tmpNode.tagName) {
219 tmpNode = tmpNode[path];
222 if (combinator.direct) { // one pass only
226 } else { // success if we made it this far
234 }// while (tmpNode = node = nodes[++i]);
235 node = tmpNode = null;
251 axis: 'previousSibling',
259 re: /^\[(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
260 fn: function(match, token) {
261 var operator = match[2] || '',
262 operators = Y.Selector.operators,
265 // add prefiltering for ID and CLASS
266 if ((match[1] === 'id' && operator === '=') ||
267 (match[1] === 'className' &&
268 Y.config.doc.documentElement.getElementsByClassName &&
269 (operator === '~=' || operator === '='))) {
270 token.prefilter = match[1];
271 token[match[1]] = match[3];
275 if (operator in operators) {
276 test = operators[operator];
277 if (typeof test === 'string') {
278 match[3] = match[3].replace(Y.Selector._reRegExpTokens, '\\$1');
279 test = Y.DOM._getRegExp(test.replace('{val}', match[3]));
283 if (!token.last || token.prefilter !== match[1]) {
284 return match.slice(1);
291 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
292 fn: function(match, token) {
293 var tag = match[1].toUpperCase();
296 if (tag !== '*' && (!token.last || token.prefilter)) {
297 return [TAG_NAME, '=', tag];
299 if (!token.prefilter) {
300 token.prefilter = 'tagName';
306 re: /^\s*([>+~]|\s)\s*/,
307 fn: function(match, token) {
312 re: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
313 fn: function(match, token) {
314 var test = Selector[PSEUDOS][match[1]];
315 if (test) { // reorder match array
316 return [match[2], test];
317 } else { // selector token not supported (possibly missing CSS3 module)
324 _getToken: function(token) {
336 Break selector into token units per simple selector.
337 Combinator is attached to the previous token.
339 _tokenize: function(selector) {
340 selector = selector || '';
341 selector = Selector._replaceShorthand(Y.Lang.trim(selector));
342 var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
343 query = selector, // original query for debug report
344 tokens = [], // array of tokens
345 found = false, // whether or not any matches were found this pass
346 match, // the regex match
351 Search for selector patterns, store, and strip them from the selector string
352 until no patterns match (invalid selector) or we run out of chars.
354 Multiple attributes and pseudos are allowed, in any order.
356 'form:first-child[type=button]:not(button)[lang|=en]'
360 found = false; // reset after full pass
361 for (i = 0; (parser = Selector._parsers[i++]);) {
362 if ( (match = parser.re.exec(selector)) ) { // note assignment
363 if (parser.name !== COMBINATOR ) {
364 token.selector = selector;
366 selector = selector.replace(match[0], ''); // strip current match from selector
367 if (!selector.length) {
371 if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
372 match[1] = Selector._attrFilters[match[1]];
375 test = parser.fn(match, token);
376 if (test === false) { // selector not supported
380 token.tests.push(test);
383 if (!selector.length || parser.name === COMBINATOR) {
385 token = Selector._getToken(token);
386 if (parser.name === COMBINATOR) {
387 token.combinator = Y.Selector.combinators[match[1]];
393 } while (found && selector.length);
395 if (!found || selector.length) { // not fully parsed
401 _replaceShorthand: function(selector) {
402 var shorthand = Selector.shorthand,
403 attrs = selector.match(Selector._re.attr), // pull attributes to avoid false pos on "." and "#"
404 pseudos = selector.match(Selector._re.pseudos), // pull attributes to avoid false pos on "." and "#"
408 selector = selector.replace(Selector._re.pseudos, '!!REPLACED_PSEUDO!!');
412 selector = selector.replace(Selector._re.attr, '!!REPLACED_ATTRIBUTE!!');
415 for (re in shorthand) {
416 if (shorthand.hasOwnProperty(re)) {
417 selector = selector.replace(Y.DOM._getRegExp(re, 'gi'), shorthand[re]);
422 for (i = 0, len = attrs.length; i < len; ++i) {
423 selector = selector.replace('!!REPLACED_ATTRIBUTE!!', attrs[i]);
427 for (i = 0, len = pseudos.length; i < len; ++i) {
428 selector = selector.replace('!!REPLACED_PSEUDO!!', pseudos[i]);
435 'class': 'className',
440 href: function(node, attr) {
441 return Y.DOM.getAttribute(node, attr);
446 Y.mix(Y.Selector, SelectorCSS2, true);
447 Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
449 // IE wants class with native queries
450 if (Y.Selector.useNative && Y.config.doc.querySelector) {
451 Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
456 }, '3.3.0' ,{requires:['selector-native']});