
本教程详细介绍了如何构建一个交互式ui系统,实现对多个组件(widgets)的选择、区域选择和拖拽功能。核心在于优化`mousedown`事件处理逻辑,确保当用户点击或拖拽一个未选中的组件时,所有已选中的组件自动取消选中;而当点击或拖拽一个已选中的组件时,则允许所有当前选中的组件一同被拖拽,从而提供直观的用户体验。
在现代Web应用中,实现类似桌面操作系统的多选和拖拽功能是提升用户体验的关键。本教程将指导您如何使用纯J*aScript、HTML和CSS构建一个能够支持以下行为的组件选择系统:
实现上述功能主要依赖于以下J*aScript事件和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;
}J*aScript是实现交互的核心。我们将主要通过一个统一的mousedown事件监听器来处理所有逻辑。
let isSelecting = false; // 标记是否正在进行区域选择
let selectionStartX, selectionStartY, selectionEndX, selectionEndY; // 选择框的起始和结束坐标
let selectionRectangle; // 选择框DOM元素
let draggedElements = []; // 存储当前被拖拽的元素(可能是一个或多个)
const widgets = document.querySelectorAll('.widgets'); // 获取所有组件这是整个系统的关键。它需要判断用户点击的是否为组件,以及该组件是否已选中。
度加剪辑
度加剪辑(原度咔剪辑),百度旗下AI创作工具
380
查看详情
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
将所有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以上就是实现可选择性拖拽与取消选中功能的教程的详细内容,更多请关注其它相关文章!
# javascript
# 药品创意营销推广策略
# 奇奇seo软件
# 遍历
# 其子
# 多个
# 移除
# 单击
# 如何使用
# 的是
# 按下
# 鼠标
# overflow
# css
# java
# html
# 操作系统
# app
# 事件冒泡
# ssl
# ai
# css样式
# 拖拽
# 吉安网站建设哪家可靠
# 吉林网络关键词排名
# 徐州平台网站建设
# 武汉seo优惠霸屏
# SEO人工优化优势
# 谷歌建设网站官网
# 赞皇智能网站建设报价
# 上海短视频seo托管
相关栏目:
【
Google疑问12 】
【
Facebook疑问10 】
【
优化推广96088 】
【
技术知识133117 】
【
IDC资讯59369 】
【
网络运营7196 】
【
IT资讯61894 】
相关推荐:
WooCommerce购物车:强制显示所有交叉销售商品教程
暴风影音官网正式版_暴风影音手机版官网下载安卓
济南公交卡手机充值指南
Win11便笺在哪打开 Win11桌面便笺(Sticky Notes)使用方法【详解】
Python中处理嵌套字典与列表的数据提取与过滤教程
汽车之家网页版免费登录_汽车之家官网首页直接进入
c++中的const关键字用法大全_c++ const正确使用指南
t3出行如何使用微信支付
Excel宏怎么删除_Excel中删除宏的详细操作流程
房产|直播|视频号怎么认证开通?|直播|需要什么资质?
解决jQuery多计算器输入字段冲突的教程
《全民k歌》网页版最新登录入口一览
POKI小游戏在线免费入口链接 POKI小游戏无下载秒玩玩
oppo手机如何通过下拉通知栏截图_oppo手机通知栏快捷截图方法
iPhone16Plus参数配置如何调整声音_iPhone16Plus参数配置声音调整详细方法
圆通快递官网入口查询单号 手机版官方查询入口
苹果自助维修计划支持哪些设备机型
悟空浏览器网页版在线工具 悟空浏览器网页版在线平台入口
菜鸟驿站的取件码忘了怎么办 手机快速查询指南
qq音乐官方网站入口_qq音乐在线听歌网页版链接
《微信》视频号原创声明开启方法
电子白板帮助菜单使用指南
《大学搜题酱》官网地址登录
告别繁琐SEO!如何使用SyliusSitemap插件自动化生成网站地图,提升搜索引擎排名
荣耀magicv5怎么上手测评
Highcharts雷达图轴线交点数值标注指南
多多买菜门店端app订单查看方法
创建您的便携版VS Code:让配置随身携带
深入理解Python对象引用与链表属性赋值
英雄联盟争者留名活动介绍
Win10显卡驱动安装失败怎么办 Win10使用DDU彻底卸载驱动【解决】
win11怎么启用或禁用休眠 Win11 powercfg命令管理休眠文件【技巧】
为什么XML解析器对大小写敏感? 理解XML规范中的大小写规则与最佳实践
word文档行距怎么调?word文档调行距的操作步骤
基于 Flink 和 Kafka 实现高效流处理:连续查询与时间窗口
宝妈做视频号该写什么标签话题?宝妈关注的话题有哪些?
word表格如何按某一列内容进行排序_Word表格按列排序方法
自定义你的VS Code状态栏,监控关键信息
广州地铁app准妈咪徽章领取方法
Golang中的rune与byte类型区别是什么_Golang字符与字节处理详解
苹果电脑如何快速截图并编辑 苹果电脑截屏标注快捷操作
厨房地面防滑垫的油污怎么洗? 机洗和手洗防滑垫的注意事项
《优志愿》修改手机号方法
b站如何剪辑视频_b站必剪app使用教程
sublime怎么在文件中显示代码结构大纲_sublime符号列表功能
使用 .htaccess 正确配置 WordPress 子目录重定向与路径保留
大众点评了却看不到是怎么回事
c++如何实现一个简单的RPC框架_c++远程过程调用原理与实践
《磁力猫》最好用的磁官网
51漫画网实时入口 51漫画网页版官方免费漫画入口
2025-11-21
运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。