selector-native.js revision f77815d81ad76d60a1f1b834efbc7ed954437e6d
(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',
TMP_PREFIX = 'yui-tmp-',
g_counter = 0;
var Selector = {
_foundCache: [],
useNative: true,
_compare: ('sourceIndex' in document.documentElement) ?
function(nodeA, nodeB) {
var a = nodeA.sourceIndex,
b = nodeB.sourceIndex;
if (a === b) {
return 0;
} else if (a > b) {
return 1;
}
return -1;
} : (document.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 The CSS Selector to test the node against.
* @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
* @param {Boolean} firstOnly optional Whether or not to return only the first match.
* @return {Array} An array of nodes that match the given selector.
* @static
*/
query: function(selector, root, firstOnly, skipNative) {
root = root || Y.config.doc;
var ret = [],
useNative = (Y.Selector.useNative && document.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);
}
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;
},
// allows element scoped queries to begin with combinator
// e.g. query('> p', document.body) === query('body > p')
_splitQueries: function(selector, node) {
var groups = selector.split(','),
queries = [],
prefix = '',
i, len;
if (node) {
// enforce for element scoping
if (node.tagName) {
node.id = node.id || Y.guid();
prefix = '#' + node.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) {
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
}
},
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;
},
test: function(node, selector, root) {
var ret = false,
groups = selector.split(','),
item,
i, group;
if (node && node.tagName) { // only test HTMLElements
root = root || node.ownerDocument;
if (!node.id) {
node.id = TMP_PREFIX + g_counter++;
}
for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
group += '#' + node.id; // add ID for uniqueness
item = Y.Selector.query(group, root, true);
ret = (item === node);
if (ret) {
break;
}
}
}
return ret;
}
};
Y.mix(Y.Selector, Selector, true);
})(Y);