这几天做了一个英文单词搜索的谷歌扩展,其中的划词搜索功能会产生一个可托拖动的DIV层来作为结果显示,为了做一个较为完善的拖动层,花费了很长时间进行设计与调试。在此把心得总结了一下,讲讲实现方法与关键点。
先来看看效果:在线实例DEMO
position
属性为absolute
或fixed
,通过更改其left
,top
来更改层的相对位置。mousedown
事件,设置一个拖动开始的标志为true
,比如本例的isMouseDown
,作为拖动的开始。document
上绑定mousemove
事件,移动鼠标时,根据拖动开始标志的真假,动态给DIV层设定根据当前鼠标位置的left
,top
值,实现按下鼠标后进行的拖动。mouseup
事件,设置拖动开始标志为false
,结束拖动。写成代码,大概就是这个样子:
CSS
body,html{
height:100%;
width: 100%;
}
#draggable {
background: #eee;
display:flex;
align-items: center;
justify-content:center;
position: fixed;
left:0;top: 0;
width: 200px;
height:200px;
font-size:2rem;
}
HTML
<div id="draggable">drag mediv>
Js
var isMouseDown;
draggable.addEventListener('mousedown', function() {
isMouseDown = true;
})
document.addEventListener('mousemove', function(e) {
if (isMouseDown) {
draggable.style.left = e.clientX + 'px';
draggable.style.top = e.clientY + 'px';
}
})
draggable.addEventListener('mouseup', function() {
isMouseDown = false;
})
通过这样的设定,可以得到最基本的一个拖放DIV:
在线实例1:simple draggable div#1
操作体验一下,再来深入研究细节内容(接下来的实例相关代码不再重复,只增加不同内容)
拖动时鼠标于DIV的相对位置
实例1中最明显的问题就是,不论在何处点击DIV,拖动时DIV都会飞到以鼠标为原点的位置。这是因为单击鼠标时,鼠标在有一个相对于DIV的坐标位置,在紧接着的拖动操作中没有计算这个相对距离,修改代码如下:
draggable.addEventListener('mousedown', function(e) {
isMouseDown = true;
initX = e.offsetX;
initY = e.offsetY;
})
document.addEventListener('mousemove', function(e) {
if (isMouseDown) {
var cx=e.clientX,cy=e.clientY;
draggable.style.left = e.clientX - initX + 'px';
draggable.style.top = e.clientY - initY + 'px';
}
})
在线实例2:simple draggable div#2
拖动时选中文字
实例2实现了在哪点击,在哪拖动的效果。但是如果拖动速度过快,就会出现文字被选中的情况,如果页面除了拖动层还有其他文字内容,那么就会出现只要拖动DIV,页面其余内容就会被选中的问题,此时需要增加一个CSS属性user-select:none
,这个属性是防止元素被选中:
CSS:
.no-select {
user-select: none;
}
HTML:
<div id="draggable" class="no-select">drag mediv>
同时,在开始拖动时,也要给body
增加no-select
的样式,并在拖动结束时,移除这个样式:
draggable.addEventListener('mousedown', function(e) {
isMouseDown = true;
document.body.classList.add('no-select');
initX = e.offsetX;
initY = e.offsetY;
})
draggable.addEventListener('mouseup', function() {
isMouseDown = false;
document.body.classList.remove('no-select');
})
这样就可以防止拖动DIV时选中其余的文字内容。
DIV被拖动到边界处,消失了,限定移动范围?
实例中仅有一个可拖动的方块演示起来倒不是很能说明问题。我在做扩展时,由于需要展示内容,拖动层设定为只有顶部可以拖动,当我拖动顶部到网页顶部时,问题出现了,我无法再拖动浮动层,只能关闭重新开启,而且当我启用固定在屏幕上功能时,拖动出边界后变无法关闭浮动层,只能刷新网页,于是我意识到拖动需要有范围限制。
逻辑很简单,当鼠标移动到边界时(页面内外部交界),会产生小于0或者大于网页宽度或高度的坐标,只要加入判断,鼠标超过边界时,手动规定窗口的left
,top
值,比如拖动时,当鼠标从页面最上端移出,鼠标Y坐标小于0时,就规定浮动层的top
值为0。
其中有个很隐秘的经验,如果在document
的mousemove
事件上输出鼠标移动的坐标,当鼠标移动出浏览器是没有任何坐标信息的,但是如果是按下鼠标左键再去移动,移动出浏览器不松开鼠标的情况下,就可以看到输出的负值或大于网页宽高的鼠标坐标信息。这个坐标信息,是限制DIV拖动范围的关键。
计算临界情况的数学关系之后,需要用到DIV的宽高,在mousemove
事件中增加判断,代码如下:
var height = draggable.offsetHeight,
width = draggable.offsetWidth;
document.addEventListener('mousemove', function(e) {
if (isMouseDown) {
var cx = e.clientX - initX,
cy = e.clientY - initY;
if (cx < 0) {
cx = 0;
}
if (cy < 0) {
cy = 0;
}
if (window.innerWidth - e.clientX + mouseX < width) {
cx = window.innerWidth - width;
}
if (e.clientY > window.innerHeight - height+ mouseY) {
cy = window.innerHeight - height;
}
draggable.style.left = cx + 'px';
draggable.style.top = cy + 'px';
}
})
在线实例3:simple draggable div#3
拖动层失去焦点
在实例3的基础上尽情拖动,很快就会发现,如果按下鼠标时,移动出网页,虽然浮动层很好的被限定在了页面内,但是会出现松开鼠标,再次回到页面,浮动层会跟随鼠标移动的情况,相当于在页面外松开鼠标无效,没有结束拖动。
这个跟上面的情况有相似的地方,因为结束拖放的mouseup
事件是绑定在DIV上,这就导致了该事件无法判断出鼠标是否移动到了页面外,根据上面的“隐秘的经验”,需要在document
上也增加一个mouseup
事件的绑定,来专门对付鼠标移出页面外时松开鼠标左键的情况:
document.addEventListener('mouseup', function(e) {
if(e.clientY > window.innerWidth || e.clientY<0 || e.clientX<0 ||e.clientX>window.innerHeight){
isMouseDown = false;
document.body.classList.remove('no-select');
}
});
终极难题:拖动层上放iframe
我最初卡壳的原因就是,查询完单词的结果显示界面是一个内嵌在拖动层上的iframe
界面,我拖动DIV移动时,如果鼠标速度过快,移动到了iframe
上,这时的鼠标事件就跑到了iframe
中,原本的mousemove
事件都监听不到鼠标了。
还有其他种种BUG都是由于iframe
导致的,我尝试了很多关于iframe
的方法,都无法实现。
最终根据给页面增加无法选择文本的CSS样式,依葫芦画瓢,只需给iframe
层增加一个pointer-events:none
的CSS样式。该样式是透明当前元素上的鼠标事件,也就是说,在元素上的鼠标事件相当于直接作用于下层的元素,该元素只有视觉上的展示,没有鼠标事件。
draggable.addEventListener('mousedown', function(e) {
content.classList.add('pointer-events');
}
draggable.addEventListener('mouseup', function(e) {
content.classList.remove('pointer-events');
}
最后,所有这些内容汇总在一起,正是开篇的DEMO。
效率有提升的空间,等我以后有机会学会优化了,再更新。