在做WEB UI设计的时候,拖动某个HTML元素已经成为一种不能忽视的用户界面模式,比较典型的应用例子就是Dialog,一个元素是怎么实现拖动的呢?其实原理非常简单,要想实现首先得了解几个基本知识。
Tips
绝对定位:只有把元素的position属性设置为absolute并且或者fixed才可以实现拖动,默认情况下元素会按文档流中的位置自行决定其出现在页面上的位置,是不能移动的,而绝对定位的元素可以使元素脱离文档流,相对于其定位的父元素或者屏幕定位,可以利用这点儿,通过改变元素与已定位父元素的位移来实现元素拖动。关于定位知识具体可以看看CSS布局 ——从display,position, float属性谈起。
鼠标事件:当鼠标按下、移动、弹起的时候都会触发相应事件,当鼠标按下的时候同时会触发相应元素click事件,并且冒泡到document,上面提到改变元素与定位父容器位移可以在这些事件中实现。关于事件相关知识可以看看JavaScript与HTML交互——事件
要拖动的Dialog
写个简易的Dialog供拖动测试使用
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style type="text/css" >
html,body
{
height:100%;
width:100%;
padding:0;
margin:0;
}
.dialog
{
width:250px;
height:250px;
position:absolute;
background-color:#ccc;
-webkit-box-shadow:1px 1px 3px #292929;
-moz-box-shadow:1px 1px 3px #292929;
box-shadow:1px 1px 3px #292929;
margin:10px;
}
.dialog-title
{
color:#fff;
background-color:#404040;
font-size:12pt;
font-weight:bold;
padding:4px 6px;
cursor:move;
}
.dialog-content
{
padding:4px;
}
</style>
</head>
<body>
<div id="dlgTest" class="dialog">
<div class="dialog-title">Dialog</div>
<div class="dialog-content">
This is a draggable test.
</div>
</div>
</body>
</html>
看起来是酱紫的
拖动一下
为了简单,这里就不照顾浏览器兼容性问题了,先基于Chrome实现。上面的Dialog定位夫容器为document,鼠标event对象包含clientX和clientY两个属性,标识鼠标当前相对ViewPort(可视窗口)位置,可以在移动的时候改变Dialog的left和top属性值实现其移动。
var isDialogTitle=false;
function down(e){
if(e.target.className.indexOf('dialog-title')!=-1){
isDialogTitle=true;
}
}
function move(e){
var dialog=document.getElementById('dlgTest');
if(isDialogTitle){//只有点击Dialog Title的时候才能拖动
dialog.style.left=e.clientX+'px';
dialog.style.top=e.clientY+'px';
}
}
function up(e){
isDialogTitle=false;
}
document.addEventListener('mousedown',down);
document.addEventListener('mousemove',move);
document.addEventListener('mouseup',up);
这样拖动效果就实现了,为了确保只有鼠标点击Dialog Title的时候才拖动,当鼠标按下的时候要判断事件源,如果是Dialog Title区域的话,把isDialogTitle标记设为true,鼠标移动的时候首先要判断isDialogTitle,在鼠标弹起的时候将标记设为false。
一跳一跳的
亲自试过demo的同学肯定可以当开始移动的时候Dialog会跳一下,这是怎么个情况?仔细看看代码发现在移动初始,代码就把Dialog的left和top设为了鼠标当前位置,可是用户在拖动的时候不会刻意去点Dialog的左上角,这样就跳了,soga!改进一下
var draggingObj=null; //dragging Dialog
var diffX=0;
var diffY=0;
function down(e){
if(e.target.className.indexOf('dialog-title')!=-1){
draggingObj=e.target.offsetParent;
diffX=event.clientX-draggingObj.offsetLeft;
diffY=event.clientY-draggingObj.offsetTop;
}
}
function move(e){
var dialog=document.getElementById('dlgTest');
if(draggingObj){//只有点击Dialog Title的时候才能拖动
dialog.style.left=(e.clientX-diffX)+'px';
dialog.style.top=(e.clientY-diffY)+'px';
}
}
function up(e){
draggingObj=null;
diffX=0;
diffY=0;
}
document.addEventListener('mousedown',down);
document.addEventListener('mousemove',move);
document.addEventListener('mouseup',up);
好赤果果
经过改动后不再跳跃了,但是很暴露的感觉,最开始定义的三个变量都暴露在window下,而且这种写法相当的没有通用性,万一以后Dialog Title变了呢,凡是用过此方法的地方都得改一遍,万一Title内部还有子元素,点击其子元素的时候怎么办?既然如此,穿件衣服封装一下
var Dragging=function(validateHandler){ //参数为验证点击区域是否为可移动区域,如果是返回欲移动元素,负责返回null
var draggingObj=null; //dragging Dialog
var diffX=0;
var diffY=0;
function mouseHandler(e){
switch(e.type){
case 'mousedown':
draggingObj=validateHandler(e);//验证是否为可点击移动区域
if(draggingObj!=null){
diffX=e.clientX-draggingObj.offsetLeft;
diffY=e.clientY-draggingObj.offsetTop;
}
break;
case 'mousemove':
if(draggingObj){
draggingObj.style.left=(e.clientX-diffX)+'px';
draggingObj.style.top=(e.clientY-diffY)+'px';
}
break;
case 'mouseup':
draggingObj =null;
diffX=0;
diffY=0;
break;
}
};
return {
enable:function(){
document.addEventListener('mousedown',mouseHandler);
document.addEventListener('mousemove',mouseHandler);
document.addEventListener('mouseup',mouseHandler);
},
disable:function(){
document.removeEventListener('mousedown',mouseHandler);
document.removeEventListener('mousemove',mouseHandler);
document.removeEventListener('mouseup',mouseHandler);
}
}
}
包装一下果真变好看多了,代码不难看懂,有几个注意点,Dragging函数的validateHandler参数并不是什么阿猫阿狗,正如注释所言为了解决刚才提到几个需求变更问题,validateHandler是一个自定义函数的句柄,这个函数用于识别点击元素是否触发移动,是的话需要返回欲移动元素,这样就可以灵活的触发移动并决定移动那个元素了(点击的和移动的不一定是一个),Dragging函数返回一个对象,对象中有两个方法,分别可以使元素可移动/禁止移动,看看怎么使用
function getDraggingDialog(e){
var target=e.target;
while(target && target.className.indexOf('dialog-title')==-1){
target=target.offsetParent;
}
if(target!=null){
return target.offsetParent;
}else{
return null;
}
}
Dragging(getDraggingDialog).enable();
首先定义一个识别函数,然后作为参数调用Dragging函数,并调用返回值的enable方法,这样元素就可以拖动了。
源码
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style type="text/css" >
html,body
{
height:100%;
width:100%;
padding:0;
margin:0;
}
.dialog
{
width:250px;
height:250px;
position:absolute;
background-color:#ccc;
-webkit-box-shadow:1px 1px 3px #292929;
-moz-box-shadow:1px 1px 3px #292929;
box-shadow:1px 1px 3px #292929;
margin:10px;
}
.dialog-title
{
color:#fff;
background-color:#404040;
font-size:12pt;
font-weight:bold;
padding:4px 6px;
cursor:move;
}
.dialog-content
{
padding:4px;
}
</style>
</head>
<body>
<div id="dlgTest" class="dialog">
<div class="dialog-title">Dialog</div>
<div class="dialog-content">
This is a draggable test.
</div>
</div>
<script type="text/javascript">
var Dragging=function(validateHandler){ //参数为验证点击区域是否为可移动区域,如果是返回欲移动元素,负责返回null
var draggingObj=null; //dragging Dialog
var diffX=0;
var diffY=0;
function mouseHandler(e){
switch(e.type){
case 'mousedown':
draggingObj=validateHandler(e);//验证是否为可点击移动区域
if(draggingObj!=null){
diffX=e.clientX-draggingObj.offsetLeft;
diffY=e.clientY-draggingObj.offsetTop;
}
break;
case 'mousemove':
if(draggingObj){
draggingObj.style.left=(e.clientX-diffX)+'px';
draggingObj.style.top=(e.clientY-diffY)+'px';
}
break;
case 'mouseup':
draggingObj =null;
diffX=0;
diffY=0;
break;
}
};
return {
enable:function(){
document.addEventListener('mousedown',mouseHandler);
document.addEventListener('mousemove',mouseHandler);
document.addEventListener('mouseup',mouseHandler);
},
disable:function(){
document.removeEventListener('mousedown',mouseHandler);
document.removeEventListener('mousemove',mouseHandler);
document.removeEventListener('mouseup',mouseHandler);
}
}
}
function getDraggingDialog(e){
var target=e.target;
while(target && target.className.indexOf('dialog-title')==-1){
target=target.offsetParent;
}
if(target!=null){
return target.offsetParent;
}else{
return null;
}
}
Dragging(getDraggingDialog).enable();
</script>
</body>
</html>
不足之处
这种拖动处理方式看起来不错了,但是还有几点儿遗憾
1. 前面提到的浏览器兼容性问题,这种写法在低版本IE浏览器上是不能运行的
2. 边界检查,细心的同学发现Dialog不但可以拖动了,还可以使页面出现滚动条无限拖动,大部分情况下我们希望Dialog在可视窗口、文档(固有滚动条内)或者固定区域内拖动,这种方式没有做到此限制
3. 拖动卡顿,在这个demo中不会出现此问题,文档结构简单拖动流畅,可视在庞大的页面中如果鼠标移动速度过快,Dialog会跟不上鼠标,出现卡顿,这时候如果鼠标在Dialog外面,mouseup事件不会生效,拖动就停不下来,只能把鼠标移回Dialog在mouseup
前两个问题好解决,拓展一下模块就可以,至于第三个现在还没想到比较好的解决办法,十一点了,明天再研究研究,然后一块儿发出来,晚安。