讲师中心 微信公众号
AI工具推荐 视频效率加速

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

阿丽姑娘_5726

阿丽姑娘_5726

发布时间:2025-11-21 10:59:22

|

429人浏览过

|

来源于php中文网

原创

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

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

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

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

核心概念

实现上述功能主要依赖于以下JavaScript事件和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;
}

JavaScript 逻辑

JavaScript是实现交互的核心。我们将主要通过一个统一的mousedown事件监听器来处理所有逻辑。

JavaStoryDevelop
JavaStoryDevelop

你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在

下载

初始化变量

let isSelecting = false; // 标记是否正在进行区域选择
let selectionStartX, selectionStartY, selectionEndX, selectionEndY; // 选择框的起始和结束坐标
let selectionRectangle; // 选择框DOM元素
let draggedElements = []; // 存储当前被拖拽的元素(可能是一个或多个)
const widgets = document.querySelectorAll('.widgets'); // 获取所有组件

mousedown 事件处理

这是整个系统的关键。它需要判断用户点击的是否为组件,以及该组件是否已选中。

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 < 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

热门AI工具

更多
WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

UP简历
UP简历 Hot

一款AI办公效率工具,主要用于基于AI技术的免费在线简历制作工具,适合需要提升相关任务效率的用户。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

Lovart
Lovart Hot

一款面向视觉设计创作的AI设计平台,可通过智能体和画布工作流辅助制作海报、Logo、网页、PPT及其他视觉内容。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

相关专题

更多
DOM是什么意思
DOM是什么意思

dom的英文全称是documentobjectmodel,表示文件对象模型,是w3c组织推荐的处理可扩展置标语言的标准编程接口;dom是html文档的内存中对象表示,它提供了使用javascript与网页交互的方式。想了解更多的相关内容,可以阅读本专题下面的文章。

6521

2024.08.14

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

40

2026.10.22

Gradle Wrapper配置教程
Gradle Wrapper配置教程

本专题介绍Gradle Wrapper的基本作用,涵盖文件组成、生成方法、版本配置和任务调用,帮助你理解项目如何使用指定的Gradle版本,减少不同开发环境带来的构建差异。

20

2026.10.22

Golangci-lint检查器使用教程
Golangci-lint检查器使用教程

本专题整理Golangci-lint常用Linters的用途与配置方法,覆盖错误处理、无效代码、安全、复杂度、命名和依赖检查,帮助项目合理选规则。

20

2026.10.22

Gradle怎么构建Java项目
Gradle怎么构建Java项目

本专题介绍Gradle Java项目构建方法,涵盖Java插件配置、依赖声明、源码编译、测试执行和打包检查,帮助你理解各阶段的任务关系,逐步完成项目构建。

0

2026.10.22

PHP时间戳漏洞修复教程
PHP时间戳漏洞修复教程

本专题汇总PHP常见时间戳漏洞,包括2038年溢出、时区漂移、时间戳伪造、重放攻击、时间盲注与日志审计失效等,并给出升级64位环境、使用 DateTimeImmutable、统一UTC、严格超时校验、预处理绑定、hash_equals 比对等修复方案,帮助开发者快速排查和加固时间相关代码。

20

2026.10.22

Golangci-lint配置文件与规则设置
Golangci-lint配置文件与规则设置

本专题聚焦Golangci-lint YAML配置文件,讲清运行参数、规则启停、目录排除、问题过滤及严重级别设置,便于团队统一检查标准。

20

2026.10.22

Golangci-lint安装与入门教程
Golangci-lint安装与入门教程

本专题讲解Golangci-lint的安装、初始化、基础命令和首次代码检查,覆盖本地环境、Docker及多平台用法,帮助Go开发者快速上手。

20

2026.10.22

PHP2Shell高危漏洞修复教程
PHP2Shell高危漏洞修复教程

PHP2Shell漏洞是什么,哪些PHP版本和运行环境会受到影响,服务器应该如何检测与修复?本专题提供版本自查脚本、Linux与Docker升级命令、PHP-FPM和Apache排查步骤,以及修复验证、临时缓解和应急响应方案。

140

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Sass 教程
Sass 教程

共14课时 | 1.5万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 6.1万人学习

CSS教程
CSS教程

共754课时 | 98.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn