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