/src/dom/selector.js
JavaScript | 337 lines | 176 code | 30 blank | 131 comment | 68 complexity | 58f6a2d957c2f05734e2ba2df3636d37 MD5 | raw file
- /**
- * @module selector
- * @author lifesinger@gmail.com
- */
- KISSY.add('selector', function(S, undefined) {
- var doc = document, DOM = S.DOM,
- SPACE = ' ', ANY = '*',
- GET_DOM_NODE = 'getDOMNode', GET_DOM_NODES = GET_DOM_NODE + 's',
- REG_ID = /^#[\w-]+$/,
- REG_QUERY = /^(?:#([\w-]+))?\s*([\w-]+|\*)?\.?([\w-]+)?$/;
- /**
- * Retrieves an Array of HTMLElement based on the given CSS selector.
- * @param {string} selector
- * @param {string|HTMLElement} context An #id string or a HTMLElement used as context
- * @return {Array} The array of found HTMLElement
- */
- function query(selector, context) {
- var match, t, ret = [], id, tag, cls;
- context = tuneContext(context);
- // Ref: http://ejohn.org/blog/selectors-that-people-actually-use/
- // 考虑 2/8 原则,仅支持以下选择器:
- // #id
- // tag
- // .cls
- // #id tag
- // #id .cls
- // tag.cls
- // #id tag.cls
- // 注 1:REG_QUERY 还会匹配 #id.cls
- // 注 2:tag 可以为 * 字符
- // 返回值为数组
- // 选择器不支持时,抛出异常
- // selector 为字符串是最常见的情况,优先考虑
- // 注:空白字符串无需判断,运行下去自动能返回空数组
- if (S.isString(selector)) {
- selector = S.trim(selector);
- // selector 为 #id 是最常见的情况,特殊优化处理
- if (REG_ID.test(selector)) {
- t = getElementById(selector.slice(1), context);
- if (t) ret = [t]; // #id 无效时,返回空数组
- }
- // selector 为支持列表中的其它 6 种
- else if ((match = REG_QUERY.exec(selector))) {
- // 获取匹配出的信息
- id = match[1];
- tag = match[2];
- cls = match[3];
- if ((context = id ? getElementById(id, context) : context)) {
- // #id .cls | #id tag.cls | .cls | tag.cls
- if (cls) {
- if (!id || selector.indexOf(SPACE) !== -1) { // 排除 #id.cls
- ret = getElementsByClassName(cls, tag, context);
- }
- // 处理 #id.cls
- else {
- t = getElementById(id, context);
- if(t && DOM.hasClass(t, cls)) {
- ret = [t];
- }
- }
- }
- // #id tag | tag
- else if (tag) { // 排除空白字符串
- ret = getElementsByTagName(tag, context);
- }
- }
- }
- // 采用外部选择器
- else if(S.ExternalSelector) {
- return S.ExternalSelector(selector, context);
- }
- // 依旧不支持,抛异常
- else {
- error(selector);
- }
- }
- // 传入的 selector 是 KISSY.Node/NodeList. 始终返回原生 DOM Node
- else if(selector && (selector[GET_DOM_NODE] || selector[GET_DOM_NODES])) {
- ret = selector[GET_DOM_NODE] ? [selector[GET_DOM_NODE]()] : selector[GET_DOM_NODES]();
- }
- // 传入的 selector 是 NodeList 或已是 Array
- else if (selector && (S.isArray(selector) || isNodeList(selector))) {
- ret = selector;
- }
- // 传入的 selector 是 Node 等非字符串对象,原样返回
- else if (selector) {
- ret = [selector];
- }
- // 传入的 selector 是其它值时,返回空数组
- // 将 NodeList 转换为普通数组
- if(isNodeList(ret)) {
- ret = S.makeArray(ret);
- }
- // attach each method
- ret.each = function(fn, context) {
- return S.each(ret, fn, context);
- };
- return ret;
- }
- // Ref: http://lifesinger.github.com/lab/2010/nodelist.html
- function isNodeList(o) {
- // 注1:ie 下,有 window.item, typeof node.item 在 ie 不同版本下,返回值不同
- // 注2:select 等元素也有 item, 要用 !node.nodeType 排除掉
- // 注3:通过 namedItem 来判断不可靠
- // 注4:getElementsByTagName 和 querySelectorAll 返回的集合不同
- // 注5: 考虑 iframe.contentWindow
- return o && !o.nodeType && o.item && !o.setTimeout;
- }
- // 调整 context 为合理值
- function tuneContext(context) {
- // 1). context 为 undefined 是最常见的情况,优先考虑
- if (context === undefined) {
- context = doc;
- }
- // 2). context 的第二使用场景是传入 #id
- else if (S.isString(context) && REG_ID.test(context)) {
- context = getElementById(context.slice(1), doc);
- // 注:#id 可能无效,这时获取的 context 为 null
- }
- // 3). context 还可以传入 HTMLElement, 此时无需处理
- // 4). 经历 1 - 3, 如果 context 还不是 HTMLElement, 赋值为 null
- else if (context && context.nodeType !== 1 && context.nodeType !== 9) {
- context = null;
- }
- return context;
- }
- // query #id
- function getElementById(id, context) {
- if(context.nodeType !== 9) {
- context = context.ownerDocument;
- }
- return context.getElementById(id);
- }
- // query tag
- function getElementsByTagName(tag, context) {
- return context.getElementsByTagName(tag);
- }
- (function() {
- // Check to see if the browser returns only elements
- // when doing getElementsByTagName('*')
- // Create a fake element
- var div = doc.createElement('div');
- div.appendChild(doc.createComment(''));
- // Make sure no comments are found
- if (div.getElementsByTagName(ANY).length > 0) {
- getElementsByTagName = function(tag, context) {
- var ret = context.getElementsByTagName(tag);
- if (tag === ANY) {
- var t = [], i = 0, j = 0, node;
- while ((node = ret[i++])) {
- // Filter out possible comments
- if (node.nodeType === 1) {
- t[j++] = node;
- }
- }
- ret = t;
- }
- return ret;
- };
- }
- })();
- // query .cls
- function getElementsByClassName(cls, tag, context) {
- var els = context.getElementsByClassName(cls),
- ret = els, i = 0, j = 0, len = els.length, el;
- if (tag && tag !== ANY) {
- ret = [];
- tag = tag.toUpperCase();
- for (; i < len; ++i) {
- el = els[i];
- if (el.tagName === tag) {
- ret[j++] = el;
- }
- }
- }
- return ret;
- }
- if (!doc.getElementsByClassName) {
- // 降级使用 querySelectorAll
- if (doc.querySelectorAll) {
- getElementsByClassName = function(cls, tag, context) {
- return context.querySelectorAll((tag ? tag : '') + '.' + cls);
- }
- }
- // 降级到普通方法
- else {
- getElementsByClassName = function(cls, tag, context) {
- var els = context.getElementsByTagName(tag || ANY),
- ret = [], i = 0, j = 0, len = els.length, el, t;
- cls = SPACE + cls + SPACE;
- for (; i < len; ++i) {
- el = els[i];
- t = el.className;
- if (t && (SPACE + t + SPACE).indexOf(cls) > -1) {
- ret[j++] = el;
- }
- }
- return ret;
- }
- }
- }
- // throw exception
- function error(msg) {
- S.error('Unsupported selector: ' + msg);
- }
- // public api
- S.query = query;
- S.get = function(selector, context) {
- return query(selector, context)[0] || null;
- };
- S.mix(DOM, {
- query: query,
- get: S.get,
- /**
- * Filters an array of elements to only include matches of a filter.
- * @param filter selector or fn
- */
- filter: function(selector, filter) {
- var elems = query(selector), match, tag, cls, ret = [];
- // 默认仅支持最简单的 tag.cls 形式
- if (S.isString(filter) && (match = REG_QUERY.exec(filter)) && !match[1]) {
- tag = match[2];
- cls = match[3];
- filter = function(elem) {
- return !((tag && elem.tagName !== tag.toUpperCase()) || (cls && !DOM.hasClass(elem, cls)));
- }
- }
- if (S.isFunction(filter)) {
- ret = S.filter(elems, filter);
- }
- // 其它复杂 filter, 采用外部选择器
- else if (filter && S.ExternalSelector) {
- ret = S.ExternalSelector._filter(selector, filter + '');
- }
- // filter 为空或不支持的 selector
- else {
- error(filter);
- }
- return ret;
- },
- /**
- * Returns true if the passed element(s) match the passed filter
- */
- test: function(selector, filter) {
- var elems = query(selector);
- return elems.length && (DOM.filter(elems, filter).length === elems.length);
- }
- });
- });
- /**
- * NOTES:
- *
- * 2010.01
- * - 对 reg exec 的结果(id, tag, className)做 cache, 发现对性能影响很小,去掉。
- * - getElementById 使用频率最高,使用直达通道优化。
- * - getElementsByClassName 性能优于 querySelectorAll, 但 IE 系列不支持。
- * - instanceof 对性能有影响。
- * - 内部方法的参数,比如 cls, context 等的异常情况,已经在 query 方法中有保证,无需冗余“防卫”。
- * - query 方法中的条件判断考虑了“频率优先”原则。最有可能出现的情况放在前面。
- * - Array 的 push 方法可以用 j++ 来替代,性能有提升。
- * - 返回值策略和 Sizzle 一致,正常时,返回数组;其它所有情况,返回空数组。
- *
- * - 从压缩角度考虑,还可以将 getElmentsByTagName 和 getElementsByClassName 定义为常量,
- * 不过感觉这样做太“压缩控”,还是保留不替换的好。
- *
- * - 调整 getElementsByClassName 的降级写法,性能最差的放最后。
- *
- * 2010.02
- * - 添加对分组选择器的支持(主要参考 Sizzle 的代码,代去除了对非 Grade A 级浏览器的支持)
- *
- * 2010.03
- * - 基于原生 dom 的两个 api: S.query 返回数组; S.get 返回第一个。
- * 基于 Node 的 api: S.one, 在 Node 中实现。
- * 基于 NodeList 的 api: S.all, 在 NodeList 中实现。
- * 通过 api 的分层,同时满足初级用户和高级用户的需求。
- *
- * 2010.05
- * - 去掉给 S.query 返回值默认添加的 each 方法,保持纯净。
- * - 对于不支持的 selector, 采用外部耦合进来的 Selector.
- *
- * 2010.06
- * - 增加 filter 和 test 方法
- *
- * 2010.07
- * - 取消对 , 分组的支持,group 直接用 Sizzle
- *
- * 2010.08
- * - 给 S.query 的结果 attach each 方法
- *
- * Bugs:
- * - S.query('#test-data *') 等带 * 号的选择器,在 IE6 下返回的值不对。jQuery 等类库也有此 bug, 诡异。
- *
- * References:
- * - http://ejohn.org/blog/selectors-that-people-actually-use/
- * - http://ejohn.org/blog/thoughts-on-queryselectorall/
- * - MDC: querySelector, querySelectorAll, getElementsByClassName
- * - Sizzle: http://github.com/jeresig/sizzle
- * - MINI: http://james.padolsey.com/javascript/mini/
- * - Peppy: http://jamesdonaghue.com/?p=40
- * - Sly: http://github.com/digitarald/sly
- * - XPath, TreeWalker:http://www.cnblogs.com/rubylouvre/archive/2009/07/24/1529640.html
- *
- * - http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
- * - http://www.quirksmode.org/dom/getElementsByTagNames.html
- * - http://ejohn.org/blog/comparing-document-position/
- * - http://github.com/jeresig/sizzle/blob/master/sizzle.js
- */