js拖拽是常見的網頁效果,本文將從零開始實現一個簡單的js插件。
一、js拖拽插件的原理
常見的拖拽操作是什么樣的呢?整過過程大概有下面幾個步驟:
1、用鼠標點擊被拖拽的元素
2、按住鼠標不放,移動鼠標
3、拖拽元素到一定位置,放開鼠標
這里的過程涉及到三個dom事件:onmousedown,onmousemove,onmouseup。所以拖拽的基本思路就是:
1、用鼠標點擊被拖拽的元素觸發onmousedown
(1)設置當前元素的可拖拽為true,表示可以拖拽
(2)記錄當前鼠標的坐標x,y
(3)記錄當前元素的坐標x,y
2、移動鼠標觸發onmousemove
(1)判斷元素是否可拖拽,如果是則進入步驟2,否則直接返回
(2)如果元素可拖拽,則設置元素的坐標
元素的x坐標 = 鼠標移動的橫向距離+元素本來的x坐標 = 鼠標現在的x坐標 - 鼠標之前的x坐標 + 元素本來的x坐標
元素的y坐標 = 鼠標移動的橫向距離+元素本來的y坐標 = 鼠標現在的y坐標 - 鼠標之前的y坐標 + 元素本來的y坐標
3、放開鼠標觸發onmouseup
(1)將鼠標的可拖拽狀態設置成false
二、根據原理實現的最基本效果
在實現基本的效果之前,有幾點需要說明的:
1、元素想要被拖動,它的postion屬性一定要是relative或absolute
2、通過event.clientX和event.clientY獲取鼠標的坐標
3、onmousemove是綁定在document元素上而不是拖拽元素本身,這樣能解決快速拖動造成的延遲或停止移動的問題
代碼如下:
1 var dragObj = document.getElementById("test"); 2 dragObj.style.left = "0px"; 3 dragObj.style.top = "0px"; 4 5 var mouseX, mouseY, objX, objY; 6 var dragging = false; 7 8 dragObj.onmousedown = function (event) { 9 event = event || window.event; 10 11 dragging = true; 12 dragObj.style.position = "relative"; 13 14 15 mouseX = event.clientX; 16 mouseY = event.clientY; 17 objX = parseInt(dragObj.style.left); 18 objY = parseInt(dragObj.style.top); 19 } 20 21 document.onmousemove = function (event) { 22 event = event || window.event; 23 if (dragging) { 24 25 dragObj.style.left = parseInt(event.clientX - mouseX + objX) + "px"; 26 dragObj.style.top = parseInt(event.clientY - mouseY + objY) + "px"; 27 } 28 29 } 30 31 document.onmouseup = function () { 32 dragging = false; 33 }
三、代碼抽象與優化
上面的代碼要做成插件,要將其抽象出來,基本結構如下:
1 ; (function (window, undefined) { 2 3 function Drag(ele) {} 4 5 window.Drag = Drag; 6 })(window, undefined);
用自執行匿名函數將代碼包起來,內部定義Drag方法並暴露到全局中,直接調用Drag,傳入被拖拽的元素。
首先對一些常用的方法進行簡單的封裝:
1 ; (function (window, undefined) { 2 var dom = { 3 //綁定事件 4 on: function (node, eventName, handler) { 5 if (node.addEventListener) { 6 node.addEventListener(eventName, handler); 7 } 8 else { 9 node.attachEvent("on" + eventName, handler); 10 } 11 }, 12 //獲取元素的樣式 13 getStyle: function (node, styleName) { 14 var realStyle = null; 15 if (window.getComputedStyle) { 16 realStyle = window.getComputedStyle(node, null)[styleName]; 17 } 18 else if (node.currentStyle) { 19 realStyle = node.currentStyle[styleName]; 20 } 21 return realStyle; 22 }, 23 //獲取設置元素的樣式 24 setCss: function (node, css) { 25 for (var key in css) { 26 node.style[key] = css[key]; 27 } 28 } 29 }; 30 31 window.Drag = Drag; 32 })(window, undefined);
在一個拖拽操作中,存在着兩個對象:被拖拽的對象和鼠標對象,我們定義了下面的兩個對象以及它們對應的操作:
首先的拖拽對象,它包含一個元素節點和拖拽之前的坐標x和y:
1 function DragElement(node) { 2 this.node = node;//被拖拽的元素節點 3 this.x = 0;//拖拽之前的x坐標 4 this.y = 0;//拖拽之前的y坐標 5 } 6 DragElement.prototype = { 7 constructor: DragElement, 8 init: function () { 9 this.setEleCss({ 10 "left": dom.getStyle(node, "left"), 11 "top": dom.getStyle(node, "top") 12 }) 13 .setXY(node.style.left, node.style.top); 14 }, 15 //設置當前的坐標 16 setXY: function (x, y) { 17 this.x = parseInt(x) || 0; 18 this.y = parseInt(y) || 0; 19 return this; 20 }, 21 //設置元素節點的樣式 22 setEleCss: function (css) { 23 dom.setCss(this.node, css); 24 return this; 25 } 26 }
還有一個對象是鼠標,它主要包含x坐標和y坐標:
1 function Mouse() { 2 this.x = 0; 3 this.y = 0; 4 } 5 Mouse.prototype.setXY = function (x, y) { 6 this.x = parseInt(x); 7 this.y = parseInt(y); 8 }
這是在拖拽操作中定義的兩個對象。
如果一個頁面可以有多個拖拽元素,那應該注意什么:
1、每個元素對應一個拖拽對象實例
2、每個頁面只能有一個正在拖拽中的元素
為此,我們定義了唯一一個對象用來保存相關的配置:
1 var draggableConfig = { 2 zIndex: 1, 3 draggingObj: null, 4 mouse: new Mouse() 5 };
這個對象中有三個屬性:
(1)zIndex:用來賦值給拖拽對象的zIndex屬性,有多個拖拽對象時,當兩個拖拽對象重疊時,會造成當前拖拽對象有可能被擋住,通過設置zIndex使其顯示在最頂層
(2)draggingObj:用來保存正在拖拽的對象,在這里去掉了前面的用來判斷是否可拖拽的變量,通過draggingObj來判斷當前是否可以拖拽以及獲取相應的拖拽對象
(3)mouse:唯一的鼠標對象,用來保存當前鼠標的坐標等信息
最后是綁定onmousedown,onmouseover,onmouseout事件,整合上面的代碼如下:
1 ; (function (window, undefined) { 2 var dom = { 3 //綁定事件 4 on: function (node, eventName, handler) { 5 if (node.addEventListener) { 6 node.addEventListener(eventName, handler); 7 } 8 else { 9 node.attachEvent("on" + eventName, handler); 10 } 11 }, 12 //獲取元素的樣式 13 getStyle: function (node, styleName) { 14 var realStyle = null; 15 if (window.getComputedStyle) { 16 realStyle = window.getComputedStyle(node, null)[styleName]; 17 } 18 else if (node.currentStyle) { 19 realStyle = node.currentStyle[styleName]; 20 } 21 return realStyle; 22 }, 23 //獲取設置元素的樣式 24 setCss: function (node, css) { 25 for (var key in css) { 26 node.style[key] = css[key]; 27 } 28 } 29 }; 30 31 //#region 拖拽元素類 32 function DragElement(node) { 33 this.node = node; 34 this.x = 0; 35 this.y = 0; 36 } 37 DragElement.prototype = { 38 constructor: DragElement, 39 init: function () { 40 this.setEleCss({ 41 "left": dom.getStyle(node, "left"), 42 "top": dom.getStyle(node, "top") 43 }) 44 .setXY(node.style.left, node.style.top); 45 }, 46 setXY: function (x, y) { 47 this.x = parseInt(x) || 0; 48 this.y = parseInt(y) || 0; 49 return this; 50 }, 51 setEleCss: function (css) { 52 dom.setCss(this.node, css); 53 return this; 54 } 55 } 56 //#endregion 57 58 //#region 鼠標元素 59 function Mouse() { 60 this.x = 0; 61 this.y = 0; 62 } 63 Mouse.prototype.setXY = function (x, y) { 64 this.x = parseInt(x); 65 this.y = parseInt(y); 66 } 67 //#endregion 68 69 //拖拽配置 70 var draggableConfig = { 71 zIndex: 1, 72 draggingObj: null, 73 mouse: new Mouse() 74 }; 75 76 function Drag(ele) { 77 this.ele = ele; 78 79 function mouseDown(event) { 80 var ele = event.target || event.srcElement; 81 82 draggableConfig.mouse.setXY(event.clientX, event.clientY); 83 84 draggableConfig.draggingObj = new DragElement(ele); 85 draggableConfig.draggingObj 86 .setXY(ele.style.left, ele.style.top) 87 .setEleCss({ 88 "zIndex": draggableConfig.zIndex++, 89 "position": "relative" 90 }); 91 } 92 93 ele.onselectstart = function () { 94 //防止拖拽對象內的文字被選中 95 return false; 96 } 97 dom.on(ele, "mousedown", mouseDown); 98 } 99 100 dom.on(document, "mousemove", function (event) { 101 if (draggableConfig.draggingObj) { 102 var mouse = draggableConfig.mouse, 103 draggingObj = draggableConfig.draggingObj; 104 draggingObj.setEleCss({ 105 "left": parseInt(event.clientX - mouse.x + draggingObj.x) + "px", 106 "top": parseInt(event.clientY - mouse.y + draggingObj.y) + "px" 107 }); 108 } 109 }) 110 111 dom.on(document, "mouseup", function (event) { 112 draggableConfig.draggingObj = null; 113 }) 114 115 116 window.Drag = Drag; 117 })(window, undefined);
調用方法:Drag(document.getElementById("obj"));
注意的一點,為了防止選中拖拽元素中的文字,通過onselectstart事件處理程序return false來處理這個問題。
四、擴展:有效的拖拽元素
我們常見的一些拖拽效果很有可能是這樣的:
彈框的頂部是可以進行拖拽操作的,內容區域是不可拖拽的,怎么實現這樣的效果呢:
首先優化拖拽元素對象如下,增加一個目標元素target,表示被拖拽對象,在上圖的登錄框中,就是整個登錄窗口。
被記錄和設置坐標的拖拽元素就是這個目標元素,但是它並不是整個部分都是拖拽的有效部分。我們在html結構中為拖拽的有效區域添加類draggable表示有效拖拽區域:
1 <div id="obj1" class="dialog" style="position:relative;left:50px"> 2 <div class="header draggable"> 3 拖拽的有效元素 4 </div> 5 <div class="content"> 6 拖拽對象1 7 </div> 8 </div>
然后修改Drag方法如下:
function drag(ele) { var dragNode = (ele.querySelector(".draggable") || ele); dom.on(dragNode, "mousedown", function (event) { var dragElement = draggableConfig.dragElement = new DragElement(ele); draggableConfig.mouse.setXY(event.clientX, event.clientY); draggableConfig.dragElement .setXY(dragElement.target.style.left, dragElement.target.style.top) .setTargetCss({ "zIndex": draggableConfig.zIndex++, "position": "relative" }); }).on(dragNode, "mouseover", function () { dom.setCss(this, draggableStyle.dragging); }).on(dragNode, "mouseout", function () { dom.setCss(this, draggableStyle.defaults); }); }
主要修改的是綁定mousedown的節點變成了包含draggable類的有效元素,如果不含有draggable,則整個元素都是有效元素。
五、性能優化和總結
由於onmousemove在一直調用,會造成一些性能問題,我們可以通過setTimout來延遲綁定onmousemove事件,改進move函數如下
1 function move(event) { 2 if (draggableConfig.dragElement) { 3 var mouse = draggableConfig.mouse, 4 dragElement = draggableConfig.dragElement; 5 dragElement.setTargetCss({ 6 "left": parseInt(event.clientX - mouse.x + dragElement.x) + "px", 7 "top": parseInt(event.clientY - mouse.y + dragElement.y) + "px" 8 }); 9 10 dom.off(document, "mousemove", move); 11 setTimeout(function () { 12 dom.on(document, "mousemove", move); 13 }, 25); 14 } 15 }
總結:
整個拖拽插件的實現其實很簡單,主要是要注意幾點
1、實現思路:元素拖拽位置的改變就等於鼠標改變的距離,關鍵在於獲取鼠標的變動和元素原本的坐標
2、通過setTimeout來延遲加載onmousemove事件來提供性能
六、jquery插件化
簡單地將其封裝成jquery插件,主要是相關的dom方法替換成jquery方法來操作
1 ; (function ($, window, undefined) { 2 //#region 拖拽元素類 3 function DragElement(node) { 4 5 this.target = node; 6 7 node.onselectstart = function () { 8 //防止拖拽對象內的文字被選中 9 return false; 10 } 11 } 12 DragElement.prototype = { 13 constructor: DragElement, 14 setXY: function (x, y) { 15 this.x = parseInt(x) || 0; 16 this.y = parseInt(y) || 0; 17 return this; 18 }, 19 setTargetCss: function (css) { 20 $(this.target).css(css); 21 return this; 22 } 23 } 24 //#endregion 25 26 //#region 鼠標元素 27 function Mouse() { 28 this.x = 0; 29 this.y = 0; 30 } 31 Mouse.prototype.setXY = function (x, y) { 32 this.x = parseInt(x); 33 this.y = parseInt(y); 34 } 35 //#endregion 36 37 //拖拽配置 38 var draggableConfig = { 39 zIndex: 1, 40 dragElement: null, 41 mouse: new Mouse() 42 }; 43 44 var draggableStyle = { 45 dragging: { 46 cursor: "move" 47 }, 48 defaults: { 49 cursor: "default" 50 } 51 } 52 53 var $document = $(document); 54 55 function drag($ele) { 56 var $dragNode = $ele.find(".draggable"); 57 $dragNode = $dragNode.length > 0 ? $dragNode : $ele; 58 59 60 $dragNode.on({ 61 "mousedown": function (event) { 62 var dragElement = draggableConfig.dragElement = new DragElement($ele.get(0)); 63 64 draggableConfig.mouse.setXY(event.clientX, event.clientY); 65 draggableConfig.dragElement 66 .setXY(dragElement.target.style.left, dragElement.target.style.top) 67 .setTargetCss({ 68 "zIndex": draggableConfig.zIndex++, 69 "position": "relative" 70 }); 71 }, 72 "mouseover": function () { 73 $(this).css(draggableStyle.dragging); 74 }, 75 "mouseout": function () { 76 $(this).css(draggableStyle.defaults); 77 } 78 }) 79 } 80 81 function move(event) { 82 if (draggableConfig.dragElement) { 83 var mouse = draggableConfig.mouse, 84 dragElement = draggableConfig.dragElement; 85 dragElement.setTargetCss({ 86 "left": parseInt(event.clientX - mouse.x + dragElement.x) + "px", 87 "top": parseInt(event.clientY - mouse.y + dragElement.y) + "px" 88 }); 89 90 $document.off("mousemove", move); 91 setTimeout(function () { 92 $document.on("mousemove", move); 93 }, 25); 94 } 95 } 96 97 $document.on({ 98 "mousemove": move, 99 "mouseup": function () { 100 draggableConfig.dragElement = null; 101 } 102 }); 103 104 $.fn.drag = function (options) { 105 drag(this); 106 } 107 108 })(jQuery, window, undefined)
點擊下載DEMO