首页 > web前端 > js教程 > 正文

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

心靈之曲
发布: 2025-11-21 10:59:22
原创
335人浏览过

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

本教程详细介绍了如何构建一个交互式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 事件处理

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

DeepBrain
DeepBrain

AI视频生成工具,ChatGPT +生成式视频AI =你可以制作伟大的视频!

DeepBrain 94
查看详情 DeepBrain
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
登录后复制

以上就是实现可选择性拖拽与取消选中功能的教程的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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