micro-jq
Version:
A micro jQuery-liked mobile component that can do many things like jQuery.
677 lines (653 loc) • 22.7 kB
JavaScript
/*
事件管理和DOM操作
*/
var $ = window._hz_$;//实现全局唯一
if (!$) $ = window._hz_$ = (function () {
"use strict";
/*cache结构为:
{元素键值1:{click:[callback1,callback2],touchend:[{fn:fn}]}
,元素键值2:{click:[{fn:fn1},{fn:fn2}...]}
...}
*/
var cache = {};
var tap = 'mytap', //避免与zepto的tap冲突,待不用zepto时可以去除
tapDelay = 300, tapMaxMove = 15; //能触发tap的最大时间间隔和最大位移
/**
* 生成Query包装对象
* @param q 可为选择器、html字符串、DOM元素、Query对象、函数。
* 为选择器时,调用querySelectorAll方法获取元素
* 为html字符串时(根据首字符是否为"<"判断),会生成内存DOM节点元素,并以所有根节点包装作为返回值
* 为DOM元素时,直接包装
* 为Query对象时,直接返回本身
* 为函数时,等同于domready
* @param c 选择器的上下文DOM元素
*/
var $ = function (q, c) {
if (typeof q == 'function')
ready(q);
else if (q instanceof Query)
return q;
else
return new Query(q, c);
};
/*
DomReady实现
*/
function ready(callback) {
if (/complete|loaded|interactive/.test(document.readyState))
callback()
else
document.addEventListener('DOMContentLoaded', onload, false);
function onload() {
document.removeEventListener('DOMContentLoaded', onload);
callback()
}
}
/**
* 读取缓存,将元素和事件关联起来
* @param el DOM元素
* @param evt 事件名称
* @returns {Array} 事件handler列表
*/
function data(el, evt) {
var key = getKey(el);
var elEvts = cache[key] || (cache[key] = {});
return elEvts[evt] || (elEvts[evt] = []);
function getKey(el) {
var key = el.__qkey;
if (!key) {
el.__qkey = key = Math.random();
}
return key;
}
}
/**
* 遍历对象属性或数组
* @param o Object类型或Array类型
* @param fn 回调函数.
* 对于Array,fn形式为function(arrItem){}
* 对于Object,fn形式为function(propertyKey, propertyValue){}
*/
$.each = function (o, fn) {
if (o instanceof Array)
o.forEach(fn);
else
for (var k in o)
fn(k, o[k])
}
/*
清除非永久性的事件绑定
通过$(selector).on方法第3个参数,来指定是否为永久性事件绑定
*/
$.clear = function () {
$.each(cache, function (key, elEvts) {
$.each(elEvts, function (evt, cbs) {
cbs.forEach(function (cb, i) {
//移除非永久的回调
if (!cb.forEver) cbs.splice(i, 1)
});
if (cbs.length == 0) delete elEvts[evt]
})
if (Object.keys(elEvts).length == 0)delete cache[key]
})
};
$.event = {
on: function (qObj, evts, fn, forEver, once) {
if (once) once = function () {
$.event.off(qObj, evts, fn)
}
tryEach(qObj, function (e) {
evts.split(',').forEach(function (evt) {
evt == 'tap' && (evt = tap);
var cb = {fn: fn, forEver: !!forEver, _fn: once};
var l = data(e, evt);
//只需绑一次事件
if (l.length == 0 && e.addEventListener) e.addEventListener(evt, $.event.dispatch, false);
l.push(cb);
})
})
},
off: function (qObj, evts, fn) {
tryEach(qObj, function (e) {
evts.split(',').forEach(function (evt) {
evt == 'tap' && (evt = tap);
var cbs = data(e, evt);
if (fn === undefined) {
cbs.splice(0, cbs.length);//clear
}
else {
for (var i = 0; i < cbs.length; i++) {
//是同一个函数,或者函数的globalId相同,即可移除
if (cbs[i].fn == fn || (fn.globalId && fn.globalId == cbs[i].fn.globalId)) {
cbs.splice(i, 1);//remove one
i--;
}
}
}
if (cbs.length == 0) {
e.removeEventListener && e.removeEventListener(evt, $.event.dispatch);
}
});
});
},
/**
* 所有绑定事件的统一代理方法
*/
dispatch: function (evt) {
var cbs = data(this, evt.type).slice(0)//复制,避免还没轮执行到就被移除
, el = this, args = [evt];
evt.args && args.push.apply(args, evt.args);
cbs.forEach(function (cb) {
cb.fn && cb.fn.apply(el, args);
//为实现one()的内部处理
cb._fn && cb._fn();
})
},
/*
构建自定义事件
*/
create: function (evt, noBubble) {
evt == 'tap' && (evt = tap);
var eType = //"click,mousedown,mouseup,mousemove".indexOf(evt) > -1 ? "MouseEvents" :
"Events";
var event = document.createEvent(eType);
event.initEvent(evt, !noBubble, true);
return event
}
};
/**
* 包装对象
* @param selector 选择器
* 如果以“<”开头,作为html构建内存节点;
* 否则作为选择器,querySelectorAll的参数
* @param context 选择器的上下文对象
*/
function Query(selector, context) {
this.es = [], this.length = 0;
var es = typeof selector == 'string'
? (selector.substr(0, 1) == '<' ? buildByFragment(selector) : select(selector, context))
: (selector ? [selector] : []);
setItems(this, es)
}
/*以元素数组来构建Query对象*/
function arrQuery(es) {
var r = $();
setItems(r, es);
return r;
}
/**
* 选择器
*/
function select(selector, context) {
context = context || document;
return context.querySelectorAll(selector);
}
/*
* 传入html,构建为内存对象
*/
function buildByFragment(html) {
var div = document.createElement('div');
div.innerHTML = html;
var nodes = toArr(div.children);
div.innerHTML = '';
return nodes;
}
//下面这些方法含义和jQuery类似
Query.prototype = $.fn = {
/**
* 绑定事件.forEver的事件不会被clear,但可以解绑
*/
on: function (evt, fn, forEver, once) {
$.event.on(this, evt, fn, forEver, once);
return this
},
/**
* 解绑事件
* @param fn 解绑的函数.
* 如果不指定,则移除此事件上绑定的所有函数
* 需和绑定时的函数是同一个函数实例才能解绑(注意:闭包中声明的函数,每次执行时不是同一个实例).
* 如果希望强制解除,可以为fn指定一个固定的globalId属性(不同的函数实例只要globalId相同,就能互相解绑)
*/
off: function (evt, fn) {
$.event.off(this, evt, fn);
return this
},
/**
* 绑定事件,执行一次后自动解绑
*/
one: function (evt, fn, forEver) {
return this.on(evt, fn, forEver, 1);
},
/*
先解绑,再绑定. 可避免重复绑定,参数与off和on方法一致
*/
offOn: function (evt, fn, forEver, once) {
return this.off(evt, fn).on(evt, fn, forEver, once)
},
find: function (selector) {
return eachJoin(this, function (e) {
return select(selector, e)
});
},
eq: function (i) {
return $(this[i]);
},
slice: function (start, end) {
return arrQuery(this.es.slice(start, end));
},
//其他方法
first: function () {
return this.eq(0);
},
last: function () {
return this.eq(this.length - 1);
},
parent: function () {
return eachJoin(this, function (e) {
return e.parentNode
});
},
closest: function (selector) {
return eachJoin(this, function (e) {
do {
e = e.parentNode;
} while (e && !elIs(e, selector));
return e;
});
},
next: function () {
return eachJoin(this, function (e) {
return sibling(e, 1)
})
},
prev: function () {
return eachJoin(this, function (e) {
return sibling(e)
})
},
children: function () {
return eachJoin(this, function (e) {
return e.children
})
},
remove: function () {
return tryEach(this, function (e) {
e.parentNode.removeChild(e);
})
},
/*css: function (p, val) {
return typeof p == 'string'
? tryEach(this, function (e) {
e.style[p] = val;
})
: tryEach(this, function (e) {
for (var n in p)
e.style[n] = p[n];
})
},*/
addClass: function (cls) {
return tryEach(this, function (e) {
cls && cls.split(' ').forEach(function (i) {
e.classList.add(i)
})
})
},
/*
@param cls 要移除的class,如果不传此参数,则移除所有class
*/
removeClass: function (cls) {
var removeAll = !arguments.length;
return tryEach(this, function (e) {
removeAll ? e.className = '' : e.classList.remove(cls);
})
},
toggleClass: function (cls, isAdd) {
var noArg2 = arguments.length == 1;
return tryEach(this, function (e) {
if (noArg2)
isAdd = !e.classList.contains(cls);
e.classList[isAdd ? 'add' : 'remove'](cls);
})
},
hasClass: function (cls) {
var e = this.es[0];
return e && e.classList.contains(cls)
},
css: function (n, val) {
var isString = typeof n == 'string', arg = arguments;
return access(this, arg, 1, function (e) {
return e.style[n];
}, function (e) {
if (isString) {
e.style[n] = val;
} else {
for (var k in n) {
e.style[k] = n[k];
}
}
}, function () {
return isString && arg.length < 2;
});
},
hide: function () {
return tryEach(this, function (e) {
e.style.display = 'none';
})
},
show: function () {
return tryEach(this, function (e) {
e.style.display = 'initial';
})
},
text: function (tx) {
return access(this, arguments, 0, function (e) {
return e.tagName == 'SELECT' ? (e = e.options[e.selectedIndex]) && e.text : e.textContent
}, function (e) {
e.textContent = tx
});
},
html: function (htm) {
return access(this, arguments, 0
, function (e) {
return e.innerHTML
}
, function (e) {
e.innerHTML = htm || '';
}
);
},
val: function (v) {
return access(this, arguments, 0, function (e) {
return e.value
}, function (e) {
e.value = v === undefined ? '' : v;
});
},
is: function (selector) {
return elIs(this.es[0], selector);
},
rect: function () {
return getRect(this.es[0]);
},
/**
* 取style.width或offsetWidth
*/
width: function (val) {
return access(this, arguments, 0
, function (e) {
return getRect(e).width
}
, function (e) {
if (/^[\d\.]+$/.test(val)) {
val += 'px'
}
e.style.width = val
}
);
},
/**
* 取style.height或offsetHeight
*/
height: function (val) {
return access(this, arguments, 0
, function (e) {
return getRect(e, 1).height
}
, function (e) {
if (/^[\d\.]+$/.test(val)) {
val += 'px';
}
e.style.height = val
}
);
},
/*
获取或设置属性.如果是元素固有属性,直接操作之;否则通过setAttribute设置
*/
attr: function (name, val) {
return access(this, arguments, 1
, function (e) {
return isElProperty(e, name) ? e[name] : e.getAttribute(name)
}
, function (e) {
if (isElProperty(e, name)) e[name] = val;
else e.setAttribute(name, val);
}
);
//判断是否是元素固有属性,如disabled或checked
function isElProperty(e, name) {
while (e != null) {
if (e.hasOwnProperty(name)) return true;
e = e.__proto__;
}
return false
}
},
/*
* 节点操作
*/
/*prepend: function (es) {
return tryEach2(this, es, function (prt, child) {
prt.insertBefore(child, prt.childNodes[0])
})
},
prependTo: function (es) {
return tryEach2(this, es, function (child, prt) {
prt.insertBefore(child, prt.childNodes[0])
})
},*/
append: function (es) {
return tryEach2(this, es, function (prt, child) {
prt.appendChild(child)
})
},
appendTo: function (es) {
return tryEach2(this, es, function (child, prt) {
prt.appendChild(child)
})
},
before: function (es) {
return tryEach2(this, es, function (e1, e2) {
e2.parentNode.insertBefore(e1, e2)
})
},
//在DOM节点上触发事件,支持冒泡
trigger: function (name, noBubble) {
var event = typeof name === 'string' ? $.event.create(name, noBubble) : name;
event.args = [].slice.call(arguments, 2);
return tryEach(this, function (e) {
e.dispatchEvent ? e.dispatchEvent(event) : $.event.dispatch.call(e, event);
})
},
//触发元素上绑定的事件操作
triggerHandler: function (name) {
var event = $.event.create(name);
return tryEach(this, function (e) {
$.event.dispatch.call(e, event)
})
},
each: function (fn) {
this.es.forEach(fn);
}
};
//一系列的事件方法,on方法的简写。移动端省略了鼠标事件
["blur", "focus", "focusin", "focusout", "load", "resize", "scroll", "unload"
, tap//, "click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave"
, "touchmove", "touchstart", "touchend", "touchcancel"
, "change", "select", "submit", "keydown", "input", "keypress", "keyup", "error"//, "contextmenu"
].forEach(function (evt) {
$.fn[evt] = function (fn, forEver, once) {
return this.on(evt, fn, forEver, once)
}
});
$.fn.tap = $.fn[tap];
/*
判断元素e是否符合选择器
*/
function elIs(e, selector) {
var m = e && (e.webkitMatchesSelector || e.matchesSelector || e.matches);
return m ? m.call(e, selector) : $(selector).es.indexOf(e) > -1;
}
/**
* 根据val是否传递,来获取值,或者设置值
*/
function access(qObj, args, i, fnGet, fnSet, fnJudge) {
//没有索引为i的参数,就作为fnGet
if (fnJudge ? fnJudge() : args.length <= i) {
var r = qObj[0] && fnGet(qObj[0]);
return r === undefined || r === null ? '' : r;
}
else if (fnSet) {
qObj.es.forEach(function (e) {
fnSet(e, args[i])
});
return qObj;
}
}
/**
* 尝试遍历内部数组
*/
function tryEach(qObj, fn) {
try {
qObj.es.forEach(fn)
} catch (e) {
}
return qObj;
}
function tryEach2(qObj, el, fn) {
qObj.es.forEach(function (e) {
try {
$(el).es.forEach(function (e2) {
fn.call(e, e, e2)
})
} catch (ex) {
}
});
return qObj;
}
/**
* 根据结果扩充或缩减内部数组
*/
function eachJoin(qObj, fn) {
try {
var es = [];
qObj.es.forEach(function (e) {
var r = fn(e);
if (r && r.length !== undefined && !r.tagName) {
r = r instanceof Array ? r : toArr(r);
es = es.concat(r);
}
else if (r && es.indexOf(r) == -1) {
es.push(r);
}
});
} catch (e) {
}
return arrQuery(es);
}
/**
* 获取非文本兄弟节点
*/
function sibling(e, next) {
try {
do {
e = (next ? e.nextSibling : e.previousSibling)
} while (e && e.nodeType != 1);
} catch (e) {
}
return e
}
/*
获取高度或宽度,位置
*/
function getRect(el, isHeight) {
var name = isHeight ? 'Height' : 'Width';
if (el === window) {
var w = window;
return {
height: w['inner' + name],
width: w['inner' + name],
left: w.pageXOffset,
top: w.pageYOffset
}
}
return el && el.getBoundingClientRect();
}
/*
将集合转换为数组
*/
function toArr(coll) {
return [].slice.call(coll, 0)
}
/**
* 根据新对象数组,设置索引属性值和length值
*/
function setItems(qObj, es) {
var i;
for (i = 0; i < qObj.length; i++) {
delete qObj[i]
}
if (!(es instanceof Array)) {
es = toArr(es);
}
for (i = 0; i < es.length; i++) {
qObj[i] = es[i];
}
qObj.es = es;
qObj.length = es.length
}
/*
模拟tap事件,模拟stroke事件
tap事件:点击屏幕,不移动,并立即抬起时触发
支持changedTouches等属性
支持preventDefault().(事件绑在body上,故stopPropagation无意义)
简化处理:如果touchstart和touchend移动范围很小,而且时间间隔短,则触发tap事件
stroke事件: 点击屏幕,拖动,然后抬起时触发
支持deltaX和deltaY属性,表示移动的距离.值的正负表示方向.
支持preventDefault()
*/
$(function () {
var time, x, y, el;
$(document.body).touchstart(function (e) {
time = Date.now();
el = e.target;
x = e.changedTouches[0].pageX;
y = e.changedTouches[0].pageY;
$(e.target).addClass('tapping');
setTimeout(function () {
$(e.target).removeClass('tapping');
}, tapDelay);
}, 1).touchend(function (e) {
if (el == e.target) {
var tch = e.changedTouches[0], timespan = Date.now() - time
, dx = tch.pageX - x, dy = tch.pageY - y
, dt = Math.abs(dx) + Math.abs(dy);
if (dt < tapMaxMove) {
if (timespan < tapDelay) {
fire(tap, e, {})
}
} else if (dt > tapMaxMove * 3) {
var attr = {deltaX: dx, deltaY: dy};
fire('stroke', e, attr);
}
}
}, 1);
/*
基于TouchEvent触发touch类模拟事件
*/
function fire(evt, e, attr) {
var tapE = document.createEvent('Event');
tapE.initEvent(evt, true, true);
['touches', 'targetTouches', 'changedTouches'].forEach(function (a) {
tapE[a] = e[a];
});
Object.keys(attr).forEach(function (a) {
tapE[a] = attr[a]
});
$(e.target).trigger(tapE);//在DOM上触发事件绑定(同步执行)
if (tapE.defaultPrevented) e.preventDefault();
}
});
return $;
})();
module.exports = $;