dom元素拖拽
之前找实习的时候,面试官出了道 “原生 JS 实现 div 元素拖拽”,当时实现了个大概,不过很多细节都没写好,刚好最近又看到了相关文章,因此这里再好好整理一下 JS 中的元素拖拽实现。
#
原生 JS 实现 div 拖拽实现效果:
要实现这样的元素拖拽效果,一般思路是这样的:
- 首先要被拖拽的 div 元素需要是定位元素,因为我们要使用
left
和top
样式来不断更新 div 的位置 - 设置一个
flag
,当鼠标左键在 div 元素上按下时,flag
设为true
,表示能够进行拖拽,且记录鼠标位置与 div 元素左上角的相对距离,用于后续 div 窗口的位置计算 - 当鼠标移动时,若
flag
为true
,那么根据鼠标位置实时更新 div 的位置,且当 div 元素超出当前视口时,要进行判断处理使其保留在视口内。 - 当鼠标松开时,
flag
重新设置为false
细节看代码:
- HTML
- JS
- CSS
<div class="container"> <div class="box"></div></div>
let box = document.getElementsByClassName('box')[0];
let flag = false;let disX, disY;let bound;
// flag 设为 true 表示按下鼠标,并记录鼠标离盒子左上角的相对距离function leftDown(e) { if (e.button === 0) { // 按下鼠标左键 flag = true; bound = box.getBoundingClientRect(); disX = e.clientX - bound.left; disY = e.clientY - bound.top; }}
function drag(e) { if (flag) { let top = e.clientY - disY; let left = e.clientX - disX;
// 处理边界条件 if (top < 0) top = 0; if (left < 0) left = 0; if (top + bound.height > document.body.clientHeight) { top = document.body.clientHeight - bound.height; } if (left + bound.width > document.body.clientWidth) { left = document.body.clientWidth - bound.width; }
box.style.top = `${top}px`; box.style.left = `${left}px`; }}
function leftUp(e) { if (e.button === 0) { flag = false; }}
box.addEventListener('mousedown', leftDown);document.addEventListener('mousemove', drag);document.addEventListener('mouseup', leftUp);
* { padding: 0; margin: 0;}
.container { background-color: #eee; display: flex; justify-content: center; align-items: center; height: 100vh;}
.box { width: 200px; height: 200px; border: rgb(105, 214, 181) solid 3px; position: absolute;}
在上面的代码中,我们使用了 dom.getBoundingClientRect() 来获得 div 元素的宽高和距内容页面左上角的距离,当然也可以使用 dom.offsetTop
、dom.offsetLeft
、dom.offsetHieght
和 dom.offsetWidth
来获取对应值计算。
另外,注意到 JS 代码的最后三行,我们将 mousedown
事件绑定在了 div 元素上,而 mousemove
和 mouseup
事件则绑定在了 document
对象上。
mousedown
事件的绑定就不用说了,因为只有在鼠标点击 div 元素时,我们才能拖拽它;而 mousemove
事件绑定在 document
对象上的原因是为了在鼠标移出视口时,元素仍能进行拖拽(如鼠标移出了视口上方,当左右仍能进行移动),若 mousemove
绑定在了 div 元素上,那么鼠标移开视口时, div 元素就不动了,拖拽体验上就变差了;mouseup
也是同理,鼠标移开视口时松开鼠标的话需要正确的设置状态。
#
HTML 拖拽 API在 HTML 中,默认只有图像、超链接和选择的文本可进行拖拽之外,其他的元素是不能进行拖拽的。
不过 HTML 中也提供了一些 API 来支持设置其他元素的拖放效果。
这边的拖拽与上面原生实现的拖拽不同,对于原生的拖拽 API,拖拽操作除了需要有一个可拖拽(draggable)元素外,还需要有一个可进行放置(droppable)的元素。在拖拽的过程中,能通过一系列的拖拽事件来对拖拽元素进行状态的监听,以便于自定义动作和数据的传输。
一些拖拽事件如下:
事件 | On型事件处理程序 | 触发时刻 |
---|---|---|
drag | ondrag | 当拖拽元素或选中的文本时触发。 |
dragend | ondragend | 当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键). (见结束拖拽) |
dragenter | ondragenter | 当拖拽元素或选中的文本到一个可释放目标时触发(见 指定释放目标)。 |
dragexit | ondragexit | 当元素变得不再是拖拽操作的选中目标时触发。 |
dragleave | ondragleave | 当拖拽元素或选中的文本离开一个可释放目标时触发。 |
dragover | ondragover | 当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次)。 |
dragstart | ondragstart | 当用户开始拖拽一个元素或选中的文本时触发(见开始拖拽操作)。 |
drop | ondrop | 当元素或选中的文本在可释放目标上被释放时触发(见执行释放)。 |
在拖拽过程中,可以使用 DataTransfer
, DataTransferItem
和DataTransferItemList
来传输与拖拽元素绑定的数据。
因此实现一个 HTML 元素的拖拽,一般需要以下几个步骤:
- 给想要拖拽的元素设置
draggable
属性为true
。 - 设置拖拽监听事件来完成自己的需求
- 若有需要,在监听事件中设置拖拽数据
下面是一个简单的拖拽示例:可以将一个图片或是 div 元素拖入蓝框中,或是从本地文件中拖入图片,在拖动的过程中,框线的颜色会随拖动元素的状态而改变

