Draggable Elements 元素拖拽功能实现代码


Posted in Javascript onMarch 30, 2011

当然我们可以研究js库的源码, 也可以自己去发明轮子试试看, 其过程还是挺有趣的...下面我就来实现下页面元素的拖拽功能
现在就开始着手实现, 让我们从最顶层的方法讲起, 它用于初始化一个drag object, 方法的声明如下
function DragObject(cfg)
这里的cfg我们用一个对象来传入, 有点像Extjs里配置属性

var dragObj = new DragObject({ 
el: 'exampleB', 
attachEl: 'exampleBHandle', 
lowerBound: new Position(0, 0), //position代表一个点,有属性x,y下面会详细讲到 
upperBound: new Position(500, 500), 
startCallback: ..., // 开始拖拽时触发的回调 这里均省略了 
moveCallback: ..., // 拖拽过程中触发的回调 
endCallback: ..., // 拖拽结束触发的回调 
attachLater: ... // 是否立刻启动拖拽事件的监听 
});

配置参数中el可以是具体元素的id, 也可以直接是个dom对象 attachEl就是例子中的handle元素, 通过拖拽它来拖拽元素, lowerBound和upperBound是用于限定拖拽范围的, 都是Position对象, 关于这个对象的封装和作用我们下面会分析到,不急哈: ), 如果没有传入的话, 拖拽的范围就没有限制. startCallback, moveCallback, endCallback都是些回调函数, attachLater为true或者false. 如果不是很明白看了下面的分析, 我想你肯定很快会清楚的..
下面就来写Position, 代码如下:
function Position(x, y) { 
this.X = x; 
thix.Y = y; 
} 
Position.prototype = { 
constructor: Position, 
add : function(val) { 
var newPos = new Position(this.X, this.Y); 
if (val) { 
newPos.X += val.X; 
newPos.Y += val.Y; 
} 
return newPos; 
}, 
subtract : function(val) { 
var newPos = new Position(this.X, this.Y); 
if (val) { 
newPos.X -= val.X; 
newPos.Y -= val.Y; 
} 
return newPos; 
}, 
min : function(val) { 
var newPos = new Position(this.X, this.Y); 
if (val) { 
newPos.X = this.X > val.X ? val.X : this.X; 
newPos.Y = this.Y > val.Y ? val.Y : this.Y; 
return newPos; 
} 
return newPos; 
}, 
max : function(val) { 
var newPos = new Position(this.X, this.Y); 
if (val) { 
newPos.X = this.X < val.X ? val.X : this.X; 
newPos.Y = this.Y < val.Y ? val.Y : this.Y; 
return newPos; 
} 
return newPos; 
}, 
bound : function(lower, upper) { 
var newPos = this.max(lower); 
return newPos.min(upper); 
}, 
check : function() { 
var newPos = new Position(this.X, this.Y); 
if (isNaN(newPos.X)) 
newPos.X = 0; 
if (isNaN(newPos.Y)) 
newPos.Y = 0; 
return newPos; 
}, 
apply : function(el) { 
if(typeof el == 'string') 
el = document.getElementById(el); 
if(!el) return; 
el.style.left = this.X + 'px'; 
el.style.top = this.Y + 'px'; 
} 
};

