selector.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /*
  2. Copyright (c) 2009, Yahoo! Inc. All rights reserved.
  3. Code licensed under the BSD License:
  4. http://developer.yahoo.net/yui/license.txt
  5. version: 2.8.0r4
  6. */
  7. /**
  8. * The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements.
  9. * @module selector
  10. * @title Selector Utility
  11. * @namespace YAHOO.util
  12. * @requires yahoo, dom
  13. */
  14. (function() {
  15. var Y = YAHOO.util;
  16. /**
  17. * Provides helper methods for collecting and filtering DOM elements.
  18. * @namespace YAHOO.util
  19. * @class Selector
  20. * @static
  21. */
  22. Y.Selector = {
  23. _foundCache: [],
  24. _regexCache: {},
  25. _re: {
  26. nth: /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/,
  27. attr: /(\[.*\])/g,
  28. urls: /^(?:href|src)/
  29. },
  30. /**
  31. * Default document for use queries
  32. * @property document
  33. * @type object
  34. * @default window.document
  35. */
  36. document: window.document,
  37. /**
  38. * Mapping of attributes to aliases, normally to work around HTMLAttributes
  39. * that conflict with JS reserved words.
  40. * @property attrAliases
  41. * @type object
  42. */
  43. attrAliases: {
  44. },
  45. /**
  46. * Mapping of shorthand tokens to corresponding attribute selector
  47. * @property shorthand
  48. * @type object
  49. */
  50. shorthand: {
  51. //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
  52. '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
  53. '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
  54. },
  55. /**
  56. * List of operators and corresponding boolean functions.
  57. * These functions are passed the attribute and the current node's value of the attribute.
  58. * @property operators
  59. * @type object
  60. */
  61. operators: {
  62. '=': function(attr, val) { return attr === val; }, // Equality
  63. '!=': function(attr, val) { return attr !== val; }, // Inequality
  64. '~=': function(attr, val) { // Match one of space seperated words
  65. var s = ' ';
  66. return (s + attr + s).indexOf((s + val + s)) > -1;
  67. },
  68. '|=': function(attr, val) { return attr === val || attr.slice(0, val.length + 1) === val + '-'; }, // Matches value followed by optional hyphen
  69. '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
  70. '$=': function(attr, val) { return attr.slice(-val.length) === val; }, // Match ends with value
  71. '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring
  72. '': function(attr, val) { return attr; } // Just test for existence of attribute
  73. },
  74. /**
  75. * List of pseudo-classes and corresponding boolean functions.
  76. * These functions are called with the current node, and any value that was parsed with the pseudo regex.
  77. * @property pseudos
  78. * @type object
  79. */
  80. pseudos: {
  81. 'root': function(node) {
  82. return node === node.ownerDocument.documentElement;
  83. },
  84. 'nth-child': function(node, val) {
  85. return Y.Selector._getNth(node, val);
  86. },
  87. 'nth-last-child': function(node, val) {
  88. return Y.Selector._getNth(node, val, null, true);
  89. },
  90. 'nth-of-type': function(node, val) {
  91. return Y.Selector._getNth(node, val, node.tagName);
  92. },
  93. 'nth-last-of-type': function(node, val) {
  94. return Y.Selector._getNth(node, val, node.tagName, true);
  95. },
  96. 'first-child': function(node) {
  97. return Y.Selector._getChildren(node.parentNode)[0] === node;
  98. },
  99. 'last-child': function(node) {
  100. var children = Y.Selector._getChildren(node.parentNode);
  101. return children[children.length - 1] === node;
  102. },
  103. 'first-of-type': function(node, val) {
  104. return Y.Selector._getChildren(node.parentNode, node.tagName)[0];
  105. },
  106. 'last-of-type': function(node, val) {
  107. var children = Y.Selector._getChildren(node.parentNode, node.tagName);
  108. return children[children.length - 1];
  109. },
  110. 'only-child': function(node) {
  111. var children = Y.Selector._getChildren(node.parentNode);
  112. return children.length === 1 && children[0] === node;
  113. },
  114. 'only-of-type': function(node) {
  115. return Y.Selector._getChildren(node.parentNode, node.tagName).length === 1;
  116. },
  117. 'empty': function(node) {
  118. return node.childNodes.length === 0;
  119. },
  120. 'not': function(node, simple) {
  121. return !Y.Selector.test(node, simple);
  122. },
  123. 'contains': function(node, str) {
  124. var text = node.innerText || node.textContent || '';
  125. return text.indexOf(str) > -1;
  126. },
  127. 'checked': function(node) {
  128. return node.checked === true;
  129. }
  130. },
  131. /**
  132. * Test if the supplied node matches the supplied selector.
  133. * @method test
  134. *
  135. * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
  136. * @param {string} selector The CSS Selector to test the node against.
  137. * @return{boolean} Whether or not the node matches the selector.
  138. * @static
  139. */
  140. test: function(node, selector) {
  141. node = Y.Selector.document.getElementById(node) || node;
  142. if (!node) {
  143. return false;
  144. }
  145. var groups = selector ? selector.split(',') : [];
  146. if (groups.length) {
  147. for (var i = 0, len = groups.length; i < len; ++i) {
  148. if ( Y.Selector._test(node, groups[i]) ) { // passes if ANY group matches
  149. return true;
  150. }
  151. }
  152. return false;
  153. }
  154. return Y.Selector._test(node, selector);
  155. },
  156. _test: function(node, selector, token, deDupe) {
  157. token = token || Y.Selector._tokenize(selector).pop() || {};
  158. if (!node.tagName ||
  159. (token.tag !== '*' && node.tagName !== token.tag) ||
  160. (deDupe && node._found) ) {
  161. return false;
  162. }
  163. if (token.attributes.length) {
  164. var val,
  165. ieFlag,
  166. re_urls = Y.Selector._re.urls;
  167. if (!node.attributes || !node.attributes.length) {
  168. return false;
  169. }
  170. for (var i = 0, attr; attr = token.attributes[i++];) {
  171. ieFlag = (re_urls.test(attr[0])) ? 2 : 0;
  172. val = node.getAttribute(attr[0], ieFlag);
  173. if (val === null || val === undefined) {
  174. return false;
  175. }
  176. if ( Y.Selector.operators[attr[1]] &&
  177. !Y.Selector.operators[attr[1]](val, attr[2])) {
  178. return false;
  179. }
  180. }
  181. }
  182. if (token.pseudos.length) {
  183. for (var i = 0, len = token.pseudos.length; i < len; ++i) {
  184. if (Y.Selector.pseudos[token.pseudos[i][0]] &&
  185. !Y.Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
  186. return false;
  187. }
  188. }
  189. }
  190. return (token.previous && token.previous.combinator !== ',') ?
  191. Y.Selector._combinators[token.previous.combinator](node, token) :
  192. true;
  193. },
  194. /**
  195. * Filters a set of nodes based on a given CSS selector.
  196. * @method filter
  197. *
  198. * @param {array} nodes A set of nodes/ids to filter.
  199. * @param {string} selector The selector used to test each node.
  200. * @return{array} An array of nodes from the supplied array that match the given selector.
  201. * @static
  202. */
  203. filter: function(nodes, selector) {
  204. nodes = nodes || [];
  205. var node,
  206. result = [],
  207. tokens = Y.Selector._tokenize(selector);
  208. if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes
  209. for (var i = 0, len = nodes.length; i < len; ++i) {
  210. if (!nodes[i].tagName) { // tagName limits to HTMLElements
  211. node = Y.Selector.document.getElementById(nodes[i]);
  212. if (node) { // skip IDs that return null
  213. nodes[i] = node;
  214. } else {
  215. }
  216. }
  217. }
  218. }
  219. result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
  220. return result;
  221. },
  222. _filter: function(nodes, token, firstOnly, deDupe) {
  223. var result = firstOnly ? null : [],
  224. foundCache = Y.Selector._foundCache;
  225. for (var i = 0, len = nodes.length; i < len; i++) {
  226. if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
  227. continue;
  228. }
  229. if (firstOnly) {
  230. return nodes[i];
  231. }
  232. if (deDupe) {
  233. if (nodes[i]._found) {
  234. continue;
  235. }
  236. nodes[i]._found = true;
  237. foundCache[foundCache.length] = nodes[i];
  238. }
  239. result[result.length] = nodes[i];
  240. }
  241. return result;
  242. },
  243. /**
  244. * Retrieves a set of nodes based on a given CSS selector.
  245. * @method query
  246. *
  247. * @param {string} selector The CSS Selector to test the node against.
  248. * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
  249. * @param {Boolean} firstOnly optional Whether or not to return only the first match.
  250. * @return {Array} An array of nodes that match the given selector.
  251. * @static
  252. */
  253. query: function(selector, root, firstOnly) {
  254. var result = Y.Selector._query(selector, root, firstOnly);
  255. return result;
  256. },
  257. _query: function(selector, root, firstOnly, deDupe) {
  258. var result = (firstOnly) ? null : [],
  259. node;
  260. if (!selector) {
  261. return result;
  262. }
  263. var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
  264. if (groups.length > 1) {
  265. var found;
  266. for (var i = 0, len = groups.length; i < len; ++i) {
  267. found = Y.Selector._query(groups[i], root, firstOnly, true);
  268. result = firstOnly ? found : result.concat(found);
  269. }
  270. Y.Selector._clearFoundCache();
  271. return result;
  272. }
  273. if (root && !root.nodeName) { // assume ID
  274. root = Y.Selector.document.getElementById(root);
  275. if (!root) {
  276. return result;
  277. }
  278. }
  279. root = root || Y.Selector.document;
  280. if (root.nodeName !== '#document') { // prepend with root selector
  281. Y.Dom.generateId(root); // TODO: cleanup after?
  282. selector = root.tagName + '#' + root.id + ' ' + selector;
  283. node = root;
  284. root = root.ownerDocument;
  285. }
  286. var tokens = Y.Selector._tokenize(selector);
  287. var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
  288. nodes = [],
  289. id,
  290. token = tokens.pop() || {};
  291. if (idToken) {
  292. id = Y.Selector._getId(idToken.attributes);
  293. }
  294. // use id shortcut when possible
  295. if (id) {
  296. node = node || Y.Selector.document.getElementById(id);
  297. if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
  298. if ( Y.Selector._test(node, null, idToken) ) {
  299. if (idToken === token) {
  300. nodes = [node]; // simple selector
  301. } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
  302. root = node; // start from here
  303. }
  304. }
  305. } else {
  306. return result;
  307. }
  308. }
  309. if (root && !nodes.length) {
  310. nodes = root.getElementsByTagName(token.tag);
  311. }
  312. if (nodes.length) {
  313. result = Y.Selector._filter(nodes, token, firstOnly, deDupe);
  314. }
  315. return result;
  316. },
  317. _clearFoundCache: function() {
  318. var foundCache = Y.Selector._foundCache;
  319. for (var i = 0, len = foundCache.length; i < len; ++i) {
  320. try { // IE no like delete
  321. delete foundCache[i]._found;
  322. } catch(e) {
  323. foundCache[i].removeAttribute('_found');
  324. }
  325. }
  326. foundCache = [];
  327. },
  328. _getRegExp: function(str, flags) {
  329. var regexCache = Y.Selector._regexCache;
  330. flags = flags || '';
  331. if (!regexCache[str + flags]) {
  332. regexCache[str + flags] = new RegExp(str, flags);
  333. }
  334. return regexCache[str + flags];
  335. },
  336. _getChildren: function() {
  337. if (document.documentElement.children && document.documentElement.children.tags) { // document for capability test
  338. return function(node, tag) {
  339. return (tag) ? node.children.tags(tag) : node.children || [];
  340. };
  341. } else {
  342. return function(node, tag) {
  343. var children = [],
  344. childNodes = node.childNodes;
  345. for (var i = 0, len = childNodes.length; i < len; ++i) {
  346. if (childNodes[i].tagName) {
  347. if (!tag || childNodes[i].tagName === tag) {
  348. children.push(childNodes[i]);
  349. }
  350. }
  351. }
  352. return children;
  353. };
  354. }
  355. }(),
  356. _combinators: {
  357. ' ': function(node, token) {
  358. while ( (node = node.parentNode) ) {
  359. if (Y.Selector._test(node, '', token.previous)) {
  360. return true;
  361. }
  362. }
  363. return false;
  364. },
  365. '>': function(node, token) {
  366. return Y.Selector._test(node.parentNode, null, token.previous);
  367. },
  368. '+': function(node, token) {
  369. var sib = node.previousSibling;
  370. while (sib && sib.nodeType !== 1) {
  371. sib = sib.previousSibling;
  372. }
  373. if (sib && Y.Selector._test(sib, null, token.previous)) {
  374. return true;
  375. }
  376. return false;
  377. },
  378. '~': function(node, token) {
  379. var sib = node.previousSibling;
  380. while (sib) {
  381. if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
  382. return true;
  383. }
  384. sib = sib.previousSibling;
  385. }
  386. return false;
  387. }
  388. },
  389. /*
  390. an+b = get every _a_th node starting at the _b_th
  391. 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
  392. 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
  393. an+0 = get every _a_th element, "0" may be omitted
  394. */
  395. _getNth: function(node, expr, tag, reverse) {
  396. Y.Selector._re.nth.test(expr);
  397. var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
  398. n = RegExp.$2, // "n"
  399. oddeven = RegExp.$3, // "odd" or "even"
  400. b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
  401. result = [],
  402. op;
  403. var siblings = Y.Selector._getChildren(node.parentNode, tag);
  404. if (oddeven) {
  405. a = 2; // always every other
  406. op = '+';
  407. n = 'n';
  408. b = (oddeven === 'odd') ? 1 : 0;
  409. } else if ( isNaN(a) ) {
  410. a = (n) ? 1 : 0; // start from the first or no repeat
  411. }
  412. if (a === 0) { // just the first
  413. if (reverse) {
  414. b = siblings.length - b + 1;
  415. }
  416. if (siblings[b - 1] === node) {
  417. return true;
  418. } else {
  419. return false;
  420. }
  421. } else if (a < 0) {
  422. reverse = !!reverse;
  423. a = Math.abs(a);
  424. }
  425. if (!reverse) {
  426. for (var i = b - 1, len = siblings.length; i < len; i += a) {
  427. if ( i >= 0 && siblings[i] === node ) {
  428. return true;
  429. }
  430. }
  431. } else {
  432. for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
  433. if ( i < len && siblings[i] === node ) {
  434. return true;
  435. }
  436. }
  437. }
  438. return false;
  439. },
  440. _getId: function(attr) {
  441. for (var i = 0, len = attr.length; i < len; ++i) {
  442. if (attr[i][0] == 'id' && attr[i][1] === '=') {
  443. return attr[i][2];
  444. }
  445. }
  446. },
  447. _getIdTokenIndex: function(tokens) {
  448. for (var i = 0, len = tokens.length; i < len; ++i) {
  449. if (Y.Selector._getId(tokens[i].attributes)) {
  450. return i;
  451. }
  452. }
  453. return -1;
  454. },
  455. _patterns: {
  456. tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
  457. attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
  458. pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
  459. combinator: /^\s*([>+~]|\s)\s*/
  460. },
  461. /**
  462. Break selector into token units per simple selector.
  463. Combinator is attached to left-hand selector.
  464. */
  465. _tokenize: function(selector) {
  466. var token = {}, // one token per simple selector (left selector holds combinator)
  467. tokens = [], // array of tokens
  468. id, // unique id for the simple selector (if found)
  469. found = false, // whether or not any matches were found this pass
  470. patterns = Y.Selector._patterns,
  471. match; // the regex match
  472. selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
  473. /*
  474. Search for selector patterns, store, and strip them from the selector string
  475. until no patterns match (invalid selector) or we run out of chars.
  476. Multiple attributes and pseudos are allowed, in any order.
  477. for example:
  478. 'form:first-child[type=button]:not(button)[lang|=en]'
  479. */
  480. do {
  481. found = false; // reset after full pass
  482. for (var re in patterns) {
  483. if (YAHOO.lang.hasOwnProperty(patterns, re)) {
  484. if (re != 'tag' && re != 'combinator') { // only one allowed
  485. token[re] = token[re] || [];
  486. }
  487. if ( (match = patterns[re].exec(selector)) ) { // note assignment
  488. found = true;
  489. if (re != 'tag' && re != 'combinator') { // only one allowed
  490. // capture ID for fast path to element
  491. if (re === 'attributes' && match[1] === 'id') {
  492. token.id = match[3];
  493. }
  494. token[re].push(match.slice(1));
  495. } else { // single selector (tag, combinator)
  496. token[re] = match[1];
  497. }
  498. selector = selector.replace(match[0], ''); // strip current match from selector
  499. if (re === 'combinator' || !selector.length) { // next token or done
  500. token.attributes = Y.Selector._fixAttributes(token.attributes);
  501. token.pseudos = token.pseudos || [];
  502. token.tag = token.tag ? token.tag.toUpperCase() : '*';
  503. tokens.push(token);
  504. token = { // prep next token
  505. previous: token
  506. };
  507. }
  508. }
  509. }
  510. }
  511. } while (found);
  512. return tokens;
  513. },
  514. _fixAttributes: function(attr) {
  515. var aliases = Y.Selector.attrAliases;
  516. attr = attr || [];
  517. for (var i = 0, len = attr.length; i < len; ++i) {
  518. if (aliases[attr[i][0]]) { // convert reserved words, etc
  519. attr[i][0] = aliases[attr[i][0]];
  520. }
  521. if (!attr[i][1]) { // use exists operator
  522. attr[i][1] = '';
  523. }
  524. }
  525. return attr;
  526. },
  527. _replaceShorthand: function(selector) {
  528. var shorthand = Y.Selector.shorthand;
  529. //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
  530. var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
  531. if (attrs) {
  532. selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
  533. }
  534. for (var re in shorthand) {
  535. if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
  536. selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
  537. }
  538. }
  539. if (attrs) {
  540. for (var i = 0, len = attrs.length; i < len; ++i) {
  541. selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
  542. }
  543. }
  544. return selector;
  545. }
  546. };
  547. if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
  548. Y.Selector.attrAliases['class'] = 'className';
  549. Y.Selector.attrAliases['for'] = 'htmlFor';
  550. }
  551. })();
  552. YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});