(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);