- /**
- * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
- * @module dom
- * @submodule selector-css2
- * @for Selector
- */
-
- /*
- * Provides helper methods for collecting and filtering DOM elements.
- */
-
- var PARENT_NODE = 'parentNode',
- TAG_NAME = 'tagName',
- ATTRIBUTES = 'attributes',
- COMBINATOR = 'combinator',
- PSEUDOS = 'pseudos',
-
- Selector = Y.Selector,
-
- SelectorCSS2 = {
- _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/,
- SORT_RESULTS: true,
-
- // TODO: better detection, document specific
- _isXML: (function() {
- var isXML = (Y.config.doc.createElement('div').tagName !== 'DIV');
- return isXML;
- }()),
-
- /**
- * Mapping of shorthand tokens to corresponding attribute selector
- * @property shorthand
- * @type object
- */
- shorthand: {
- '\\#(-?[_a-z0-9]+[-\\w\\uE000]*)': '[id=$1]',
- '\\.(-?[_a-z]+[-\\w\\uE000]*)': '[className~=$1]'
- },
-
- /**
- * List of operators and corresponding boolean functions.
- * These functions are passed the attribute and the current node's value of the attribute.
- * @property operators
- * @type object
- */
- operators: {
- '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
- '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
- '|=': '^{val}-?' // optional hyphen-delimited
- },
-
- pseudos: {
- 'first-child': function(node) {
- return Y.DOM._children(node[PARENT_NODE])[0] === node;
- }
- },
-
- _bruteQuery: function(selector, root, firstOnly) {
- var ret = [],
- nodes = [],
- visited,
- tokens = Selector._tokenize(selector),
- token = tokens[tokens.length - 1],
- rootDoc = Y.DOM._getDoc(root),
- child,
- id,
- className,
- tagName,
- isUniversal;
-
- if (token) {
- // prefilter nodes
- id = token.id;
- className = token.className;
- tagName = token.tagName || '*';
-
- if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
- // try ID first, unless no root.all && root not in document
- // (root.all works off document, but not getElementById)
- if (id && (root.all || (root.nodeType === 9 || Y.DOM.inDoc(root)))) {
- nodes = Y.DOM.allById(id, root);
- // try className
- } else if (className) {
- nodes = root.getElementsByClassName(className);
- } else { // default to tagName
- nodes = root.getElementsByTagName(tagName);
- }
-
- } else { // brute getElementsByTagName()
- visited = [];
- child = root.firstChild;
- isUniversal = tagName === "*";
- while (child) {
- while (child) {
- // IE 6-7 considers comment nodes as element nodes, and gives them the tagName "!".
- // We can filter them out by checking if its tagName is > "@".
- // This also avoids a superflous nodeType === 1 check.
- if (child.tagName > "@" && (isUniversal || child.tagName === tagName)) {
- nodes.push(child);
- }
-
- // We may need to traverse back up the tree to find more unvisited subtrees.
- visited.push(child);
- child = child.firstChild;
- }
-
- // Find the most recently visited node who has a next sibling.
- while (visited.length > 0 && !child) {
- child = visited.pop().nextSibling;
- }
- }
- }
-
- if (nodes.length) {
- ret = Selector._filterNodes(nodes, tokens, firstOnly);
- }
- }
-
- return ret;
- },
-
- _filterNodes: function(nodes, tokens, firstOnly) {
- var i = 0,
- j,
- len = tokens.length,
- n = len - 1,
- result = [],
- node = nodes[0],
- tmpNode = node,
- getters = Y.Selector.getters,
- operator,
- combinator,
- token,
- path,
- pass,
- value,
- tests,
- test;
-
- for (i = 0; (tmpNode = node = nodes[i++]);) {
- n = len - 1;
- path = null;
-
- testLoop:
- while (tmpNode && tmpNode.tagName) {
- token = tokens[n];
- tests = token.tests;
- j = tests.length;
- if (j && !pass) {
- while ((test = tests[--j])) {
- operator = test[1];
- if (getters[test[0]]) {
- value = getters[test[0]](tmpNode, test[0]);
- } else {
- value = tmpNode[test[0]];
- if (test[0] === 'tagName' && !Selector._isXML) {
- value = value.toUpperCase();
- }
- if (typeof value != 'string' && value !== undefined && value.toString) {
- value = value.toString(); // coerce for comparison
- } else if (value === undefined && tmpNode.getAttribute) {
- // use getAttribute for non-standard attributes
- value = tmpNode.getAttribute(test[0], 2); // 2 === force string for IE
- }
- }
-
- if ((operator === '=' && value !== test[2]) || // fast path for equality
- (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
- operator.test && !operator.test(value)) || // regex test
- (!operator.test && // protect against RegExp as function (webkit)
- typeof operator === 'function' && !operator(tmpNode, test[0], test[2]))) { // function test
-
- // skip non element nodes or non-matching tags
- if ((tmpNode = tmpNode[path])) {
- while (tmpNode &&
- (!tmpNode.tagName ||
- (token.tagName && token.tagName !== tmpNode.tagName))
- ) {
- tmpNode = tmpNode[path];
- }
- }
- continue testLoop;
- }
- }
- }
-
- n--; // move to next token
- // now that we've passed the test, move up the tree by combinator
- if (!pass && (combinator = token.combinator)) {
- path = combinator.axis;
- tmpNode = tmpNode[path];
-
- // skip non element nodes
- while (tmpNode && !tmpNode.tagName) {
- tmpNode = tmpNode[path];
- }
-
- if (combinator.direct) { // one pass only
- path = null;
- }
-
- } else { // success if we made it this far
- result.push(node);
- if (firstOnly) {
- return result;
- }
- break;
- }
- }
- }
- node = tmpNode = null;
- return result;
- },
-
- combinators: {
- ' ': {
- axis: 'parentNode'
- },
-
- '>': {
- axis: 'parentNode',
- direct: true
- },
-
-
- '+': {
- axis: 'previousSibling',
- direct: true
- }
- },
-
- _parsers: [
- {
- name: ATTRIBUTES,
- re: /^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,
- fn: function(match, token) {
- var operator = match[2] || '',
- operators = Selector.operators,
- escVal = (match[3]) ? match[3].replace(/\\/g, '') : '',
- test;
-
- // add prefiltering for ID and CLASS
- if ((match[1] === 'id' && operator === '=') ||
- (match[1] === 'className' &&
- Y.config.doc.documentElement.getElementsByClassName &&
- (operator === '~=' || operator === '='))) {
- token.prefilter = match[1];
-
-
- match[3] = escVal;
-
- // escape all but ID for prefilter, which may run through QSA (via Dom.allById)
- token[match[1]] = (match[1] === 'id') ? match[3] : escVal;
-
- }
-
- // add tests
- if (operator in operators) {
- test = operators[operator];
- if (typeof test === 'string') {
- match[3] = escVal.replace(Selector._reRegExpTokens, '\\$1');
- test = new RegExp(test.replace('{val}', match[3]));
- }
- match[2] = test;
- }
- if (!token.last || token.prefilter !== match[1]) {
- return match.slice(1);
- }
- }
- },
- {
- name: TAG_NAME,
- re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
- fn: function(match, token) {
- var tag = match[1];
-
- if (!Selector._isXML) {
- tag = tag.toUpperCase();
- }
-
- token.tagName = tag;
-
- if (tag !== '*' && (!token.last || token.prefilter)) {
- return [TAG_NAME, '=', tag];
- }
- if (!token.prefilter) {
- token.prefilter = 'tagName';
- }
- }
- },
- {
- name: COMBINATOR,
- re: /^\s*([>+~]|\s)\s*/,
- fn: function(match, token) {
- }
- },
- {
- name: PSEUDOS,
- re: /^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,
- fn: function(match, token) {
- var test = Selector[PSEUDOS][match[1]];
- if (test) { // reorder match array and unescape special chars for tests
- if (match[2]) {
- match[2] = match[2].replace(/\\/g, '');
- }
- return [match[2], test];
- } else { // selector token not supported (possibly missing CSS3 module)
- return false;
- }
- }
- }
- ],
-
- _getToken: function(token) {
- return {
- tagName: null,
- id: null,
- className: null,
- attributes: {},
- combinator: null,
- tests: []
- };
- },
-
- /*
- Break selector into token units per simple selector.
- Combinator is attached to the previous token.
- */
- _tokenize: function(selector) {
- selector = selector || '';
- selector = Selector._parseSelector(Y.Lang.trim(selector));
- var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
- query = selector, // original query for debug report
- tokens = [], // array of tokens
- found = false, // whether or not any matches were found this pass
- match, // the regex match
- test,
- i, parser;
-
- /*
- Search for selector patterns, store, and strip them from the selector string
- until no patterns match (invalid selector) or we run out of chars.
-
- Multiple attributes and pseudos are allowed, in any order.
- for example:
- 'form:first-child[type=button]:not(button)[lang|=en]'
- */
- outer:
- do {
- found = false; // reset after full pass
- for (i = 0; (parser = Selector._parsers[i++]);) {
- if ( (match = parser.re.exec(selector)) ) { // note assignment
- if (parser.name !== COMBINATOR ) {
- token.selector = selector;
- }
- selector = selector.replace(match[0], ''); // strip current match from selector
- if (!selector.length) {
- token.last = true;
- }
-
- if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
- match[1] = Selector._attrFilters[match[1]];
- }
-
- test = parser.fn(match, token);
- if (test === false) { // selector not supported
- found = false;
- break outer;
- } else if (test) {
- token.tests.push(test);
- }
-
- if (!selector.length || parser.name === COMBINATOR) {
- tokens.push(token);
- token = Selector._getToken(token);
- if (parser.name === COMBINATOR) {
- token.combinator = Y.Selector.combinators[match[1]];
- }
- }
- found = true;
- }
- }
- } while (found && selector.length);
-
- if (!found || selector.length) { // not fully parsed
- Y.log('query: ' + query + ' contains unsupported token in: ' + selector, 'warn', 'Selector');
- tokens = [];
- }
- return tokens;
- },
-
- _replaceMarkers: function(selector) {
- selector = selector.replace(/\[/g, '\uE003');
- selector = selector.replace(/\]/g, '\uE004');
-
- selector = selector.replace(/\(/g, '\uE005');
- selector = selector.replace(/\)/g, '\uE006');
- return selector;
- },
-
- _replaceShorthand: function(selector) {
- var shorthand = Y.Selector.shorthand,
- re;
-
- for (re in shorthand) {
- if (shorthand.hasOwnProperty(re)) {
- selector = selector.replace(new RegExp(re, 'gi'), shorthand[re]);
- }
- }
-
- return selector;
- },
-
- _parseSelector: function(selector) {
- var replaced = Y.Selector._replaceSelector(selector),
- selector = replaced.selector;
-
- // replace shorthand (".foo, #bar") after pseudos and attrs
- // to avoid replacing unescaped chars
- selector = Y.Selector._replaceShorthand(selector);
-
- selector = Y.Selector._restore('attr', selector, replaced.attrs);
- selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
-
- // replace braces and parens before restoring escaped chars
- // to avoid replacing ecaped markers
- selector = Y.Selector._replaceMarkers(selector);
- selector = Y.Selector._restore('esc', selector, replaced.esc);
-
- return selector;
- },
-
- _attrFilters: {
- 'class': 'className',
- 'for': 'htmlFor'
- },
-
- getters: {
- href: function(node, attr) {
- return Y.DOM.getAttribute(node, attr);
- },
-
- id: function(node, attr) {
- return Y.DOM.getId(node);
- }
- }
- };
-
- Y.mix(Y.Selector, SelectorCSS2, true);
- Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
-
- // IE wants class with native queries
- if (Y.Selector.useNative && Y.config.doc.querySelector) {
- Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
- }
-
-