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-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';
35 _compare: ('sourceIndex' in Y.config.doc.documentElement) ?
36 function(nodeA, nodeB) {
37 var a = nodeA.sourceIndex,
38 b = nodeB.sourceIndex;
48 } : (Y.config.doc.documentElement[COMPARE_DOCUMENT_POSITION] ?
49 function(nodeA, nodeB) {
50 if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
56 function(nodeA, nodeB) {
57 var rangeA, rangeB, compare;
59 rangeA = nodeA[OWNER_DOCUMENT].createRange();
60 rangeA.setStart(nodeA, 0);
61 rangeB = nodeB[OWNER_DOCUMENT].createRange();
62 rangeB.setStart(nodeB, 0);
63 compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
70 _sort: function(nodes) {
72 nodes = Y.Array(nodes, 0, true);
74 nodes.sort(Selector._compare);
81 _deDupe: function(nodes) {
85 for (i = 0; (node = nodes[i++]);) {
87 ret[ret.length] = node;
92 for (i = 0; (node = ret[i++]);) {
94 node.removeAttribute('_found');
101 * Retrieves a set of nodes based on a given CSS selector.
104 * @param {string} selector The CSS Selector to test the node against.
105 * @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
106 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
107 * @return {Array} An array of nodes that match the given selector.
110 query: function(selector, root, firstOnly, skipNative) {
111 root = root || Y.config.doc;
113 useNative = (Y.Selector.useNative && Y.config.doc.querySelector && !skipNative),
114 queries = [[selector, root]],
118 fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
120 if (selector && fn) {
121 // split group into seperate queries
122 if (!skipNative && // already done if skipping
123 (!useNative || root.tagName)) { // split native when element scoping is needed
124 queries = Selector._splitQueries(selector, root);
127 for (i = 0; (query = queries[i++]);) {
128 result = fn(query[0], query[1], firstOnly);
129 if (!firstOnly) { // coerce DOM Collection to Array
130 result = Y.Array(result, 0, true);
133 ret = ret.concat(result);
137 if (queries.length > 1) { // remove dupes and sort by doc order
138 ret = Selector._sort(Selector._deDupe(ret));
142 return (firstOnly) ? (ret[0] || null) : ret;
146 // allows element scoped queries to begin with combinator
147 // e.g. query('> p', document.body) === query('body > p')
148 _splitQueries: function(selector, node) {
149 var groups = selector.split(','),
155 // enforce for element scoping
157 node.id = node.id || Y.guid();
158 prefix = '[id="' + node.id + '"] ';
161 for (i = 0, len = groups.length; i < len; ++i) {
162 selector = prefix + groups[i];
163 queries.push([selector, node]);
170 _nativeQuery: function(selector, root, one) {
171 if (Y.UA.webkit && selector.indexOf(':checked') > -1 &&
172 (Y.Selector.pseudos && Y.Selector.pseudos.checked)) { // webkit (chrome, safari) fails to find "selected"
173 return Y.Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
176 return root['querySelector' + (one ? '' : 'All')](selector);
177 } catch(e) { // fallback to brute if available
178 return Y.Selector.query(selector, root, one, true); // redo with skipNative true
182 filter: function(nodes, selector) {
186 if (nodes && selector) {
187 for (i = 0; (node = nodes[i++]);) {
188 if (Y.Selector.test(node, selector)) {
189 ret[ret.length] = node;
198 test: function(node, selector, root) {
200 groups = selector.split(','),
208 if (node && node.tagName) { // only test HTMLElements
210 // we need a root if off-doc
211 if (!root && !Y.DOM.inDoc(node)) {
212 parent = node.parentNode;
215 } else { // only use frag when no parent to query
216 frag = node[OWNER_DOCUMENT].createDocumentFragment();
217 frag.appendChild(node);
222 root = root || node[OWNER_DOCUMENT];
227 for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
228 group += '[id="' + node.id + '"]';
229 items = Y.Selector.query(group, root);
231 for (j = 0; item = items[j++];) {
242 if (useFrag) { // cleanup
243 frag.removeChild(node);
251 * A convenience function to emulate Y.Node's aNode.ancestor(selector).
252 * @param {HTMLElement} element An HTMLElement to start the query from.
253 * @param {String} selector The CSS selector to test the node against.
254 * @return {HTMLElement} The ancestor node matching the selector, or null.
255 * @param {Boolean} testSelf optional Whether or not to include the element in the scan
259 ancestor: function (element, selector, testSelf) {
260 return Y.DOM.ancestor(element, function(n) {
261 return Y.Selector.test(n, selector);
266 Y.mix(Y.Selector, Selector, true);
271 }, '3.3.0' ,{requires:['dom-base']});
272 YUI.add('selector-css2', function(Y) {
275 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
277 * @submodule selector-css2
282 * Provides helper methods for collecting and filtering DOM elements.
285 var PARENT_NODE = 'parentNode',
286 TAG_NAME = 'tagName',
287 ATTRIBUTES = 'attributes',
288 COMBINATOR = 'combinator',
291 Selector = Y.Selector,
294 _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/, // TODO: move?
296 _children: function(node, tag) {
297 var ret = node.children,
303 if (node.children && tag && node.children.tags) {
304 children = node.children.tags(tag);
305 } else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
306 childNodes = ret || node.childNodes;
308 for (i = 0; (child = childNodes[i++]);) {
310 if (!tag || tag === child.tagName) {
322 attr: /(\[[^\]]*\])/g,
323 pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\)))*/i
327 * Mapping of shorthand tokens to corresponding attribute selector
328 * @property shorthand
332 '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
333 '\\.(-?[_a-z]+[-\\w]*)': '[className~=$1]'
337 * List of operators and corresponding boolean functions.
338 * These functions are passed the attribute and the current node's value of the attribute.
339 * @property operators
343 '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
345 //'=': '^{val}$', // equality
346 '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
347 '|=': '^{val}-?' // optional hyphen-delimited
351 'first-child': function(node) {
352 return Y.Selector._children(node[PARENT_NODE])[0] === node;
356 _bruteQuery: function(selector, root, firstOnly) {
359 tokens = Selector._tokenize(selector),
360 token = tokens[tokens.length - 1],
361 rootDoc = Y.DOM._getDoc(root),
368 // if we have an initial ID, set to root when in document
370 if (tokens[0] && rootDoc === root &&
371 (id = tokens[0].id) &&
372 rootDoc.getElementById(id)) {
373 root = rootDoc.getElementById(id);
380 className = token.className;
381 tagName = token.tagName || '*';
383 if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
384 // try ID first, unless no root.all && root not in document
385 // (root.all works off document, but not getElementById)
386 // TODO: move to allById?
387 if (id && (root.all || (root.nodeType === 9 || Y.DOM.inDoc(root)))) {
388 nodes = Y.DOM.allById(id, root);
390 } else if (className) {
391 nodes = root.getElementsByClassName(className);
392 } else { // default to tagName
393 nodes = root.getElementsByTagName(tagName);
396 } else { // brute getElementsByTagName('*')
397 child = root.firstChild;
399 if (child.tagName) { // only collect HTMLElements
402 child = child.nextSilbing || child.firstChild;
406 ret = Selector._filterNodes(nodes, tokens, firstOnly);
413 _filterNodes: function(nodes, tokens, firstOnly) {
421 getters = Y.Selector.getters,
427 //FUNCTION = 'function',
433 for (i = 0; (tmpNode = node = nodes[i++]);) {
438 while (tmpNode && tmpNode.tagName) {
443 while ((test = tests[--j])) {
445 if (getters[test[0]]) {
446 value = getters[test[0]](tmpNode, test[0]);
448 value = tmpNode[test[0]];
449 // use getAttribute for non-standard attributes
450 if (value === undefined && tmpNode.getAttribute) {
451 value = tmpNode.getAttribute(test[0]);
455 if ((operator === '=' && value !== test[2]) || // fast path for equality
456 (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
457 operator.test && !operator.test(value)) || // regex test
458 (!operator.test && // protect against RegExp as function (webkit)
459 typeof operator === 'function' && !operator(tmpNode, test[0]))) { // function test
461 // skip non element nodes or non-matching tags
462 if ((tmpNode = tmpNode[path])) {
465 (token.tagName && token.tagName !== tmpNode.tagName))
467 tmpNode = tmpNode[path];
475 n--; // move to next token
476 // now that we've passed the test, move up the tree by combinator
477 if (!pass && (combinator = token.combinator)) {
478 path = combinator.axis;
479 tmpNode = tmpNode[path];
481 // skip non element nodes
482 while (tmpNode && !tmpNode.tagName) {
483 tmpNode = tmpNode[path];
486 if (combinator.direct) { // one pass only
490 } else { // success if we made it this far
498 }// while (tmpNode = node = nodes[++i]);
499 node = tmpNode = null;
515 axis: 'previousSibling',
523 re: /^\[(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
524 fn: function(match, token) {
525 var operator = match[2] || '',
526 operators = Y.Selector.operators,
529 // add prefiltering for ID and CLASS
530 if ((match[1] === 'id' && operator === '=') ||
531 (match[1] === 'className' &&
532 Y.config.doc.documentElement.getElementsByClassName &&
533 (operator === '~=' || operator === '='))) {
534 token.prefilter = match[1];
535 token[match[1]] = match[3];
539 if (operator in operators) {
540 test = operators[operator];
541 if (typeof test === 'string') {
542 match[3] = match[3].replace(Y.Selector._reRegExpTokens, '\\$1');
543 test = Y.DOM._getRegExp(test.replace('{val}', match[3]));
547 if (!token.last || token.prefilter !== match[1]) {
548 return match.slice(1);
555 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
556 fn: function(match, token) {
557 var tag = match[1].toUpperCase();
560 if (tag !== '*' && (!token.last || token.prefilter)) {
561 return [TAG_NAME, '=', tag];
563 if (!token.prefilter) {
564 token.prefilter = 'tagName';
570 re: /^\s*([>+~]|\s)\s*/,
571 fn: function(match, token) {
576 re: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
577 fn: function(match, token) {
578 var test = Selector[PSEUDOS][match[1]];
579 if (test) { // reorder match array
580 return [match[2], test];
581 } else { // selector token not supported (possibly missing CSS3 module)
588 _getToken: function(token) {
600 Break selector into token units per simple selector.
601 Combinator is attached to the previous token.
603 _tokenize: function(selector) {
604 selector = selector || '';
605 selector = Selector._replaceShorthand(Y.Lang.trim(selector));
606 var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
607 query = selector, // original query for debug report
608 tokens = [], // array of tokens
609 found = false, // whether or not any matches were found this pass
610 match, // the regex match
615 Search for selector patterns, store, and strip them from the selector string
616 until no patterns match (invalid selector) or we run out of chars.
618 Multiple attributes and pseudos are allowed, in any order.
620 'form:first-child[type=button]:not(button)[lang|=en]'
624 found = false; // reset after full pass
625 for (i = 0; (parser = Selector._parsers[i++]);) {
626 if ( (match = parser.re.exec(selector)) ) { // note assignment
627 if (parser.name !== COMBINATOR ) {
628 token.selector = selector;
630 selector = selector.replace(match[0], ''); // strip current match from selector
631 if (!selector.length) {
635 if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
636 match[1] = Selector._attrFilters[match[1]];
639 test = parser.fn(match, token);
640 if (test === false) { // selector not supported
644 token.tests.push(test);
647 if (!selector.length || parser.name === COMBINATOR) {
649 token = Selector._getToken(token);
650 if (parser.name === COMBINATOR) {
651 token.combinator = Y.Selector.combinators[match[1]];
657 } while (found && selector.length);
659 if (!found || selector.length) { // not fully parsed
665 _replaceShorthand: function(selector) {
666 var shorthand = Selector.shorthand,
667 attrs = selector.match(Selector._re.attr), // pull attributes to avoid false pos on "." and "#"
668 pseudos = selector.match(Selector._re.pseudos), // pull attributes to avoid false pos on "." and "#"
672 selector = selector.replace(Selector._re.pseudos, '!!REPLACED_PSEUDO!!');
676 selector = selector.replace(Selector._re.attr, '!!REPLACED_ATTRIBUTE!!');
679 for (re in shorthand) {
680 if (shorthand.hasOwnProperty(re)) {
681 selector = selector.replace(Y.DOM._getRegExp(re, 'gi'), shorthand[re]);
686 for (i = 0, len = attrs.length; i < len; ++i) {
687 selector = selector.replace('!!REPLACED_ATTRIBUTE!!', attrs[i]);
691 for (i = 0, len = pseudos.length; i < len; ++i) {
692 selector = selector.replace('!!REPLACED_PSEUDO!!', pseudos[i]);
699 'class': 'className',
704 href: function(node, attr) {
705 return Y.DOM.getAttribute(node, attr);
710 Y.mix(Y.Selector, SelectorCSS2, true);
711 Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
713 // IE wants class with native queries
714 if (Y.Selector.useNative && Y.config.doc.querySelector) {
715 Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
720 }, '3.3.0' ,{requires:['selector-native']});
723 YUI.add('selector', function(Y){}, '3.3.0' ,{use:['selector-native', 'selector-css2']});