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 相关文章推荐
提高代码性能技巧谈—以创建千行表格为例
Jul 01 Javascript
Firefox outerHTML实现代码
Jun 04 Javascript
Javascript 面向对象 继承
May 13 Javascript
javascript 跨浏览器开发经验总结(五) js 事件
May 19 Javascript
Jquery获取和修改img的src值的方法
Feb 17 Javascript
js实现正方形颜色从下往上升的效果
Aug 04 Javascript
jQuery表格插件datatables用法总结
Sep 05 Javascript
Vue2实现组件props双向绑定
Dec 02 Javascript
mpvue实现微信小程序快递单号查询代码
Apr 03 Javascript
Node.js设置定时任务之node-schedule模块的使用详解
Apr 28 Javascript
详解Vue串联过滤器的使用场景
Apr 30 Javascript
JS将指定的某个字符全部转换为其他字符实例代码
Oct 13 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
Zend的MVC机制使用分析(一)
2013/05/02 PHP
PHP错误提示的关闭方法详解
2013/06/23 PHP
PHP使用Pthread实现的多线程操作实例
2015/11/14 PHP
分享php代码将360浏览器导出的favdb的sqlite数据库文件转换为html
2015/12/09 PHP
PHP模板引擎Smarty内置变量调解器用法详解
2016/04/11 PHP
Prototype使用指南之hash.js
2007/01/10 Javascript
Three.js源码阅读笔记(物体是如何组织的)
2012/12/27 Javascript
jQuery中ajax的post()方法用法实例
2014/12/26 Javascript
《JavaScript DOM 编程艺术》读书笔记之JavaScript 简史
2015/01/09 Javascript
jQuery实现自动与手动切换的滚动新闻特效代码分享
2015/08/27 Javascript
JS实现鼠标移上去显示图片或微信二维码
2016/12/14 Javascript
基于JavaScript实现的希尔排序算法分析
2017/04/14 Javascript
nodejs入门教程四:URL相关模块用法分析
2017/04/24 NodeJs
基于axios封装fetch方法及调用实例
2018/02/05 Javascript
Vuejs 单文件组件实例详解
2018/02/09 Javascript
JavaScript 隐性类型转换步骤浅析
2018/03/15 Javascript
jQuery实现的页面弹幕效果【测试可用】
2018/08/17 jQuery
vue element中axios下载文件(后端Python)
2019/05/10 Javascript
浅谈Webpack4 Tree Shaking 终极优化指南
2019/11/18 Javascript
微信小程序webSocket的使用方法
2020/02/20 Javascript
vuex 多模块时 模块内部的mutation和action的调用方式
2020/07/24 Javascript
微信跳一跳自动运行python脚本
2018/01/08 Python
使用DataFrame删除行和列的实例讲解
2018/04/08 Python
使用python中的in ,not in来检查元素是不是在列表中的方法
2018/07/06 Python
Pytorch中的自动求梯度机制和Variable类实例
2020/02/29 Python
通过python调用adb命令对App进行性能测试方式
2020/04/23 Python
3种适用于Python的疯狂秘密武器及原因解析
2020/04/29 Python
顶级宝石首饰网络零售商:Angara
2016/10/25 全球购物
远程Wi-Fi宠物监控相机:Petcube
2017/04/26 全球购物
中医临床专业自我鉴定范文
2014/01/15 职场文书
实用的简历自我评价
2014/03/06 职场文书
小学开学标语
2014/07/01 职场文书
领导干部保密承诺书
2014/08/30 职场文书
单位工作证明
2014/10/07 职场文书
商家认证委托书格式
2014/10/16 职场文书
武侯祠导游词
2015/02/04 职场文书