selector.js revision 744aec5933ba55f438dc42a241edb3c95df5abff
3ad07fa335d40330cd1859da42e67f2457443990Andreas Gustafsson * Provides helper methods for collecting and filtering DOM elements.
3ad07fa335d40330cd1859da42e67f2457443990Andreas Gustafsson * @submodule selector
1094dec52a86e57df53f6167d86de94360a7a382Mark Andrews * Provides helper methods for collecting and filtering DOM elements.
fa2fb620c7c0a907b220c257007d8fb6d38bb3a4Andreas Gustafsson * @class Selector
e69b9ffb0f8b4d1117a682908c9143ebe3efcd6bAndreas Gustafssonvar reNth = /^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/;
5419c0c2d0b77682021084c69f2a5c5e2f9a5525Andreas Gustafsson attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
a77ad145d0109081c5da6ac40a2303369db89735Andreas Gustafsson pseudos: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
8ba4e82f5358815fd94f34fde408ffd047ba3430Andreas Gustafsson combinator: /^\s*([>+~]|\s)\s*/
ada9b8ab20b81716c7ff1f4f3365929b2f7c8ff8Mark Andrewsvar Selector = {
ada9b8ab20b81716c7ff1f4f3365929b2f7c8ff8Mark Andrews * Default document for use queries
3c9b2e62502460c34c2e0ceba1a5d138b3a13cc1Andreas Gustafsson * @property document
3c9b2e62502460c34c2e0ceba1a5d138b3a13cc1Andreas Gustafsson * @type object
3c9b2e62502460c34c2e0ceba1a5d138b3a13cc1Andreas Gustafsson * @default window.document
bb60abb44549428414cd55a022f2b8cc4488f7adAndreas Gustafsson document: Y.config.doc,
bb60abb44549428414cd55a022f2b8cc4488f7adAndreas Gustafsson * Mapping of attributes to aliases, normally to work around HTMLAttributes
bb60abb44549428414cd55a022f2b8cc4488f7adAndreas Gustafsson * that conflict with JS reserved words.
024face21cdfbfc7a862a3be061e6780533ef755Andreas Gustafsson * @property attrAliases
024face21cdfbfc7a862a3be061e6780533ef755Andreas Gustafsson * @type object
1beaa9e45738ad18cb7cae55aea95a1b16a14f94Andreas Gustafsson attrAliases: {},
f953788d75c7df2db43907c68da18ed75c235dd3Andreas Gustafsson * Mapping of shorthand tokens to corresponding attribute selector
f953788d75c7df2db43907c68da18ed75c235dd3Andreas Gustafsson * @property shorthand
f953788d75c7df2db43907c68da18ed75c235dd3Andreas Gustafsson * @type object
a7e1dcd84ada7e4e4c78f3f281e8a4d99adaf4d1Andreas Gustafsson * List of operators and corresponding boolean functions.
a7e1dcd84ada7e4e4c78f3f281e8a4d99adaf4d1Andreas Gustafsson * These functions are passed the attribute and the current node's value of the attribute.
2975d0f819762614526c650b9c2077ef22f81328Andreas Gustafsson '=': function(attr, val) { return attr === val; }, // Equality
0aba41458d345ea901cf945d47162e5f23647de9Mark Andrews '!=': function(attr, val) { return attr !== val; }, // Inequality
0bd2ea544e95601e0f0b056acfa079c99d5f6b57Andreas Gustafsson '~=': function(attr, val) { // Match one of space seperated words
0bd2ea544e95601e0f0b056acfa079c99d5f6b57Andreas Gustafsson return (s + attr + s).indexOf((s + val + s)) > -1;
79432444e84d2d104119fe6a3d5cbc04b1375bd4Andreas Gustafsson '|=': function(attr, val) { return Y.DOM._getRegExp('^' + val + '[-]?').test(attr); }, // Match start with value followed by optional hyphen
79432444e84d2d104119fe6a3d5cbc04b1375bd4Andreas Gustafsson '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
2e24e82fc3551e3228bcacaa7c45cb61daa49195Mark Andrews '$=': function(attr, val) { return attr.lastIndexOf(val) === attr[LENGTH] - val[LENGTH]; }, // Match ends with value
3c17010ba5a6b8dd8a2bbc550813c7f051f45a08Andreas Gustafsson '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring
3c17010ba5a6b8dd8a2bbc550813c7f051f45a08Andreas Gustafsson '': function(attr, val) { return attr; } // Just test for existence of attribute
df7596a03eea7f1c2df89bd63d3bd4b73f274565Mark Andrews * List of pseudo-classes and corresponding boolean functions.
df7596a03eea7f1c2df89bd63d3bd4b73f274565Mark Andrews * These functions are called with the current node, and any value that was parsed with the pseudo regex.
df7596a03eea7f1c2df89bd63d3bd4b73f274565Mark Andrews * @property pseudos
f08782f0923d11227983a352c26301cf703383cfMark Andrews * @type object
ed2cefaf0ea367ee408cb7f6a54a413814240fa7Andreas Gustafsson return node === node.ownerDocument.documentElement;
edf97be2b54cbdc4f3f3a46776df3e912892e960Andreas Gustafsson return Selector.getNth(node, val, null, true);
9e46f410e716f73abb345be215ccb4c61782b718Andreas Gustafsson return Selector.getNth(node, val, node[TAG_NAME]);
5419c0c2d0b77682021084c69f2a5c5e2f9a5525Andreas Gustafsson return Selector.getNth(node, val, node[TAG_NAME], true);
3d3445447225ab63f49fc24362963ea49ce94901Andreas Gustafsson return Y.DOM.firstChild(node[PARENT_NODE]) === node;
3dff229f5dd223568476acec4df1f513acb00b1dAndreas Gustafsson return Y.DOM.lastChild(node[PARENT_NODE]) === node;
792de65053d8a48d05746b35a21a9fa1792e71acAndreas Gustafsson return Y.DOM.firstChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
3e934267660cb13029bcdbddf318fe1cc27b6718Andreas Gustafsson return Y.DOM.lastChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
7655e78c366cc0d25e24e2a96ba58e04a96042faAndreas Gustafsson var children = Y.DOM.children(node[PARENT_NODE]);
7655e78c366cc0d25e24e2a96ba58e04a96042faAndreas Gustafsson return children[LENGTH] === 1 && children[0] === node;
f558da602e8b74ed181d9189f20bf32dfa6d8723Brian Wellington return Y.DOM.childrenByTag(node[PARENT_NODE], node[TAG_NAME])[LENGTH] === 1;
808b909f27c30d36b27efb5aa5ef2d18f83b6d4bAndreas Gustafsson var text = node.innerText || node.textContent || '';
640923da589bc5b8492ac407ef89ea1ee9a1c358Andreas Gustafsson * Test if the supplied node matches the supplied selector.
640923da589bc5b8492ac407ef89ea1ee9a1c358Andreas Gustafsson * @method test
640923da589bc5b8492ac407ef89ea1ee9a1c358Andreas Gustafsson * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
1299e93989afbe1fee0739811b05fd1641ea14aeAndreas Gustafsson * @param {string} selector The CSS Selector to test the node against.
0bd1b2fbfed4aa489e9d5fcbc7f48acb96ba7248Mark Andrews * @return{boolean} Whether or not the node matches the selector.
28cf7340b9c82fc62ca1a1782cb1bd7b0de11aebAndreas Gustafsson var groups = selector ? selector.split(',') : [];
28cf7340b9c82fc62ca1a1782cb1bd7b0de11aebAndreas Gustafsson for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
1de63e34f163b7a4708a6ad1779f93ae7636b92eAndreas Gustafsson if ( Selector._testNode(node, groups[i]) ) { // passes if ANY group matches
5e4c83cfec3f267ea8f22fbb535c61434c94d43cDanny Mayer * Filters a set of nodes based on a given CSS selector.
6e1b2ebcd65c6d0cc90d7789f884aea11184eb5dAndreas Gustafsson * @method filter
e6f17474cb43a138bf7fc9ad30c6b3a2847cb7a7Mark Andrews * @param {array} nodes A set of nodes/ids to filter.
5fe21da364d4397c9a413fe689ce82dea36a7b29Mark Andrews * @param {string} selector The selector used to test each node.
5fe21da364d4397c9a413fe689ce82dea36a7b29Mark Andrews * @return{array} An array of nodes from the supplied array that match the given selector.
5c831a1a1b14470037de6d8bc0501aea5dc6cacdAndreas Gustafsson var result = Selector._filter(nodes, Selector._tokenize(selector)[0]);
d4196128b31d511c8513edacc70dea7e8d0c053aMark Andrews * Retrieves a set of nodes based on a given CSS selector.
d4196128b31d511c8513edacc70dea7e8d0c053aMark Andrews * @method query
4a20a92f4f96cf2b2fd77898c6afec6c45e481b3Andreas Gustafsson * @param {string} selector The CSS Selector to test the node against.
4a20a92f4f96cf2b2fd77898c6afec6c45e481b3Andreas Gustafsson * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
4a20a92f4f96cf2b2fd77898c6afec6c45e481b3Andreas Gustafsson * @param {Boolean} firstOnly optional Whether or not to return only the first match.
4a20a92f4f96cf2b2fd77898c6afec6c45e481b3Andreas Gustafsson * @return {Array} An array of nodes that match the given selector.
e60b3717f0e6f28d6fb2c5124ffb3bd31cc3a746Mark Andrews var result = Selector._query(selector, root, firstOnly);
3d8ab44d14f3de797b8454fc2edb7421a6bfc874Andreas Gustafsson _query: function(selector, root, firstOnly, deDupe) {
64a5004a66accd190bfd5ddf115667726537be50Andreas Gustafsson var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
64a5004a66accd190bfd5ddf115667726537be50Andreas Gustafsson for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
b1ae7a591a4b99a26036e919b87247b65abfcd77Mark Andrews found = arguments.callee(groups[i], root, firstOnly, true);
b1ae7a591a4b99a26036e919b87247b65abfcd77Mark Andrews result = firstOnly ? found : result.concat(found);
cab0ee644db604d56b45ec39429d505d635da347Andreas Gustafsson var tokens = Selector._tokenize(selector);
cab0ee644db604d56b45ec39429d505d635da347Andreas Gustafsson var idToken = tokens[Selector._getIdTokenIndex(tokens)],
6dbc6fae496db1f584c055e63bcd7afd332fe8f6Andreas Gustafsson id = Selector._getId(idToken[ATTRIBUTES]);
a24d253a3f4e6f4036800744b348fba858d4959eMark Andrews // use id shortcut when possible
3f543c371fff724d1fb05eb564f732476e946b5bBrian Wellington node = Selector.document.getElementById(id);
3f543c371fff724d1fb05eb564f732476e946b5bBrian Wellington if (node && (root[NODE_TYPE] === 9 || Y.DOM.contains(root, node))) {
847169dab2d0496df1d66842b2cce67c66bf9680Andreas Gustafsson if ( Selector._testNode(node, null, idToken) ) {
98a5dc52bf668b093cda7901c057f7b54e18a2fcAndreas Gustafsson result = Selector._filter(nodes, token, firstOnly, deDupe);
452b30ddb32dd9370b2e5ee10427dd3758ef98b4Mark Andrews _filter: function(nodes, token, firstOnly, deDupe) {
452b30ddb32dd9370b2e5ee10427dd3758ef98b4Mark Andrews result = Y.DOM.filterElementsBy(nodes, function(node) {
6668eca26bf3123750afda48b69991bd29d83807Mark Andrews if (! Selector._testNode(node, '', token, deDupe)) {
1eaad22e111709254c70953a4dc768b6d4d31646Mark Andrews return false;
cad3210bb95057a37aaed20bc8a1542e0534422cAndreas Gustafsson Selector._foundCache[Selector._foundCache[LENGTH]] = node;
afeded2289de8d193b072da2b44a2d580cc235c1Danny Mayer _testNode: function(node, selector, token, deDupe) {
afeded2289de8d193b072da2b44a2d580cc235c1Danny Mayer token = token || Selector._tokenize(selector).pop() || {};
aa9a67adeb48069f5c2e5d8936a8ed5aac7d6ad7Andreas Gustafsson (token[TAG] !== '*' && node[TAG_NAME].toUpperCase() !== token[TAG]) ||
330b421487d7c3a5e699472fe889aa633772057fMark Andrews for (i = 0, len = token[ATTRIBUTES][LENGTH]; i < len; ++i) {
e22dca2a9ad30d493a869586abed86f7268204f9Mark Andrews attribute = node.getAttribute(token[ATTRIBUTES][i][0], 2);
e22dca2a9ad30d493a869586abed86f7268204f9Mark Andrews return false;
2c0b26955ee32fcee1757ec1be5a8caf8fe695a6Mark Andrews !ops[token[ATTRIBUTES][i][1]](attribute, token[ATTRIBUTES][i][2])) {
2449f41e75d3b3f1c0ec3f05b1603fd8f80d8ae0Mark Andrews return false;
aec9f4d0723b0cffcfa9152533fb8f616ec7313bAndreas Gustafsson for (i = 0, len = token[PSEUDOS][LENGTH]; i < len; ++i) {
a1898260ad19d02e88ab76c1855d33c67add9defMark Andrews !pseudos[token[PSEUDOS][i][0]](node, token[PSEUDOS][i][1])) {
a1898260ad19d02e88ab76c1855d33c67add9defMark Andrews return false;
305b0eda33b16493355db1f1c86313a6f5fbfc3bDanny Mayer Selector.combinators[prev[COMBINATOR]](node, token) :
aec9f4d0723b0cffcfa9152533fb8f616ec7313bAndreas Gustafsson for (var i = 0, len = Selector._foundCache[LENGTH]; i < len; ++i) {
4e400cb7a2edd25af98ebc25fcbb5b36ca08f9a0Mark Andrews try { // IE no like delete
c0b6c1a5ab50722793cb99b0d8a1e9e910c146a5Andreas Gustafsson Selector._foundCache[i].removeAttribute('_found');
907ec2c618d08d8322b04729779b24bd778d49e7Mark Andrews if (Selector._testNode(node, '', token.previous)) {
907ec2c618d08d8322b04729779b24bd778d49e7Mark Andrews return true;
23a020bc1312fc35e7c4ea36df846c550cb13634Andreas Gustafsson return Selector._testNode(node[PARENT_NODE], null, token.previous);
7250c1a2616761395bdb9ae7cd1ba43f20d3edc4Andreas Gustafsson if (sib && Selector._testNode(sib, null, token.previous)) {
c38b92000c0f1a95daaad5468777e165b8047de9Mark Andrews return false;
22f0b13f28a7df3b348b18848d0ccd745ea88c3cAndreas Gustafsson if (sib[NODE_TYPE] === 1 && Selector._testNode(sib, null, token.previous)) {
ee3ab6063dd13b5947d3fbe88b9ce8f38d65df9dBrian Wellington return false;
a5b9c2b208b51b039c8f4006cddf3d37dd781561Brian Wellington an+b = get every _a_th node starting at the _b_th
2da0b7dfbd02fab454b8ba60f1fdb7e2a5cbd2dbMark Andrews 0n+b = no repeat ("0" and "n" may both be omitted (together) , e.g. "0n+1" or "1", not "0+1"), return only the _b_th element
2da0b7dfbd02fab454b8ba60f1fdb7e2a5cbd2dbMark Andrews 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
2da0b7dfbd02fab454b8ba60f1fdb7e2a5cbd2dbMark Andrews an+0 = get every _a_th element, "0" may be omitted
9261ca5fc8a564968f34e108eb862157471ca50eAndreas Gustafsson getNth: function(node, expr, tag, reverse) {
6443201354efa09f16ada26dab99e9b7f8271521Andreas Gustafsson var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
e980502db40155234b4e8d320b748b34dbaba3a2Brian Wellington b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
ecd1addb86319bacc6c0bff2c68373619eebbffcMark Andrews siblings = Y.DOM.childrenByTag(node[PARENT_NODE], tag);
aa0dc8d920a1f79626c3564408db9c5c9a5319a7Andreas Gustafsson } else if ( isNaN(a) ) {
aa0dc8d920a1f79626c3564408db9c5c9a5319a7Andreas Gustafsson a = (n) ? 1 : 0; // start from the first or no repeat
22815444822da17fab82d4ab115da6e055ea1754Brian Wellington return false;
22815444822da17fab82d4ab115da6e055ea1754Brian Wellington } else if (a < 0) {
bd6504aa9aa16a912412fbe010046aaf4bf23621Brian Wellington for (i = b - 1, len = siblings[LENGTH]; i < len; i += a) {
e9596e1fb3dfa560216776acdbfac3cf5ef97157Mark Andrews for (i = siblings[LENGTH] - b, len = siblings[LENGTH]; i >= 0; i -= a) {
c54210716ee55b55e22d8dad56fd696a641fc98dBob Halley return false;
1e289d3cca5cdd01dda650fa6e4c1de1aa8b4196Andreas Gustafsson for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
1e289d3cca5cdd01dda650fa6e4c1de1aa8b4196Andreas Gustafsson if (attr[i][0] == 'id' && attr[i][1] === '=') {
96ed62425310854fd6f6f06bfb7651b3e4c17ee7Andreas Gustafsson for (var i = 0, len = tokens[LENGTH]; i < len; ++i) {
5733d25b06b46067b3751d10436d82aef09cd705Brian Wellington if (Selector._getId(tokens[i][ATTRIBUTES])) {
eb6e3b04169a766d2b968bcc978191605c2ef24cAndreas Gustafsson Break selector into token units per simple selector.
eb6e3b04169a766d2b968bcc978191605c2ef24cAndreas Gustafsson Combinator is attached to left-hand selector.
eb6e3b04169a766d2b968bcc978191605c2ef24cAndreas Gustafsson var token = {}, // one token per simple selector (left selector holds combinator)
7d8c3693d0426b56750b14d80c47df5e42fc75e4Andreas Gustafsson found = false, // whether or not any matches were found this pass
fed846067d265db1037483d81d01f3651c8a3f28Brian Wellington selector = Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
a26ad011f382d12058478704cb5e90e6f4366d01Andreas Gustafsson Search for selector patterns, store, and strip them from the selector string
a26ad011f382d12058478704cb5e90e6f4366d01Andreas Gustafsson until no patterns match (invalid selector) or we run out of chars.
7d8c3693d0426b56750b14d80c47df5e42fc75e4Andreas Gustafsson Multiple attributes and pseudos are allowed, in any order.
7d8c3693d0426b56750b14d80c47df5e42fc75e4Andreas Gustafsson 'form:first-child[type=button]:not(button)[lang|=en]'
07c336a9a85791dff886b1e28514589a25d9b720Andreas Gustafsson if (re != TAG && re != COMBINATOR) { // only one allowed
07c336a9a85791dff886b1e28514589a25d9b720Andreas Gustafsson if ((match = patterns[re].exec(selector))) { // note assignment
712fa28946312882a60b0c6a913914d3e8c69867Mark Andrews if (re != TAG && re != COMBINATOR) { // only one allowed
f2a16ec2e8970615d39f8fe339b215ad0a893b85Mark Andrews //token[re] = token[re] || [];
63fd201fde27ce408cde1c73a054e401fcfb9e3bDavid Lawrence // capture ID for fast path to element
f8644da8d948dbc973f6dd4c94a79774e16ec07bDavid Lawrence if (re === ATTRIBUTES && match[1] === 'id') {
9bfa90768ab83ea5a8571c98d3774377da4bdcbeDavid Lawrence } else { // single selector (tag, combinator)
2d67c2474475acf52c8251dc48bfb7565ee5f2ffDavid Lawrence selector = selector.replace(match[0], ''); // strip current match from selector
2d67c2474475acf52c8251dc48bfb7565ee5f2ffDavid Lawrence if (re === COMBINATOR || !selector[LENGTH]) { // next token or done
2d67c2474475acf52c8251dc48bfb7565ee5f2ffDavid Lawrence token[ATTRIBUTES] = Selector._fixAttributes(token[ATTRIBUTES]);
6a7a69e9f764812872ec2db775be2ac8bb073102Andreas Gustafsson token[TAG] = token[TAG] ? token[TAG].toUpperCase() : '*';
519f8475ff8218e3981ae2b249eb1403da7c52f6Andreas Gustafsson for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
519f8475ff8218e3981ae2b249eb1403da7c52f6Andreas Gustafsson if (aliases[attr[i][0]]) { // convert reserved words, etc
edc1c60621b44fbc8131ad1542f657dd129f9a30Andreas Gustafsson if (!attr[i][1]) { // use exists operator
6112718b0dbb01ffbfd3fabc61e30c7e4485b0a7David Lawrence var attrs = selector.match(patterns[ATTRIBUTES]); // pull attributes to avoid false pos on "." and "#"
6112718b0dbb01ffbfd3fabc61e30c7e4485b0a7David Lawrence selector = selector.replace(patterns[ATTRIBUTES], 'REPLACED_ATTRIBUTE');
04260c5c48d234734863f0222e207b6564cd41a8David Lawrence selector = selector.replace(Y.DOM._getRegExp(re, 'gi'), shorthand[re]);
f479c9ff5576b3d138c7e52cfc2319b185b7ebcfDavid Lawrence for (var i = 0, len = attrs[LENGTH]; i < len; ++i) {
6c35e4dd17e6562a6b4d106cbf1d824b9f529356David Lawrence selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
c2c275f5f4ead0943c76b6463cf7a93095559c64Andreas Gustafssonif (Y.UA.ie && Y.UA.ie < 8) { // rewrite class for IE (others use getAttribute('class')
3b6bcedffe1d326fd9f6aa3bfb1537af0975fab8Brian Wellington Selector.attrAliases['class'] = 'className';
841179549b6433e782c164a562eb3422f603533dAndreas Gustafsson}, '@VERSION@' ,{requires:['dom-base'], skinnable:false});