JavaScript实现拖拽交互:从基础到高级应用

作者:小圆点

时间:2026-10-28

浏览:245

本文详细解析如何使用JavaScript实现网页元素的拖拽功能。内容涵盖从基础的鼠标坐标计算、元素位置追踪,到解决拖拽时事件被遮挡的难题。重点介绍了通过手动检测鼠标释放位置来实现“放置”功能,以及构建支持容器间自由移动和排序的完整拖拽系统,包含阴影效果、状态管理及事件监听等核心代码逻辑。

文章目录
<base href='http://www.blueidea.com'> <FIELDSET id=Demo5 style="HEIGHT: 70px"><h3>Demo - Drag any of the  images</h3><IMG id=DragImage5  src="/articleimg/2006/07/3791/drag_drop_spade.gif"><IMG  id=DragImage6  src="/articleimg/2006/07/3791/drag_drop_heart.gif"><IMG  id=DragImage7  src="/articleimg/2006/07/3791/drag_drop_diamond.gif"><IMG  id=DragImage8  src="/articleimg/2006/07/3791/drag_drop_club.gif"> </FIELDSET>

JavaScript拖拽基础与坐标计算

[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]


这段代码实际上是前文示例的完整集合,将各个部分整合后即可实现完整的拖拽效果。



当用户点击某个元素(item)时,我们需要获取一系列关键变量,包括当前的鼠标位置。鼠标位置天然包含了该元素在页面中的坐标信息。例如,如果点击一个20x20像素图像的正中心,鼠标的相对坐标为{x:10, y:10};若点击左上角,则相对坐标为{x:0, y:0}。通过此方法,我们可以获取鼠标与图片对齐的相关信息。需要注意的是,如果页面元素未正确加载,获取的信息将基于整个document,从而忽略特定点击元素的信息。



mouseOffset函数依赖于另一个名为getPosition的辅助函数,其作用是返回元素相对于页面左上角的绝对坐标。如果直接尝试获取item.offsetLeft或item.style.left,得到的是元素相对于其父级容器的位置,而非整个文档的位置。为了确保所有脚本逻辑基于整个document坐标系运行,这样处理会更加准确和便捷。



为了完成getPosition的任务,必须通过循环遍历获取元素的父级节点,从而累加其相对于父级的左/上位置。我们需要维护一个包含top和left值的列表来进行计算。



一旦定义了mousemove函数,mouseMove逻辑就会持续运行。首先,我们需要确保元素的style.position属性被设置为absolute;其次,根据之前定义的位置移动该元素。当鼠标点击被释放时,dragObject被设置为null,mouseMove函数将不再执行任何操作。



解决拖拽放置检测难题

放置元素(Dropping an Item)



前面的示例目的很简单,即将元素拖拽到用户希望放置的位置。但在实际应用中,我们往往有其他目的,例如删除元素。比如,我们可以将元素拖入垃圾桶图标,或者页面定义的其他特定区域。



这里存在一个显著的技术难题:在拖拽过程中,被拖拽的元素位于鼠标指针的下方。这意味着mouseover、mousedown、mouseup或其他鼠标事件会作用在被拖拽的元素上,而不是下方的目标区域(如垃圾桶)。因此,即使我们将元素拖到垃圾桶上方,鼠标事件依然记录在拖拽元素上,而非垃圾桶上。



如何解决这个问题呢?有几种方法。第一种是以前比较推荐的做法:在移动鼠标时,让元素跟随鼠标移动,但不占用mouseover/mousemove等鼠标事件。这种方法虽然能解决问题,但视觉效果不佳,因为我们通常希望元素直接跟随在鼠标指针下方。



另一种选择是不进行实际的元素拖拽,而是改变鼠标指针样式以指示正在拖拽的元素,并在鼠标释放时放置该元素。出于美学考虑,这个方案通常也被排除。



最终的解决方案是保留拖拽视觉效果,但通过复杂的逻辑处理放置检测。我们需要定义一个“可放置目标”列表,当鼠标释放时,手动检查释放位置是否位于目标列表的范围内。如果在范围内,则视为成功放置在目标位置上。


代码如下:

/* 
前一个示例的所有代码都需要保留,除了
mouseUp函数,它被下面的代码替换
*/ 
var dropTargets = [];
function addDropTarget(dropTarget){
dropTargets.push(dropTarget);
} 
function mouseUp(ev){
ev = ev || window.event;
var mousePos = mouseCoords(ev);
for(var i=0; i<dropTargets.length; i++){
var curTarget = dropTargets[i];
var targPos = getPosition(curTarget);
var targWidth = parseInt(curTarget.offsetWidth);
var targHeight = parseInt(curTarget.offsetHeight);
if(
(mousePos.x > targPos.x) &&
(mousePos.x < (targPos.x + targWidth)) &&
(mousePos.y > targPos.y) &&
(mousePos.y < (targPos.y + targHeight))){
// dragObject was dropped onto curTarget!
  } 
 } 
dragObject = null;
} 

<base href='http://www.blueidea.com'> <FIELDSET id=Demo6 style="HEIGHT: 70px"><h3>Demo - Drag any image onto the  trashcan</h3><IMG id=TrashImage1  src="/articleimg/2006/07/3791/drag_drop_trash.gif"  timeout="600" newSrc="/articleimg/2006/07/3791/transparent.gif">        <IMG  id=DragImage9  src="/articleimg/2006/07/3791/drag_drop_spade.gif"> <IMG  id=DragImage10  src="/articleimg/2006/07/3791/drag_drop_heart.gif"> <IMG  id=DragImage11  src="/articleimg/2006/07/3791/drag_drop_diamond.gif"> <IMG  id=DragImage12  src="/articleimg/2006/07/3791/drag_drop_club.gif">  </FIELDSET> 

[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]



在鼠标释放时,系统会检查是否存在drop属性。如果存在且鼠标指针仍在drop目标的范围内,则执行drop操作。我们通过判断(mousePos.x > targetPos.x)且(mousePos.x < (targPos.x + targWidth))以及Y轴对应的条件,来确认鼠标指针是否确实在目标范围内。如果所有条件符合,说明指针在范围内,可以执行drop指令。



构建完整的拖拽容器系统

整合所有功能(Pulling It All Together)



最后,我们拥有了所有拖拽/放置的脚本片段!下一步是创建一个DOM处理程序。如果你不熟悉DOM操作,请先阅读我的JavaScript DOM Manipulation Primer。



下面的代码将创建容器(container),并使任何需要拖拽/放置的元素成为容器的子项。这段代码位于文章第二个demo之后,它可以用于记录列表、设定导航窗口在左侧或右侧,或者实现你所能想到的更多功能。



接下来,我们将通过“伪代码”向读者展示真实代码的逻辑结构,推荐步骤如下:



1、当document首次载入时,创建dragHelper DIV。dragHelper将为移动的元素添加阴影效果。真实的元素并未被真正拖拽,只是通过insertBefore和appendChild进行移动,我们隐藏了dragHelper。



2、设置mouseDown与mouseUp函数。所有操作对应到iMouseDown状态中,只有当鼠标左键按下时iMouseDown为真,否则为假。



3、创建全局变量DragDrops与全局函数CreateDragContainer。DragDrops包含了一系列相互关联的容器。任何参数(containers)将通过CreateDragContainer进行重组与序列化,从而实现自由移动。CreateDragContainer函数还将对元素进行绑定并设置属性。



4、现在代码已知每个元素的加入情况。当触发mouseMove时,mouseMove函数首先设置变量target,即鼠标悬停其上的元素。如果该元素在容器中(通过getAttribute检查):



运行一小段代码来改变目标的样式,创造悬停(rollover)效果


检查鼠标是否未放开,如果未放开


设置curTarget代表当前元素


记录元素的当前位置,如果需要的话,我们可以将其返回


克隆当前元素到dragHelper中,我们可以移动带有阴影效果的元素。


元素拷贝到dragHelper后,原有元素仍在鼠标指针下,我们必须删除dragObj,这样脚本起作用,dragObj被包含在一个容器中。


