API Docs for: 3.17.2
Show:

File: dom/js/selector-native.js

  1. (function(Y) {
  2. /**
  3. * The selector-native module provides support for native querySelector
  4. * @module dom
  5. * @submodule selector-native
  6. * @for Selector
  7. */
  8.  
  9. /**
  10. * Provides support for using CSS selectors to query the DOM
  11. * @class Selector
  12. * @static
  13. * @for Selector
  14. */
  15.  
  16. Y.namespace('Selector'); // allow native module to standalone
  17.  
  18. var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
  19. OWNER_DOCUMENT = 'ownerDocument';
  20.  
  21. var Selector = {
  22. _types: {
  23. esc: {
  24. token: '\uE000',
  25. re: /\\[:\[\]\(\)#\.\'\>+~"]/gi
  26. },
  27.  
  28. attr: {
  29. token: '\uE001',
  30. re: /(\[[^\]]*\])/g
  31. },
  32.  
  33. pseudo: {
  34. token: '\uE002',
  35. re: /(\([^\)]*\))/g
  36. }
  37. },
  38.  
  39. /**
  40. * Use the native version of `querySelectorAll`, if it exists.
  41. *
  42. * @property useNative
  43. * @default true
  44. * @static
  45. */
  46. useNative: true,
  47.  
  48. _escapeId: function(id) {
  49. if (id) {
  50. id = id.replace(/([:\[\]\(\)#\.'<>+~"])/g,'\\$1');
  51. }
  52. return id;
  53. },
  54.  
  55. _compare: ('sourceIndex' in Y.config.doc.documentElement) ?
  56. function(nodeA, nodeB) {
  57. var a = nodeA.sourceIndex,
  58. b = nodeB.sourceIndex;
  59.  
  60. if (a === b) {
  61. return 0;
  62. } else if (a > b) {
  63. return 1;
  64. }
  65.  
  66. return -1;
  67.  
  68. } : (Y.config.doc.documentElement[COMPARE_DOCUMENT_POSITION] ?
  69. function(nodeA, nodeB) {
  70. if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
  71. return -1;
  72. } else {
  73. return 1;
  74. }
  75. } :
  76. function(nodeA, nodeB) {
  77. var rangeA, rangeB, compare;
  78. if (nodeA && nodeB) {
  79. rangeA = nodeA[OWNER_DOCUMENT].createRange();
  80. rangeA.setStart(nodeA, 0);
  81. rangeB = nodeB[OWNER_DOCUMENT].createRange();
  82. rangeB.setStart(nodeB, 0);
  83. compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
  84. }
  85.  
  86. return compare;
  87.  
  88. }),
  89.  
  90. _sort: function(nodes) {
  91. if (nodes) {
  92. nodes = Y.Array(nodes, 0, true);
  93. if (nodes.sort) {
  94. nodes.sort(Selector._compare);
  95. }
  96. }
  97.  
  98. return nodes;
  99. },
  100.  
  101. _deDupe: function(nodes) {
  102. var ret = [],
  103. i, node;
  104.  
  105. for (i = 0; (node = nodes[i++]);) {
  106. if (!node._found) {
  107. ret[ret.length] = node;
  108. node._found = true;
  109. }
  110. }
  111.  
  112. for (i = 0; (node = ret[i++]);) {
  113. node._found = null;
  114. node.removeAttribute('_found');
  115. }
  116.  
  117. return ret;
  118. },
  119.  
  120. /**
  121. * Retrieves a set of nodes based on a given CSS selector.
  122. * @method query
  123. *
  124. * @param {String} selector A CSS selector.
  125. * @param {HTMLElement} root optional A node to start the query from. Defaults to `Y.config.doc`.
  126. * @param {Boolean} firstOnly optional Whether or not to return only the first match.
  127. * @return {HTMLElement[]} The array of nodes that matched the given selector.
  128. * @static
  129. */
  130. query: function(selector, root, firstOnly, skipNative) {
  131. root = root || Y.config.doc;
  132. var ret = [],
  133. useNative = (Y.Selector.useNative && Y.config.doc.querySelector && !skipNative),
  134. queries = [[selector, root]],
  135. query,
  136. result,
  137. i,
  138. fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
  139.  
  140. if (selector && fn) {
  141. // split group into seperate queries
  142. if (!skipNative && // already done if skipping
  143. (!useNative || root.tagName)) { // split native when element scoping is needed
  144. queries = Selector._splitQueries(selector, root);
  145. }
  146.  
  147. for (i = 0; (query = queries[i++]);) {
  148. result = fn(query[0], query[1], firstOnly);
  149. if (!firstOnly) { // coerce DOM Collection to Array
  150. result = Y.Array(result, 0, true);
  151. }
  152. if (result) {
  153. ret = ret.concat(result);
  154. }
  155. }
  156.  
  157. if (queries.length > 1) { // remove dupes and sort by doc order
  158. ret = Selector._sort(Selector._deDupe(ret));
  159. }
  160. }
  161.  
  162. Y.log('query: ' + selector + ' returning: ' + ret.length, 'info', 'Selector');
  163. return (firstOnly) ? (ret[0] || null) : ret;
  164.  
  165. },
  166.  
  167. _replaceSelector: function(selector) {
  168. var esc = Y.Selector._parse('esc', selector), // pull escaped colon, brackets, etc.
  169. attrs,
  170. pseudos;
  171.  
  172. // first replace escaped chars, which could be present in attrs or pseudos
  173. selector = Y.Selector._replace('esc', selector);
  174.  
  175. // then replace pseudos before attrs to avoid replacing :not([foo])
  176. pseudos = Y.Selector._parse('pseudo', selector);
  177. selector = Selector._replace('pseudo', selector);
  178.  
  179. attrs = Y.Selector._parse('attr', selector);
  180. selector = Y.Selector._replace('attr', selector);
  181.  
  182. return {
  183. esc: esc,
  184. attrs: attrs,
  185. pseudos: pseudos,
  186. selector: selector
  187. };
  188. },
  189.  
  190. _restoreSelector: function(replaced) {
  191. var selector = replaced.selector;
  192. selector = Y.Selector._restore('attr', selector, replaced.attrs);
  193. selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
  194. selector = Y.Selector._restore('esc', selector, replaced.esc);
  195. return selector;
  196. },
  197.  
  198. _replaceCommas: function(selector) {
  199. var replaced = Y.Selector._replaceSelector(selector),
  200. selector = replaced.selector;
  201.  
  202. if (selector) {
  203. selector = selector.replace(/,/g, '\uE007');
  204. replaced.selector = selector;
  205. selector = Y.Selector._restoreSelector(replaced);
  206. }
  207. return selector;
  208. },
  209.  
  210. // allows element scoped queries to begin with combinator
  211. // e.g. query('> p', document.body) === query('body > p')
  212. _splitQueries: function(selector, node) {
  213. if (selector.indexOf(',') > -1) {
  214. selector = Y.Selector._replaceCommas(selector);
  215. }
  216.  
  217. var groups = selector.split('\uE007'), // split on replaced comma token
  218. queries = [],
  219. prefix = '',
  220. id,
  221. i,
  222. len;
  223.  
  224. if (node) {
  225. // enforce for element scoping
  226. if (node.nodeType === 1) { // Elements only
  227. id = Y.Selector._escapeId(Y.DOM.getId(node));
  228.  
  229. if (!id) {
  230. id = Y.guid();
  231. Y.DOM.setId(node, id);
  232. }
  233.  
  234. prefix = '[id="' + id + '"] ';
  235. }
  236.  
  237. for (i = 0, len = groups.length; i < len; ++i) {
  238. selector = prefix + groups[i];
  239. queries.push([selector, node]);
  240. }
  241. }
  242.  
  243. return queries;
  244. },
  245.  
  246. _nativeQuery: function(selector, root, one) {
  247. if (
  248. (Y.UA.webkit || Y.UA.opera) && // webkit (chrome, safari) and Opera
  249. selector.indexOf(':checked') > -1 && // fail to pick up "selected" with ":checked"
  250. (Y.Selector.pseudos && Y.Selector.pseudos.checked)
  251. ) {
  252. return Y.Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
  253. }
  254. try {
  255. //Y.log('trying native query with: ' + selector, 'info', 'selector-native');
  256. return root['querySelector' + (one ? '' : 'All')](selector);
  257. } catch(e) { // fallback to brute if available
  258. //Y.log('native query error; reverting to brute query with: ' + selector, 'info', 'selector-native');
  259. return Y.Selector.query(selector, root, one, true); // redo with skipNative true
  260. }
  261. },
  262.  
  263. /**
  264. * Filters out nodes that do not match the given CSS selector.
  265. * @method filter
  266. *
  267. * @param {HTMLElement[]} nodes An array of nodes.
  268. * @param {String} selector A CSS selector to test each node against.
  269. * @return {HTMLElement[]} The nodes that matched the given CSS selector.
  270. * @static
  271. */
  272. filter: function(nodes, selector) {
  273. var ret = [],
  274. i, node;
  275.  
  276. if (nodes && selector) {
  277. for (i = 0; (node = nodes[i++]);) {
  278. if (Y.Selector.test(node, selector)) {
  279. ret[ret.length] = node;
  280. }
  281. }
  282. } else {
  283. Y.log('invalid filter input (nodes: ' + nodes +
  284. ', selector: ' + selector + ')', 'warn', 'Selector');
  285. }
  286.  
  287. return ret;
  288. },
  289.  
  290. /**
  291. * Determines whether or not the given node matches the given CSS selector.
  292. * @method test
  293. *
  294. * @param {HTMLElement} node A node to test.
  295. * @param {String} selector A CSS selector to test the node against.
  296. * @param {HTMLElement} root optional A node to start the query from. Defaults to the parent document of the node.
  297. * @return {Boolean} Whether or not the given node matched the given CSS selector.
  298. * @static
  299. */
  300. test: function(node, selector, root) {
  301. var ret = false,
  302. useFrag = false,
  303. groups,
  304. parent,
  305. item,
  306. items,
  307. frag,
  308. id,
  309. i, j, group;
  310.  
  311. if (node && node.tagName) { // only test HTMLElements
  312.  
  313. if (typeof selector == 'function') { // test with function
  314. ret = selector.call(node, node);
  315. } else { // test with query
  316. // we need a root if off-doc
  317. groups = selector.split(',');
  318. if (!root && !Y.DOM.inDoc(node)) {
  319. parent = node.parentNode;
  320. if (parent) {
  321. root = parent;
  322. } else { // only use frag when no parent to query
  323. frag = node[OWNER_DOCUMENT].createDocumentFragment();
  324. frag.appendChild(node);
  325. root = frag;
  326. useFrag = true;
  327. }
  328. }
  329. root = root || node[OWNER_DOCUMENT];
  330.  
  331. id = Y.Selector._escapeId(Y.DOM.getId(node));
  332. if (!id) {
  333. id = Y.guid();
  334. Y.DOM.setId(node, id);
  335. }
  336.  
  337. for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
  338. group += '[id="' + id + '"]';
  339. items = Y.Selector.query(group, root);
  340.  
  341. for (j = 0; item = items[j++];) {
  342. if (item === node) {
  343. ret = true;
  344. break;
  345. }
  346. }
  347. if (ret) {
  348. break;
  349. }
  350. }
  351.  
  352. if (useFrag) { // cleanup
  353. frag.removeChild(node);
  354. }
  355. };
  356. }
  357.  
  358. return ret;
  359. },
  360.  
  361. /**
  362. * A convenience method to emulate Y.Node's aNode.ancestor(selector).
  363. * @method ancestor
  364. *
  365. * @param {HTMLElement} node A node to start the query from.
  366. * @param {String} selector A CSS selector to test the node against.
  367. * @param {Boolean} testSelf optional Whether or not to include the node in the scan.
  368. * @return {HTMLElement} The ancestor node matching the selector, or null.
  369. * @static
  370. */
  371. ancestor: function (node, selector, testSelf) {
  372. return Y.DOM.ancestor(node, function(n) {
  373. return Y.Selector.test(n, selector);
  374. }, testSelf);
  375. },
  376.  
  377. _parse: function(name, selector) {
  378. return selector.match(Y.Selector._types[name].re);
  379. },
  380.  
  381. _replace: function(name, selector) {
  382. var o = Y.Selector._types[name];
  383. return selector.replace(o.re, o.token);
  384. },
  385.  
  386. _restore: function(name, selector, items) {
  387. if (items) {
  388. var token = Y.Selector._types[name].token,
  389. i, len;
  390. for (i = 0, len = items.length; i < len; ++i) {
  391. selector = selector.replace(token, items[i]);
  392. }
  393. }
  394. return selector;
  395. }
  396. };
  397.  
  398. Y.mix(Y.Selector, Selector, true);
  399.  
  400. })(Y);
  401.