新闻中心

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

2025-11-21
浏览次数:
返回列表

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

本教程详细介绍了如何构建一个交互式ui系统,实现对多个组件(widgets)的选择、区域选择和拖拽功能。核心在于优化`mousedown`事件处理逻辑,确保当用户点击或拖拽一个未选中的组件时,所有已选中的组件自动取消选中;而当点击或拖拽一个已选中的组件时,则允许所有当前选中的组件一同被拖拽,从而提供直观的用户体验。

在现代Web应用中,实现类似桌面操作系统的多选和拖拽功能是提升用户体验的关键。本教程将指导您如何使用纯J*aScript、HTML和CSS构建一个能够支持以下行为的组件选择系统:

  1. 单击未选中组件时: 取消所有当前选中组件的选中状态,并开始拖拽当前单击的组件。
  2. 单击已选中组件时: 保持所有选中组件的选中状态,并开始拖拽所有已选中的组件。
  3. 在空白区域拖拽时: 创建一个选择框,通过框选来选择或取消选择组件。

核心概念

实现上述功能主要依赖于以下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 Waifulabs

一键生成动漫二次元头像和插图

Waifulabs 317 查看详情 Waifulabs
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');
    });
  }
});

逻辑解析:

  • 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种神仙用法【技巧】 

搜索