1 /**
  2  * @fileOverview selector
  3  * @author lifesinger@gmail.com , yiminghe@gmail.com
  4  */
  5 KISSY.add('dom/selector', function (S, DOM, undefined) {
  6 
  7     var doc = S.Env.host.document,
  8         filter = S.filter,
  9         require = function (selector) {
 10             return S.require(selector);
 11         },
 12         isArray = S.isArray,
 13         isString = S.isString,
 14         makeArray = S.makeArray,
 15         isNodeList = DOM._isNodeList,
 16         getNodeName = DOM.nodeName,
 17         push = Array.prototype.push,
 18         SPACE = ' ',
 19         COMMA = ',',
 20         trim = S.trim,
 21         ANY = '*',
 22         REG_ID = /^#[\w-]+$/,
 23         REG_QUERY = /^(?:#([\w-]+))?\s*([\w-]+|\*)?\.?([\w-]+)?$/;
 24 
 25     function query_each(f) {
 26         var self = this, el, i;
 27         for (i = 0; i < self.length; i++) {
 28             el = self[i];
 29             if (f(el, i) === false) {
 30                 break;
 31             }
 32         }
 33     }
 34 
 35     /**
 36      * Accepts a string containing a CSS selector which is then used to match a set of elements.
 37      * @param {String|HTMLElement[]} selector
 38      * A string containing a selector expression.
 39      * or
 40      * array of HTMLElements.
 41      * @param {String|HTMLElement[]|Document} [context] context under which to find elements matching selector.
 42      * @return {HTMLElement[]} The array of found HTMLElements
 43      */
 44     function query(selector, context) {
 45         var ret,
 46             i,
 47             simpleContext,
 48             isSelectorString = isString(selector),
 49             // optimize common usage
 50             contexts = (context === undefined && (simpleContext = 1)) ?
 51                 [doc] :
 52                 tuneContext(context);
 53         // 常见的空
 54         if (!selector) {
 55             ret = [];
 56         }
 57         // 常见的选择器
 58         // DOM.query("#x")
 59         else if (isSelectorString) {
 60             selector = trim(selector);
 61             // shortcut
 62             if (simpleContext && selector == "body") {
 63                 ret = [doc.body]
 64             } else if (contexts.length == 1 && selector) {
 65                 ret = quickFindBySelectorStr(selector, contexts[0]);
 66             }
 67         }
 68         // 不写 context,就是包装一下
 69         else if (simpleContext) {
 70             // 1.常见的单个元素
 71             // DOM.query(document.getElementById("xx"))
 72             if (selector['nodeType'] || selector['setTimeout']) {
 73                 ret = [selector];
 74             }
 75             // 2.KISSY NodeList 特殊点直接返回,提高性能
 76             else if (selector['getDOMNodes']) {
 77                 return selector;
 78             }
 79             // 3.常见的数组
 80             // var x=DOM.query(".l");
 81             // DOM.css(x,"color","red");
 82             else if (isArray(selector)) {
 83                 ret = selector;
 84             }
 85             // 4.selector.item
 86             // DOM.query(document.getElementsByTagName("a"))
 87             // note:
 88             // document.createElement("select").item 已经在 1 处理了
 89             // S.all().item 已经在 2 处理了
 90             else if (isNodeList(selector)) {
 91                 ret = S.makeArray(selector);
 92             } else {
 93                 ret = [ selector ];
 94             }
 95         }
 96 
 97         if (!ret) {
 98             ret = [];
 99             if (selector) {
100                 for (i = 0; i < contexts.length; i++) {
101                     push.apply(ret, queryByContexts(selector, contexts[i]));
102                 }
103                 //必要时去重排序
104                 if (ret.length > 1 &&
105                     // multiple contexts
106                     (contexts.length > 1 ||
107                         (isSelectorString &&
108                             // multiple selector
109                             selector.indexOf(COMMA) > -1))) {
110                     unique(ret);
111                 }
112             }
113         }
114 
115         // attach each method
116         ret.each = query_each;
117 
118         return ret;
119     }
120 
121     function queryByContexts(selector, context) {
122         var ret = [],
123             isSelectorString = isString(selector);
124         if (isSelectorString && selector.match(REG_QUERY) ||
125             !isSelectorString) {
126             // 简单选择器自己处理
127             ret = queryBySimple(selector, context);
128         }
129         // 如果选择器有 , 分开递归一部分一部分来
130         else if (isSelectorString && selector.indexOf(COMMA) > -1) {
131             ret = queryBySelectors(selector, context);
132         }
133         else {
134             // 复杂了,交给 sizzle
135             ret = queryBySizzle(selector, context);
136         }
137         return ret;
138     }
139 
140     // 交给 sizzle 模块处理
141     function queryBySizzle(selector, context) {
142         var ret = [],
143             sizzle = require("sizzle");
144         if (sizzle) {
145             sizzle(selector, context, ret);
146         } else {
147             // 原生不支持
148             error(selector);
149         }
150         return ret;
151     }
152 
153     // 处理 selector 的每个部分
154     function queryBySelectors(selector, context) {
155         var ret = [],
156             i,
157             selectors = selector.split(/\s*,\s*/);
158         for (i = 0; i < selectors.length; i++) {
159             push.apply(ret, queryByContexts(selectors[i], context));
160         }
161         // 多部分选择器可能得到重复结果
162         return ret;
163     }
164 
165     function quickFindBySelectorStr(selector, context) {
166         var ret, t, match, id, tag, cls;
167         // selector 为 #id 是最常见的情况,特殊优化处理
168         if (REG_ID.test(selector)) {
169             t = getElementById(selector.slice(1), context);
170             if (t) {
171                 // #id 无效时,返回空数组
172                 ret = [t];
173             } else {
174                 ret = [];
175             }
176         }
177         // selector 为支持列表中的其它 6 种
178         else {
179             match = REG_QUERY.exec(selector);
180             if (match) {
181                 // 获取匹配出的信息
182                 id = match[1];
183                 tag = match[2];
184                 cls = match[3];
185                 // 空白前只能有 id ,取出来作为 context
186                 context = (id ? getElementById(id, context) : context);
187                 if (context) {
188                     // #id .cls | #id tag.cls | .cls | tag.cls | #id.cls
189                     if (cls) {
190                         if (!id || selector.indexOf(SPACE) != -1) { // 排除 #id.cls
191                             ret = [].concat(getElementsByClassName(cls, tag, context));
192                         }
193                         // 处理 #id.cls
194                         else {
195                             t = getElementById(id, context);
196                             if (t && hasClass(t, cls)) {
197                                 ret = [t];
198                             }
199                         }
200                     }
201                     // #id tag | tag
202                     else if (tag) { // 排除空白字符串
203                         ret = getElementsByTagName(tag, context);
204                     }
205                 }
206                 ret = ret || [];
207             }
208         }
209         return ret;
210     }
211 
212     // 最简单情况了,单个选择器部分,单个上下文
213     function queryBySimple(selector, context) {
214         var ret,
215             isSelectorString = isString(selector);
216         if (isSelectorString) {
217             ret = quickFindBySelectorStr(selector, context) || [];
218         }
219         // 传入的 selector 是 NodeList 或已是 Array
220         else if (isArray(selector) || isNodeList(selector)) {
221             // 只能包含在 context 里面
222             // filter 会转换为 nodelist 为数组
223             ret = filter(selector, function (s) {
224                 return testByContext(s, context);
225             });
226         }
227         // 传入的 selector 是 HTMLNode 查看约束
228         // 否则 window/document,原样返回
229         else if (testByContext(selector, context)) {
230             ret = [selector];
231         }
232         return ret;
233     }
234 
235     function testByContext(element, context) {
236         if (!element) {
237             return false;
238         }
239         // 防止 element 节点还没添加到 document ,但是也可以获取到 query(element) => [element]
240         // document 的上下文一律放行
241         // context == doc 意味着没有提供第二个参数,到这里只是想单纯包装原生节点,则不检测
242         if (context == doc) {
243             return true;
244         }
245         // 节点受上下文约束
246         return DOM.contains(context, element);
247     }
248 
249     var unique = S.noop;
250     (function () {
251         var sortOrder,
252             hasDuplicate,
253             baseHasDuplicate = true;
254 
255         // Here we check if the JavaScript engine is using some sort of
256         // optimization where it does not always call our comparision
257         // function. If that is the case, discard the hasDuplicate value.
258         // Thus far that includes Google Chrome.
259         [0, 0].sort(function () {
260             baseHasDuplicate = false;
261             return 0;
262         });
263 
264         // 排序去重
265         unique = function (elements) {
266             if (sortOrder) {
267                 hasDuplicate = baseHasDuplicate;
268                 elements.sort(sortOrder);
269 
270                 if (hasDuplicate) {
271                     var i = 1, len = elements.length;
272                     while (i < len) {
273                         if (elements[i] === elements[ i - 1 ]) {
274                             elements.splice(i, 1);
275                         } else {
276                             i++;
277                         }
278                     }
279                 }
280             }
281             return elements;
282         };
283 
284         // 貌似除了 ie 都有了...
285         if (doc.documentElement.compareDocumentPosition) {
286             sortOrder = function (a, b) {
287                 if (a == b) {
288                     hasDuplicate = true;
289                     return 0;
290                 }
291 
292                 if (!a.compareDocumentPosition || !b.compareDocumentPosition) {
293                     return a.compareDocumentPosition ? -1 : 1;
294                 }
295 
296                 return a.compareDocumentPosition(b) & 4 ? -1 : 1;
297             };
298 
299         } else {
300             sortOrder = function (a, b) {
301                 // The nodes are identical, we can exit early
302                 if (a == b) {
303                     hasDuplicate = true;
304                     return 0;
305                     // Fallback to using sourceIndex (in IE) if it's available on both nodes
306                 } else if (a.sourceIndex && b.sourceIndex) {
307                     return a.sourceIndex - b.sourceIndex;
308                 }
309             };
310         }
311     })();
312 
313 
314     // 调整 context 为合理值
315     function tuneContext(context) {
316         return query(context, undefined);
317     }
318 
319     // query #id
320     function getElementById(id, context) {
321         var doc = context,
322             el;
323         if (context.nodeType !== DOM.DOCUMENT_NODE) {
324             doc = context.ownerDocument;
325         }
326         el = doc.getElementById(id);
327         if (el && el.id === id) {
328             // optimize for common usage
329         }
330         else if (el && el.parentNode) {
331             // ie opera confuse name with id
332             // https://github.com/kissyteam/kissy/issues/67
333             // 不能直接 el.id ,否则 input shadow form attribute
334             if (!idEq(el, id)) {
335                 // 直接在 context 下的所有节点找
336                 el = DOM.filter(ANY, "#" + id, context)[0] || null;
337             }
338             // ie 特殊情况下以及指明在 context 下找了,不需要再判断
339             // 如果指定了 context node , 还要判断 id 是否处于 context 内
340             else if (!testByContext(el, context)) {
341                 el = null;
342             }
343         } else {
344             el = null;
345         }
346         return el;
347     }
348 
349     // query tag
350     function getElementsByTagName(tag, context) {
351         return context && makeArray(context.getElementsByTagName(tag)) || [];
352     }
353 
354     (function () {
355         // Check to see if the browser returns only elements
356         // when doing getElementsByTagName('*')
357 
358         // Create a fake element
359         var div = doc.createElement('div');
360         div.appendChild(doc.createComment(''));
361 
362         // Make sure no comments are found
363         if (div.getElementsByTagName(ANY).length > 0) {
364             getElementsByTagName = function (tag, context) {
365                 var ret = makeArray(context.getElementsByTagName(tag));
366                 if (tag === ANY) {
367                     var t = [], i = 0, node;
368                     while ((node = ret[i++])) {
369                         // Filter out possible comments
370                         if (node.nodeType === 1) {
371                             t.push(node);
372                         }
373                     }
374                     ret = t;
375                 }
376                 return ret;
377             };
378         }
379     })();
380 
381     // query .cls
382     var getElementsByClassName = doc.getElementsByClassName ? function (cls, tag, context) {
383         // query("#id1 xx","#id2")
384         // #id2 内没有 #id1 , context 为 null , 这里防御下
385         if (!context) {
386             return [];
387         }
388         var els = context.getElementsByClassName(cls),
389             ret,
390             i = 0,
391             len = els.length,
392             el;
393 
394         if (tag && tag !== ANY) {
395             ret = [];
396             for (; i < len; ++i) {
397                 el = els[i];
398                 if (getNodeName(el)==tag) {
399                     ret.push(el);
400                 }
401             }
402         } else {
403             ret = makeArray(els);
404         }
405         return ret;
406     } : ( doc.querySelectorAll ? function (cls, tag, context) {
407         // ie8 return staticNodeList 对象,[].concat 会形成 [ staticNodeList ] ,手动转化为普通数组
408         return context && makeArray(context.querySelectorAll((tag ? tag : '') + '.' + cls)) || [];
409     } : function (cls, tag, context) {
410         if (!context) {
411             return [];
412         }
413         var els = context.getElementsByTagName(tag || ANY),
414             ret = [],
415             i = 0,
416             len = els.length,
417             el;
418         for (; i < len; ++i) {
419             el = els[i];
420             if (hasClass(el, cls)) {
421                 ret.push(el);
422             }
423         }
424         return ret;
425     });
426 
427     function hasClass(el, cls) {
428         var className;
429         return (className = el.className) && (" " + className + " ").indexOf(" " + cls + " ") !== -1;
430     }
431 
432     // throw exception
433     function error(msg) {
434         S.error('Unsupported selector: ' + msg);
435     }
436 
437     S.mix(DOM,
438         /**
439          * @lends DOM
440          */
441         {
442 
443             /**
444              * Accepts a string containing a CSS selector which is then used to match a set of elements.
445              * @param {String|HTMLElement[]} selector
446              * A string containing a selector expression.
447              * or
448              * array of HTMLElements.
449              * @param {String|HTMLElement[]|Document|HTMLElement} [context] context under which to find elements matching selector.
450              * @return {HTMLElement[]} The array of found HTMLElements
451              */
452             query:query,
453 
454             /**
455              * Accepts a string containing a CSS selector which is then used to match a set of elements.
456              * @param {String|HTMLElement[]} selector
457              * A string containing a selector expression.
458              * or
459              * array of HTMLElements.
460              * @param {String|HTMLElement[]|Document|HTMLElement|window} [context] context under which to find elements matching selector.
461              * @return {HTMLElement} The first of found HTMLElements
462              */
463             get:function (selector, context) {
464                 return query(selector, context)[0] || null;
465             },
466 
467             /**
468              * Sorts an array of DOM elements, in place, with the duplicates removed.
469              * Note that this only works on arrays of DOM elements, not strings or numbers.
470              * @param {HTMLElement[]} The Array of DOM elements.
471              * @function
472              * @return {HTMLElement[]}
473              */
474             unique:unique,
475 
476             /**
477              * Reduce the set of matched elements to those that match the selector or pass the function's test.
478              * @param {String|HTMLElement[]|NodeList} selector Matched elements
479              * @param {String|Function} filter Selector string or filter function
480              * @param {String|HTMLElement[]|Document} [context] Context under which to find matched elements
481              * @return {HTMLElement[]}
482              */
483             filter:function (selector, filter, context) {
484                 var elems = query(selector, context),
485                     sizzle = require("sizzle"),
486                     match,
487                     tag,
488                     id,
489                     cls,
490                     ret = [];
491 
492                 // 默认仅支持最简单的 tag.cls 或 #id 形式
493                 if (isString(filter) &&
494                     (filter = trim(filter)) &&
495                     (match = REG_QUERY.exec(filter))) {
496                     id = match[1];
497                     tag = match[2];
498                     cls = match[3];
499                     if (!id) {
500                         filter = function (elem) {
501                             var tagRe = true, clsRe = true;
502 
503                             // 指定 tag 才进行判断
504                             if (tag) {
505                                 tagRe = getNodeName(elem)==tag;
506                             }
507 
508                             // 指定 cls 才进行判断
509                             if (cls) {
510                                 clsRe = hasClass(elem, cls);
511                             }
512 
513                             return clsRe && tagRe;
514                         }
515                     } else if (id && !tag && !cls) {
516                         filter = function (elem) {
517                             return idEq(elem, id);
518                         };
519                     }
520                 }
521 
522                 if (S.isFunction(filter)) {
523                     ret = S.filter(elems, filter);
524                 }
525                 // 其它复杂 filter, 采用外部选择器
526                 else if (filter && sizzle) {
527                     ret = sizzle.matches(filter, elems);
528                 }
529                 // filter 为空或不支持的 selector
530                 else {
531                     error(filter);
532                 }
533 
534                 return ret;
535             },
536 
537             /**
538              * Returns true if the matched element(s) pass the filter test
539              * @param {String|HTMLElement[]} selector Matched elements
540              * @param {String|Function} filter Selector string or filter function
541              * @param {String|HTMLElement[]|Document} [context] Context under which to find matched elements
542              * @returns {Boolean}
543              */
544             test:function (selector, filter, context) {
545                 var elements = query(selector, context);
546                 return elements.length && (DOM.filter(elements, filter, context).length === elements.length);
547             }
548         });
549 
550 
551     function idEq(elem, id) {
552         // form !
553         var idNode = elem.getAttributeNode("id");
554         return idNode && idNode.nodeValue === id;
555     }
556 
557     return DOM;
558 }, {
559     requires:["./base"]
560 });
561 
562 /**
563  * NOTES:
564  *
565  * 2011.08.02
566  *  - 利用 sizzle 重构选择器
567  *  - 1.1.6 修正,原来 context 只支持 #id 以及 document
568  *    1.2 context 支持任意,和 selector 格式一致
569  *  - 简单选择器也和 jquery 保持一致 DOM.query("xx","yy") 支持
570  *    - context 不提供则为当前 document ,否则通过 query 递归取得
571  *    - 保证选择出来的节点(除了 document window)都是位于 context 范围内
572  *
573  *
574  * 2010.01
575  *  - 对 reg exec 的结果(id, tag, className)做 cache, 发现对性能影响很小,去掉。
576  *  - getElementById 使用频率最高,使用直达通道优化。
577  *  - getElementsByClassName 性能优于 querySelectorAll, 但 IE 系列不支持。
578  *  - instanceof 对性能有影响。
579  *  - 内部方法的参数,比如 cls, context 等的异常情况,已经在 query 方法中有保证,无需冗余“防卫”。
580  *  - query 方法中的条件判断考虑了“频率优先”原则。最有可能出现的情况放在前面。
581  *  - Array 的 push 方法可以用 j++ 来替代,性能有提升。
582  *  - 返回值策略和 Sizzle 一致,正常时,返回数组;其它所有情况,返回空数组。
583  *
584  *  - 从压缩角度考虑,还可以将 getElmentsByTagName 和 getElementsByClassName 定义为常量,
585  *    不过感觉这样做太“压缩控”,还是保留不替换的好。
586  *
587  *  - 调整 getElementsByClassName 的降级写法,性能最差的放最后。
588  *
589  * 2010.02
590  *  - 添加对分组选择器的支持(主要参考 Sizzle 的代码,代去除了对非 Grade A 级浏览器的支持)
591  *
592  * 2010.03
593  *  - 基于原生 dom 的两个 api: S.query 返回数组; S.get 返回第一个。
594  *    基于 Node 的 api: S.one, 在 Node 中实现。
595  *    基于 NodeList 的 api: S.all, 在 NodeList 中实现。
596  *    通过 api 的分层,同时满足初级用户和高级用户的需求。
597  *
598  * 2010.05
599  *  - 去掉给 S.query 返回值默认添加的 each 方法,保持纯净。
600  *  - 对于不支持的 selector, 采用外部耦合进来的 Selector.
601  *
602  * 2010.06
603  *  - 增加 filter 和 test 方法
604  *
605  * 2010.07
606  *  - 取消对 , 分组的支持,group 直接用 Sizzle
607  *
608  * 2010.08
609  *  - 给 S.query 的结果 attach each 方法
610  *
611  * 2011.05
612  *  - 承玉:恢复对简单分组支持
613  *
614  * Ref: http://ejohn.org/blog/selectors-that-people-actually-use/
615  * 考虑 2/8 原则,仅支持以下选择器:
616  * #id
617  * tag
618  * .cls
619  * #id tag
620  * #id .cls
621  * tag.cls
622  * #id tag.cls
623  * 注 1:REG_QUERY 还会匹配 #id.cls
624  * 注 2:tag 可以为 * 字符
625  * 注 3: 支持 , 号分组
626  *
627  *
628  * Bugs:
629  *  - S.query('#test-data *') 等带 * 号的选择器,在 IE6 下返回的值不对。jQuery 等类库也有此 bug, 诡异。
630  *
631  * References:
632  *  - http://ejohn.org/blog/selectors-that-people-actually-use/
633  *  - http://ejohn.org/blog/thoughts-on-queryselectorall/
634  *  - MDC: querySelector, querySelectorAll, getElementsByClassName
635  *  - Sizzle: http://github.com/jeresig/sizzle
636  *  - MINI: http://james.padolsey.com/javascript/mini/
637  *  - Peppy: http://jamesdonaghue.com/?p=40
638  *  - Sly: http://github.com/digitarald/sly
639  *  - XPath, TreeWalker:http://www.cnblogs.com/rubylouvre/archive/2009/07/24/1529640.html
640  *
641  *  - http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
642  *  - http://www.quirksmode.org/dom/getElementsByTagNames.html
643  *  - http://ejohn.org/blog/comparing-document-position/
644  *  - http://github.com/jeresig/sizzle/blob/master/sizzle.js
645  */
646