2 Copyright (c) 2011, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 Y_DOM = YAHOO.util.Dom,
13 Y_DOCUMENT_ELEMENT = Y_DOC.documentElement,
15 Y_DOM_inDoc = Y_DOM.inDocument,
16 Y_mix = Y_Lang.augmentObject,
17 Y_guid = Y_DOM.generateId,
19 Y_getDoc = function(element) {
22 doc = (element.nodeType === 9) ? element : // element === document
23 element.ownerDocument || // element === DOM node
24 element.document || // element === window
31 Y_Array = function(o, startIdx) {
32 var l, a, start = startIdx || 0;
34 // IE errors when trying to slice HTMLElement collections
36 return Array.prototype.slice.call(o, start);
40 for (; start < l; start++) {
47 Y_DOM_allById = function(id, root) {
54 if (root.querySelectorAll) {
55 ret = root.querySelectorAll('[id="' + id + '"]');
56 } else if (root.all) {
60 // root.all may return HTMLElement or HTMLCollection.
61 // some elements are also HTMLCollection (FORM, SELECT).
63 if (nodes.id === id) { // avoid false positive on name
65 nodes = EMPTY_ARRAY; // done, no need to filter
66 } else { // prep for filtering
72 // filter out matches on node.name
73 // and element.id as reference to element with id === 'id'
74 for (i = 0; node = nodes[i++];) {
76 (node.attributes && node.attributes.id &&
77 node.attributes.id.value === id)) {
84 ret = [Y_getDoc(root).getElementById(id)];
91 * The selector-native module provides support for native querySelector
93 * @submodule selector-native
98 * Provides support for using CSS selectors to query the DOM
104 var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
105 OWNER_DOCUMENT = 'ownerDocument',
112 _compare: ('sourceIndex' in Y_DOCUMENT_ELEMENT) ?
113 function(nodeA, nodeB) {
114 var a = nodeA.sourceIndex,
115 b = nodeB.sourceIndex;
125 } : (Y_DOCUMENT_ELEMENT[COMPARE_DOCUMENT_POSITION] ?
126 function(nodeA, nodeB) {
127 if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
133 function(nodeA, nodeB) {
134 var rangeA, rangeB, compare;
135 if (nodeA && nodeB) {
136 rangeA = nodeA[OWNER_DOCUMENT].createRange();
137 rangeA.setStart(nodeA, 0);
138 rangeB = nodeB[OWNER_DOCUMENT].createRange();
139 rangeB.setStart(nodeB, 0);
140 compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
147 _sort: function(nodes) {
149 nodes = Y_Array(nodes, 0, true);
151 nodes.sort(Selector._compare);
158 _deDupe: function(nodes) {
162 for (i = 0; (node = nodes[i++]);) {
164 ret[ret.length] = node;
169 for (i = 0; (node = ret[i++]);) {
171 node.removeAttribute('_found');
178 * Retrieves a set of nodes based on a given CSS selector.
181 * @param {string} selector The CSS Selector to test the node against.
182 * @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
183 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
184 * @return {Array} An array of nodes that match the given selector.
187 query: function(selector, root, firstOnly, skipNative) {
188 if (root && typeof root == 'string') {
189 root = Y_DOM.get(root);
191 return (firstOnly) ? null : [];
194 root = root || Y_DOC;
198 useNative = (Selector.useNative && Y_DOC.querySelector && !skipNative),
199 queries = [[selector, root]],
203 fn = (useNative) ? Selector._nativeQuery : Selector._bruteQuery;
205 if (selector && fn) {
206 // split group into seperate queries
207 if (!skipNative && // already done if skipping
208 (!useNative || root.tagName)) { // split native when element scoping is needed
209 queries = Selector._splitQueries(selector, root);
212 for (i = 0; (query = queries[i++]);) {
213 result = fn(query[0], query[1], firstOnly);
214 if (!firstOnly) { // coerce DOM Collection to Array
215 result = Y_Array(result, 0, true);
218 ret = ret.concat(result);
222 if (queries.length > 1) { // remove dupes and sort by doc order
223 ret = Selector._sort(Selector._deDupe(ret));
227 return (firstOnly) ? (ret[0] || null) : ret;
231 // allows element scoped queries to begin with combinator
232 // e.g. query('> p', document.body) === query('body > p')
233 _splitQueries: function(selector, node) {
234 var groups = selector.split(','),
240 // enforce for element scoping
242 node.id = node.id || Y_guid();
243 prefix = '[id="' + node.id + '"] ';
246 for (i = 0, len = groups.length; i < len; ++i) {
247 selector = prefix + groups[i];
248 queries.push([selector, node]);
255 _nativeQuery: function(selector, root, one) {
256 if (Y_UA.webkit && selector.indexOf(':checked') > -1 &&
257 (Selector.pseudos && Selector.pseudos.checked)) { // webkit (chrome, safari) fails to find "selected"
258 return Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
261 return root['querySelector' + (one ? '' : 'All')](selector);
262 } catch(e) { // fallback to brute if available
263 return Selector.query(selector, root, one, true); // redo with skipNative true
267 filter: function(nodes, selector) {
271 if (nodes && selector) {
272 for (i = 0; (node = nodes[i++]);) {
273 if (Selector.test(node, selector)) {
274 ret[ret.length] = node;
283 test: function(node, selector, root) {
285 groups = selector.split(','),
293 if (node && node.tagName) { // only test HTMLElements
295 // we need a root if off-doc
296 if (!root && !Y_DOM_inDoc(node)) {
297 parent = node.parentNode;
300 } else { // only use frag when no parent to query
301 frag = node[OWNER_DOCUMENT].createDocumentFragment();
302 frag.appendChild(node);
307 root = root || node[OWNER_DOCUMENT];
312 for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
313 group += '[id="' + node.id + '"]';
314 items = Selector.query(group, root);
316 for (j = 0; item = items[j++];) {
327 if (useFrag) { // cleanup
328 frag.removeChild(node);
337 YAHOO.util.Selector = Selector;
339 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
341 * @submodule selector-css2
346 * Provides helper methods for collecting and filtering DOM elements.
349 var PARENT_NODE = 'parentNode',
350 TAG_NAME = 'tagName',
351 ATTRIBUTES = 'attributes',
352 COMBINATOR = 'combinator',
356 _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/, // TODO: move?
358 _children: function(node, tag) {
359 var ret = node.children,
365 if (node.children && tag && node.children.tags) {
366 children = node.children.tags(tag);
367 } else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
368 childNodes = ret || node.childNodes;
370 for (i = 0; (child = childNodes[i++]);) {
372 if (!tag || tag === child.tagName) {
384 attr: /(\[[^\]]*\])/g,
385 //esc: /\\[:\[][\w\d\]]*/gi,
386 esc: /\\[:\[\]\(\)#\.\'\>+~"]/gi,
387 //pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\))*)/i
388 pseudos: /(\([^\)]*\))/g
392 * Mapping of shorthand tokens to corresponding attribute selector
393 * @property shorthand
397 //'\\#([^\\s\\\\(\\[:]*)': '[id=$1]',
398 '\\#(-?[_a-z]+[-\\w\\uE000]*)': '[id=$1]',
399 //'\\#([^\\s\\\.:\\[\\]]*)': '[id=$1]',
400 //'\\.([^\\s\\\\(\\[:]*)': '[className=$1]'
401 '\\.(-?[_a-z]+[-\\w\\uE000]*)': '[className~=$1]'
405 * List of operators and corresponding boolean functions.
406 * These functions are passed the attribute and the current node's value of the attribute.
407 * @property operators
411 '': function(node, attr) { return !!node.getAttribute(attr); }, // Just test for existence of attribute
413 //'=': '^{val}$', // equality
414 '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
415 '|=': '^{val}(?:-|$)' // optional hyphen-delimited
419 'first-child': function(node) {
420 return Selector._children(node[PARENT_NODE])[0] === node;
424 _bruteQuery: function(selector, root, firstOnly) {
427 tokens = Selector._tokenize(selector),
428 token = tokens[tokens.length - 1],
429 rootDoc = Y_getDoc(root),
436 // if we have an initial ID, set to root when in document
438 if (tokens[0] && rootDoc === root &&
439 (id = tokens[0].id) &&
440 rootDoc.getElementById(id)) {
441 root = rootDoc.getElementById(id);
448 className = token.className;
449 tagName = token.tagName || '*';
451 if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
452 // try ID first, unless no root.all && root not in document
453 // (root.all works off document, but not getElementById)
454 // TODO: move to allById?
455 if (id && (root.all || (root.nodeType === 9 || Y_DOM_inDoc(root)))) {
456 nodes = Y_DOM_allById(id, root);
458 } else if (className) {
459 nodes = root.getElementsByClassName(className);
460 } else { // default to tagName
461 nodes = root.getElementsByTagName(tagName);
464 } else { // brute getElementsByTagName('*')
465 child = root.firstChild;
467 if (child.tagName) { // only collect HTMLElements
470 child = child.nextSilbing || child.firstChild;
474 ret = Selector._filterNodes(nodes, tokens, firstOnly);
481 _filterNodes: function(nodes, tokens, firstOnly) {
489 getters = Selector.getters,
495 //FUNCTION = 'function',
501 for (i = 0; (tmpNode = node = nodes[i++]);) {
506 while (tmpNode && tmpNode.tagName) {
511 while ((test = tests[--j])) {
513 if (getters[test[0]]) {
514 value = getters[test[0]](tmpNode, test[0]);
516 value = tmpNode[test[0]];
517 // use getAttribute for non-standard attributes
518 if (value === undefined && tmpNode.getAttribute) {
519 value = tmpNode.getAttribute(test[0]);
523 if ((operator === '=' && value !== test[2]) || // fast path for equality
524 (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
525 operator.test && !operator.test(value)) || // regex test
526 (!operator.test && // protect against RegExp as function (webkit)
527 typeof operator === 'function' && !operator(tmpNode, test[0], test[2]))) { // function test
529 // skip non element nodes or non-matching tags
530 if ((tmpNode = tmpNode[path])) {
533 (token.tagName && token.tagName !== tmpNode.tagName))
535 tmpNode = tmpNode[path];
543 n--; // move to next token
544 // now that we've passed the test, move up the tree by combinator
545 if (!pass && (combinator = token.combinator)) {
546 path = combinator.axis;
547 tmpNode = tmpNode[path];
549 // skip non element nodes
550 while (tmpNode && !tmpNode.tagName) {
551 tmpNode = tmpNode[path];
554 if (combinator.direct) { // one pass only
558 } else { // success if we made it this far
566 }// while (tmpNode = node = nodes[++i]);
567 node = tmpNode = null;
583 axis: 'previousSibling',
591 //re: /^\[(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
592 re: /^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,
593 fn: function(match, token) {
594 var operator = match[2] || '',
595 operators = Selector.operators,
596 escVal = (match[3]) ? match[3].replace(/\\/g, '') : '',
599 // add prefiltering for ID and CLASS
600 if ((match[1] === 'id' && operator === '=') ||
601 (match[1] === 'className' &&
602 Y_DOCUMENT_ELEMENT.getElementsByClassName &&
603 (operator === '~=' || operator === '='))) {
604 token.prefilter = match[1];
609 // escape all but ID for prefilter, which may run through QSA (via Dom.allById)
610 token[match[1]] = (match[1] === 'id') ? match[3] : escVal;
615 if (operator in operators) {
616 test = operators[operator];
617 if (typeof test === 'string') {
618 match[3] = escVal.replace(Selector._reRegExpTokens, '\\$1');
619 test = new RegExp(test.replace('{val}', match[3]));
623 if (!token.last || token.prefilter !== match[1]) {
624 return match.slice(1);
631 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
632 fn: function(match, token) {
633 var tag = match[1].toUpperCase();
636 if (tag !== '*' && (!token.last || token.prefilter)) {
637 return [TAG_NAME, '=', tag];
639 if (!token.prefilter) {
640 token.prefilter = 'tagName';
646 re: /^\s*([>+~]|\s)\s*/,
647 fn: function(match, token) {
652 re: /^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,
653 fn: function(match, token) {
654 var test = Selector[PSEUDOS][match[1]];
655 if (test) { // reorder match array and unescape special chars for tests
657 match[2] = match[2].replace(/\\/g, '');
659 return [match[2], test];
660 } else { // selector token not supported (possibly missing CSS3 module)
667 _getToken: function(token) {
679 Break selector into token units per simple selector.
680 Combinator is attached to the previous token.
682 _tokenize: function(selector) {
683 selector = selector || '';
684 selector = Selector._replaceShorthand(Y_Lang.trim(selector));
685 var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
686 query = selector, // original query for debug report
687 tokens = [], // array of tokens
688 found = false, // whether or not any matches were found this pass
689 match, // the regex match
694 Search for selector patterns, store, and strip them from the selector string
695 until no patterns match (invalid selector) or we run out of chars.
697 Multiple attributes and pseudos are allowed, in any order.
699 'form:first-child[type=button]:not(button)[lang|=en]'
704 found = false; // reset after full pass
706 for (i = 0; (parser = Selector._parsers[i++]);) {
707 if ( (match = parser.re.exec(selector)) ) { // note assignment
708 if (parser.name !== COMBINATOR ) {
709 token.selector = selector;
711 selector = selector.replace(match[0], ''); // strip current match from selector
712 if (!selector.length) {
716 if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
717 match[1] = Selector._attrFilters[match[1]];
720 test = parser.fn(match, token);
721 if (test === false) { // selector not supported
725 token.tests.push(test);
728 if (!selector.length || parser.name === COMBINATOR) {
730 token = Selector._getToken(token);
731 if (parser.name === COMBINATOR) {
732 token.combinator = Selector.combinators[match[1]];
740 } while (found && selector.length);
742 if (!found || selector.length) { // not fully parsed
748 _replaceShorthand: function(selector) {
749 var shorthand = Selector.shorthand,
750 esc = selector.match(Selector._re.esc), // pull escaped colon, brackets, etc.
756 selector = selector.replace(Selector._re.esc, '\uE000');
759 attrs = selector.match(Selector._re.attr);
760 pseudos = selector.match(Selector._re.pseudos);
763 selector = selector.replace(Selector._re.attr, '\uE001');
767 selector = selector.replace(Selector._re.pseudos, '\uE002');
771 for (re in shorthand) {
772 if (shorthand.hasOwnProperty(re)) {
773 selector = selector.replace(new RegExp(re, 'gi'), shorthand[re]);
778 for (i = 0, len = attrs.length; i < len; ++i) {
779 selector = selector.replace(/\uE001/, attrs[i]);
784 for (i = 0, len = pseudos.length; i < len; ++i) {
785 selector = selector.replace(/\uE002/, pseudos[i]);
789 selector = selector.replace(/\[/g, '\uE003');
790 selector = selector.replace(/\]/g, '\uE004');
792 selector = selector.replace(/\(/g, '\uE005');
793 selector = selector.replace(/\)/g, '\uE006');
796 for (i = 0, len = esc.length; i < len; ++i) {
797 selector = selector.replace('\uE000', esc[i]);
805 'class': 'className',
810 href: function(node, attr) {
811 return Y_DOM.getAttribute(node, attr);
816 Y_mix(Selector, SelectorCSS2, true);
817 Selector.getters.src = Selector.getters.rel = Selector.getters.href;
819 // IE wants class with native queries
820 if (Selector.useNative && Y_DOC.querySelector) {
821 Selector.shorthand['\\.([^\\s\\\\(\\[:]*)'] = '[class~=$1]';
825 * The selector css3 module provides support for css3 selectors.
827 * @submodule selector-css3
832 an+b = get every _a_th node starting at the _b_th
833 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
834 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
835 an+0 = get every _a_th element, "0" may be omitted
838 Selector._reNth = /^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/;
840 Selector._getNth = function(node, expr, tag, reverse) {
841 Selector._reNth.test(expr);
842 var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
843 n = RegExp.$2, // "n"
844 oddeven = RegExp.$3, // "odd" or "even"
845 b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
847 siblings = Selector._children(node.parentNode, tag),
851 a = 2; // always every other
854 b = (oddeven === 'odd') ? 1 : 0;
855 } else if ( isNaN(a) ) {
856 a = (n) ? 1 : 0; // start from the first or no repeat
859 if (a === 0) { // just the first
861 b = siblings.length - b + 1;
864 if (siblings[b - 1] === node) {
876 for (var i = b - 1, len = siblings.length; i < len; i += a) {
877 if ( i >= 0 && siblings[i] === node ) {
882 for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
883 if ( i < len && siblings[i] === node ) {
891 Y_mix(Selector.pseudos, {
892 'root': function(node) {
893 return node === node.ownerDocument.documentElement;
896 'nth-child': function(node, expr) {
897 return Selector._getNth(node, expr);
900 'nth-last-child': function(node, expr) {
901 return Selector._getNth(node, expr, null, true);
904 'nth-of-type': function(node, expr) {
905 return Selector._getNth(node, expr, node.tagName);
908 'nth-last-of-type': function(node, expr) {
909 return Selector._getNth(node, expr, node.tagName, true);
912 'last-child': function(node) {
913 var children = Selector._children(node.parentNode);
914 return children[children.length - 1] === node;
917 'first-of-type': function(node) {
918 return Selector._children(node.parentNode, node.tagName)[0] === node;
921 'last-of-type': function(node) {
922 var children = Selector._children(node.parentNode, node.tagName);
923 return children[children.length - 1] === node;
926 'only-child': function(node) {
927 var children = Selector._children(node.parentNode);
928 return children.length === 1 && children[0] === node;
931 'only-of-type': function(node) {
932 var children = Selector._children(node.parentNode, node.tagName);
933 return children.length === 1 && children[0] === node;
936 'empty': function(node) {
937 return node.childNodes.length === 0;
940 'not': function(node, expr) {
941 return !Selector.test(node, expr);
944 'contains': function(node, expr) {
945 var text = node.innerText || node.textContent || '';
946 return text.indexOf(expr) > -1;
949 'checked': function(node) {
950 return (node.checked === true || node.selected === true);
953 enabled: function(node) {
954 return (node.disabled !== undefined && !node.disabled);
957 disabled: function(node) {
958 return (node.disabled);
962 Y_mix(Selector.operators, {
963 '^=': '^{val}', // Match starts with value
964 '!=': function(node, attr, val) { return node[attr] !== val; }, // Match starts with value
965 '$=': '{val}$', // Match ends with value
966 '*=': '{val}' // Match contains value as substring
969 Selector.combinators['~'] = {
970 axis: 'previousSibling'
972 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.9.0", build: "2800"});