一个坐标点的简单封装, 它保存两个值: x, y坐标. 我们能够通过add和substract方法跟别的坐标点进行+运算和-运算, 返回一个计算过的新坐标点. min和max函数顾名思义用于跟其他坐标点进行比较,并返回其中较小和教大的值.bound方法返回一个在限定范围内的坐标点. check方法用于确保属性x, y的值是数字类型的, 否则会置0. 最后apply方法就是把属性x,y作用于元素style.left和top上. 接着我把剩下的大部分代码拿出来, 再一点一点看:
function DragObject(cfg) { 
var el = cfg.el, 
attachEl = cfg.attachEl, 
lowerBound = cfg.lowerBound, 
upperBound = cfg.upperBound, 
startCallback = cfg.startCallback, 
moveCallback = cfg.moveCallback, 
endCallback = cfg.endCallback, 
attachLater = cfg.attachLater; 
if(typeof el == 'string') 
el = document.getElementById(el); 
if(!el) return; 
if(lowerBound != undefined && upperBound != undefined) { 
var tempPos = lowerBound.min(upperBound); 
upperBound = lowerBound.max(upperBound); 
lowerBound = tempPos; 
} 
var cursorStartPos, 
elementStartPos, 
dragging = false, 
listening = false, 
disposed = false; 
function dragStart(eventObj) { 
if(dragging || !listening || disposed) return; 
dragging = true; 
if(startCallback) 
startCallback(eventObj, el); 
cursorStartPos = absoluteCursorPosition(eventObj); 
elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top'))); 
elementStartPos = elementStartPos.check(); 
hookEvent(document, 'mousemove', dragGo); 
hookEvent(document, 'mouseup', dragStopHook); 
return cancelEvent(eventObj); 
} 
function dragGo(e) { 
if(!dragging || disposed) return; 
var newPos = absoluteCursorPosition(e); 
newPos = newPos.add(elementStartPos) 
.subtract(cursorStartPos) 
.bound(lowerBound, upperBound); 
newPos.apply(el); 
if(moveCallback) 
moveCallback(newPos, el); 
return cancelEvent(e); 
} 
function dragStopHook(e) { 
dragStop(); 
return cancelEvent(e); 
} 
function dragStop() { 
if(!dragging || disposed) return; 
unhookEvent(document, 'mousemove', dragGo); 
unhookEvent(document, 'mouseup', dragStopHook); 
cursorStartPos = null; 
elementStartPos = null; 
if(endCallback) 
endCallback(el); 
dragging = false; 
} 
this.startListening = function() { 
if(listening || disposed) return; 
listening = true; 
hookEvent(attachEl, 'mousedown', dragStart); 
}; 
this.stopListening = function(stopCurrentDragging) { 
if(!listening || disposed) 
return; 
unhookEvent(attachEl, 'mousedown', dragStart); 
listening = false; 
if(stopCurrentDragging && dragging) 
dragStop(); 
}; 
this.dispose = function() { 
if(disposed) return; 
this.stopListening(true); 
el = null; 
attachEl = null; 
lowerBound = null; 
upperBound = null; 
startCallback = null; 
moveCallback = null; 
endCallback = null; 
disposed = true; 
}; 
this.isDragging = function() { 
return dragging; 
}; 
this.isListening = function() { 
return listening; 
}; 
this.isDisposed = function() { 
return disposed; 
}; 
if(typeof attachEl == 'string') 
attachEl = document.getElementById(attachEl); 
// 如果没有配置, 或者没找到该Dom对象, 则用el 
if(!attachEl) attachEl = el; 
if(!attachLater) 
this.startListening(); 
}

其中一些未给出方法, 在往下分析的过程中, 会一一给出....
我们先通过cfg来使el和attachEl指向实际的Dom对象, 如果attachEl没配置或者没找到对应元素则用el替代. 我们同时设置了一些在拖拽中要用到的变量. cursorStartPos用于保存鼠标按下开始拖拽时鼠标的坐标点. elementStartPos用于保存元素开始拖拽时的起始点. dragging, listening, disposed是一些状态变量. listening: 指drag object是否正在监听拖拽开始事件. dragging: 元素是否正在被拖拽. disposed: drag object被清理, 不能再被拖拽了.
在代码的最后, 我们看到如果attachLater不为true, 那么就调用startListening, 这是一个 public方法定义在drag object中, 让我们看下它的实现
this.startListening = function() { 
if(listening || disposed) return; 
listening = true; 
hookEvent(attachEl, 'mousedown', dragStart); 
};

前两行就是做个判断, 如果已经开始对拖拽事件进行监听或者清理过了, 就什么都不做直接return. 否则把listening状态设为true, 表示我们开始监听啦, 把dragStart函数关联到attachEl的mousedown事件上. 这里碰到个hookEvent函数, 我们来看看它的样子:
function hookEvent(el, eventName, callback) { 
if(typeof el == 'string') 
el = document.getElementById(el); 
if(!el) return; 
if(el.addEventListener) 
el.addEventListener(eventName, callback, false); 
else if (el.attachEvent) 
el.attachEvent('on' + eventName, callback); 
}

其实也没什么, 就是对元素事件的监听做了个跨浏览器的封装, 同样的unhookEvent方法如下
function unhookEvent(el, eventName, callback) { 
if(typeof el == 'string') 
el = document.getElementById(el); 
if(!el) return; 
if(el.removeEventListener) 
el.removeEventListener(eventName, callback, false); 
else if(el.detachEvent) 
el.detachEvent('on' + eventName, callback); 
}

接着我们来看看dragStart函数的实现, 它是drag object的一个私有函数
function dragStart(eventObj) { 
if(dragging || !listening || disposed) return; 
dragging = true; 
if(startCallback) 
startCallback(eventObj, el); 
cursorStartPos = absoluteCursorPosition(eventObj); 
elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top'))); 
elementStartPos = elementStartPos.check(); 
hookEvent(document, 'mousemove', dragGo); 
hookEvent(document, 'mouseup', dragStopHook); 
return cancelEvent(eventObj); 
}

