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),以便我们能够统一管理它们。组件内部可以包含一个头部区域,用于指示可拖拽部分。

Widget 1
Widget 2
Widget 3

注意,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 事件处理

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

Viggle AI
Viggle AI

Viggle AI是一个AI驱动的3D动画生成平台,可以帮助用户创建可控角色的3D动画视频。

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

完整代码示例

将所有JavaScript、HTML和CSS代码整合到一起,即可运行此交互系统。




    
    
    可选择性拖拽与取消选中
    



    
Widget 1
Widget 2
Widget 3

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

552

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

731

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

475

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

656

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

551

2023.09.20

PHP 表单处理与文件上传安全实战
PHP 表单处理与文件上传安全实战

本专题聚焦 PHP 在表单处理与文件上传场景中的实战与安全问题,系统讲解表单数据获取与校验、XSS 与 CSRF 防护、文件类型与大小限制、上传目录安全配置、恶意文件识别以及常见安全漏洞的防范策略。通过贴近真实业务的案例,帮助学习者掌握 安全、规范地处理用户输入与文件上传的完整开发流程。

1

2026.01.13

热门下载

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

精品课程

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

共14课时 | 0.8万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.9万人学习

CSS教程
CSS教程

共754课时 | 18.6万人学习

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

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