selector-debug.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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. YAHOO.log('filter: scanning input for HTMLElements/IDs', 'info', 'Selector');
  210. for (var i = 0, len = nodes.length; i < len; ++i) {
  211. if (!nodes[i].tagName) { // tagName limits to HTMLElements
  212. node = Y.Selector.document.getElementById(nodes[i]);
  213. if (node) { // skip IDs that return null
  214. nodes[i] = node;
  215. } else {
  216. YAHOO.log('filter: skipping invalid node', 'warn', 'Selector');
  217. }
  218. }
  219. }
  220. }
  221. result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
  222. YAHOO.log('filter: returning:' + result.length, 'info', 'Selector');
  223. return result;
  224. },
  225. _filter: function(nodes, token, firstOnly, deDupe) {
  226. var result = firstOnly ? null : [],
  227. foundCache = Y.Selector._foundCache;
  228. for (var i = 0, len = nodes.length; i < len; i++) {
  229. if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
  230. continue;
  231. }
  232. if (firstOnly) {
  233. return nodes[i];
  234. }
  235. if (deDupe) {
  236. if (nodes[i]._found) {
  237. continue;
  238. }
  239. nodes[i]._found = true;
  240. foundCache[foundCache.length] = nodes[i];
  241. }
  242. result[result.length] = nodes[i];
  243. }
  244. return result;
  245. },
  246. /**
  247. * Retrieves a set of nodes based on a given CSS selector.
  248. * @method query
  249. *
  250. * @param {string} selector The CSS Selector to test the node against.
  251. * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
  252. * @param {Boolean} firstOnly optional Whether or not to return only the first match.
  253. * @return {Array} An array of nodes that match the given selector.
  254. * @static
  255. */
  256. query: function(selector, root, firstOnly) {
  257. var result = Y.Selector._query(selector, root, firstOnly);
  258. YAHOO.log('query: returning ' + result, 'info', 'Selector');
  259. return result;
  260. },
  261. _query: function(selector, root, firstOnly, deDupe) {
  262. var result = (firstOnly) ? null : [],
  263. node;
  264. if (!selector) {
  265. return result;
  266. }
  267. var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
  268. if (groups.length > 1) {
  269. var found;
  270. for (var i = 0, len = groups.length; i < len; ++i) {
  271. found = Y.Selector._query(groups[i], root, firstOnly, true);
  272. result = firstOnly ? found : result.concat(found);
  273. }
  274. Y.Selector._clearFoundCache();
  275. return result;
  276. }
  277. if (root && !root.nodeName) { // assume ID
  278. root = Y.Selector.document.getElementById(root);
  279. if (!root) {
  280. YAHOO.log('invalid root node provided', 'warn', 'Selector');
  281. return result;
  282. }
  283. }
  284. root = root || Y.Selector.document;
  285. if (root.nodeName !== '#document') { // prepend with root selector
  286. Y.Dom.generateId(root); // TODO: cleanup after?
  287. selector = root.tagName + '#' + root.id + ' ' + selector;
  288. node = root;
  289. root = root.ownerDocument;
  290. }
  291. var tokens = Y.Selector._tokenize(selector);
  292. var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
  293. nodes = [],
  294. id,
  295. token = tokens.pop() || {};
  296. if (idToken) {
  297. id = Y.Selector._getId(idToken.attributes);
  298. }
  299. // use id shortcut when possible
  300. if (id) {
  301. node = node || Y.Selector.document.getElementById(id);
  302. if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
  303. if ( Y.Selector._test(node, null, idToken) ) {
  304. if (idToken === token) {
  305. nodes = [node]; // simple selector
  306. } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
  307. root = node; // start from here
  308. }
  309. }
  310. } else {
  311. return result;
  312. }
  313. }
  314. if (root && !nodes.length) {
  315. nodes = root.getElementsByTagName(token.tag);
  316. }
  317. if (nodes.length) {
  318. result = Y.Selector._filter(nodes, token, firstOnly, deDupe);
  319. }
  320. return result;
  321. },
  322. _clearFoundCache: function() {
  323. var foundCache = Y.Selector._foundCache;
  324. YAHOO.log('getBySelector: clearing found cache of ' + foundCache.length + ' elements');
  325. for (var i = 0, len = foundCache.length; i < len; ++i) {
  326. try { // IE no like delete
  327. delete foundCache[i]._found;
  328. } catch(e) {
  329. foundCache[i].removeAttribute('_found');
  330. }
  331. }
  332. foundCache = [];
  333. YAHOO.log('getBySelector: done clearing foundCache');
  334. },
  335. _getRegExp: function(str, flags) {
  336. var regexCache = Y.Selector._regexCache;
  337. flags = flags || '';
  338. if (!regexCache[str + flags]) {
  339. regexCache[str + flags] = new RegExp(str, flags);
  340. }
  341. return regexCache[str + flags];
  342. },
  343. _getChildren: function() {
  344. if (document.documentElement.children && document.documentElement.children.tags) { // document for capability test
  345. return function(node, tag) {
  346. return (tag) ? node.children.tags(tag) : node.children || [];
  347. };
  348. } else {
  349. return function(node, tag) {
  350. var children = [],
  351. childNodes = node.childNodes;
  352. for (var i = 0, len = childNodes.length; i < len; ++i) {
  353. if (childNodes[i].tagName) {
  354. if (!tag || childNodes[i].tagName === tag) {
  355. children.push(childNodes[i]);
  356. }
  357. }
  358. }
  359. return children;
  360. };
  361. }
  362. }(),
  363. _combinators: {
  364. ' ': function(node, token) {
  365. while ( (node = node.parentNode) ) {
  366. if (Y.Selector._test(node, '', token.previous)) {
  367. return true;
  368. }
  369. }
  370. return false;
  371. },
  372. '>': function(node, token) {
  373. return Y.Selector._test(node.parentNode, null, token.previous);
  374. },
  375. '+': function(node, token) {
  376. var sib = node.previousSibling;
  377. while (sib && sib.nodeType !== 1) {
  378. sib = sib.previousSibling;
  379. }
  380. if (sib && Y.Selector._test(sib, null, token.previous)) {
  381. return true;
  382. }
  383. return false;
  384. },
  385. '~': function(node, token) {
  386. var sib = node.previousSibling;
  387. while (sib) {
  388. if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
  389. return true;
  390. }
  391. sib = sib.previousSibling;
  392. }
  393. return false;
  394. }
  395. },
  396. /*
  397. an+b = get every _a_th node starting at the _b_th
  398. 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
  399. 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
  400. an+0 = get every _a_th element, "0" may be omitted
  401. */
  402. _getNth: function(node, expr, tag, reverse) {
  403. Y.Selector._re.nth.test(expr);
  404. var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
  405. n = RegExp.$2, // "n"
  406. oddeven = RegExp.$3, // "odd" or "even"
  407. b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
  408. result = [],
  409. op;
  410. var siblings = Y.Selector._getChildren(node.parentNode, tag);
  411. if (oddeven) {
  412. a = 2; // always every other
  413. op = '+';
  414. n = 'n';
  415. b = (oddeven === 'odd') ? 1 : 0;
  416. } else if ( isNaN(a) ) {
  417. a = (n) ? 1 : 0; // start from the first or no repeat
  418. }
  419. if (a === 0) { // just the first
  420. if (reverse) {
  421. b = siblings.length - b + 1;
  422. }
  423. if (siblings[b - 1] === node) {
  424. return true;
  425. } else {
  426. return false;
  427. }
  428. } else if (a < 0) {
  429. reverse = !!reverse;
  430. a = Math.abs(a);
  431. }
  432. if (!reverse) {
  433. for (var i = b - 1, len = siblings.length; i < len; i += a) {
  434. if ( i >= 0 && siblings[i] === node ) {
  435. return true;
  436. }
  437. }
  438. } else {
  439. for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
  440. if ( i < len && siblings[i] === node ) {
  441. return true;
  442. }
  443. }
  444. }
  445. return false;
  446. },
  447. _getId: function(attr) {
  448. for (var i = 0, len = attr.length; i < len; ++i) {
  449. if (attr[i][0] == 'id' && attr[i][1] === '=') {
  450. return attr[i][2];
  451. }
  452. }
  453. },
  454. _getIdTokenIndex: function(tokens) {
  455. for (var i = 0, len = tokens.length; i < len; ++i) {
  456. if (Y.Selector._getId(tokens[i].attributes)) {
  457. return i;
  458. }
  459. }
  460. return -1;
  461. },
  462. _patterns: {
  463. tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
  464. attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
  465. pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
  466. combinator: /^\s*([>+~]|\s)\s*/
  467. },
  468. /**
  469. Break selector into token units per simple selector.
  470. Combinator is attached to left-hand selector.
  471. */
  472. _tokenize: function(selector) {
  473. var token = {}, // one token per simple selector (left selector holds combinator)
  474. tokens = [], // array of tokens
  475. id, // unique id for the simple selector (if found)
  476. found = false, // whether or not any matches were found this pass
  477. patterns = Y.Selector._patterns,
  478. match; // the regex match
  479. selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
  480. /*
  481. Search for selector patterns, store, and strip them from the selector string
  482. until no patterns match (invalid selector) or we run out of chars.
  483. Multiple attributes and pseudos are allowed, in any order.
  484. for example:
  485. 'form:first-child[type=button]:not(button)[lang|=en]'
  486. */
  487. do {
  488. found = false; // reset after full pass
  489. for (var re in patterns) {
  490. if (YAHOO.lang.hasOwnProperty(patterns, re)) {
  491. if (re != 'tag' && re != 'combinator') { // only one allowed
  492. token[re] = token[re] || [];
  493. }
  494. if ( (match = patterns[re].exec(selector)) ) { // note assignment
  495. found = true;
  496. if (re != 'tag' && re != 'combinator') { // only one allowed
  497. // capture ID for fast path to element
  498. if (re === 'attributes' && match[1] === 'id') {
  499. token.id = match[3];
  500. }
  501. token[re].push(match.slice(1));
  502. } else { // single selector (tag, combinator)
  503. token[re] = match[1];
  504. }
  505. selector = selector.replace(match[0], ''); // strip current match from selector
  506. if (re === 'combinator' || !selector.length) { // next token or done
  507. token.attributes = Y.Selector._fixAttributes(token.attributes);
  508. token.pseudos = token.pseudos || [];
  509. token.tag = token.tag ? token.tag.toUpperCase() : '*';
  510. tokens.push(token);
  511. token = { // prep next token
  512. previous: token
  513. };
  514. }
  515. }
  516. }
  517. }
  518. } while (found);
  519. return tokens;
  520. },
  521. _fixAttributes: function(attr) {
  522. var aliases = Y.Selector.attrAliases;
  523. attr = attr || [];
  524. for (var i = 0, len = attr.length; i < len; ++i) {
  525. if (aliases[attr[i][0]]) { // convert reserved words, etc
  526. attr[i][0] = aliases[attr[i][0]];
  527. }
  528. if (!attr[i][1]) { // use exists operator
  529. attr[i][1] = '';
  530. }
  531. }
  532. return attr;
  533. },
  534. _replaceShorthand: function(selector) {
  535. var shorthand = Y.Selector.shorthand;
  536. //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
  537. var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
  538. if (attrs) {
  539. selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
  540. }
  541. for (var re in shorthand) {
  542. if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
  543. selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
  544. }
  545. }
  546. if (attrs) {
  547. for (var i = 0, len = attrs.length; i < len; ++i) {
  548. selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
  549. }
  550. }
  551. return selector;
  552. }
  553. };
  554. if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
  555. Y.Selector.attrAliases['class'] = 'className';
  556. Y.Selector.attrAliases['for'] = 'htmlFor';
  557. }
  558. })();
  559. YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});