attachEl所指的dom对象捕获到mousedown事件后调用此函数. 首先我们先确定drag object在一个适合拖拽的状态, 如果拖拽正在进行, 或者没有在监听拖拽事件, 再或者已经处理完"后事"了, 那就什么都不做. 如果一切ok, 我们把 dragging状态设为true, 然后"开工了", 如果startCallback定义了, 那我们就调用下它, 以mousedown event和el为参数. 接着我们定位鼠标的绝对位置, 保存到cursorStartPos中. 然后拿到拖拽元素当前的top, left,封装成Position对象保存到elementStartPos中. 保险起见我们检查下elementStartPos中属性是否合法. 再看两个hookEvent的调用, 一个是mousemove事件, 表示正在dragging,调用dragGo函数. 一个是mouseup事件, 代表拖拽的结束, 调用dragStopHook函数.可能你会问,为什么事件绑定在document上, 而不是要拖拽的元素上,比如我们这里的el或者attachEl.因为考虑到直接将事件绑定到元素上,可能由于浏览器的一些延时会影响效果,所以直接把事件绑定到document上. 如果实在不是很理解, 或许影响也不大: P.... 看最后一句话中的cancelEvent(eventObj)
function cancelEvent(e) { 
e = e ? e : window.event; 
if(e.stopPropagation) 
e.stopPropagation(); 
if(e.preventDefault) 
e.preventDefault(); 
e.cancelBubble = true; 
e.returnValue = false; 
return false; 
}

用于停止冒泡, 阻止默认事件, 可以理解为安全考虑....在dragStart中有些方法需要介绍下,先来 看看absoluteCursorPosition, 再看下getStyle
function absoluteCursorPosition(e) { 
e = e ? e : window.event; 
var x = e.clientX + (document.documentElement || document.body).scrollLeft; 
var y = e.clientY + (document.documentElement || document.body).scrollTop; 
return new Position(x, y); 
}

此方法就只是用于获得鼠标在浏览器中的绝对位置, 把滚动条考虑进去就行了
function getStyle(el, property) { 
if(typeof el == 'string') 
el = document.getElementById(el); 
if(!el || !property) return; 
var value = el.style[property]; 
if(!value) { 
if(document.defaultView && document.defaultView.getComputedStyle) { 
var css = document.defaultView.getComputedStyle(el, null); 
value = css ? css.getPropertyValue(property) : null; 
} else if (el.currentStyle) { 
value = el.currentStyle[property]; 
} 
} 
return value == 'auto' ? '' : value; 
}

getStyle方法用于获取元素的css属性值, 这样不管你样式是写成内联形式还是定义在css中, 我们都能拿到正确的值, 当然我们这里只要获取元素的top, left属性即可..下面真正处理拖拽工作的方法dragGo
function dragGo(e) { 
if(!dragging || disposed) return; 
var newPos = absoluteCursorPosition(e); 
newPos = newPos.add(elementStartPos) 
.subtract(cursorStartPos) 
.bound(lowerBound, upperBound); 
newPos.apply(el); 
if(moveCallback) 
moveCallback(newPos, el); 
return cancelEvent(e); 
}

这个方法并不复杂, 像其他的方法一样, 我们先查看下状态如何, 如果没有在拖拽中或者已经清理了, 那么什么都不做. 如果一切顺利, 我们利用鼠标当前位置, 元素初始位置, 鼠标初始位置, 和限定范围(如果配置upperBound, lowerBound的话)来计算出一个结果点, 通过apply方法我们把计算的坐标赋给元素style.top和style.left, 让拖拽元素确定其位置. 如果配置了moveCallback, 那么就调用下, 最后来个cancelEvent...这里的新坐标运算,类似于jquery的操作, 因为Position对象的每个方法都返回了一个Position对像...dragStart里还有个方法dragStopHook
function dragStopHook(e) { 
dragStop(); 
return cancelEvent(e); 
} 
function dragStop() { 
if(!dragging || disposed) return; 
unhookEvent(document, 'mousemove', dragGo); 
unhookEvent(document, 'mouseup', dragStopHook); 
cursorStartPos = null; 
elementStartPos = null; 
if(endCallback) 
endCallback(el); 
dragging = false; 
}

关键看下dragStop方法, 同样先判断下状态, 一切ok的话, 我们移除事件的绑定mousemove和mouseup, 并把 cursorStartPos和elementStartPos的值释放掉, 一次拖拽结束啦..如果配置了endCallback那就调用下, 最后把dragging状态设置为false......最后给出会用到的public方法
this.stopListening = function(stopCurrentDragging) { 
if(!listening || disposed) 
return; 
unhookEvent(attachEl, 'mousedown', dragStart); 
listening = false; 
if(stopCurrentDragging && dragging) 
dragStop(); 
}; 
this.dispose = function() { 
if(disposed) return; 
this.stopListening(true); 
el = null; 
attachEl = null; 
lowerBound = null; 
upperBound = null; 
startCallback = null; 
moveCallback = null; 
endCallback = null; 
disposed = true; 
}; 
this.isDragging = function() { 
return dragging; 
}; 
this.isListening = function() { 
return listening; 
}; 
this.isDisposed = function() { 
return disposed; 
};

