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 YUI.add('selector-native', function(Y) {
12 * The selector-native module provides support for native querySelector
14 * @submodule selector-native
19 * Provides support for using CSS selectors to query the DOM
25 Y.namespace('Selector'); // allow native module to standalone
27 var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
28 OWNER_DOCUMENT = 'ownerDocument',
29 TMP_PREFIX = 'yui-tmp-',
37 _compare: ('sourceIndex' in document.documentElement) ?
38 function(nodeA, nodeB) {
39 var a = nodeA.sourceIndex,
40 b = nodeB.sourceIndex;
50 } : (document.documentElement[COMPARE_DOCUMENT_POSITION] ?
51 function(nodeA, nodeB) {
52 if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
58 function(nodeA, nodeB) {
59 var rangeA, rangeB, compare;
61 rangeA = nodeA[OWNER_DOCUMENT].createRange();
62 rangeA.setStart(nodeA, 0);
63 rangeB = nodeB[OWNER_DOCUMENT].createRange();
64 rangeB.setStart(nodeB, 0);
65 compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
72 _sort: function(nodes) {
74 nodes = Y.Array(nodes, 0, true);
76 nodes.sort(Selector._compare);
83 _deDupe: function(nodes) {
87 for (i = 0; (node = nodes[i++]);) {
89 ret[ret.length] = node;
94 for (i = 0; (node = ret[i++]);) {
96 node.removeAttribute('_found');
103 * Retrieves a set of nodes based on a given CSS selector.
106 * @param {string} selector The CSS Selector to test the node against.
107 * @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
108 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
109 * @return {Array} An array of nodes that match the given selector.
112 query: function(selector, root, firstOnly, skipNative) {
113 root = root || Y.config.doc;
115 useNative = (Y.Selector.useNative && document.querySelector && !skipNative),
116 queries = [[selector, root]],
120 fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
122 if (selector && fn) {
123 // split group into seperate queries
124 if (!skipNative && // already done if skipping
125 (!useNative || root.tagName)) { // split native when element scoping is needed
126 queries = Selector._splitQueries(selector, root);
129 for (i = 0; (query = queries[i++]);) {
130 result = fn(query[0], query[1], firstOnly);
131 if (!firstOnly) { // coerce DOM Collection to Array
132 result = Y.Array(result, 0, true);
135 ret = ret.concat(result);
139 if (queries.length > 1) { // remove dupes and sort by doc order
140 ret = Selector._sort(Selector._deDupe(ret));
144 return (firstOnly) ? (ret[0] || null) : ret;
148 // allows element scoped queries to begin with combinator
149 // e.g. query('> p', document.body) === query('body > p')
150 _splitQueries: function(selector, node) {
151 var groups = selector.split(','),
157 // enforce for element scoping
159 node.id = node.id || Y.guid();
160 prefix = '#' + node.id + ' ';
163 for (i = 0, len = groups.length; i < len; ++i) {
164 selector = prefix + groups[i];
165 queries.push([selector, node]);
172 _nativeQuery: function(selector, root, one) {
174 return root['querySelector' + (one ? '' : 'All')](selector);
175 } catch(e) { // fallback to brute if available
176 return Y.Selector.query(selector, root, one, true); // redo with skipNative true
180 filter: function(nodes, selector) {
184 if (nodes && selector) {
185 for (i = 0; (node = nodes[i++]);) {
186 if (Y.Selector.test(node, selector)) {
187 ret[ret.length] = node;
196 test: function(node, selector, root) {
198 groups = selector.split(','),
202 if (node && node.tagName) { // only test HTMLElements
203 root = root || node.ownerDocument;
206 node.id = TMP_PREFIX + g_counter++;
208 for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
209 group += '#' + node.id; // add ID for uniqueness
210 item = Y.Selector.query(group, root, true);
211 ret = (item === node);
222 Y.mix(Y.Selector, Selector, true);
227 }, '3.0.0' ,{requires:['dom-base']});
228 YUI.add('selector-css2', function(Y) {
231 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
233 * @submodule selector-css2
238 * Provides helper methods for collecting and filtering DOM elements.
241 var PARENT_NODE = 'parentNode',
242 TAG_NAME = 'tagName',
243 ATTRIBUTES = 'attributes',
244 COMBINATOR = 'combinator',
247 Selector = Y.Selector,
251 _children: function(node, tag) {
252 var ret = node.children,
258 if (node.children && tag && node.children.tags) {
259 children = node.children.tags(tag);
260 } else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
261 childNodes = ret || node.childNodes;
263 for (i = 0; (child = childNodes[i++]);) {
265 if (!tag || tag === child.tagName) {
279 pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\)))*/i
283 * Mapping of shorthand tokens to corresponding attribute selector
284 * @property shorthand
288 '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
289 '\\.(-?[_a-z]+[-\\w]*)': '[className~=$1]'
293 * List of operators and corresponding boolean functions.
294 * These functions are passed the attribute and the current node's value of the attribute.
295 * @property operators
299 '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
301 //'=': '^{val}$', // equality
302 '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
303 '|=': '^{val}-?' // optional hyphen-delimited
307 'first-child': function(node) {
308 return Y.Selector._children(node[PARENT_NODE])[0] === node;
312 _bruteQuery: function(selector, root, firstOnly) {
315 tokens = Selector._tokenize(selector),
316 token = tokens[tokens.length - 1],
317 rootDoc = Y.DOM._getDoc(root),
323 // if we have an initial ID, set to root when in document
324 if (tokens[0] && rootDoc === root &&
325 (id = tokens[0].id) &&
326 rootDoc.getElementById(id)) {
327 root = rootDoc.getElementById(id);
333 className = token.className;
334 tagName = token.tagName || '*';
338 if (rootDoc.getElementById(id)) { // if in document
339 nodes = [rootDoc.getElementById(id)]; // TODO: DOM.byId?
341 // try className if supported
342 } else if (className) {
343 nodes = root.getElementsByClassName(className);
344 } else if (tagName) { // default to tagName
345 nodes = root.getElementsByTagName(tagName || '*');
349 ret = Selector._filterNodes(nodes, tokens, firstOnly);
356 _filterNodes: function(nodes, tokens, firstOnly) {
364 getters = Y.Selector.getters,
370 //FUNCTION = 'function',
376 for (i = 0; (tmpNode = node = nodes[i++]);) {
381 while (tmpNode && tmpNode.tagName) {
386 while ((test = tests[--j])) {
388 if (getters[test[0]]) {
389 value = getters[test[0]](tmpNode, test[0]);
391 value = tmpNode[test[0]];
392 // use getAttribute for non-standard attributes
393 if (value === undefined && tmpNode.getAttribute) {
394 value = tmpNode.getAttribute(test[0]);
398 if ((operator === '=' && value !== test[2]) || // fast path for equality
399 (operator.test && !operator.test(value)) || // regex test
400 (operator.call && !operator(tmpNode, test[0]))) { // function test
402 // skip non element nodes or non-matching tags
403 if ((tmpNode = tmpNode[path])) {
406 (token.tagName && token.tagName !== tmpNode.tagName))
408 tmpNode = tmpNode[path];
416 n--; // move to next token
417 // now that we've passed the test, move up the tree by combinator
418 if (!pass && (combinator = token.combinator)) {
419 path = combinator.axis;
420 tmpNode = tmpNode[path];
422 // skip non element nodes
423 while (tmpNode && !tmpNode.tagName) {
424 tmpNode = tmpNode[path];
427 if (combinator.direct) { // one pass only
431 } else { // success if we made it this far
439 }// while (tmpNode = node = nodes[++i]);
440 node = tmpNode = null;
444 _getRegExp: function(str, flags) {
445 var regexCache = Selector._regexCache;
447 if (!regexCache[str + flags]) {
448 regexCache[str + flags] = new RegExp(str, flags);
450 return regexCache[str + flags];
465 axis: 'previousSibling',
473 re: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
474 fn: function(match, token) {
475 var operator = match[2] || '',
476 operators = Y.Selector.operators,
479 // add prefiltering for ID and CLASS
480 if ((match[1] === 'id' && operator === '=') ||
481 (match[1] === 'className' &&
482 document.getElementsByClassName &&
483 (operator === '~=' || operator === '='))) {
484 token.prefilter = match[1];
485 token[match[1]] = match[3];
489 if (operator in operators) {
490 test = operators[operator];
491 if (typeof test === 'string') {
492 test = Y.Selector._getRegExp(test.replace('{val}', match[3]));
496 if (!token.last || token.prefilter !== match[1]) {
497 return match.slice(1);
504 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
505 fn: function(match, token) {
506 var tag = match[1].toUpperCase();
509 if (tag !== '*' && (!token.last || token.prefilter)) {
510 return [TAG_NAME, '=', tag];
512 if (!token.prefilter) {
513 token.prefilter = 'tagName';
519 re: /^\s*([>+~]|\s)\s*/,
520 fn: function(match, token) {
525 re: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
526 fn: function(match, token) {
527 var test = Selector[PSEUDOS][match[1]];
528 if (test) { // reorder match array
529 return [match[2], test];
530 } else { // selector token not supported (possibly missing CSS3 module)
537 _getToken: function(token) {
549 Break selector into token units per simple selector.
550 Combinator is attached to the previous token.
552 _tokenize: function(selector) {
553 selector = selector || '';
554 selector = Selector._replaceShorthand(Y.Lang.trim(selector));
555 var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
556 query = selector, // original query for debug report
557 tokens = [], // array of tokens
558 found = false, // whether or not any matches were found this pass
559 match, // the regex match
564 Search for selector patterns, store, and strip them from the selector string
565 until no patterns match (invalid selector) or we run out of chars.
567 Multiple attributes and pseudos are allowed, in any order.
569 'form:first-child[type=button]:not(button)[lang|=en]'
573 found = false; // reset after full pass
574 for (i = 0; (parser = Selector._parsers[i++]);) {
575 if ( (match = parser.re.exec(selector)) ) { // note assignment
576 if (parser !== COMBINATOR ) {
577 token.selector = selector;
579 selector = selector.replace(match[0], ''); // strip current match from selector
580 if (!selector.length) {
584 if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
585 match[1] = Selector._attrFilters[match[1]];
588 test = parser.fn(match, token);
589 if (test === false) { // selector not supported
593 token.tests.push(test);
596 if (!selector.length || parser.name === COMBINATOR) {
598 token = Selector._getToken(token);
599 if (parser.name === COMBINATOR) {
600 token.combinator = Y.Selector.combinators[match[1]];
606 } while (found && selector.length);
608 if (!found || selector.length) { // not fully parsed
614 _replaceShorthand: function(selector) {
615 var shorthand = Selector.shorthand,
616 attrs = selector.match(Selector._re.attr), // pull attributes to avoid false pos on "." and "#"
617 pseudos = selector.match(Selector._re.pseudos), // pull attributes to avoid false pos on "." and "#"
621 selector = selector.replace(Selector._re.pseudos, '!!REPLACED_PSEUDO!!');
625 selector = selector.replace(Selector._re.attr, '!!REPLACED_ATTRIBUTE!!');
628 for (re in shorthand) {
629 if (shorthand.hasOwnProperty(re)) {
630 selector = selector.replace(Selector._getRegExp(re, 'gi'), shorthand[re]);
635 for (i = 0, len = attrs.length; i < len; ++i) {
636 selector = selector.replace('!!REPLACED_ATTRIBUTE!!', attrs[i]);
640 for (i = 0, len = pseudos.length; i < len; ++i) {
641 selector = selector.replace('!!REPLACED_PSEUDO!!', pseudos[i]);
648 'class': 'className',
653 href: function(node, attr) {
654 return Y.DOM.getAttribute(node, attr);
659 Y.mix(Y.Selector, SelectorCSS2, true);
660 Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
662 // IE wants class with native queries
663 if (Y.Selector.useNative && document.querySelector) {
664 Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
669 }, '3.0.0' ,{requires:['selector-native']});
672 YUI.add('selector', function(Y){}, '3.0.0' ,{use:['selector-native', 'selector-css2']});