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