stopListening移除监听拖拽的mousedown事件, 把监听状态listening设置为false, 这里有个参数stopCurrentDragging见名知意. dispose方法用于些处理工作, 如果你不想让drag object能被拖拽,那么调用一下dispose就可以了, 至于下面的三个小方法isDragging, isListening, isDisposed一看便知, 返回相关的状态. 最后给个源码的下拉链接 下载点我 欢迎园友留言, 交流!
Javascript 相关文章推荐
使用JS获取当前地理位置方法汇总
Dec 18 Javascript
javascript 动态创建表格
Jan 08 Javascript
jQuery实现点击小图显示大图代码分享
Aug 25 Javascript
详解BootStrap中Affix控件的使用及保持布局的美观的方法
Jul 08 Javascript
JS正则替换掉小括号及内容的方法
Nov 29 Javascript
javascript动画之磁性吸附效果篇
Dec 09 Javascript
selenium 与 chrome 进行qq登录并发邮件操作实例详解
Apr 06 Javascript
微信小程序 http请求的session管理
Jun 07 Javascript
详解angularJS自定义指令间的相互交互
Jul 05 Javascript
详解搭建es6+devServer简单开发环境
Sep 25 Javascript
使用layui 的layedit定义自己的toolbar方法
Sep 18 Javascript
vue项目里面引用svg文件并给svg里面的元素赋值
Aug 17 Javascript
使用jQuery实现dropdownlist的联动效果(sharepoint 2007)
Mar 30 #Javascript
使用jquery为table动态添加行的实现代码
Mar 30 #Javascript
jquery 查找select ,并触发事件的实现代码
Mar 30 #Javascript
lyhucSelect基于Jquery的Select数据联动插件
Mar 29 #Javascript
基于JQuery实现异步刷新的代码(转载)
Mar 29 #Javascript
使用隐藏的new来创建对象
Mar 29 #Javascript
myeclipse安装jQuery插件的方法
Mar 29 #Javascript
You might like
php 删除无限级目录与文件代码共享
2008/11/22 PHP
PHP四舍五入精确小数位及取整
2014/01/14 PHP
php创建无限级树型菜单
2015/11/05 PHP
php时间计算相关问题小结
2016/05/09 PHP
PHP让网站移动访问更加友好方法
2019/02/14 PHP
javascript 新浪背投广告实现代码
2009/07/07 Javascript
jQuery 对Select的操作备忘记录
2011/07/04 Javascript
Extjs4 GridPanel 的几种样式使用介绍
2013/04/18 Javascript
js校验表单后提交表单的三种方法总结
2014/02/28 Javascript
鼠标左键单击冲突的问题解决方法(防止冒泡)
2014/05/14 Javascript
Node.js中创建和管理外部进程详解
2014/08/16 Javascript
Jquery对象和Dom对象的区别分析
2014/11/20 Javascript
js操作XML文件的实现方法兼容IE与FireFox
2016/06/25 Javascript
vue实现图书管理demo详解
2017/10/17 Javascript
AngularJS实现的省市二级联动功能示例【可对选项实现增删】
2017/10/26 Javascript
AngularJS自定义表单验证功能实例详解
2018/08/24 Javascript
js实现移动端吸顶效果
2020/01/08 Javascript
JavaScript实现多球运动效果
2020/09/07 Javascript
JavaScript实现单点登录的示例
2020/09/23 Javascript
vue 使用微信jssdk,调用微信相册上传图片功能
2020/11/13 Javascript
JavaScript 异步时序问题
2020/11/20 Javascript
JavaScript缓动动画函数的封装方法
2020/11/25 Javascript
[46:12]完美世界DOTA2联赛循环赛 DM vs Matador BO2第一场 11.04
2020/11/04 DOTA
python去掉行尾的换行符方法
2017/01/04 Python
火车票抢票python代码公开揭秘!
2018/03/08 Python
python opencv设置摄像头分辨率以及各个参数的方法
2018/04/02 Python
创建pycharm的自定义python模板方法
2018/05/23 Python
kafka-python 获取topic lag值方式
2019/12/23 Python
windows10环境下用anaconda和VScode配置的图文教程
2020/03/30 Python
python -v 报错问题的解决方法
2020/09/15 Python
澳大利亚儿童和婴儿产品在线商店:Lime Tree Kids
2017/10/05 全球购物
美国基督教约会网站:ChristianCafe.com
2020/02/04 全球购物
教师找工作推荐信
2013/11/23 职场文书
高中体育教学反思
2014/01/24 职场文书
网上商城创业计划书范文
2014/01/31 职场文书
中学生期中自我鉴定
2014/04/20 职场文书