javascript的拖放(第1部分)
既然说是入门,本文给出的函数都尽可能短小,但不失强大,并兼容所有浏览器。要想拖动页面上的一个元素,说白了就是让它实现位移。CSS中最能体现这种思想的是绝对定位,因为绝对定位能使元素脱离原来的文档流,但原来的物理空间还保留着,这就不影响周围的元素了。接着下来就是设置事件侦听器,分别在mousedown,mousemove,mouseup绑定对应的回调函数,一旦触发这些事件,浏览器就会自动调用它们。我们分别把这些回调函数命名为dragstart,drag与dragend。
- 在dragstart方法中,我们的工作取得鼠标相对于事件源的距离。IE中,我们可以很轻松地用offsetX与offsetY实现,在firefox中我们要用layerX与layerY。其他浏览器大多数是墙头草,两个都实现,但Opera是IE那一方的。为了实现全面兼容,就要绕点远路,利用e.clientX - el.offsetLeft与e.clientY - el.offsetTop获取它们。并在此方法中开始监听mousemove与mouseup事件。
- 在drag方法,我们要将拖动的距离加到原来的top与left上,以实现位移。拖动过程,可能引发沿文本的被选中,我们需要清除文本。
- 在dragend方法,我们要卸载绑定事件,释放内存。
为了共享方法,我们把它们都做成原型方法。
var Drag = function(id){ this.node = document.getElementById(id); this.node.style.position = "absolute" this.node.me = this;//保存自身的引用 this.node.onmousedown = this.dragstart;//监听mousedown事件 } Drag.prototype = { constructor:Drag, dragstart:function(e){ var e = e || window.event,//获得事件对象 self = this.me,//获得拖动对象 node = self.node;//获得拖动元素 //鼠标光标相对于事件源对象的坐标 node.offset_x = e.clientX - node.offsetLeft; node.offset_y = e.clientY - node.offsetTop; node.onmousemove = self.drag;//监听mousemove事件 node.onmouseup = self.dragend;//监听mouseup事件 }, drag:function(e){ var e = e || window.event,//获得事件对象 self = this.me,//获得拖动对象 node = self.node;//获得拖动元素 node.style.cursor = "pointer"; //将拖动的距离加再在原先的left与top上,以实现位移 !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges(); node.style.left = e.clientX - node.offset_x + "px"; node.style.top = e.clientY - node.offset_y + "px"; node.onmouseup = self.dragend;//监听mouseup事件 }, dragend:function(){ var self = this.me,//获得拖动对象 node = self.node;//获得拖动元素 node.onmousemove = null; node.onmouseup = null; } }
现在我们的类就可以运作了,但正如你们所看到的那样,当鼠标拖动太快会出现鼠标移出div的情况。这是因为移动得越快,位移的距离就越大,拖动元素一下子从我们的鼠标溜走了,就无法调用mouseup事件。在IE中我们可以利用setCapture()来补救,但一旦某个元素调用setCapture(),文档中所有后续的鼠标事件都会在冒泡之前传到该元素,直到调用了releaseCapture()。换言之,在完成这些鼠标事件之前,它是不执行其他事件,一直占着线程,于是出现了我们的光标离开拖动元素的上方也能拖动元素的怪异现象。
你在拖动块上点一下,然后再到拖动块外面点一下,就可以实现"隔空拖动"的神奇效果了!(当然只限IE)
由于鼠标事件一直接着线程,所以在我们不用的时候,一定要releaseCapture()来解放它。
在firefox中我们可以使用window.captureEvents(),火狐说这方法已经废弃,但我怎么在各标准浏览器中运作良好呢?!不过不管怎么样,来来回回要设置捕获与取消捕获非常麻烦与吃内存,我们需要转换思路。因为如果鼠标离开拖动元素上方,我们的绑定函数就无法运作,要是把它们绑定在document上呢,鼠标就无论在何处都能监听拖动元素。但触发拖动的onmousedown事件我们还保留在拖动元素上,这事件不会因为不执行就引起差错之虞。不过,由于绑定对象一变,我们要在这些事件中获得拖动对象的引用的难度就陡然加大,这里我就直接把它们做成构造函数内的私有函数吧。
//版本2 var Drag = function(id){ var el = document.getElementById(id); el.style.position = "absolute"; var drag = function(e) { e = e || window.event; el.style.cursor = "pointer"; !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges(); el.style.left = e.clientX - el.offset_x + "px"; el.style.top = e.clientY - el.offset_y + "px"; el.innerHTML = parseInt(el.style.left,10)+ "X"+parseInt(el.style.top,10); } var dragend = function(){ document.onmouseup = null; document.onmousemove = null; } var dragstart = function(e){ e = e || window.event; el.offset_x = e.clientX - el.offsetLeft; el.offset_y = e.clientY - el.offsetTop; document.onmouseup = dragend; document.onmousemove = drag; return false; } el.onmousedown = dragstart; } //版本3(RP大爆发,搞出上面对应的prototype版本) var Drag = function(id){ this.el = document.getElementById(id); this.el.style.position = "absolute" this.el.me = this;//保存自身的引用 this.el.onmousedown = this.dragstart;//监听mousedown事件 } Drag.prototype = { constructor:Drag, dragstart:function(e,self,el){//事件在标准浏览器中被当作第一个参数传入 e = e || event;//获得事件对象 self = this.me;//获得拖动对象 el = self.el;//获得拖动元素 el.offset_x = e.clientX - el.offsetLeft; el.offset_y = e.clientY - el.offsetTop; document.onmousemove = function(e){ self.drag(e,el) } document.onmouseup = function(){ self.dragend() } }, drag:function(e,el){ e = e || event;//获得事件对象 with(el.style){ cursor = "pointer"; left = e.clientX - el.offset_x + "px"; top = e.clientY - el.offset_y + "px"; } !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges(); }, dragend:function(){ document.onmouseup = document.onmousemove = null; } }
进一步改进,不使用mouseup事件,这样就减少了错过mouseup事件带来的鼠标粘着卡壳的问题。但由于标准浏览器不会自动切换e.button和e.which的值,我被迫动用一个功能键Shirt来停止鼠标拖拽。
var Drag = function(id){ var el = document.getElementById(id); el.style.position = "absolute"; var drag = function(e){ var e = e || window.event, button = e.button || e.which; if(button == 1 && e.shiftKey == false){ el.style.cursor = "pointer"; !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges(); el.style.left = e.clientX - el.offset_x + "px"; el.style.top = e.clientY - el.offset_y + "px"; el.innerHTML = parseInt(el.style.left,10)+ " x "+parseInt(el.style.top,10); }else { document.onmousemove = null; } } var dragstart = function(e){ e = e || window.event; el.offset_x = e.clientX - el.offsetLeft; el.offset_y = e.clientY - el.offsetTop; el.style.zIndex = ++Drag.z; document.onmousemove = drag; return false; } Drag.z = 999; el.onmousedown = dragstart; }
虽然不绑定mouseup的确在IE爽到high起,但在火狐等浏览要多按一个键才能终止拖动,怎么说也对用户体验造成影响,因此当一种知识学学就算了。我们还是选择方案2。
接着下来我们为方案扩展一下功能。首先范围拖动,就是存在一个让它在上面拖动的容器。如果存在容器,我们就取得其容器的四个点的坐标,与拖动元素的四个点的坐标比较,从而修正top与left。由于我已经实现了getCoords函数,取得页面上某一点的坐标易如反掌。同时这样做,我们就不用把此容器设置成offsetParent。总之,多一事不如少一事。
var getCoords = function(el){ var box = el.getBoundingClientRect(), doc = el.ownerDocument, body = doc.body, html = doc.documentElement, clientTop = html.clientTop || body.clientTop || 0, clientLeft = html.clientLeft || body.clientLeft || 0, top = box.top + (self.pageYOffset || html.scrollTop || body.scrollTop ) - clientTop, left = box.left + (self.pageXOffset || html.scrollLeft || body.scrollLeft) - clientLeft return { 'top': top, 'left': left }; };
接着我们要取得容器的四个点的坐标:
_cCoords = getCoords(container), _cLeft = _cCoords.left, _cTop = _cCoords.top, _cRight = _cLeft + container.clientWidth, _cBottom = _cTop + container.clientHeight;
接着是拖动元素的四个点的坐标:
var _left = el.offsetLeft, _top = el.offsetTop, _right = _left + el.offsetWidth, _bottom = _top + el.offsetHeight,
但是这样取得的是没有拖动前的坐标,拖动时的坐标在上面的方案2也已给出了:
var _left = e.clientX - el.offset_x, _top = e.clientY - el.offset_y,
修正坐标很简单,如果拖动元素在左边或上边超出容器,就让拖动元素的top与left取容器的_cLeft或_cTop值,如果从右边超出容器,我们用_cRight减去容器的宽度再赋给拖动元素的top,下边的处理相仿。
if(_left < _cLeft){ _left = _cLeft } if(_top < _cTop){ _top = _cTop } if(_right > _cRight){ _left = _cRight - el.offsetWidth; } if(_bottom > _cBottom){ _top = _cBottom - el.offsetHeight; } el.style.left = _left + "px"; el.style.top = _top + "px";
水平锁定与垂直锁直也是两个很常用的功能,我们也来实现它。原理很简单,在开始拖动时就把top与left保存起来,待到拖动时再赋给它就行了。代码请直接看运行框的代码: