- (function(Y) {
- /**
- * The selector-native module provides support for native querySelector
- * @module dom
- * @submodule selector-native
- * @for Selector
- */
-
- /**
- * Provides support for using CSS selectors to query the DOM
- * @class Selector
- * @static
- * @for Selector
- */
-
- Y.namespace('Selector'); // allow native module to standalone
-
- var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
- OWNER_DOCUMENT = 'ownerDocument';
-
- var Selector = {
- _types: {
- esc: {
- token: '\uE000',
- re: /\\[:\[\]\(\)#\.\'\>+~"]/gi
- },
-
- attr: {
- token: '\uE001',
- re: /(\[[^\]]*\])/g
- },
-
- pseudo: {
- token: '\uE002',
- re: /(\([^\)]*\))/g
- }
- },
-
- /**
- * Use the native version of `querySelectorAll`, if it exists.
- *
- * @property useNative
- * @default true
- * @static
- */
- useNative: true,
-
- _escapeId: function(id) {
- if (id) {
- id = id.replace(/([:\[\]\(\)#\.'<>+~"])/g,'\\$1');
- }
- return id;
- },
-
- _compare: ('sourceIndex' in Y.config.doc.documentElement) ?
- function(nodeA, nodeB) {
- var a = nodeA.sourceIndex,
- b = nodeB.sourceIndex;
-
- if (a === b) {
- return 0;
- } else if (a > b) {
- return 1;
- }
-
- return -1;
-
- } : (Y.config.doc.documentElement[COMPARE_DOCUMENT_POSITION] ?
- function(nodeA, nodeB) {
- if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
- return -1;
- } else {
- return 1;
- }
- } :
- function(nodeA, nodeB) {
- var rangeA, rangeB, compare;
- if (nodeA && nodeB) {
- rangeA = nodeA[OWNER_DOCUMENT].createRange();
- rangeA.setStart(nodeA, 0);
- rangeB = nodeB[OWNER_DOCUMENT].createRange();
- rangeB.setStart(nodeB, 0);
- compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
- }
-
- return compare;
-
- }),
-
- _sort: function(nodes) {
- if (nodes) {
- nodes = Y.Array(nodes, 0, true);
- if (nodes.sort) {
- nodes.sort(Selector._compare);
- }
- }
-
- return nodes;
- },
-
- _deDupe: function(nodes) {
- var ret = [],
- i, node;
-
- for (i = 0; (node = nodes[i++]);) {
- if (!node._found) {
- ret[ret.length] = node;
- node._found = true;
- }
- }
-
- for (i = 0; (node = ret[i++]);) {
- node._found = null;
- node.removeAttribute('_found');
- }
-
- return ret;
- },
-
- /**
- * Retrieves a set of nodes based on a given CSS selector.
- * @method query
- *
- * @param {String} selector A CSS selector.
- * @param {HTMLElement} root optional A node to start the query from. Defaults to `Y.config.doc`.
- * @param {Boolean} firstOnly optional Whether or not to return only the first match.
- * @return {HTMLElement[]} The array of nodes that matched the given selector.
- * @static
- */
- query: function(selector, root, firstOnly, skipNative) {
- root = root || Y.config.doc;
- var ret = [],
- useNative = (Y.Selector.useNative && Y.config.doc.querySelector && !skipNative),
- queries = [[selector, root]],
- query,
- result,
- i,
- fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
-
- if (selector && fn) {
- // split group into seperate queries
- if (!skipNative && // already done if skipping
- (!useNative || root.tagName)) { // split native when element scoping is needed
- queries = Selector._splitQueries(selector, root);
- }
-
- for (i = 0; (query = queries[i++]);) {
- result = fn(query[0], query[1], firstOnly);
- if (!firstOnly) { // coerce DOM Collection to Array
- result = Y.Array(result, 0, true);
- }
- if (result) {
- ret = ret.concat(result);
- }
- }
-
- if (queries.length > 1) { // remove dupes and sort by doc order
- ret = Selector._sort(Selector._deDupe(ret));
- }
- }
-
- Y.log('query: ' + selector + ' returning: ' + ret.length, 'info', 'Selector');
- return (firstOnly) ? (ret[0] || null) : ret;
-
- },
-
- _replaceSelector: function(selector) {
- var esc = Y.Selector._parse('esc', selector), // pull escaped colon, brackets, etc.
- attrs,
- pseudos;
-
- // first replace escaped chars, which could be present in attrs or pseudos
- selector = Y.Selector._replace('esc', selector);
-
- // then replace pseudos before attrs to avoid replacing :not([foo])
- pseudos = Y.Selector._parse('pseudo', selector);
- selector = Selector._replace('pseudo', selector);
-
- attrs = Y.Selector._parse('attr', selector);
- selector = Y.Selector._replace('attr', selector);
-
- return {
- esc: esc,
- attrs: attrs,
- pseudos: pseudos,
- selector: selector
- };
- },
-
- _restoreSelector: function(replaced) {
- var selector = replaced.selector;
- selector = Y.Selector._restore('attr', selector, replaced.attrs);
- selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
- selector = Y.Selector._restore('esc', selector, replaced.esc);
- return selector;
- },
-
- _replaceCommas: function(selector) {
- var replaced = Y.Selector._replaceSelector(selector),
- selector = replaced.selector;
-
- if (selector) {
- selector = selector.replace(/,/g, '\uE007');
- replaced.selector = selector;
- selector = Y.Selector._restoreSelector(replaced);
- }
- return selector;
- },
-
- // allows element scoped queries to begin with combinator
- // e.g. query('> p', document.body) === query('body > p')
- _splitQueries: function(selector, node) {
- if (selector.indexOf(',') > -1) {
- selector = Y.Selector._replaceCommas(selector);
- }
-
- var groups = selector.split('\uE007'), // split on replaced comma token
- queries = [],
- prefix = '',
- id,
- i,
- len;
-
- if (node) {
- // enforce for element scoping
- if (node.nodeType === 1) { // Elements only
- id = Y.Selector._escapeId(Y.DOM.getId(node));
-
- if (!id) {
- id = Y.guid();
- Y.DOM.setId(node, id);
- }
-
- prefix = '[id="' + id + '"] ';
- }
-
- for (i = 0, len = groups.length; i < len; ++i) {
- selector = prefix + groups[i];
- queries.push([selector, node]);
- }
- }
-
- return queries;
- },
-
- _nativeQuery: function(selector, root, one) {
- if (
- (Y.UA.webkit || Y.UA.opera) && // webkit (chrome, safari) and Opera
- selector.indexOf(':checked') > -1 && // fail to pick up "selected" with ":checked"
- (Y.Selector.pseudos && Y.Selector.pseudos.checked)
- ) {
- return Y.Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
- }
- try {
- //Y.log('trying native query with: ' + selector, 'info', 'selector-native');
- return root['querySelector' + (one ? '' : 'All')](selector);
- } catch(e) { // fallback to brute if available
- //Y.log('native query error; reverting to brute query with: ' + selector, 'info', 'selector-native');
- return Y.Selector.query(selector, root, one, true); // redo with skipNative true
- }
- },
-
- /**
- * Filters out nodes that do not match the given CSS selector.
- * @method filter
- *
- * @param {HTMLElement[]} nodes An array of nodes.
- * @param {String} selector A CSS selector to test each node against.
- * @return {HTMLElement[]} The nodes that matched the given CSS selector.
- * @static
- */
- filter: function(nodes, selector) {
- var ret = [],
- i, node;
-
- if (nodes && selector) {
- for (i = 0; (node = nodes[i++]);) {
- if (Y.Selector.test(node, selector)) {
- ret[ret.length] = node;
- }
- }
- } else {
- Y.log('invalid filter input (nodes: ' + nodes +
- ', selector: ' + selector + ')', 'warn', 'Selector');
- }
-
- return ret;
- },
-
- /**
- * Determines whether or not the given node matches the given CSS selector.
- * @method test
- *
- * @param {HTMLElement} node A node to test.
- * @param {String} selector A CSS selector to test the node against.
- * @param {HTMLElement} root optional A node to start the query from. Defaults to the parent document of the node.
- * @return {Boolean} Whether or not the given node matched the given CSS selector.
- * @static
- */
- test: function(node, selector, root) {
- var ret = false,
- useFrag = false,
- groups,
- parent,
- item,
- items,
- frag,
- id,
- i, j, group;
-
- if (node && node.tagName) { // only test HTMLElements
-
- if (typeof selector == 'function') { // test with function
- ret = selector.call(node, node);
- } else { // test with query
- // we need a root if off-doc
- groups = selector.split(',');
- if (!root && !Y.DOM.inDoc(node)) {
- parent = node.parentNode;
- if (parent) {
- root = parent;
- } else { // only use frag when no parent to query
- frag = node[OWNER_DOCUMENT].createDocumentFragment();
- frag.appendChild(node);
- root = frag;
- useFrag = true;
- }
- }
- root = root || node[OWNER_DOCUMENT];
-
- id = Y.Selector._escapeId(Y.DOM.getId(node));
- if (!id) {
- id = Y.guid();
- Y.DOM.setId(node, id);
- }
-
- for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
- group += '[id="' + id + '"]';
- items = Y.Selector.query(group, root);
-
- for (j = 0; item = items[j++];) {
- if (item === node) {
- ret = true;
- break;
- }
- }
- if (ret) {
- break;
- }
- }
-
- if (useFrag) { // cleanup
- frag.removeChild(node);
- }
- };
- }
-
- return ret;
- },
-
- /**
- * A convenience method to emulate Y.Node's aNode.ancestor(selector).
- * @method ancestor
- *
- * @param {HTMLElement} node A node to start the query from.
- * @param {String} selector A CSS selector to test the node against.
- * @param {Boolean} testSelf optional Whether or not to include the node in the scan.
- * @return {HTMLElement} The ancestor node matching the selector, or null.
- * @static
- */
- ancestor: function (node, selector, testSelf) {
- return Y.DOM.ancestor(node, function(n) {
- return Y.Selector.test(n, selector);
- }, testSelf);
- },
-
- _parse: function(name, selector) {
- return selector.match(Y.Selector._types[name].re);
- },
-
- _replace: function(name, selector) {
- var o = Y.Selector._types[name];
- return selector.replace(o.re, o.token);
- },
-
- _restore: function(name, selector, items) {
- if (items) {
- var token = Y.Selector._types[name].token,
- i, len;
- for (i = 0, len = items.length; i < len; ++i) {
- selector = selector.replace(token, items[i]);
- }
- }
- return selector;
- }
- };
-
- Y.mix(Y.Selector, Selector, true);
-
- })(Y);
-
-