3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('selector-css2', function(Y) {
10 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
12 * @submodule selector-css2
17 * Provides helper methods for collecting and filtering DOM elements.
20 var PARENT_NODE = 'parentNode',
22 ATTRIBUTES = 'attributes',
23 COMBINATOR = 'combinator',
26 Selector = Y.Selector,
29 _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/,
32 // TODO: better detection, document specific
34 var isXML = (Y.config.doc.createElement('div').tagName !== 'DIV');
39 * Mapping of shorthand tokens to corresponding attribute selector
44 '\\#(-?[_a-z0-9]+[-\\w\\uE000]*)': '[id=$1]',
45 '\\.(-?[_a-z]+[-\\w\\uE000]*)': '[className~=$1]'
49 * List of operators and corresponding boolean functions.
50 * These functions are passed the attribute and the current node's value of the attribute.
55 '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
56 '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
57 '|=': '^{val}-?' // optional hyphen-delimited
61 'first-child': function(node) {
62 return Y.DOM._children(node[PARENT_NODE])[0] === node;
66 _bruteQuery: function(selector, root, firstOnly) {
69 tokens = Selector._tokenize(selector),
70 token = tokens[tokens.length - 1],
71 rootDoc = Y.DOM._getDoc(root),
80 className = token.className;
81 tagName = token.tagName || '*';
83 if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
84 // try ID first, unless no root.all && root not in document
85 // (root.all works off document, but not getElementById)
86 if (id && (root.all || (root.nodeType === 9 || Y.DOM.inDoc(root)))) {
87 nodes = Y.DOM.allById(id, root);
89 } else if (className) {
90 nodes = root.getElementsByClassName(className);
91 } else { // default to tagName
92 nodes = root.getElementsByTagName(tagName);
95 } else { // brute getElementsByTagName()
96 child = root.firstChild;
98 // only collect HTMLElements
99 // match tag to supplement missing getElementsByTagName
100 if (child.tagName && (tagName === '*' || child.tagName === tagName)) {
103 child = child.nextSibling || child.firstChild;
107 ret = Selector._filterNodes(nodes, tokens, firstOnly);
114 _filterNodes: function(nodes, tokens, firstOnly) {
122 getters = Y.Selector.getters,
132 for (i = 0; (tmpNode = node = nodes[i++]);) {
137 while (tmpNode && tmpNode.tagName) {
142 while ((test = tests[--j])) {
144 if (getters[test[0]]) {
145 value = getters[test[0]](tmpNode, test[0]);
147 value = tmpNode[test[0]];
148 if (test[0] === 'tagName' && !Selector._isXML) {
149 value = value.toUpperCase();
151 if (typeof value != 'string' && value !== undefined && value.toString) {
152 value = value.toString(); // coerce for comparison
153 } else if (value === undefined && tmpNode.getAttribute) {
154 // use getAttribute for non-standard attributes
155 value = tmpNode.getAttribute(test[0], 2); // 2 === force string for IE
159 if ((operator === '=' && value !== test[2]) || // fast path for equality
160 (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
161 operator.test && !operator.test(value)) || // regex test
162 (!operator.test && // protect against RegExp as function (webkit)
163 typeof operator === 'function' && !operator(tmpNode, test[0], test[2]))) { // function test
165 // skip non element nodes or non-matching tags
166 if ((tmpNode = tmpNode[path])) {
169 (token.tagName && token.tagName !== tmpNode.tagName))
171 tmpNode = tmpNode[path];
179 n--; // move to next token
180 // now that we've passed the test, move up the tree by combinator
181 if (!pass && (combinator = token.combinator)) {
182 path = combinator.axis;
183 tmpNode = tmpNode[path];
185 // skip non element nodes
186 while (tmpNode && !tmpNode.tagName) {
187 tmpNode = tmpNode[path];
190 if (combinator.direct) { // one pass only
194 } else { // success if we made it this far
203 node = tmpNode = null;
219 axis: 'previousSibling',
227 re: /^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,
228 fn: function(match, token) {
229 var operator = match[2] || '',
230 operators = Selector.operators,
231 escVal = (match[3]) ? match[3].replace(/\\/g, '') : '',
234 // add prefiltering for ID and CLASS
235 if ((match[1] === 'id' && operator === '=') ||
236 (match[1] === 'className' &&
237 Y.config.doc.documentElement.getElementsByClassName &&
238 (operator === '~=' || operator === '='))) {
239 token.prefilter = match[1];
244 // escape all but ID for prefilter, which may run through QSA (via Dom.allById)
245 token[match[1]] = (match[1] === 'id') ? match[3] : escVal;
250 if (operator in operators) {
251 test = operators[operator];
252 if (typeof test === 'string') {
253 match[3] = escVal.replace(Selector._reRegExpTokens, '\\$1');
254 test = new RegExp(test.replace('{val}', match[3]));
258 if (!token.last || token.prefilter !== match[1]) {
259 return match.slice(1);
265 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
266 fn: function(match, token) {
269 if (!Selector._isXML) {
270 tag = tag.toUpperCase();
275 if (tag !== '*' && (!token.last || token.prefilter)) {
276 return [TAG_NAME, '=', tag];
278 if (!token.prefilter) {
279 token.prefilter = 'tagName';
285 re: /^\s*([>+~]|\s)\s*/,
286 fn: function(match, token) {
291 re: /^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,
292 fn: function(match, token) {
293 var test = Selector[PSEUDOS][match[1]];
294 if (test) { // reorder match array and unescape special chars for tests
296 match[2] = match[2].replace(/\\/g, '');
298 return [match[2], test];
299 } else { // selector token not supported (possibly missing CSS3 module)
306 _getToken: function(token) {
318 Break selector into token units per simple selector.
319 Combinator is attached to the previous token.
321 _tokenize: function(selector) {
322 selector = selector || '';
323 selector = Selector._parseSelector(Y.Lang.trim(selector));
324 var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
325 query = selector, // original query for debug report
326 tokens = [], // array of tokens
327 found = false, // whether or not any matches were found this pass
328 match, // the regex match
333 Search for selector patterns, store, and strip them from the selector string
334 until no patterns match (invalid selector) or we run out of chars.
336 Multiple attributes and pseudos are allowed, in any order.
338 'form:first-child[type=button]:not(button)[lang|=en]'
342 found = false; // reset after full pass
343 for (i = 0; (parser = Selector._parsers[i++]);) {
344 if ( (match = parser.re.exec(selector)) ) { // note assignment
345 if (parser.name !== COMBINATOR ) {
346 token.selector = selector;
348 selector = selector.replace(match[0], ''); // strip current match from selector
349 if (!selector.length) {
353 if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
354 match[1] = Selector._attrFilters[match[1]];
357 test = parser.fn(match, token);
358 if (test === false) { // selector not supported
362 token.tests.push(test);
365 if (!selector.length || parser.name === COMBINATOR) {
367 token = Selector._getToken(token);
368 if (parser.name === COMBINATOR) {
369 token.combinator = Y.Selector.combinators[match[1]];
375 } while (found && selector.length);
377 if (!found || selector.length) { // not fully parsed
378 Y.log('query: ' + query + ' contains unsupported token in: ' + selector, 'warn', 'Selector');
384 _replaceMarkers: function(selector) {
385 selector = selector.replace(/\[/g, '\uE003');
386 selector = selector.replace(/\]/g, '\uE004');
388 selector = selector.replace(/\(/g, '\uE005');
389 selector = selector.replace(/\)/g, '\uE006');
393 _replaceShorthand: function(selector) {
394 var shorthand = Y.Selector.shorthand,
397 for (re in shorthand) {
398 if (shorthand.hasOwnProperty(re)) {
399 selector = selector.replace(new RegExp(re, 'gi'), shorthand[re]);
406 _parseSelector: function(selector) {
407 var replaced = Y.Selector._replaceSelector(selector),
408 selector = replaced.selector;
410 // replace shorthand (".foo, #bar") after pseudos and attrs
411 // to avoid replacing unescaped chars
412 selector = Y.Selector._replaceShorthand(selector);
414 selector = Y.Selector._restore('attr', selector, replaced.attrs);
415 selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
417 // replace braces and parens before restoring escaped chars
418 // to avoid replacing ecaped markers
419 selector = Y.Selector._replaceMarkers(selector);
420 selector = Y.Selector._restore('esc', selector, replaced.esc);
426 'class': 'className',
431 href: function(node, attr) {
432 return Y.DOM.getAttribute(node, attr);
435 id: function(node, attr) {
436 return Y.DOM.getId(node);
441 Y.mix(Y.Selector, SelectorCSS2, true);
442 Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
444 // IE wants class with native queries
445 if (Y.Selector.useNative && Y.config.doc.querySelector) {
446 Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
451 }, '3.5.0' ,{requires:['selector-native']});