0

0

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

心靈之曲

心靈之曲

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

|

369人浏览过

|

来源于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事件监听器来处理所有逻辑。

初始化变量

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

mousedown 事件处理

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

Sora
Sora

Sora是OpenAI发布的一种文生视频AI大模型,可以根据文本指令创建现实和富有想象力的场景。

下载
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工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

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

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

4336

2024.08.14

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

38

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

83

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

97

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

223

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

458

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

169

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

246

2026.03.03

热门下载

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

精品课程

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

共14课时 | 0.9万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

CSS教程
CSS教程

共754课时 | 42.4万人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号