本文详细解析如何使用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用于记录事件历史。当你的鼠标在元素上移动时,将记录所发生的事件。如果你不明白,可以尝试一下鼠标的划过或者拖动,观察有什么变化发生。 [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]<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>