拖拽事件
dataset
dataset 是 HTMLElement 接口的只读属性, dataset 提供了对元素上自定义数据属性(data-*)读/写访问。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dataset</title>
</head>
<body>
<div id="user" data-id="123456"></div>
<script>
window.onload = () => {
const userEle = document.getElementById("user");
if (userEle) {
userEle.innerText = `元素的data-id是${userEle.dataset.id}`;
userEle.dataset.id = 7890;
let timeId = setTimeout(() => {
userEle.innerText = `元素的data-id是${userEle.dataset.id}`;
clearTimeout(timeId);
}, 1000);
}
};
</script>
</body>
</html>
前置知识
为便于理解,以下顺序与元素被拖动-放置-修改位置触发事件顺序一致:
draggable 是一种枚举属性,用于标识元素是否允许使用浏览器原生行为或 HTML 拖放操作 API 拖动。
dragstart 事件 在用户开始拖动元素或被选择的文本时调用。
dragover 事件 在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发(该事件在放置目标上触发)。
drop 事件 在元素或文本选择被放置到有效的放置目标上时触发(该事件在放置目标上触发)。为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。
dragend 事件 在拖放操作结束时触发(通过释放鼠标按钮或单击 escape 键,该事件无法取消)。
mousedown 事件 在定点设备(如鼠标或触摸板)按钮在元素内按下时,会在该元素上触发。
mousemove 事件 在定点设备(通常指鼠标)的光标在元素内移动时,会在该元素上触发。
mousedown 事件 在定点设备(如鼠标或触摸板)按钮在元素内释放时,在该元素上触发。
拖动流程
元素结构如下:
<div class="top">
<div id="left-container">
<div class="draggable-item" draggable="true" data-icon="camera"></div>
</div>
<div id="right-container"></div>
</div>
1、元素开始拖动
获取物料区所有的物料元素添加上对应的拖动事件监听
const draggableItems = document.querySelectorAll(".draggable-item");
draggableItems.forEach((item) => {
item.addEventListener("dragstart", (e) => {
// 设置拖拽数据
e.dataTransfer.setData(
"text/plain",
JSON.stringify({
offsetX: e.offsetX,
offsetY: e.offsetY,
icon: e.target.dataset.icon,
})
);
});
});
2、元素拖动中
const dragover = (e) => {
// 阻止默认行为,允许拖放
e.preventDefault();
};
const drop = (e) => {
// 阻止默认行为,允许拖放
e.preventDefault();
// 获取拖拽数据
const data = JSON.parse(e.dataTransfer.getData("text/plain"));
// 创建一个新的元素,将拖拽数据显示在其中
const newItem = document.createElement("div");
newItem.classList.add("dropped-item");
newItem.style.top = e.offsetY - data.offsetY + "px";
newItem.style.left = e.offsetX - data.offsetX + "px";
// 将新元素添加到右侧容器中
rightContainer.appendChild(newItem);
};
// 添加拖动时的事件监听
draggableItems.forEach((item) => {
// 此处代码省略
// 为右侧容器添加拖放事件处理程序
rightContainer.addEventListener("dragover", dragover);
rightContainer.addEventListener("drop", drop);
});
3、元素拖动结束
为提高性能,在拖动结束时应该将监听事件给移除。
draggableItems.forEach((item) => {
// 此处代码省略
// 拖动结束
item.addEventListener("dragend", (e) => {
// 为右侧容器添加拖放事件处理程序
rightContainer.removeEventListener("dragover", dragover);
rightContainer.removeEventListener("drop", drop);
});
});
移动内容区元素
完成以上的代码,会发现元素只能从物料区拖动至内容区域,内容区域的位置不能进行二次移动,这是不符合常理的。以下为内容区域的元素拖动代码:
const drop = (e) => {
// 此处代码省略
// 鼠标按下
newItem.addEventListener("mousedown", (e) => {
e.preventDefault();
e.stopPropagation();
// 鼠标按下时 记录点击的位置
let disx = e.pageX - newItem.offsetLeft;
let disy = e.pageY - newItem.offsetTop;
// 鼠标移动
document.onmousemove = function (e) {
let x = e.pageX - disx;
let y = e.pageY - disy;
let maxX =
parseInt(window.getComputedStyle(rightContainer).width) -
parseInt(window.getComputedStyle(newItem).width);
let maxY =
parseInt(window.getComputedStyle(rightContainer).height) -
parseInt(window.getComputedStyle(newItem).height);
if (x < 0) {
x = 0;
} else if (x >= maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
newItem.style.left = x + "px";
newItem.style.top = y + "px";
};
// 鼠标抬起
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
});
// 将新元素添加到右侧容器中
rightContainer.appendChild(newItem);
};
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>元素拖拽与元素移动</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
display: flex;
flex-direction: column;
}
.top {
flex: 1 0 0;
display: flex;
overflow: hidden;
}
#left-container {
position: relative;
width: 200px;
background-color: #ccc;
padding: 40px;
}
#left-container::after {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
content: "物料区";
font-size: 30px;
writing-mode: vertical-rl;
letter-spacing: 1em;
opacity: 0.6;
z-index: 0;
pointer-events: none;
}
#right-container {
flex: 1 0 0;
position: relative;
overflow: hidden;
}
#right-container::after {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
content: "内容区";
font-size: 30px;
letter-spacing: 1em;
opacity: 0.3;
z-index: 0;
pointer-events: none;
}
.draggable-item {
width: 20px;
height: 20px;
background-color: skyblue;
border-radius: 25%;
z-index: 1;
}
.dropped-item {
position: absolute;
width: 20px;
height: 20px;
background-color: skyblue;
border-radius: 25%;
cursor: move;
}
</style>
</head>
<body>
<div class="top">
<div id="left-container">
<div class="draggable-item" draggable="true" data-icon="camera"></div>
</div>
<div id="right-container"></div>
</div>
<script>
// 获取左右容器和所有可拖拽的物料
const leftContainer = document.getElementById("left-container");
const rightContainer = document.getElementById("right-container");
const draggableItems = document.querySelectorAll(".draggable-item");
const dragover = (e) => {
// 阻止默认行为,允许拖放
e.preventDefault();
};
const drop = (e) => {
// 阻止默认行为,允许拖放
e.preventDefault();
// 获取拖拽数据
const data = JSON.parse(e.dataTransfer.getData("text/plain"));
// 创建一个新的元素,将拖拽数据显示在其中
const newItem = document.createElement("div");
newItem.classList.add("dropped-item");
newItem.style.top = e.offsetY - data.offsetY + "px";
newItem.style.left = e.offsetX - data.offsetX + "px";
newItem.addEventListener("mousedown", (e) => {
e.preventDefault();
e.stopPropagation();
// 鼠标按下时 记录点击的位置
let disx = e.pageX - newItem.offsetLeft;
let disy = e.pageY - newItem.offsetTop;
// 鼠标移动
document.onmousemove = function (e) {
let x = e.pageX - disx;
let y = e.pageY - disy;
let maxX =
parseInt(window.getComputedStyle(rightContainer).width) -
parseInt(window.getComputedStyle(newItem).width);
let maxY =
parseInt(window.getComputedStyle(rightContainer).height) -
parseInt(window.getComputedStyle(newItem).height);
if (x < 0) {
x = 0;
} else if (x >= maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
newItem.style.left = x + "px";
newItem.style.top = y + "px";
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
});
// 将新元素添加到右侧容器中
rightContainer.appendChild(newItem);
};
// 为可拖拽的物料添加拖放事件处理程序
draggableItems.forEach((item) => {
item.addEventListener("dragstart", (e) => {
// 设置拖拽数据
e.dataTransfer.setData(
"text/plain",
JSON.stringify({
offsetX: e.offsetX,
offsetY: e.offsetY,
icon: e.target.dataset.icon,
})
);
// 为右侧容器添加拖放事件处理程序
rightContainer.addEventListener("dragover", dragover);
rightContainer.addEventListener("drop", drop);
});
item.addEventListener("dragend", (e) => {
// 为右侧容器添加拖放事件处理程序
rightContainer.removeEventListener("dragover", dragover);
rightContainer.removeEventListener("drop", drop);
});
});
</script>
</body>
</html>