拖放效果,也叫拖拽,学名Drag-and-drop ,是最常见的js特效之一。 这里考虑到有的人可能只需要简单的拖放,所以有一个 简化版的拖放SimpleDrag ,方便学习。 程序原理 这里以SimpleDrag为例说一下基本原理。 首先初始化程序中要一个拖放对象: this.Drag = $(drag); 还要两个参数在开始时记录鼠标相对拖放对象的坐标: this._x = this._y = 0; 还有两个事件对象函数用于添加移除事件: this._fM = BindAsEventListener(this, this.Move); 分别是拖动程序和停止拖动程序。 this.Drag.style.position = “absolute”; 最后把Start开始拖放程序绑定到拖放对象mousedown事件: addEventHandler(this.Drag, “mousedown”, BindAsEventListener(this, this.Start)); 鼠标在拖放对象按住,就会触发Start程序,主要是用来准备拖动,在这里记录鼠标相对拖放对象的坐标: this._x = oEvent.clientX - this.Drag.offsetLeft; 并把_fM拖动程序和_fS停止拖动程序分别绑定到document的mousemove和mouseup事件: addEventHandler(document, “mousemove”, this._fM); 绑定到document可以保证事件在整个窗口文档中都有效。 当鼠标在文档上移动时,就会触发Move程序了,这里就是实现拖动的程序。 this.Drag.style.left = oEvent.clientX - this._x + “px”; 最后放开鼠标后就触发Stop程序结束拖放。 removeEventHandler(document, “mousemove”, this._fM); 这样一个简单的拖放程序就做好了,下面说说其他扩展和细节部分。 拖放锁定 锁定分三种,分别是:水平方向锁定(LockX)、垂直方向锁定(LockY)、完全锁定(Lock)。 if(!this.LockX){ this.Drag.style.left = …; } 触发对象 触发对象是用来触发拖放程序的。有的时候不需要整个拖放对象都用来触发,这时就需要触发对象了。 范围限制 要设置范围限制必须先把Limit设为true。范围限制分两种,分别是固定范围和容器范围限制,主要在Move程序中设置。 固定范围限制 容器范围限制就是指定上下左右的拖放范围。 上(mxTop):top限制; 下(mxBottom):top+offsetHeight限制; 左(mxLeft):left限制; 右(mxRight):left+offsetWidth限制。 如果范围设置不正确,可能导致上下或左右同时超过范围的情况,程序中有一个Repair程序用来修正范围参数的。 this.mxRight = Math.max(this.mxRight, this.mxLeft + this.Drag.offsetWidth); 其中mxLeft+offsetWidth和mxTop+offsetHeight分别是mxRight和mxBottom的最小范围值。 根据范围参数修正移动参数: iLeft = Math.max(Math.min(iLeft, mxRight - this.Drag.offsetWidth), mxLeft); 对于左边上边要取更大的值,对于右边下面就要取更小的值。 容器范围限制 容器范围限制的意思就是把范围限制在一个容器_mxContainer内。 当设置了容器,会自动把position设为relative来相对定位: !this._mxContainer || CurrentStyle(this._mxContainer).position == “relative” || (this._mxContainer.style.position = “relative”); 注意relative要在获取offsetLeft和offsetTop即设置_x和_y之前设置,offset才能正确获取值。 由于是相对定位,对于容器范围来说范围参数上下左右的值分别是0、clientHeight、0、clientWidth。 clientWidth和clientHeight是容器可视部分的宽度和高度(详细参考这里)。 mxLeft = Math.max(mxLeft, 0); 注意如果在程序执行之前设置过拖放对象的left和top而容器没有设置relative,在自动设置relative时会发生移位现象,所以程序在初始化时就执行一次Repair程序防止这种情况。因为offsetLeft和offsetTop要在设置relative之前获取才能正确获取值,所以在Start程序中Repair要在设置_x和_y之前执行。 因为设置相对定位的关系,容器_mxContainer设置过后一般不要取消或修改,否则很容易造成移位异常。
鼠标捕获 我在一个拖放实例中看到,即使鼠标移动到浏览器外面,拖放程序依然能够执行,仔细查看后发现是用了setCapture。 this._Handle.setCapture(); setCapture捕获以下鼠标事件:onmousedown、onmouseup、onmousemove、onclick、ondblclick、onmouseover和onmouseout。 <html> 这里的参数是true,一开始body会捕获所有鼠标事件,即使鼠标经过div也不会触发onmousemove事件。 拖放结束后还要使用releaseCapture释放鼠标,这个可以放在Stop程序中: this._Handle.releaseCapture(); setCapture是ie的鼠标捕获方法,对于ff也有对应的captureEvents和releaseEvents方法。 下面都是我的猜测,ff的鼠标捕获相当于能自动设置和释放的document.body.setCapture(false)。 <html> ff: <html> 可惜没有权威的资料参考就只能猜猜了,还有很多还没有理解的地方以后再研究拉。 注意ff2下的鼠标捕获有一个bug,当拖放对象内部没有文本内容并拖放到浏览器外时捕获就会失效。 焦点丢失 一般情况下,鼠标捕获都能正常捕获事件,但如果浏览器窗口的焦点丢失就会导致捕获失效。 addEventHandler(this._Handle, “losecapture”, this._fS); 并在Stop程序中移除: removeEventHandler(this._Handle, “losecapture”, this._fS); 但ff没有类似的方法,不过muxrwc找到一个替代losecapture的window.onblur事件,那么可以在Start程序中设置: addEventHandler(window, “blur”, this._fS); 在Stop程序中移除: removeEventHandler(window, “blur”, this._fS); 那ie也有window.onblur事件,那用window.onblur代替losecapture不就可以省一段代码了吗。 于是我逐一排除测试和程序代码,结果发现如果使用了DTD,那么window.onblur会在再次获得焦点时才会触发。 <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”> 在切换到其他程序后,再切换回来才会触发window.onblur,还有几个比较怪异的状况就不说了,反正ie用window.onblur是不理想的了。 默认动作 对选择状态的文本内容、连接和图片等进行拖放操作会触发系统的默认动作,例如ie中拖动图片鼠标会变成禁止操作状态,这样会导致这个拖放程序执行失败。 不过ie在设置了setCapture之后,通过用户界面用鼠标进行拖放操作和内容选择都会被禁止。 而ff的鼠标捕获没有这个功能,但可以用preventDefault来取消事件的默认动作来解决: oEvent.preventDefault(); ps:据说使用preventDefault会出现mouseup丢失的情况,但我在ff3中测试没有发现,如果各位发现任何mouseup丢失的情况,务必告诉我啊。 清除选择 ie在设置setCapture之后内容选择都会被禁止,但也因此不会清除在设置之前就已经选择的内容,而且设置之后也能通过其他方式选择内容, 以前我用禁止拖放对象被选择的方法来达到目的,即ie中设置拖放对象的onselectstart返回false,在ff中设置样式MozUserSelect(css:-moz-user-select)为none。 ie: document.selection.empty() ff: window.getSelection().removeAllRanges() 为了防止在拖放过程中选择内容,所以把它放到Move程序中,下面是兼容的写法: window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); margin 还有一个情况,当拖放对象设置了margin,那么拖放的时候就会错位(给SimpleDrag的拖放对象设置margin就可以测试)。 this._marginLeft = parseInt(CurrentStyle(this.Drag).marginLeft) || 0; 其中CurrentStyle是用来获取最终样式,详细看这里的最终样式部分。 在Move程序中设置值: this.Drag.style.left = iLeft - this._marginLeft + “px”; 要注意margin要在范围修正只后再设置,否则会错位。 【透明背景bug】 在ie有一个透明背景bug(不知算不算bug),可以用下面的代码测试: <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd“> 会发现背景点击触发不了事件,不过点击边框的话还是可以触发。 <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd“> 应该能看出个大概了,下面两个div超出body(即超出红色框)的部分就触发不了事件。 那程序中只要给拖放对象设一个背景色就可以解决了,但有时需求正好是要透明(例如切割效果),那怎么办呢? with(this._Handle.appendChild(document.createElement(”div”)).style){ 当发现程序有这个bug出现,把程序可选参数Transparent设为true就会自动插入这样一个层了。 暂时就研究到这里,不过还有iframe,滚屏等这些还没考虑到,等以后有需要了再来研究拉。 (责任编辑:admin) |