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 相关文章推荐
Prototype使用指南之hash.js
Jan 10 Javascript
jquery.artwl.thickbox.js  一个非常简单好用的jQuery弹出层插件
Mar 01 Javascript
javascript动态向网页中添加表格实现代码
Feb 19 Javascript
JS实现的用来对比两个用指定分隔符分割的字符串是否相同
Sep 19 Javascript
完美兼容各大浏览器的jQuery仿新浪图文淡入淡出间歇滚动特效
Nov 12 Javascript
jquery实现简单实用的弹出层效果代码
Oct 15 Javascript
拥有一个属于自己的javascript表单验证插件
Mar 24 Javascript
使用vue.js制作分页组件
Jun 27 Javascript
js时间比较 js计算时间差的简单实现方法
Aug 26 Javascript
详解JavaScript中return的用法
May 08 Javascript
js判断传入时间和当前时间大小实例(超简单)
Jan 11 Javascript
微信小程序实现手指触摸画板
Jul 09 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 文件扩展名 获取函数
2009/06/03 PHP
php加密解密字符串示例
2016/10/13 PHP
Yii2.0实现生成二维码功能实例
2017/10/24 PHP
jQuery 1.0.4 - New Wave Javascript(js源文件)
2007/01/15 Javascript
JS Excel读取和写入操作(模板操作)实现代码
2010/04/11 Javascript
JavaScript 原型与继承说明
2010/06/09 Javascript
商城常用滚动的焦点图效果代码简单实用
2013/03/28 Javascript
JS实现可改变列宽的table实例
2013/07/02 Javascript
JS扩展方法实例分析
2015/04/15 Javascript
详解js的六大数据类型
2016/12/27 Javascript
Node.js如何响应Ajax的POST请求并且保存为JSON文件详解
2017/03/10 Javascript
关于使用axios的一些心得技巧分享
2017/07/02 Javascript
AngularJS中重新加载当前路由页面的方法
2018/03/09 Javascript
微信小程序通过保存图片分享到朋友圈功能
2018/05/24 Javascript
vue生命周期和react生命周期对比【推荐】
2018/09/19 Javascript
基于Koa(nodejs框架)对json文件进行增删改查的示例代码
2019/02/02 NodeJs
如何为你的JavaScript代码日志着色详解
2019/04/08 Javascript
在nodejs中创建child process的方法
2021/01/26 NodeJs
[39:52]2018DOTA2亚洲邀请赛 4.3 突围赛 EG vs Newbee 第一场
2018/04/04 DOTA
[01:07:57]DOTA2-DPC中国联赛 正赛 Ehome vs Magma BO3 第二场 1月19日
2021/03/11 DOTA
在Python的列表中利用remove()方法删除元素的教程
2015/05/21 Python
利用python画一颗心的方法示例
2017/01/31 Python
Python建立Map写Excel表实例解析
2018/01/17 Python
numpy找出array中的最大值,最小值实例
2018/04/03 Python
python实现飞机大战
2018/09/11 Python
python+numpy实现的基本矩阵操作示例
2019/07/19 Python
Python基于Webhook实现github自动化部署
2020/11/28 Python
英国巧克力贸易公司:Chocolate Trading Company
2017/03/21 全球购物
2013届毕业生求职信范文
2013/11/20 职场文书
团组织关系介绍信
2014/01/12 职场文书
酒店优秀员工事迹材料
2014/06/02 职场文书
选秀节目策划方案
2014/06/06 职场文书
2015年学校安全管理工作总结
2015/05/11 职场文书
公司管理建议书
2015/09/14 职场文书
职业生涯规划书之大学四年
2019/08/07 职场文书
python爬取某网站原图作为壁纸
2021/06/02 Python