代码如下:
- HTML
- JS
- CSS
<div className="html-drag-container"> <div id="html-drag-box-container"> <div class="html-drag-box" draggable="true"></div> </div> <div id="html-drag-img-container"> <img class="html-drag-img" src="https://gitee.com/yleave/imagehost1/raw/master/img/cat.png"></img> </div> <div class="html-drop-box"></div>
<button id="reset-btn">reset</button></div>
let dragbox = document.getElementsByClassName('html-drag-box')[0];let dropbox = document.getElementsByClassName('html-drop-box')[0];let dragimg = document.getElementsByClassName('html-drag-img')[0];let boxcontainer = document.getElementById('html-drag-box-container');let imgcontainer = document.getElementById('html-drag-img-container');let resetBtn = document.getElementById('reset-btn');
dragbox.addEventListener('dragstart', onDragStart);dragbox.addEventListener('dragend', onDragEnd);
dragimg.addEventListener('dragstart', onDragStart);dragimg.addEventListener('dragend', onDragEnd);
dropbox.addEventListener('dragover', onDragOver);dropbox.addEventListener('dragleave', onDragLeave);dropbox.addEventListener('drop', onDrop);
resetBtn.addEventListener('click', resetElement);
function onDragStart(e) { e.dataTransfer.setData('text/plain', e.target.className); e.target.style.opacity = .7;}
function onDragEnd(e) { e.target.style.opacity = "";}
function onDragOver(e) { // 默认情况下是不允许放置拖拽元素的,因此需要取消默认设置 e.preventDefault(); dropbox.classList.add('html-dropover');}
function onDragLeave(e) { dropbox.classList.remove('html-dropover');}
function onDrop(e) { // 阻止一些默认操作,如拖拽超链接会默认打开页面 e.preventDefault(); resetElement();
dropbox.classList.remove('html-dropover'); dropbox.classList.add('html-dropped');
let isFile = false;
// 若拖拽对象是文件 [...e.dataTransfer.items].forEach((item) => { if (item.kind === "file") { const file = item.getAsFile(); createPreview(file); isFile = true; } });
if (isFile) return; dropbox.appendChild(document.getElementsByClassName(e.dataTransfer.getData('text/plain'))[0]);}
function createPreview(imageFile) { if (!imageFile.type.startsWith("image/")) { return; }
const image = document.createElement("img"); image.src = URL.createObjectURL(imageFile); image.onload = function () { URL.revokeObjectURL(this.src); }; dropbox.appendChild(image);}
function resetElement() { if (dropbox.classList.contains('html-dropped')) { dropbox.classList.remove('html-dropped') } boxcontainer.appendChild(dragbox); imgcontainer.appendChild(dragimg)}
.html-drag-container { height: 70vh; background-color: #eee; display: grid; grid-template-rows: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr); justify-items: center; align-items: center;}
#html-drag-box-container { grid-row: 2 / 3; grid-column: 1/ 2;}
.html-drag-box { width: 10vw; height: 12vh; background-color: tomato; border-radius: 10px; position: relative;}
#html-drag-img-container { grid-row: 2 / 3; grid-column: 3/ 4;}
.html-drag-img { width: 10vw; height: 12vh; border-radius: 10px; position: relative;}
.html-drop-box { grid-row: 2 / 3; grid-column: 2/ 3; border: rgb(20, 179, 241) dashed 4px; border-radius: 10px; width: 14vw; height: 18vh; display: flex; justify-content: center; align-items: center;}
.html-drop-box::before { display: block; content: "请拖放到此区域"; position: absolute; color: rgb(160, 157, 157); font-family: sans-serif; font-size: 2vh;}
.html-dropped { border-color: rgb(17, 241, 28);}
.html-dropover { border-color: rgb(252, 83, 32);}/* 用于限制从本地拖入图片时的大小 */.html-drop-box img { width: 10vw; height: 12vh; position: relative ;}