touchkit
Version:
a touch kit based on mtouch,it make you create a gesture app more quickly and simply
783 lines (723 loc) • 24.7 kB
JavaScript
import MT from 'mtouch';
import MC from 'mcanvas';
import removeBtn from './removeBtn';
import ZIndex from './zIndex';
import _ from './utils';
const EVENT = ['touchstart','touchmove','touchend','drag','dragstart','dragend','pinch','pinchstart','pinchend','rotate','rotatestart','rotatend','singlePinchstart','singlePinch','singlePinchend','singleRotate','singleRotatestart','singleRotatend'];
const noop = function () {};
window.requestAnimFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
export default function Touchkit(ops) {
// 兼容不使用 new 的方式;
if (!(this instanceof Touchkit))
return new Touchkit(ops);
this._ops = {
el: null,
use:{
drag:false,
pinch:false,
rotate:false,
singlePinch:false,
singleRotate:false,
},
limit:false,
// event
event: {},
};
EVENT.map(eventName => this._ops.event[eventName] = noop);
if(typeof ops == 'object'){
this._ops = _.extend(this._ops, ops);
}else if(typeof ops == 'string'){
this._ops.el = ops;
}
// 手势容器;
this.el = _.getEl(this._ops.el);
_.addClass(this.el,'mt-touch-box');
// 容器宽高,优先使用clientWidth,避免边框等因素的影响;
this.elStatus = {
width:this.el.clientWidth || this.el.offsetWidth,
height:this.el.clientHeight || this.el.offsetHeight,
};
// 初始化mtouch;
this.mt = MT(this.el);
this._insertCss()._init()._bind();
}
Touchkit.prototype._init = function(childs = {}){
// 操作元素
this.operator = null;
this.operatorStatus = null;
this._cropBox = false;
this.transform = null;
this.freezed = false;
// 子元素仓库,index用于标记子元素;
this._childs = childs;
this._childIndex = 0;
this._activeChild = null;
// 管理子元素之间的zindex层级关系;
this._zIndexBox = new ZIndex();
return this;
};
Touchkit.prototype.background = function(ops){
let _ops = {
// 背景图片,type: url/HTMLImageElement/HTMLCanvasElement
image:'' ,
// 绘制方式: crop / contain
// crop : 裁剪模式,背景图自适应铺满画布,多余部分裁剪;
// contain : 包含模式, 类似于 background-size:contain; 可通过left/top值进行位置的控制;
type:'contain',
// 背景图片距离画布左上角的距离,
left:0,
top:0,
// 在type=crop时使用,背景图只需启动拖动操作;
use:{},
static:false,
success(){},
error(){},
};
_ops = _.extend(_ops,ops);
_.getImage(_ops.image, img => {
// 背景图真实宽高及宽高比;
let {iw,ih} = this._getSize(img);
let iratio = iw / ih;
// 容器宽高及宽高比;
let pw = this.elStatus.width,
ph = this.elStatus.height,
pratio = pw / ph;
let left,top,width,height;
let minX = 0 , minY = 0;
let ratio;
let template = _.domify(`<div class="mt-background" data-mt-index="background" data-mt-bg-type=${_ops.type}><div class="mt-prevent"></div></div>`)[0];
// 初始化背景图属性;
_.addClass(img,'mt-image');
template.appendChild(img);
if(_ops.type == 'contain'){
if(iratio > pratio){
left = _ops.left || 0;
top = _ops.top || (ph - pw/iratio) / 2;
width = pw;
height = pw / iratio;
ratio = iw / width;
}else{
left = _ops.left || (pw - ph*iratio) / 2;
top = _ops.top || 0;
width = ph*iratio;
height = ph;
ratio = ih / height;
}
if(!ops.static){
_ops.use = {
drag:true,
pinch:true,
rotate:true,
};
}
}else if(_ops.type == 'crop'){
left = 0;
top = 0;
if(iratio > pratio){
width = ph*iratio;
height = ph;
minX = (width - pw)/width;
ratio = ih / height;
}else{
width = pw;
height = pw/iratio;
minY = (height - ph)/height;
ratio = iw / width;
}
_ops.limit = {
x:minX,
y:minY,
maxScale:1,
minScale:1,
};
if(!ops.static){
_ops.use = {
drag : true,
};
}
}
_.setStyle(template,{
width:`${width}px`,
height:`${height}px`,
transform:`translate(${left}px,${top}px)`,
webkitTransform:`translate(${left}px,${top}px)`,
});
_ops.pos = {width,height,left,top};
this.el.appendChild(template);
// 记录背景图参数;
_ops.ratio = ratio;
this._childs.background = {
el:template,
ops: _ops,
type:'background',
};
_ops.success(this);
},(err)=>{
_ops.error(err);
});
return this;
};
Touchkit.prototype.add = function(ops){
let _ops = {
image:'',
width:'',
use:{
drag:false,
pinch:false,
rotate:false,
singlePinch:false,
singleRotate:false,
},
limit:false,
pos:{
x:0,
y:0,
scale:1,
rotate:0,
},
close:false,
success(){},
error(){},
};
if(!_.isArr(ops))ops = [ops];
ops.forEach(v=>{
_.getImage(v.image,img=>{
if(v.use == 'all'){
v.use ={
drag:true,
pinch:true,
rotate:true,
singlePinch:true,
singleRotate:true,
};
}
this._add(img,_.extend(_ops,v));
},err=>{
_ops.error(err);
});
});
return this;
};
Touchkit.prototype._add = function(img,ops){
let {iw,ih} = this._getSize(img);
let iratio = iw / ih;
let _templateEl = img;
let _ele = _.domify(`<div class="mt-child" id="mt-child-${this._childIndex}" data-mt-index="${this._childIndex}"><div class="mt-prevent"></div></div>`)[0];
let originWidth = this._get('hor',ops.width),
originHeight = originWidth / iratio;
// space 为因为缩放造成的偏移误差;
let spaceX = (ops.pos.scale - 1) * originWidth/2,
spaceY = (ops.pos.scale - 1) * originHeight/2;
_.setStyle(_ele,{
width:`${originWidth}px`,
height:`${originHeight}px`,
});
_.addClass(_templateEl,'mt-image');
_ele.appendChild(_templateEl);
// 是否添加关闭按钮;
if(ops.close || this._ops.close){
_ele.appendChild(_.domify(`<div class="mt-close-btn"></div>`)[0]);
}
this.el.appendChild(_ele);
ops.pos ={
x:this._get('hor',ops.pos.x) + spaceX,
y:this._get('ver',ops.pos.y) + spaceY,
scale:ops.pos.scale,
rotate:ops.pos.rotate,
};
// 记录数据;
this._childs[this._childIndex] = {
el:_ele,
ops: ops,
type:'element',
};
// 根据id进行zIndex的设置;
this._zIndexBox.setIndex(`mt-child-${this._childIndex}`);
// 没有开启单指操作时,不添加单指按钮;
let addButton = ((ops.use.singlePinch || this._ops.use.singlePinch) || (ops.use.singleRotate || this._ops.use.singleRotate)) ? true : false;
// 切换operator到新添加的元素上;
this.switch(_ele,addButton);
this._setTransform('all', _ele , ops.pos);
_.setStyle(_ele,{
visibility:'visible',
});
this._childIndex++;
ops.success(this);
};
Touchkit.prototype.cropBox = function(){
let cropBox = _.domify(`<div class="mt-crop-box" data-mt-index="cropBox"><div class="mt-close-btn"></div></div>`)[0];
this.el.appendChild(cropBox);
this.switch(cropBox);
this._cropBox = true;
this._childs['cropBox'] = {
el:cropBox,
type:'cropBox',
ops: {
width:cropBox.offsetWidth,
height:cropBox.offsetHeight,
use:{
drag:true,
pinch:false,
rotate:false,
singlePinch:true,
singleRotate:false,
},
limit:{
x:0,
y:0,
maxScale:1,
minScale:0.2,
},
},
};
};
// 使用 mcanvas 合成图片后导出 base64;
Touchkit.prototype.exportImage = function(cbk,cropOps){
let cwidth = this.elStatus.width,
cheight = this.elStatus.height;
let ratio = 1;
let addChilds =[];
if(this._childs.background){
let bg = this._childs.background;
ratio = bg.ops.ratio;
let image = bg.el.querySelector('.mt-image');
let bgPos = _.xRatio(_.getPos(bg.el),ratio);
addChilds.push({
image:image,
options:{
width:image.width * ratio,
pos:bgPos,
},
});
}
let mc = new MC(cwidth*ratio,cheight*ratio,'#ffffff');
this._zIndexBox.zIndexArr.forEach(v=>{
let child = document.querySelector('#'+v);
let image = child.querySelector('.mt-image');
let childPos = _.xRatio(_.getPos(child),ratio);
let width = image.clientWidth || image.offsetWidth;
addChilds.push({
image:image,
options:{
width: width * ratio,
pos:childPos,
},
});
});
mc.add(addChilds).draw(b64=>{
if(this._cropBox){
let cropBoxOps = this._childs.cropBox;
let cropBox = cropBoxOps.el;
let cropBoxPos = _.getPos(cropBox);
let corpBoxMc = new MC(cropBoxOps.ops.width * ratio,cropBoxOps.ops.height * ratio);
corpBoxMc.add(mc.canvas,{
width:mc.canvas.width,
pos:{
x: -cropBoxPos.x * ratio,
y: -cropBoxPos.y * ratio,
scale:1,
rotate:0,
},
}).draw(b64=>{
cbk(b64);
});
}else if(cropOps){
let _default = {
x:0,
y:0,
width:'100%',
height:'100%',
};
cropOps = _.extend(_default,cropOps);
cropOps.width = this._get('hor',cropOps.width,(mc.canvas.width-cropOps.x));
cropOps.height = this._get('ver',cropOps.height,(mc.canvas.height-cropOps.y));
let cropMc = new MC(cropOps.width,cropOps.height);
cropMc.add(mc.canvas,{
width:mc.canvas.width,
pos:{
x: -cropOps.x ,
y: -cropOps.y,
scale:1,
rotate:0,
},
}).draw(b64=>{
cbk(b64);
});
}else{
cbk(b64);
}
});
};
Touchkit.prototype._bind = function(){
// 绑定所有事件;
EVENT.forEach(evName=>{
if(!this[evName]){
this[evName] = () =>{
this._ops.event[evName]();
};
}
this.mt.on(evName,this[evName].bind(this));
});
// 切换子元素;
let bgStart,childStart;
_.delegate(this.el,'touchstart','.mt-background',()=>{
bgStart = new Date().getTime();
});
_.delegate(this.el,'touchend','.mt-background',ev=>{
if(new Date().getTime() - bgStart > 300 || ev.touches.length)return;
this.switch(ev.delegateTarget,false);
});
_.delegate(this.el,'touchend','.mt-crop-box',ev=>{
if(ev.touches.length > 0)return;
this.switch(ev.delegateTarget);
});
// 点击子元素外的区域失去焦点;
this.el.addEventListener('click',ev=>{
if(!this._isAdd(ev.target)){
this.switch(null);
}
});
_.delegate(this.el,'touchstart','.mt-child',()=>{
childStart = new Date().getTime();
});
// 切换子元素;
_.delegate(this.el,'touchend','.mt-child',ev=>{
if(new Date().getTime() - childStart > 300 || ev.touches.length > 0)return;
let el = ev.delegateTarget,
_ops = this._getOperatorOps(el),
_addButton = ((_ops.use.singlePinch || this._ops.use.singlePinch) || (_ops.use.singleRotate || this._ops.use.singleRotate)) ? true : false;
this.switch(el,_addButton);
this._zIndexBox.toTop(el.id);
});
// 关闭按钮事件;
_.delegate(this.el,'click','.mt-close-btn',ev=>{
let _el = ev.delegateTarget;
let _child = _el.parentNode || _el.parentElement;
let index = _.data(_child,'mt-index');
if(index == 'cropBox'){
this.switch(null);
this._cropBox = false;
}else{
this._zIndexBox.removeIndex(_child.id);
}
_.remove(_child);
this._childs[index] = null;
});
return this;
};
Touchkit.prototype.touchstart = function(ev){
if(!this.freezed){
if(this.operator){
this.transform = _.getPos(this.operator);
}
this._ops.event.touchstart(ev);
}
};
Touchkit.prototype.drag = function(ev){
if(!this.freezed){
if(this.operator){
let ops = this._getOperatorOps();
if(ops.use.drag || this._ops.use.drag){
this.transform.x += ev.delta.deltaX;
this.transform.y += ev.delta.deltaY;
this._setTransform('drag');
}
}
this._ops.event.drag(ev);
}
};
Touchkit.prototype.pinch = function(ev){
if(!this.freezed){
if(this.operator){
let ops = this._getOperatorOps();
if(ops.use.pinch || this._ops.use.pinch){
this.transform.scale *= ev.delta.scale;
this._setTransform('pinch');
}
}
this._ops.event.pinch(ev);
}
};
Touchkit.prototype.rotate = function(ev){
if(!this.freezed){
if(this.operator){
let ops = this._getOperatorOps();
if(ops.use.rotate || this._ops.use.rotate){
this.transform.rotate += ev.delta.rotate;
this._setTransform('rotate');
}
}
this._ops.event.rotate(ev);
}
};
Touchkit.prototype.singlePinch = function(ev){
if(!this.freezed){
if(this.operator){
let ops = this._getOperatorOps();
if(_.data(this.operator,'mt-index') == 'cropBox'){
if(ops.use.singlePinch || this._ops.use.singlePinch){
let cropBoxPos = _.getPos(this.operator);
if((ops.width + ev.delta.deltaX + cropBoxPos.x) < this.elStatus.width){
ops.width += ev.delta.deltaX;
}
if((ops.height + ev.delta.deltaY + cropBoxPos.y) < this.elStatus.height){
ops.height += ev.delta.deltaY;
}
_.setStyle(this.operator,{
width:`${ops.width}px`,
height:`${ops.height}px`,
});
}
}else{
if(ops.use.singlePinch || this._ops.use.singlePinch){
this.transform.scale *= ev.delta.scale;
this._setTransform('pinch');
}
}
}
this._ops.event.singlePinch(ev);
}
};
Touchkit.prototype.singleRotate = function(ev){
if(!this.freezed){
if(this.operator){
let ops = this._getOperatorOps();
if(ops.use.singleRotate || this._ops.use.singleRotate){
this.transform.rotate += ev.delta.rotate;
this._setTransform('rotate');
}
}
this._ops.event.singleRotate(ev);
}
};
Touchkit.prototype._setTransform = function(type, el = this.operator, transform = this.transform) {
let trans = JSON.parse(JSON.stringify(transform));
let ops = this._getOperatorOps();
let defaulLimit = (this._ops.limit && typeof this._ops.limit == 'object') ? _.extend({
x:0.5,
y:0.5,
maxScale:3,
minScale:0.4,
},this._ops.limit) : {
x:0.5,
y:0.5,
maxScale:3,
minScale:0.4,
};
let _limit = (ops.limit && ops.limit !== true) ? _.extend(defaulLimit,ops.limit) : defaulLimit;
if(ops.limit || this._ops.limit){
trans = this._limitOperator(trans,_limit);
}
if((ops.use.singlePinch || this._ops.use.singlePinch) && (type == 'all' || type == 'pinch')){
let singlePinchBtn = el.querySelector(`.mtouch-singleButton`);
_.setStyle(singlePinchBtn,{
transform:`scale(${1/trans.scale})`,
webkitTransform:`scale(${1/trans.scale})`,
});
}
if((ops.close || this._ops.close) && (type == 'all' || type == 'pinch')){
let closeBtn = el.querySelector(`.mt-close-btn`);
_.setStyle(closeBtn,{
transform:`scale(${1/trans.scale})`,
webkitTransform:`scale(${1/trans.scale})`,
});
}
window.requestAnimFrame(()=>{
_.setPos(el, trans);
});
};
Touchkit.prototype._limitOperator = function(transform,limit) {
// 实时获取操作元素的状态;
let {minScale, maxScale} = limit;
let operatorStatus,spaceX,spaceY,boundaryX,boundaryY,minX,minY,maxX,maxY;
if (minScale && transform.scale < minScale){
transform.scale = minScale;
}
if (maxScale && transform.scale > maxScale){
transform.scale = maxScale;
}
operatorStatus = _.getOffset(this.operator);
// 因缩放产生的间隔;
spaceX = operatorStatus.width * (transform.scale - 1) / 2;
spaceY = operatorStatus.height * (transform.scale - 1) / 2;
// 参数设置的边界值;
boundaryX = operatorStatus.width * transform.scale * (limit.x);
boundaryY = operatorStatus.height * transform.scale * (limit.y);
// 4个边界状态;
minX = spaceX - boundaryX;
minY = spaceX - boundaryY;
maxX = this.elStatus.width - operatorStatus.width * transform.scale + spaceX + boundaryX;
maxY = this.elStatus.height - operatorStatus.height * transform.scale + spaceY + boundaryY;
if(limit.x || limit.x == 0){
if(transform.x >= maxX)transform.x = maxX;
if(transform.x < minX)transform.x = minX;
}
if(limit.y || limit.y == 0){
if(transform.y > maxY)transform.y = maxY;
if(transform.y < minY)transform.y = minY;
}
return transform;
};
Touchkit.prototype.switch = function(el,addButton){
if(!this.mt || this.freezed)return;
if(el)el = _.getEl(el);
_.forin(this._childs,(k,v)=>{
if(v){
_.removeClass(v.el,'mt-active');
}
});
// 转换操作元素后,也需要重置 mtouch 中的单指缩放基本点 singleBasePoint;
this.mt.switch(el,addButton);
// 切换operator;
this.operator = el;
if(el){
_.addClass(el,'mt-active');
this._activeChild = el;
}
return this;
};
Touchkit.prototype._getOperatorOps = function(target){
let _tar = target || this.operator;
let index = _.data(_tar,'mt-index');
if(this._childs[index]){
return this._childs[index].ops;
}
};
// 冻结手势容器,暂停所有操作,且失去焦点;
// 解冻后恢复最后状态;
Touchkit.prototype.freeze = function(boolean){
if(boolean){
_.forin(this._childs,(k,v)=>{
if(v){
_.removeClass(v.el,'mt-active');
}
});
}else{
_.addClass(this._activeChild,'mt-active');
}
this.freezed = boolean ? true:false;
return this;
};
Touchkit.prototype.clear = function(index = null){
if (index !== null) {
try {
_.remove(this.getChild(index).el);
this._childs[index] = null;
} catch (error) {
console.error('Can not find this el or has been deleted!');
}
} else {
_.forin(this._childs,(k,v)=>{
if(v && v.type == 'element'){
_.remove(v.el);
this._childs[k] = null;
}
});
this._init(this._childs);
}
return this;
};
// 重置所有状态到初始化阶段;
Touchkit.prototype.reset = function(){
_.forin(this._childs,(k,v)=>{
if(v){
_.remove(v.el);
}
});
this._init();
return this;
};
// 销毁,但保持原有样式,失去焦点与事件绑定;
Touchkit.prototype.destory = function(){
_.forin(this._childs,(k,v)=>{
if(v){
_.removeClass(v.el,'mt-active');
}
});
this.mt && this.mt.destroy();
this.mt = null;
};
// 参数加工函数;
// 兼容 5 种 value 值:
// x:250, x:'250px', x:'100%', x:'left:250',x:'center',
// width:100,width:'100px',width:'100%'
Touchkit.prototype._get = function(drection,str,par , child){
let result = str;
let k,_par,_child;
if(document.body && document.body.clientWidth){
k = drection == 'hor' ? 'clientWidth':'clientHeight';
}else{
k = drection == 'hor' ? 'offsetWidth' : 'offsetHeight';
}
_par = par || this.el[k];
_child = child || (this.operator ? this.operator[k] : 0);
if(typeof str === 'string'){
if(_.include(str,':')){
let arr = str.split(':');
switch (arr[0]) {
case 'left':
case 'top':
result = +(arr[1].replace('px',''));
break;
case 'right':
case 'bottom':
result = _par - (+(arr[1].replace('px',''))) - _child;
break;
default:
}
}else if (_.include(str,'px')) {
result = (+str.replace('px', ''));
} else if (_.include(str,'%')) {
result = _par * (+str.replace('%', '')) / 100;
}else if(str == 'center'){
result = (_par-_child)/2;
}else{
result = +str;
}
}
return result;
};
Touchkit.prototype.getChild = function(index){
return this._childs[index] || null;
};
Touchkit.prototype._isAdd = function(el){
let target = el;
while(target !== this.el || target.tagName.toLowerCase() == 'body'){
if(_.include(target.className,'mt-child') || _.include(target.className,'mt-background') || _.include(target.className,'mt-crop-box')){
return true;
}
target = target.parentNode;
}
return false;
};
Touchkit.prototype._getSize = function(img){
let iw,ih;
if(img.tagName === 'IMG'){
iw = img.naturalWidth;
ih = img.naturalHeight;
}else if(img.tagName === 'CANVAS'){
iw = img.width;
ih = img.height;
}else{
iw = img.offsetWidth;
ih = img.offsetHeight;
}
return{iw,ih};
};
Touchkit.prototype._insertCss = function(){
_.addCssRule('.mt-touch-box','-webkit-user-select: none;');
_.addCssRule('.mtouch-singleButton','display: none;');
_.addCssRule('.mt-child.mt-active','z-index: 99;outline:2px solid hsla(0,0%,100%,.5);');
_.addCssRule('.mt-active .mtouch-singleButton,.mt-active .mt-close-btn','display: inline-block;');
_.addCssRule('.mt-child','position:absolute;text-align:left;visibility:hidden;');
_.addCssRule('.mt-image','width:100%;height:100%;position:absolute;left:0;top:0;text-align:left;');
_.addCssRule('.mt-close-btn',`z-index:999;position:absolute;width:30px;height:30px;top:-15px;right:-15px;background-size:100%;display:none;background-image:url(${removeBtn})`);
_.addCssRule('.mt-background','position:absolute;left:0;top:0;');
_.addCssRule('.mt-crop-box','position:absolute;left:5px;top:5px;width:90%;height:90%;border:2px dashed #996699;box-sizing:border-box;z-index:20;');
_.addCssRule('.mt-prevent','width:100%;height:100%;position:absolute;left:0;top:0;z-index:99;');
return this;
};