3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('selector-native', function(Y) {
11 * The selector-native module provides support for native querySelector
13 * @submodule selector-native
18 * Provides support for using CSS selectors to query the DOM
24 Y.namespace('Selector'); // allow native module to standalone
26 var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
27 OWNER_DOCUMENT = 'ownerDocument';
33 re: /\\[:\[\]\(\)#\.\'\>+~"]/gi
49 _escapeId: function(id) {
51 id = id.replace(/([:\[\]\(\)#\.'<>+~"])/g,'\\$1');
56 _compare: ('sourceIndex' in Y.config.doc.documentElement) ?
57 function(nodeA, nodeB) {
58 var a = nodeA.sourceIndex,
59 b = nodeB.sourceIndex;
69 } : (Y.config.doc.documentElement[COMPARE_DOCUMENT_POSITION] ?
70 function(nodeA, nodeB) {
71 if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
77 function(nodeA, nodeB) {
78 var rangeA, rangeB, compare;
80 rangeA = nodeA[OWNER_DOCUMENT].createRange();
81 rangeA.setStart(nodeA, 0);
82 rangeB = nodeB[OWNER_DOCUMENT].createRange();
83 rangeB.setStart(nodeB, 0);
84 compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
91 _sort: function(nodes) {
93 nodes = Y.Array(nodes, 0, true);
95 nodes.sort(Selector._compare);
102 _deDupe: function(nodes) {
106 for (i = 0; (node = nodes[i++]);) {
108 ret[ret.length] = node;
113 for (i = 0; (node = ret[i++]);) {
115 node.removeAttribute('_found');
122 * Retrieves a set of nodes based on a given CSS selector.
125 * @param {string} selector The CSS Selector to test the node against.
126 * @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
127 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
128 * @return {Array} An array of nodes that match the given selector.
131 query: function(selector, root, firstOnly, skipNative) {
132 root = root || Y.config.doc;
134 useNative = (Y.Selector.useNative && Y.config.doc.querySelector && !skipNative),
135 queries = [[selector, root]],
139 fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
141 if (selector && fn) {
142 // split group into seperate queries
143 if (!skipNative && // already done if skipping
144 (!useNative || root.tagName)) { // split native when element scoping is needed
145 queries = Selector._splitQueries(selector, root);
148 for (i = 0; (query = queries[i++]);) {
149 result = fn(query[0], query[1], firstOnly);
150 if (!firstOnly) { // coerce DOM Collection to Array
151 result = Y.Array(result, 0, true);
154 ret = ret.concat(result);
158 if (queries.length > 1) { // remove dupes and sort by doc order
159 ret = Selector._sort(Selector._deDupe(ret));
163 return (firstOnly) ? (ret[0] || null) : ret;
167 _replaceSelector: function(selector) {
168 var esc = Y.Selector._parse('esc', selector), // pull escaped colon, brackets, etc.
172 // first replace escaped chars, which could be present in attrs or pseudos
173 selector = Y.Selector._replace('esc', selector);
175 // then replace pseudos before attrs to avoid replacing :not([foo])
176 pseudos = Y.Selector._parse('pseudo', selector);
177 selector = Selector._replace('pseudo', selector);
179 attrs = Y.Selector._parse('attr', selector);
180 selector = Y.Selector._replace('attr', selector);
190 _restoreSelector: function(replaced) {
191 var selector = replaced.selector;
192 selector = Y.Selector._restore('attr', selector, replaced.attrs);
193 selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
194 selector = Y.Selector._restore('esc', selector, replaced.esc);
198 _replaceCommas: function(selector) {
199 var replaced = Y.Selector._replaceSelector(selector),
200 selector = replaced.selector;
203 selector = selector.replace(/,/g, '\uE007');
204 replaced.selector = selector;
205 selector = Y.Selector._restoreSelector(replaced);
210 // allows element scoped queries to begin with combinator
211 // e.g. query('> p', document.body) === query('body > p')
212 _splitQueries: function(selector, node) {
213 if (selector.indexOf(',') > -1) {
214 selector = Y.Selector._replaceCommas(selector);
217 var groups = selector.split('\uE007'), // split on replaced comma token
225 // enforce for element scoping
226 if (node.nodeType === 1) { // Elements only
227 id = Y.Selector._escapeId(Y.DOM.getId(node));
231 Y.DOM.setId(node, id);
234 prefix = '[id="' + id + '"] ';
237 for (i = 0, len = groups.length; i < len; ++i) {
238 selector = prefix + groups[i];
239 queries.push([selector, node]);
246 _nativeQuery: function(selector, root, one) {
247 if (Y.UA.webkit && selector.indexOf(':checked') > -1 &&
248 (Y.Selector.pseudos && Y.Selector.pseudos.checked)) { // webkit (chrome, safari) fails to pick up "selected" with "checked"
249 return Y.Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
252 return root['querySelector' + (one ? '' : 'All')](selector);
253 } catch(e) { // fallback to brute if available
254 return Y.Selector.query(selector, root, one, true); // redo with skipNative true
258 filter: function(nodes, selector) {
262 if (nodes && selector) {
263 for (i = 0; (node = nodes[i++]);) {
264 if (Y.Selector.test(node, selector)) {
265 ret[ret.length] = node;
274 test: function(node, selector, root) {
285 if (node && node.tagName) { // only test HTMLElements
287 if (typeof selector == 'function') { // test with function
288 ret = selector.call(node, node);
289 } else { // test with query
290 // we need a root if off-doc
291 groups = selector.split(',');
292 if (!root && !Y.DOM.inDoc(node)) {
293 parent = node.parentNode;
296 } else { // only use frag when no parent to query
297 frag = node[OWNER_DOCUMENT].createDocumentFragment();
298 frag.appendChild(node);
303 root = root || node[OWNER_DOCUMENT];
305 id = Y.Selector._escapeId(Y.DOM.getId(node));
308 Y.DOM.setId(node, id);
311 for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
312 group += '[id="' + id + '"]';
313 items = Y.Selector.query(group, root);
315 for (j = 0; item = items[j++];) {
326 if (useFrag) { // cleanup
327 frag.removeChild(node);
336 * A convenience function to emulate Y.Node's aNode.ancestor(selector).
337 * @param {HTMLElement} element An HTMLElement to start the query from.
338 * @param {String} selector The CSS selector to test the node against.
339 * @return {HTMLElement} The ancestor node matching the selector, or null.
340 * @param {Boolean} testSelf optional Whether or not to include the element in the scan
344 ancestor: function (element, selector, testSelf) {
345 return Y.DOM.ancestor(element, function(n) {
346 return Y.Selector.test(n, selector);
350 _parse: function(name, selector) {
351 return selector.match(Y.Selector._types[name].re);
354 _replace: function(name, selector) {
355 var o = Y.Selector._types[name];
356 return selector.replace(o.re, o.token);
359 _restore: function(name, selector, items) {
361 var token = Y.Selector._types[name].token,
363 for (i = 0, len = items.length; i < len; ++i) {
364 selector = selector.replace(token, items[i]);
371 Y.mix(Y.Selector, Selector, true);
376 }, '3.5.1' ,{requires:['dom-base']});