
本教程详细介绍了如何构建一个交互式ui系统,实现对多个组件(widgets)的选择、区域选择和拖拽功能。核心在于优化`mousedown`事件处理逻辑,确保当用户点击或拖拽一个未选中的组件时,所有已选中的组件自动取消选中;而当点击或拖拽一个已选中的组件时,则允许所有当前选中的组件一同被拖拽,从而提供直观的用户体验。
在现代Web应用中,实现类似桌面操作系统的多选和拖拽功能是提升用户体验的关键。本教程将指导您如何使用纯JavaScript、HTML和CSS构建一个能够支持以下行为的组件选择系统:
实现上述功能主要依赖于以下JavaScript事件和DOM操作:
首先,定义我们的可拖拽组件。每个组件都应具有一个共同的类名(例如widgets),以便我们能够统一管理它们。组件内部可以包含一个头部区域,用于指示可拖拽部分。
<div id="widget1" class="widgets" style="left: 50px; top: 50px;"> <div id="widget1header" class="widgets">Widget 1</div> </div> <div id="widget2" class="widgets" style="left: 150px; top: 150px;"> <div id="widget2header" class="widgets">Widget 2</div> </div> <div id="widget3" class="widgets" style="left: 250px; top: 250px;"> <div id="widget3header" class="widgets">Widget 3</div> </div>
注意,widget1header等内部元素也带有widgets类,这有助于在事件冒泡时正确识别点击目标。
为了视觉上区分选中状态和拖拽区域,我们需要定义一些CSS样式。selected类将为选中的组件添加边框,selection-rectangle用于绘制区域选择框。
#selection-rectangle {
position: absolute;
border: 2px dashed blue;
pointer-events: none; /* 确保选择框不阻碍鼠标事件 */
display: none;
z-index: 9999999;
}
.widgets.selected {
outline-color: blue;
outline-width: 2px;
outline-style: dashed;
}
/* 基础widget样式 */
.widgets {
position: absolute;
z-index: 9;
background-color: #ff0000;
color: white;
font-size: 25px;
font-family: Arial, Helvetica, sans-serif;
border: 2px solid #212128;
text-align: center;
width: 100px;
height: 100px;
box-sizing: border-box; /* 确保padding和border不增加额外尺寸 */
}
/* widget头部样式 */
.widgets > div { /* 针对内部header div */
padding: 10px;
cursor: move;
z-index: 10;
background-color: #040c14;
outline-color: rgb(0, 0, 0);
outline-width: 2px;
outline-style: solid;
height: 100%; /* 确保header占据整个widget高度 */
display: flex; /* 使文本居中 */
align-items: center;
justify-content: center;
}JavaScript是实现交互的核心。我们将主要通过一个统一的mousedown事件监听器来处理所有逻辑。
let isSelecting = false; // 标记是否正在进行区域选择
let selectionStartX, selectionStartY, selectionEndX, selectionEndY; // 选择框的起始和结束坐标
let selectionRectangle; // 选择框DOM元素
let draggedElements = []; // 存储当前被拖拽的元素(可能是一个或多个)
const widgets = document.querySelectorAll('.widgets'); // 获取所有组件这是整个系统的关键。它需要判断用户点击的是否为组件,以及该组件是否已选中。
document.addEventListener('mousedown', (event) => {
// 1. 判断点击目标是否是组件
if (event.target.classList.contains('widgets')) {
// 获取所有当前选中的组件
draggedElements = Array.from(widgets).filter((widget) => widget.classList.contains('selected'));
// 判断点击的目标是否是已选中的组件,或者其父级是已选中的组件
// event.target.matches('.selected') 检查目标本身
// event.target.closest('.selected') 检查目标或其祖先是否是已选中的组件
const draggingSelected = event.target.matches('.selected') || event.target.closest('.selected');
// 如果点击的目标是已选中的组件(或其子元素)
if (draggingSelected) {
// 遍历所有已选中的组件,并为它们添加拖拽逻辑
draggedElements.forEach((widget) => {
const shiftX = event.clientX - widget.getBoundingClientRect().left;
const shiftY = event.clientY - widget.getBoundingClientRect().top;
function moveElement(moveEvent) {
const x = moveEvent.clientX - shiftX;
const y = moveEvent.clientY - shiftY;
widget.style.left = x + 'px';
widget.style.top = y + 'px';
}
function stopMoving() {
document.removeEventListener('mousemove', moveElement);
document.removeEventListener('mouseup', stopMoving);
}
document.addEventListener('mousemove', moveElement);
document.addEventListener('mouseup', stopMoving);
});
} else {
// 如果点击的目标是未选中的组件
// 首先,取消所有组件的选中状态
widgets.forEach((widget) => {
widget.classList.remove('selected');
});
// 然后,将当前点击的组件设为选中状态
// 这里需要确保event.target是实际的widget元素,而不是其header子元素
const targetWidget = event.target.closest('.widgets');
if (targetWidget) {
targetWidget.classList.add('selected');
// 同时,将当前点击的组件添加到draggedElements中,以便后续拖拽
draggedElements = [targetWidget];
// 为当前点击的(现在已选中)组件添加拖拽逻辑
const shiftX = event.clientX - targetWidget.getBoundingClientRect().left;
const shiftY = event.clientY - targetWidget.getBoundingClientRect().top;
function moveElement(moveEvent) {
const x = moveEvent.clientX - shiftX;
const y = moveEvent.clientY - shiftY;
targetWidget.style.left = x + 'px';
targetWidget.style.top = y + 'px';
}
function stopMoving() {
document.removeEventListener('mousemove', moveElement);
document.removeEventListener('mouseup', stopMoving);
}
document.addEventListener('mousemove', moveElement);
document.addEventListener('mouseup', stopMoving);
}
}
return; // 阻止后续的区域选择逻辑
}
// 2. 如果点击目标不是组件,且不是选择框本身,则开始区域选择
if (!event.target.classList.contains('widgets') && event.target.id !== 'selection-rectangle') {
isSelecting = true;
selectionStartX = event.clientX;
selectionStartY = event.clientY;
selectionRectangle = document.createElement('div');
selectionRectangle.id = 'selection-rectangle';
selectionRectangle.style.position = 'absolute';
selectionRectangle.style.border = '2px dashed blue';
selectionRectangle.style.pointerEvents = 'none';
selectionRectangle.style.display = 'none';
document.body.appendChild(selectionRectangle);
// 在开始新的区域选择前,取消所有当前选中状态
widgets.forEach((widget) => {
widget.classList.remove('selected');
});
}
});逻辑解析:
当鼠标按下并在非组件区域移动时,更新选择框的大小和位置,并根据选择框与组件的交集来更新组件的选中状态。
document.addEventListener('mousemove', (event) => {
if (isSelecting) {
selectionEndX = event.clientX;
selectionEndY = event.clientY;
let width = Math.abs(selectionEndX - selectionStartX);
let height = Math.abs(selectionEndY - selectionStartY);
selectionRectangle.style.width = width + 'px';
selectionRectangle.style.height = height + 'px';
selectionRectangle.style.left = Math.min(selectionEndX, selectionStartX) + 'px';
selectionRectangle.style.top = Math.min(selectionEndY, selectionStartY) + 'px';
selectionRectangle.style.display = 'block';
widgets.forEach((widget) => {
const widgetRect = widget.getBoundingClientRect();
const isIntersecting = isRectangleIntersecting(widgetRect, {
x: Math.min(selectionStartX, selectionEndX),
y: Math.min(selectionStartY, selectionEndY),
width,
height,
});
if (isIntersecting) {
widget.classList.add('selected');
} else {
widget.classList.remove('selected');
}
});
}
});鼠标释放时,结束区域选择并移除选择框。
document.addEventListener('mouseup', () => {
if (isSelecting) {
isSelecting = false;
if (selectionRectangle) {
selectionRectangle.remove();
selectionRectangle = null; // 清除引用
}
}
});function isRectangleIntersecting(rect1, rect2) {
return (
rect1.left < rect2.x + rect2.width &&
rect1.right > rect2.x &&
rect1.top < rect2.y + rect2.height &&
rect1.bottom > rect2.y
);
}注意: 原始代码中的isRectangleIntersecting函数判断条件有误,rect1.left >= rect2.x等应改为rect1.left < rect2.x + rect2.width等,以正确判断两个矩形是否重叠。上述代码已修正。
将所有JavaScript、HTML和CSS代码整合到一起,即可运行此交互系统。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>可选择性拖拽与取消选中</title>
<style>
body {
margin: 0;
overflow: hidden; /* 防止滚动条出现 */
font-family: Arial, sans-serif;
user-select: none; /* 防止文本被选中 */
}
#selection-rectangle {
position: absolute;
border: 2px dashed blue;
pointer-events: none;
display: none;
z-index: 9999999;
}
.widgets.selected {
outline-color: blue;
outline-width: 2px;
outline-style: dashed;
}
.widgets {
position: absolute;
z-index: 9;
background-color: #ff0000;
color: white;
font-size: 25px;
font-family: Arial, Helvetica, sans-serif;
border: 2px solid #212128;
text-align: center;
width: 100px;
height: 100px;
box-sizing: border-box;
}
.widgets > div { /* 针对内部header div */
padding: 10px;
cursor: move;
z-index: 10;
background-color: #040c14;
outline-color: rgb(0, 0, 0);
outline-width: 2px;
outline-style: solid;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div id="widget1" class="widgets" style="left: 50px; top: 50px;">
<div id="widget1header" class="widgets">Widget 1</div>
</div>
<div id="widget2" class="widgets" style="left: 150px; top: 150px;">
<div id="widget2header" class="widgets">Widget 2</div>
</div>
<div id="widget3" class="widgets" style="left: 250px; top: 250px;">
<div id="widget3header" class="widgets">Widget 3</div>
</div>
<script>
let isSelecting = false;
let selectionStartX, selectionStartY, selectionEndX, selectionEndY;
let selectionRectangle;
let draggedElements = [];
const widgets = document.querySelectorAll('.widgets');
document.addEventListener('mousedown', (event) => {
// 阻止默认的文本选择行为
event.preventDefault();
if (event.target.classList.contains('widgets')) {
// 找到实际的 widget 元素(可能是点击了 header 子元素)
const clickedWidget = event.target.closest('.widgets');
if (!clickedWidget) return; // 如果没有找到有效的 widget,则退出
draggedElements = Array.from(widgets).filter((widget) => widget.classList.contains('selected'));
// 判断点击的目标是否是已选中的组件(或其子元素)
const draggingSelected = clickedWidget.classList.contains('selected');
if (draggingSelected) {
// 如果点击的是已选中的组件,则拖拽所有选中的组件
draggedElements.forEach((widget) => {
const shiftX = event.clientX - widget.getBoundingClientRect().left;
const shiftY = event.clientY - widget.getBoundingClientRect().top;
function moveElement(moveEvent) {
const x = moveEvent.clientX - shiftX;
const y = moveEvent.clientY - shiftY;
widget.style.left = x + 'px';
widget.style.top = y + 'px';
}
function stopMoving() {
document.removeEventListener('mousemove', moveElement);
document.removeEventListener('mouseup', stopMoving);
}
document.addEventListener('mousemove', moveElement);
document.addEventListener('mouseup', stopMoving);
});
} else {
// 如果点击的是未选中的组件
// 1. 取消所有组件的选中状态
widgets.forEach((widget) => {
widget.classList.remove('selected');
});
// 2. 将当前点击的组件设为选中状态
clickedWidget.classList.add('selected');
// 3. 开始拖拽当前点击的组件
const shiftX = event.clientX - clickedWidget.getBoundingClientRect().left;
const shiftY = event.clientY - clickedWidget.getBoundingClientRect().top;
function moveElement(moveEvent) {
const x = moveEvent.clientX - shiftX;
const y = moveEvent.clientY - shiftY;
clickedWidget.style.left = x + 'px';
clickedWidget.style.top = y + 'px';
}
function stopMoving() {
document.removeEventListener('mousemove', moveElement);
document.removeEventListener('mouseup', stopMoving);
}
document.addEventListener('mousemove', moveElement);
document.addEventListener('mouseup', stopMoving);
}
return; // 阻止后续的区域选择逻辑
}
// 如果点击目标不是组件,且不是选择框本身,则开始区域选择
if (!event.target.classList.contains('widgets') && event.target.id !== 'selection-rectangle') {
isSelecting = true;
selectionStartX = event.clientX;
selectionStartY = event.clientY;
selectionRectangle = document.createElement('div');
selectionRectangle.id = 'selection-rectangle';
selectionRectangle.style.position = 'absolute';
selectionRectangle.style.border = '2px dashed blue';
selectionRectangle.style.pointerEvents = 'none';
selectionRectangle.style.display = 'none';
document.body.appendChild(selectionRectangle);
// 在开始新的区域选择前,取消所有当前选中状态
widgets.forEach((widget) => {
widget.classList.remove('selected');
});
}
});
document.addEventListener('mousemove', (event) => {
if (isSelecting) {
selectionEndX = event.clientX;
selectionEndY = event.clientY;
let width = Math.abs(selectionEndX - selectionStartX);
let height = Math.abs(selectionEndY - selectionStartY);
selectionRectangle.style.width = width + 'px';
selectionRectangle.style.height = height + 'px';
selectionRectangle.style.left = Math以上就是实现可选择性拖拽与取消选中功能的教程的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号