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技术-屏蔽类
Aug 15 Javascript
jquery实现带二级菜单的导航示例
Apr 28 Javascript
jQuery提示效果代码分享
Nov 20 Javascript
对比分析AngularJS中的$http.post与jQuery.post的区别
Feb 27 Javascript
jQuery制作圣诞主题页面 更像是爱情影集
Aug 10 Javascript
jQuery插件FusionCharts实现的2D柱状图效果示例【附demo源码下载】
Mar 06 Javascript
详解react-webpack2-热模块替换[HMR]
Aug 03 Javascript
关于JS与jQuery中的文档加载问题
Aug 22 jQuery
详解vue-cli 本地开发mock数据使用方法
May 29 Javascript
Vuex modules模式下mapState/mapMutations的操作实例
Oct 17 Javascript
vue2.0实现列表数据增加和删除
Jun 17 Javascript
给原生html中添加水印遮罩层的实现示例
Apr 02 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中使用与Perl兼容的正则表达式
2006/11/26 PHP
PHP中empty和isset对于参数结构的判断及empty()和isset()的区别
2015/11/15 PHP
PHP实现基于栈的后缀表达式求值功能
2017/11/10 PHP
thinkPHP框架实现类似java过滤器的简单方法示例
2018/09/05 PHP
JS合并数组的几种方法及优劣比较
2014/09/19 Javascript
JavaScript Sort 的一个错误用法示例
2015/03/20 Javascript
jquery+ajax请求且带返回值的代码
2015/08/12 Javascript
日常收集整理的JavaScript常用函数方法
2015/12/10 Javascript
jQuery实现的左右移动焦点图效果
2016/01/14 Javascript
原生JS实现图片左右轮播
2016/12/30 Javascript
简单实现bootstrap选项卡效果
2017/02/08 Javascript
jQuery中table数据的值拷贝和拆分
2017/03/19 Javascript
Angular2学习教程之组件中的DOM操作详解
2017/05/28 Javascript
VueJS 取得 URL 参数值的方法
2019/07/19 Javascript
微信小程序实现点击卡片 翻转效果
2019/09/04 Javascript
Webpack按需加载打包chunk命名的方法
2019/09/22 Javascript
node.JS事件机制与events事件模块的使用方法详解
2020/02/06 Javascript
解决vue做详情页跳转的时候使用created方法 数据不会更新问题
2020/07/24 Javascript
[43:24]VG vs Serenity 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/20 DOTA
Windows下Python使用Pandas模块操作Excel文件的教程
2016/05/31 Python
详解python之简单主机批量管理工具
2017/01/27 Python
利用Python实现Windows下的鼠标键盘模拟的实例代码
2017/07/13 Python
快速入门python学习笔记
2017/12/06 Python
解决Shell执行python文件,传参空格引起的问题
2018/10/30 Python
关于PyTorch源码解读之torchvision.models
2019/08/17 Python
Python模块zipfile原理及使用方法详解
2020/08/04 Python
基于Python爬取股票数据过程详解
2020/10/21 Python
吃透移动端 Html5 响应式布局
2019/12/16 HTML / CSS
Jo Malone美国官网:祖玛珑香水
2017/03/27 全球购物
人力资源主管职责范本
2014/03/05 职场文书
竞选副班长演讲稿
2014/04/24 职场文书
优质护理服务演讲稿
2014/05/07 职场文书
浪漫婚礼主题活动策划方案
2014/09/15 职场文书
四风批评与自我批评发言稿
2014/10/14 职场文书
大学生村官工作总结2015
2015/04/09 职场文书
Python实战之疫苗研发情况可视化
2021/05/18 Python