(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 defaultId,
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) {
defaultId = true;
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);
}
if (defaultId) {
node.removeAttribute('id');
}
};
}
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);