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


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

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

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

度加剪辑 度加剪辑

度加剪辑(原度咔剪辑),百度旗下AI创作工具

度加剪辑 380 查看详情 度加剪辑
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

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


# javascript  # 药品创意营销推广策略  # 奇奇seo软件  # 遍历  # 其子  # 多个  # 移除  # 单击  # 如何使用  # 的是  # 按下  # 鼠标  # overflow  # css  # java  # html  # 操作系统  # app  # 事件冒泡  # ssl  # ai  # css样式  # 拖拽  # 吉安网站建设哪家可靠  # 吉林网络关键词排名  # 徐州平台网站建设  # 武汉seo优惠霸屏  # SEO人工优化优势  # 谷歌建设网站官网  # 赞皇智能网站建设报价  # 上海短视频seo托管 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894 】


相关推荐: WooCommerce购物车:强制显示所有交叉销售商品教程  暴风影音官网正式版_暴风影音手机版官网下载安卓  济南公交卡手机充值指南  Win11便笺在哪打开 Win11桌面便笺(Sticky Notes)使用方法【详解】  Python中处理嵌套字典与列表的数据提取与过滤教程  汽车之家网页版免费登录_汽车之家官网首页直接进入  c++中的const关键字用法大全_c++ const正确使用指南  t3出行如何使用微信支付  Excel宏怎么删除_Excel中删除宏的详细操作流程  房产|直播|视频号怎么认证开通?|直播|需要什么资质?  解决jQuery多计算器输入字段冲突的教程  《全民k歌》网页版最新登录入口一览  POKI小游戏在线免费入口链接 POKI小游戏无下载秒玩玩  oppo手机如何通过下拉通知栏截图_oppo手机通知栏快捷截图方法  iPhone16Plus参数配置如何调整声音_iPhone16Plus参数配置声音调整详细方法  圆通快递官网入口查询单号 手机版官方查询入口  苹果自助维修计划支持哪些设备机型  悟空浏览器网页版在线工具 悟空浏览器网页版在线平台入口  菜鸟驿站的取件码忘了怎么办 手机快速查询指南  qq音乐官方网站入口_qq音乐在线听歌网页版链接  《微信》视频号原创声明开启方法  电子白板帮助菜单使用指南  《大学搜题酱》官网地址登录  告别繁琐SEO!如何使用SyliusSitemap插件自动化生成网站地图,提升搜索引擎排名  荣耀magicv5怎么上手测评  Highcharts雷达图轴线交点数值标注指南  多多买菜门店端app订单查看方法  创建您的便携版VS Code:让配置随身携带  深入理解Python对象引用与链表属性赋值  英雄联盟争者留名活动介绍  Win10显卡驱动安装失败怎么办 Win10使用DDU彻底卸载驱动【解决】  win11怎么启用或禁用休眠 Win11 powercfg命令管理休眠文件【技巧】  为什么XML解析器对大小写敏感? 理解XML规范中的大小写规则与最佳实践  word文档行距怎么调?word文档调行距的操作步骤  基于 Flink 和 Kafka 实现高效流处理:连续查询与时间窗口  宝妈做视频号该写什么标签话题?宝妈关注的话题有哪些?  word表格如何按某一列内容进行排序_Word表格按列排序方法  自定义你的VS Code状态栏,监控关键信息  广州地铁app准妈咪徽章领取方法  Golang中的rune与byte类型区别是什么_Golang字符与字节处理详解  苹果电脑如何快速截图并编辑 苹果电脑截屏标注快捷操作  厨房地面防滑垫的油污怎么洗? 机洗和手洗防滑垫的注意事项  《优志愿》修改手机号方法  b站如何剪辑视频_b站必剪app使用教程  sublime怎么在文件中显示代码结构大纲_sublime符号列表功能  使用 .htaccess 正确配置 WordPress 子目录重定向与路径保留  大众点评了却看不到是怎么回事  c++如何实现一个简单的RPC框架_c++远程过程调用原理与实践  《磁力猫》最好用的磁官网  51漫画网实时入口 51漫画网页版官方免费漫画入口 

 2025-11-21

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.