在做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
前兩個問題好解決,拓展一下模塊就可以,至於第三個現在還沒想到比較好的解決辦法,十一點了,明天再研究研究,然后一塊兒發出來,晚安。

