新闻中心
实现可选择性拖拽与取消选中功能的教程

本教程详细介绍了如何构建一个交互式ui系统,实现对多个组件(widgets)的选择、区域选择和拖拽功能。核心在于优化`mousedown`事件处理逻辑,确保当用户点击或拖拽一个未选中的组件时,所有已选中的组件自动取消选中;而当点击或拖拽一个已选中的组件时,则允许所有当前选中的组件一同被拖拽,从而提供直观的用户体验。
在现代Web应用中,实现类似桌面操作系统的多选和拖拽功能是提升用户体验的关键。本教程将指导您如何使用纯J*aScript、HTML和CSS构建一个能够支持以下行为的组件选择系统:
- 单击未选中组件时: 取消所有当前选中组件的选中状态,并开始拖拽当前单击的组件。
- 单击已选中组件时: 保持所有选中组件的选中状态,并开始拖拽所有已选中的组件。
- 在空白区域拖拽时: 创建一个选择框,通过框选来选择或取消选择组件。
核心概念
实现上述功能主要依赖于以下J*aScript事件和DOM操作:
- mousedown事件: 监测鼠标按下动作,判断是开始拖拽、开始区域选择,还是取消选中。
- mousemove事件: 在鼠标按下并移动时,用于更新组件位置(拖拽)或更新选择框大小(区域选择)。
- mouseup事件: 在鼠标释放时,结束拖拽或区域选择操作。
- classList.add() / classList.remove(): 用于动态添加或移除表示选中状态的CSS类。
- getBoundingClientRect(): 获取元素在视口中的大小和位置,用于判断区域选择框与组件的交集。
HTML 结构
首先,定义我们的可拖拽组件。每个组件都应具有一个共同的类名(例如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 样式
为了视觉上区分选中状态和拖拽区域,我们需要定义一些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;
}J*aScript 逻辑
J*aScript是实现交互的核心。我们将主要通过一个统一的mousedown事件监听器来处理所有逻辑。
初始化变量
let isSelecting = false; // 标记是否正在进行区域选择
let selectionStartX, selectionStartY, selectionEndX, selectionEndY; // 选择框的起始和结束坐标
let selectionRectangle; // 选择框DOM元素
let draggedElements = []; // 存储当前被拖拽的元素(可能是一个或多个)
const widgets = document.querySelectorAll('.widgets'); // 获取所有组件mousedown 事件处理
这是整个系统的关键。它需要判断用户点击的是否为组件,以及该组件是否已选中。
Waifulabs
一键生成动漫二次元头像和插图
317
查看详情
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 = mov
eEvent.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');
});
}
});逻辑解析:
- event.target.classList.contains('widgets'): 检查鼠标按下的元素是否为组件(或其子元素,因为子元素也带有widgets类)。
-
draggingSelected: 这是一个关键的布尔值,用于判断用户是否在拖拽一个已经选中的组件。
- 如果draggingSelected为真,表示用户想要拖拽所有已选中的组件,因此遍历draggedElements(所有已选中的组件)并为它们绑定mousemove和mouseup事件,实现多组件同步拖拽。
- 如果draggingSelected为假(即点击了一个未选中的组件),则先移除所有组件的selected类,然后将当前点击的组件标记为选中,并只拖拽这一个组件。
- return;: 在处理完组件的拖拽逻辑后,立即返回,防止执行后续的区域选择逻辑。
- 空白区域点击: 如果点击的既不是组件也不是选择框,则初始化区域选择。同时,为了确保清晰的交互,在开始新的区域选择时,会清除所有旧的选中状态。
mousemove 事件处理(区域选择)
当鼠标按下并在非组件区域移动时,更新选择框的大小和位置,并根据选择框与组件的交集来更新组件的选中状态。
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');
}
});
}
});mouseup 事件处理
鼠标释放时,结束区域选择并移除选择框。
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
完整代码示例
将所有J*aScript、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以上就是实现可选择性拖拽与取消选中功能的教程的详细内容,更多请关注其它相关文章!
# 单击
# 洛阳优化网站哪个好
# 消费贷推广与营销的关系
# 薛城推广网站建设
# 俄罗斯市场推广网站官网
# 京山县建设网站建设代理
# 昆明神马网站推广
# 沈阳抖音seo招商公司
# 青岛茶叶网站建设
# 奶粉的营销推广策略
# 素材网站市场推广
# 设为
# 遍历
# 其子
# 多个
# 移除
# css
# 的是
# 按下
# 鼠标
# 拖拽
# overflow
# css样式
# ai
# ssl
# 事件冒泡
# app
# 操作系统
# html
# java
# javascript
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
Steam官网入口直达 Steam注册及登录步骤
win11如何卸载Windows更新补丁 Win11解决更新导致系统不稳定的问题【修复】
印象笔记如何设离线包出差查阅_印象笔记设离线包出差查阅【离线阅读】
AO3网页版合集入口 Archive of Our Own同人作品浏览指南
Lar*el 递归关系中排除指定分支的教程
Safari怎么安装扩展程序 浏览器插件安装与管理方法【详解】
Go语言中JSON数据解码与字段访问指南
C++指针和引用有什么区别_C++内存管理核心概念深度解析
高德地图家和公司地址在哪设置 高德地图通勤路线设置方法【超详细】
c++如何使用Meson构建系统_c++比CMake更快的构建工具
Python Socket多播通信中指定源IP地址的实践指南
在Go Martini框架中高效服务动态生成图像的实践指南
J*aScript中安全有效地处理localStorage字符串数据
C++如何实现一个装饰器模式_C++设计模式之动态地给对象添加额外职责
Django表单提交验证失败后保持字段值不刷新
J*aScript井字棋(Tic-Tac-Toe)核心交互逻辑实现教程
天眼查企业查询官网入口 天眼查官方网页版查询
深入理解Go语言中Map值与方法接收器的交互:为什么需要临时变量
Sublime Text怎么设置垂直标尺_Sublime配置Rulers规范代码长度
Yandex官网免登录入口_俄罗斯Yandex搜索引擎一键访问
在Go语言中利用后缀数组处理多字符串:实现高效文本匹配与自动补全
C++如何实现异步操作_C++11使用std::future和std::async进行异步编程
Go语言中高效处理x-www-form-urlencoded表单数据
Lar*el 8 多关键词数据库搜索优化实践
c++如何使用Catch2编写单元测试_c++简洁易用的BDD风格测试框架
如何在复杂的电商平台中优雅地管理共享资源并确保正确重定向,使用spryker-shop/resource-share-page模块助你一臂之力
如何仅使用CSS更改登录界面背景图像图标的颜色
在命令行怎么运行html项目_命令行运行html项目方法【教程】
c++中的std::basic_string的SSO优化_c++短字符串优化深度解析
谷歌浏览器如何快速清除某个网站的数据_Chrome网站缓存清理方法
AWS EC2实例间SQL Server连接超时:安全组配置与故障排除指南
PyTorch模型训练准确率不提升:诊断与修复常见指标计算错误
解决Django多数据库/多Schema环境下外键迁移问题
sublime怎么设置启动时打开的窗口_sublime会话管理与热退出
如何使用CaptainHook和Composer管理Git钩子_在提交前自动运行代码检查的Composer配置
C++20的source_location是什么_C++在编译期获取源码位置信息用于日志和断言
Python类型检查:优化关联可选属性的Mypy推断策略
sublime怎么格式化代码_sublime代码美化与一键排版插件配置
手机屏幕碎了但能正常使用怎么办 手机外屏碎裂的修复建议
Win10系统怎么查看已安装更新_Win10卸载有问题的更新补丁
CSS自定义字体样式被系统字体替换怎么办_font-face方式指定font-display控制渲染策略
在J*a中如何使用Exception包装底层异常_异常包装与信息传递方法说明
圆通快递查询实时追踪 圆通物流包裹状态快速查看
邮政快递包裹最新位置 邮政快递实时追踪入口
C#中解析不规范的HTML为XML 常见的坑与解决办法
C++如何操作大型数据集_使用C++流式处理(Streaming)技术避免一次性加载大文件
Safari浏览器输入栏卡顿如何解决 Safari搜索建议与缓存清理
Promise错误处理:在catch后终止链式then执行的策略
离线运行Go语言之旅:本地部署与GOPATH配置指南
Windows电脑怎么截图最方便_系统自带截图工具的5种神仙用法【技巧】


2025-11-21
浏览次数:次
返回列表
eEvent.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');
});
}
});