抓取容器中所有元素的当前坐标、高度/宽度,这样只需要记录一次,当元素被拖拽时,每次鼠标移动都会记录成千上万次。


如果没有,不需要做任何事,因为这不是一个需要移动的元素


5、检查curTarget,它应该包含一个被移动的元素。如果存在,进行下面操作:



开始移动带有阴影的元素,这个元素就是前文所创建的


检查每个当前容器中的container,是否鼠标已经移动到这些范围内了


我们检查看一下正在拖动的元素是属于哪个container


放置元素在一个container的某一个元素之前,或者整个container之后


确认元素是可见的


如果鼠标不在container中,确认元素是不可见的。


6、剩下的事就是捕捉mouseUp的事件了


<base href='http://www.blueidea.com'><STYLE>LI {     MARGIN-BOTTOM: 10px } OL {     MARGIN-TOP: 5px } .DragContainer {     BORDER-RIGHT: #669999 2px solid; PADDING-RIGHT: 5px; BORDER-TOP: #669999 2px solid; PADDING-LEFT: 5px; FLOAT: left; PADDING-BOTTOM: 0px; MARGIN: 3px; BORDER-LEFT: #669999 2px solid; WIDTH: 100px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 2px solid } .OverDragContainer {     BORDER-RIGHT: #669999 2px solid; PADDING-RIGHT: 5px; BORDER-TOP: #669999 2px solid; PADDING-LEFT: 5px; FLOAT: left; PADDING-BOTTOM: 0px; MARGIN: 3px; BORDER-LEFT: #669999 2px solid; WIDTH: 100px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 2px solid } .OverDragContainer {     BACKGROUND-COLOR: #eee } .DragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .OverDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .DragDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .miniDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .OverDragBox {     BACKGROUND-COLOR: #ffff99 } .DragDragBox {     BACKGROUND-COLOR: #ffff99 } .DragDragBox {     FILTER: alpha(opacity=50); BACKGROUND-COLOR: #ff99cc } LEGEND {     FONT-WEIGHT: bold; FONT-SIZE: 12px; COLOR: #666699; FONT-FAMILY: verdana, tahoma, arial } FIELDSET {     PADDING-RIGHT: 3px; PADDING-LEFT: 3px; PADDING-BOTTOM: 3px; PADDING-TOP: 3px } .History {     FONT-SIZE: 10px; OVERFLOW: auto; WIDTH: 100%; FONT-FAMILY: verdana, tahoma, arial; HEIGHT: 82px } #DragContainer8 {     BORDER-RIGHT: #669999 1px solid; PADDING-RIGHT: 0px; BORDER-TOP: #669999 1px solid; PADDING-LEFT: 5px; PADDING-BOTTOM: 0px; BORDER-LEFT: #669999 1px solid; WIDTH: 110px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 1px solid; HEIGHT: 110px } .miniDragBox {     FLOAT: left; MARGIN: 0px 5px 5px 0px; WIDTH: 20px; HEIGHT: 20px } pre{border:1 solid #CCC;background-color:#F8F8F0;padding:10px;} </STYLE>  <!--the mouse over and dragging class are defined on each item--> <div class="DragContainer" id="DragContainer1">     <div class="DragBox" id="Item1"  overClass="OverDragBox" dragClass="DragDragBox">Item #1</div>     <div class="DragBox" id="Item2"  overClass="OverDragBox" dragClass="DragDragBox">Item #2</div>     <div class="DragBox" id="Item3"  overClass="OverDragBox" dragClass="DragDragBox">Item #3</div>     <div class="DragBox" id="Item4"  overClass="OverDragBox" dragClass="DragDragBox">Item #4</div> </div> <div class="DragContainer" id="DragContainer2">     <div class="DragBox" id="Item5"  overClass="OverDragBox" dragClass="DragDragBox">Item #5</div>     <div class="DragBox" id="Item6"  overClass="OverDragBox" dragClass="DragDragBox">Item #6</div>     <div class="DragBox" id="Item7"  overClass="OverDragBox" dragClass="DragDragBox">Item #7</div>     <div class="DragBox" id="Item8"  overClass="OverDragBox" dragClass="DragDragBox">Item #8</div> </div> <div class="DragContainer" id="DragContainer3">     <div class="DragBox" id="Item9"  overClass="OverDragBox" dragClass="DragDragBox">Item #9</div>     <div class="DragBox" id="Item10" overClass="OverDragBox" dragClass="DragDragBox">Item #10</div>     <div class="DragBox" id="Item11" overClass="OverDragBox" dragClass="DragDragBox">Item #11</div>     <div class="DragBox" id="Item12" overClass="OverDragBox" dragClass="DragDragBox">Item #12</div> 

[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]


你现在拥有了拖拽的所有功能。

下面的三个demo用于记录事件历史。当你的鼠标在元素上移动时,将记录所发生的事件。如果你不明白,可以尝试一下鼠标的划过或者拖动,观察有什么变化发生。

<base href='http://www.blueidea.com'> <STYLE type=text/css>LI {     MARGIN-BOTTOM: 10px } OL {     MARGIN-TOP: 5px } .DragContainer {     BORDER-RIGHT: #669999 2px solid; PADDING-RIGHT: 5px; BORDER-TOP: #669999 2px solid; PADDING-LEFT: 5px; FLOAT: left; PADDING-BOTTOM: 0px; MARGIN: 3px; BORDER-LEFT: #669999 2px solid; WIDTH: 100px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 2px solid } .OverDragContainer {     BORDER-RIGHT: #669999 2px solid; PADDING-RIGHT: 5px; BORDER-TOP: #669999 2px solid; PADDING-LEFT: 5px; FLOAT: left; PADDING-BOTTOM: 0px; MARGIN: 3px; BORDER-LEFT: #669999 2px solid; WIDTH: 100px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 2px solid } .OverDragContainer {     BACKGROUND-COLOR: #eee } .DragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .OverDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .DragDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .miniDragBox {     BORDER-RIGHT: #000 1px solid; PADDING-RIGHT: 2px; BORDER-TOP: #000 1px solid; PADDING-LEFT: 2px; FONT-SIZE: 10px; MARGIN-BOTTOM: 5px; PADDING-BOTTOM: 2px; BORDER-LEFT: #000 1px solid; WIDTH: 94px; CURSOR: pointer; PADDING-TOP: 2px; BORDER-BOTTOM: #000 1px solid; FONT-FAMILY: verdana, tahoma, arial; BACKGROUND-COLOR: #eee } .OverDragBox {     BACKGROUND-COLOR: #ffff99 } .DragDragBox {     BACKGROUND-COLOR: #ffff99 } .DragDragBox {     FILTER: alpha(opacity=50); BACKGROUND-COLOR: #ff99cc } LEGEND {     FONT-WEIGHT: bold; FONT-SIZE: 12px; COLOR: #666699; FONT-FAMILY: verdana, tahoma, arial } FIELDSET {     PADDING-RIGHT: 3px; PADDING-LEFT: 3px; PADDING-BOTTOM: 3px; PADDING-TOP: 3px } .History {     FONT-SIZE: 10px; OVERFLOW: auto; WIDTH: 100%; FONT-FAMILY: verdana, tahoma, arial; HEIGHT: 82px } #DragContainer8 {     BORDER-RIGHT: #669999 1px solid; PADDING-RIGHT: 0px; BORDER-TOP: #669999 1px solid; PADDING-LEFT: 5px; PADDING-BOTTOM: 0px; BORDER-LEFT: #669999 1px solid; WIDTH: 110px; PADDING-TOP: 5px; BORDER-BOTTOM: #669999 1px solid; HEIGHT: 110px } .miniDragBox {     FLOAT: left; MARGIN: 0px 5px 5px 0px; WIDTH: 20px; HEIGHT: 20px } PRE {     BORDER-RIGHT: #ccc 1px solid; PADDING-RIGHT: 10px; BORDER-TOP: #ccc 1px solid; PADDING-LEFT: 10px; PADDING-BOTTOM: 10px; BORDER-LEFT: #ccc 1px solid; PADDING-TOP: 10px; BORDER-BOTTOM: #ccc 1px solid; BACKGROUND-COLOR: #f8f8f0 } </STYLE> <body>       <FIELDSET id=Demo0>       <H3>Demo - Drag and Drop any item</H3>       <DIV>       <DIV class=DragContainer id=DragContainer1 overclass="OverDragContainer"        history="History1">       <DIV class=DragBox id=Item1 overclass="OverDragBox"        dragclass="DragDragBox">Item #1</DIV>       <DIV class=DragBox id=Item2 overclass="OverDragBox"        dragclass="DragDragBox">Item #2</DIV>       <DIV class=DragBox id=Item3 overclass="OverDragBox"        dragclass="DragDragBox">Item #3</DIV>       <DIV class=DragBox id=Item4 overclass="OverDragBox"        dragclass="DragDragBox">Item #4</DIV></DIV>       <DIV class=DragContainer id=DragContainer2 overclass="OverDragContainer"        history="History1">       <DIV class=DragBox id=Item5 overclass="OverDragBox"        dragclass="DragDragBox">Item #5</DIV>       <DIV class=DragBox id=Item6 overclass="OverDragBox"        dragclass="DragDragBox">Item #6</DIV>       <DIV class=DragBox id=Item7 overclass="OverDragBox"        dragclass="DragDragBox">Item #7</DIV>       <DIV class=DragBox id=Item8 overclass="OverDragBox"        dragclass="DragDragBox">Item #8</DIV></DIV>       <DIV class=DragContainer id=DragContainer3 overclass="OverDragContainer"        history="History1">       <DIV class=DragBox id=Item9 overclass="OverDragBox"        dragclass="DragDragBox">Item #9</DIV>       <DIV class=DragBox id=Item10 overclass="OverDragBox"        dragclass="DragDragBox">Item #10</DIV>       <DIV class=DragBox id=Item11 overclass="OverDragBox"        dragclass="DragDragBox">Item #11</DIV>       <DIV class=DragBox id=Item12 overclass="OverDragBox"        dragclass="DragDragBox">Item #12</DIV></DIV>       <FIELDSET><LEGEND>History</LEGEND>       <DIV class=History id=History1></DIV></FIELDSET> </DIV></FIELDSET>        <FIELDSET id=Demo7><LEGEND>Demo - Drag and Drop any item</LEGEND>       <DIV>       <OL id=DragContainer7 history="History1">         <LI id=ListItem1>Item #1          <LI id=ListItem2>Item #2          <LI id=ListItem3>Item #3          <LI id=ListItem4>Item #4          <LI id=ListItem5>Item #5 </LI></OL></DIV></FIELDSET>        <FIELDSET id=Demo8>       <H3>Demo - Drag and Drop any item</H3>       <DIV id=DragContainer8 history="History1">       <DIV class=miniDragBox id=MiniBox1>1</DIV>       <DIV class=miniDragBox id=MiniBox2>2</DIV>       <DIV class=miniDragBox id=MiniBox3>3</DIV>       <DIV class=miniDragBox id=MiniBox4>4</DIV>       <DIV class=miniDragBox id=MiniBox5>5</DIV>       <DIV class=miniDragBox id=MiniBox6>6</DIV>       <DIV class=miniDragBox id=MiniBox7>7</DIV>       <DIV class=miniDragBox id=MiniBox8>8</DIV>       <DIV class=miniDragBox id=MiniBox9>9</DIV>       <DIV class=miniDragBox id=MiniBox10>10</DIV>       <DIV class=miniDragBox id=MiniBox11>11</DIV>       <DIV class=miniDragBox id=MiniBox12>12</DIV>       <DIV class=miniDragBox id=MiniBox13>13</DIV>       <DIV class=miniDragBox id=MiniBox14>14</DIV>       <DIV class=miniDragBox id=MiniBox15>15</DIV>       <DIV class=miniDragBox id=MiniBox16>16</DIV></DIV></FIELDSET>        <DIV></DIV> </body> </html>

[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章