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 相关文章推荐
javascript 子窗体父窗体相互传值方法
May 31 Javascript
IE6,IE7下js动态加载图片不显示错误
Jul 17 Javascript
JavaScript随机排序(随即出牌)
Sep 17 Javascript
JS+CSS制作DIV层可(最小化/拖拽/排序)功能实现代码
Feb 25 Javascript
A标签触发onclick事件而不跳转的多种解决方法
Jun 27 Javascript
以JSON形式将JS中Array对象数组传至后台的方法
Jan 06 Javascript
js监听滚动条滚动事件使得某个标签内容始终位于同一位置
Jan 24 Javascript
浅析tr的隐藏和显示问题
Mar 05 Javascript
关于JS中prototype的理解
Sep 07 Javascript
微信小程序学习(4)-系统配置app.json详解
Jan 12 Javascript
微信小程序视图容器(swiper)组件创建轮播图
Jun 19 Javascript
函数式编程入门实践(一)
Apr 20 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生成静态HTML文档的原理
2012/10/29 PHP
PHP 自定义错误处理函数trigger_error()
2013/03/26 PHP
解析thinkphp import 文件内容变量失效的问题
2013/06/20 PHP
php自定义函数实现二维数组按指定key排序的方法
2016/09/29 PHP
php array_pop 删除数组最后一个元素实例
2016/11/02 PHP
php 根据自增id创建唯一编号类
2017/04/06 PHP
PHP实现将上传图片自动缩放到指定分辨率,并保持清晰度封装类示例
2019/06/17 PHP
php中try catch捕获异常实例详解
2020/08/06 PHP
理解javascript模块化
2016/03/28 Javascript
JS实现HTML标签转义及反转义
2020/04/14 Javascript
js实现无缝滚动图
2017/02/22 Javascript
js input输入百分号保存数据库失败的解决方法
2018/05/26 Javascript
vue实现购物车抛物线小球动画效果的方法详解
2019/02/13 Javascript
JavaScript实现密码强度实时验证
2020/03/18 Javascript
Vue中key的作用示例代码详解
2020/06/10 Javascript
微信小程序实现导航栏和内容上下联动功能代码
2020/06/29 Javascript
pycharm 使用心得(八)如何调用另一文件中的函数
2014/06/06 Python
Python中的匿名函数使用简介
2015/04/27 Python
Python max内置函数详细介绍
2016/11/17 Python
Django的HttpRequest和HttpResponse对象详解
2018/01/26 Python
用pandas按列合并两个文件的实例
2018/04/12 Python
Python图像处理之简单画板实现方法示例
2018/08/30 Python
Python qqbot 实现qq机器人的示例代码
2019/07/11 Python
Django在admin后台集成TinyMCE富文本编辑器的例子
2019/08/09 Python
使用python远程操作linux过程解析
2019/12/04 Python
python获取网络图片方法及整理过程详解
2019/12/20 Python
纯CSS实现的大小渐变、渐远效果
2014/04/15 HTML / CSS
澳大利亚最早和最古老的巨型游戏专家:Yardgames
2020/02/20 全球购物
自主招生自荐信范文
2013/12/04 职场文书
甲方资料员岗位职责
2013/12/13 职场文书
银行门卫岗位职责
2013/12/29 职场文书
护士工作失误检讨书
2014/09/14 职场文书
学生抄作业检讨书(2篇)
2014/10/17 职场文书
《海上日出》教学反思
2016/02/23 职场文书
2016年基层党组织公开承诺书
2016/03/25 职场文书
Win11怎么把合并的任务栏分开 Win11任务栏合并分开教程
2022/04/06 数码科技