PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src/dom/selector.js

https://bitbucket.org/cng1985/kissy
JavaScript | 337 lines | 176 code | 30 blank | 131 comment | 68 complexity | 58f6a2d957c2f05734e2ba2df3636d37 MD5 | raw file
  1. /**
  2. * @module selector
  3. * @author lifesinger@gmail.com
  4. */
  5. KISSY.add('selector', function(S, undefined) {
  6. var doc = document, DOM = S.DOM,
  7. SPACE = ' ', ANY = '*',
  8. GET_DOM_NODE = 'getDOMNode', GET_DOM_NODES = GET_DOM_NODE + 's',
  9. REG_ID = /^#[\w-]+$/,
  10. REG_QUERY = /^(?:#([\w-]+))?\s*([\w-]+|\*)?\.?([\w-]+)?$/;
  11. /**
  12. * Retrieves an Array of HTMLElement based on the given CSS selector.
  13. * @param {string} selector
  14. * @param {string|HTMLElement} context An #id string or a HTMLElement used as context
  15. * @return {Array} The array of found HTMLElement
  16. */
  17. function query(selector, context) {
  18. var match, t, ret = [], id, tag, cls;
  19. context = tuneContext(context);
  20. // Ref: http://ejohn.org/blog/selectors-that-people-actually-use/
  21. // 考虑 2/8 原则,仅支持以下选择器:
  22. // #id
  23. // tag
  24. // .cls
  25. // #id tag
  26. // #id .cls
  27. // tag.cls
  28. // #id tag.cls
  29. // 注 1:REG_QUERY 还会匹配 #id.cls
  30. // 注 2:tag 可以为 * 字符
  31. // 返回值为数组
  32. // 选择器不支持时,抛出异常
  33. // selector 为字符串是最常见的情况,优先考虑
  34. // 注:空白字符串无需判断,运行下去自动能返回空数组
  35. if (S.isString(selector)) {
  36. selector = S.trim(selector);
  37. // selector 为 #id 是最常见的情况,特殊优化处理
  38. if (REG_ID.test(selector)) {
  39. t = getElementById(selector.slice(1), context);
  40. if (t) ret = [t]; // #id 无效时,返回空数组
  41. }
  42. // selector 为支持列表中的其它 6 种
  43. else if ((match = REG_QUERY.exec(selector))) {
  44. // 获取匹配出的信息
  45. id = match[1];
  46. tag = match[2];
  47. cls = match[3];
  48. if ((context = id ? getElementById(id, context) : context)) {
  49. // #id .cls | #id tag.cls | .cls | tag.cls
  50. if (cls) {
  51. if (!id || selector.indexOf(SPACE) !== -1) { // 排除 #id.cls
  52. ret = getElementsByClassName(cls, tag, context);
  53. }
  54. // 处理 #id.cls
  55. else {
  56. t = getElementById(id, context);
  57. if(t && DOM.hasClass(t, cls)) {
  58. ret = [t];
  59. }
  60. }
  61. }
  62. // #id tag | tag
  63. else if (tag) { // 排除空白字符串
  64. ret = getElementsByTagName(tag, context);
  65. }
  66. }
  67. }
  68. // 采用外部选择器
  69. else if(S.ExternalSelector) {
  70. return S.ExternalSelector(selector, context);
  71. }
  72. // 依旧不支持,抛异常
  73. else {
  74. error(selector);
  75. }
  76. }
  77. // 传入的 selector 是 KISSY.Node/NodeList. 始终返回原生 DOM Node
  78. else if(selector && (selector[GET_DOM_NODE] || selector[GET_DOM_NODES])) {
  79. ret = selector[GET_DOM_NODE] ? [selector[GET_DOM_NODE]()] : selector[GET_DOM_NODES]();
  80. }
  81. // 传入的 selector 是 NodeList 或已是 Array
  82. else if (selector && (S.isArray(selector) || isNodeList(selector))) {
  83. ret = selector;
  84. }
  85. // 传入的 selector 是 Node 等非字符串对象,原样返回
  86. else if (selector) {
  87. ret = [selector];
  88. }
  89. // 传入的 selector 是其它值时,返回空数组
  90. // 将 NodeList 转换为普通数组
  91. if(isNodeList(ret)) {
  92. ret = S.makeArray(ret);
  93. }
  94. // attach each method
  95. ret.each = function(fn, context) {
  96. return S.each(ret, fn, context);
  97. };
  98. return ret;
  99. }
  100. // Ref: http://lifesinger.github.com/lab/2010/nodelist.html
  101. function isNodeList(o) {
  102. // 注1:ie 下,有 window.item, typeof node.item 在 ie 不同版本下,返回值不同
  103. // 注2:select 等元素也有 item, 要用 !node.nodeType 排除掉
  104. // 注3:通过 namedItem 来判断不可靠
  105. // 注4:getElementsByTagName 和 querySelectorAll 返回的集合不同
  106. // 注5: 考虑 iframe.contentWindow
  107. return o && !o.nodeType && o.item && !o.setTimeout;
  108. }
  109. // 调整 context 为合理值
  110. function tuneContext(context) {
  111. // 1). context 为 undefined 是最常见的情况,优先考虑
  112. if (context === undefined) {
  113. context = doc;
  114. }
  115. // 2). context 的第二使用场景是传入 #id
  116. else if (S.isString(context) && REG_ID.test(context)) {
  117. context = getElementById(context.slice(1), doc);
  118. // 注:#id 可能无效,这时获取的 context 为 null
  119. }
  120. // 3). context 还可以传入 HTMLElement, 此时无需处理
  121. // 4). 经历 1 - 3, 如果 context 还不是 HTMLElement, 赋值为 null
  122. else if (context && context.nodeType !== 1 && context.nodeType !== 9) {
  123. context = null;
  124. }
  125. return context;
  126. }
  127. // query #id
  128. function getElementById(id, context) {
  129. if(context.nodeType !== 9) {
  130. context = context.ownerDocument;
  131. }
  132. return context.getElementById(id);
  133. }
  134. // query tag
  135. function getElementsByTagName(tag, context) {
  136. return context.getElementsByTagName(tag);
  137. }
  138. (function() {
  139. // Check to see if the browser returns only elements
  140. // when doing getElementsByTagName('*')
  141. // Create a fake element
  142. var div = doc.createElement('div');
  143. div.appendChild(doc.createComment(''));
  144. // Make sure no comments are found
  145. if (div.getElementsByTagName(ANY).length > 0) {
  146. getElementsByTagName = function(tag, context) {
  147. var ret = context.getElementsByTagName(tag);
  148. if (tag === ANY) {
  149. var t = [], i = 0, j = 0, node;
  150. while ((node = ret[i++])) {
  151. // Filter out possible comments
  152. if (node.nodeType === 1) {
  153. t[j++] = node;
  154. }
  155. }
  156. ret = t;
  157. }
  158. return ret;
  159. };
  160. }
  161. })();
  162. // query .cls
  163. function getElementsByClassName(cls, tag, context) {
  164. var els = context.getElementsByClassName(cls),
  165. ret = els, i = 0, j = 0, len = els.length, el;
  166. if (tag && tag !== ANY) {
  167. ret = [];
  168. tag = tag.toUpperCase();
  169. for (; i < len; ++i) {
  170. el = els[i];
  171. if (el.tagName === tag) {
  172. ret[j++] = el;
  173. }
  174. }
  175. }
  176. return ret;
  177. }
  178. if (!doc.getElementsByClassName) {
  179. // 降级使用 querySelectorAll
  180. if (doc.querySelectorAll) {
  181. getElementsByClassName = function(cls, tag, context) {
  182. return context.querySelectorAll((tag ? tag : '') + '.' + cls);
  183. }
  184. }
  185. // 降级到普通方法
  186. else {
  187. getElementsByClassName = function(cls, tag, context) {
  188. var els = context.getElementsByTagName(tag || ANY),
  189. ret = [], i = 0, j = 0, len = els.length, el, t;
  190. cls = SPACE + cls + SPACE;
  191. for (; i < len; ++i) {
  192. el = els[i];
  193. t = el.className;
  194. if (t && (SPACE + t + SPACE).indexOf(cls) > -1) {
  195. ret[j++] = el;
  196. }
  197. }
  198. return ret;
  199. }
  200. }
  201. }
  202. // throw exception
  203. function error(msg) {
  204. S.error('Unsupported selector: ' + msg);
  205. }
  206. // public api
  207. S.query = query;
  208. S.get = function(selector, context) {
  209. return query(selector, context)[0] || null;
  210. };
  211. S.mix(DOM, {
  212. query: query,
  213. get: S.get,
  214. /**
  215. * Filters an array of elements to only include matches of a filter.
  216. * @param filter selector or fn
  217. */
  218. filter: function(selector, filter) {
  219. var elems = query(selector), match, tag, cls, ret = [];
  220. // 默认仅支持最简单的 tag.cls 形式
  221. if (S.isString(filter) && (match = REG_QUERY.exec(filter)) && !match[1]) {
  222. tag = match[2];
  223. cls = match[3];
  224. filter = function(elem) {
  225. return !((tag && elem.tagName !== tag.toUpperCase()) || (cls && !DOM.hasClass(elem, cls)));
  226. }
  227. }
  228. if (S.isFunction(filter)) {
  229. ret = S.filter(elems, filter);
  230. }
  231. // 其它复杂 filter, 采用外部选择器
  232. else if (filter && S.ExternalSelector) {
  233. ret = S.ExternalSelector._filter(selector, filter + '');
  234. }
  235. // filter 为空或不支持的 selector
  236. else {
  237. error(filter);
  238. }
  239. return ret;
  240. },
  241. /**
  242. * Returns true if the passed element(s) match the passed filter
  243. */
  244. test: function(selector, filter) {
  245. var elems = query(selector);
  246. return elems.length && (DOM.filter(elems, filter).length === elems.length);
  247. }
  248. });
  249. });
  250. /**
  251. * NOTES:
  252. *
  253. * 2010.01
  254. * - 对 reg exec 的结果(id, tag, className)做 cache, 发现对性能影响很小,去掉。
  255. * - getElementById 使用频率最高,使用直达通道优化。
  256. * - getElementsByClassName 性能优于 querySelectorAll, 但 IE 系列不支持。
  257. * - instanceof 对性能有影响。
  258. * - 内部方法的参数,比如 cls, context 等的异常情况,已经在 query 方法中有保证,无需冗余“防卫”。
  259. * - query 方法中的条件判断考虑了“频率优先”原则。最有可能出现的情况放在前面。
  260. * - Array 的 push 方法可以用 j++ 来替代,性能有提升。
  261. * - 返回值策略和 Sizzle 一致,正常时,返回数组;其它所有情况,返回空数组。
  262. *
  263. * - 从压缩角度考虑,还可以将 getElmentsByTagName 和 getElementsByClassName 定义为常量,
  264. * 不过感觉这样做太“压缩控”,还是保留不替换的好。
  265. *
  266. * - 调整 getElementsByClassName 的降级写法,性能最差的放最后。
  267. *
  268. * 2010.02
  269. * - 添加对分组选择器的支持(主要参考 Sizzle 的代码,代去除了对非 Grade A 级浏览器的支持)
  270. *
  271. * 2010.03
  272. * - 基于原生 dom 的两个 api: S.query 返回数组; S.get 返回第一个。
  273. * 基于 Node 的 api: S.one, 在 Node 中实现。
  274. * 基于 NodeList 的 api: S.all, 在 NodeList 中实现。
  275. * 通过 api 的分层,同时满足初级用户和高级用户的需求。
  276. *
  277. * 2010.05
  278. * - 去掉给 S.query 返回值默认添加的 each 方法,保持纯净。
  279. * - 对于不支持的 selector, 采用外部耦合进来的 Selector.
  280. *
  281. * 2010.06
  282. * - 增加 filter 和 test 方法
  283. *
  284. * 2010.07
  285. * - 取消对 , 分组的支持,group 直接用 Sizzle
  286. *
  287. * 2010.08
  288. * - 给 S.query 的结果 attach each 方法
  289. *
  290. * Bugs:
  291. * - S.query('#test-data *') 等带 * 号的选择器,在 IE6 下返回的值不对。jQuery 等类库也有此 bug, 诡异。
  292. *
  293. * References:
  294. * - http://ejohn.org/blog/selectors-that-people-actually-use/
  295. * - http://ejohn.org/blog/thoughts-on-queryselectorall/
  296. * - MDC: querySelector, querySelectorAll, getElementsByClassName
  297. * - Sizzle: http://github.com/jeresig/sizzle
  298. * - MINI: http://james.padolsey.com/javascript/mini/
  299. * - Peppy: http://jamesdonaghue.com/?p=40
  300. * - Sly: http://github.com/digitarald/sly
  301. * - XPath, TreeWalker:http://www.cnblogs.com/rubylouvre/archive/2009/07/24/1529640.html
  302. *
  303. * - http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
  304. * - http://www.quirksmode.org/dom/getElementsByTagNames.html
  305. * - http://ejohn.org/blog/comparing-document-position/
  306. * - http://github.com/jeresig/sizzle/blob/master/sizzle.js
  307. */