[深入26] Drag

465 阅读10分钟

前置知识

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

复习笔记-01
复习笔记-02
复习笔记-03

(1) 一些单词

illegal 非法的
notation 符号 标记法
identifier 标识符
efficiency 效率

notification 通知 // notify 通知
draggable 拖动
destination 目的地
verification 验证 // verification code 验证码
annotation 注释 注解

(2) 拖动事件

  • 触发先后顺序
    • dragstart -> dragenter -> dragover -> dragleave -> drop -> dragend
    • 开始拖拽 -> 拖拽进入 -> 拖拽经过 -> 拖拽离开 -> 拖拽释放 -> 拖拽结束
  • 需要阻止 ( 默认行为 ) 的两个方法
    • dragover 和 drop
    • dragover 和 drop 事件需要e.preventDefault()阻止默认方法,事件中后续代码才会生效

(3) 鼠标点击相关事件

  • 触发先后顺序
    • mousedown -> mousemove -> mouseup
    • 鼠标按下 -> 鼠标移动 -> 鼠标释放
  • 双击鼠标左键时,触发的事件顺序如下
    • mousedown -> mouseup -> click -> mousedown -> mouseup -> click -> dblclick

image.png

(4) mouseenter,mouseleave 和 mouseover,mouseout 的区别?

  • 配对
    • mouserenter 和 mouserleave 配对
    • mouserover 和 mouserout 配对
  • 进入
    • mouseenter
    • mouseover
  • 离开
    • mouseleave
    • mouseout
  • 具有冒泡性质的是两个带o的事件,进入子元素后事件冒泡到父元素
    • mouseover
    • mouseout
    • 当进入绑定事件的元素的子元素内部时,mouserover会触发,mouseenter不会触发
    • 当离开绑定事件的元素的子元素内部时,mouserout会触发,mouseleave不会触发 image.png

(5) DataTransfer 对象

  • 作用
    • DataTransfer 对象用来保存 ( 拖动并放下 ) ( drag and drop ) 过程中的 数据
    • 它可以保存 ( 一项或多项数据 ),这些数据项可以是 ( 一种或者多种数据类型 )
  • 获取
    • DataTransfer 对象可以从 ( 所有拖动事件 ) 的 ( dataTransfer ) 属性上获取
  • 构造函数
    • dataTransfer = new DataTransfer()
    • 生成并且返回一个新的 DataTransfer 对象
  • 5个标准属性
    • DataTransfer.dropEffect
      • 获取当前所选拖放操作的类型,或将拖拽操作设置为新类型
      • 值必须为nonecopylinkmove中的一个
      • dropEffect属性顾名思意拖拽效果,在PC web端主要表现在 ( 鼠标手形 ) 上
    • DataTransfer.effectAllowed
      • 提供可能的所有类型的操作
      • 枚举类型:nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized
        • none 不允许拖拽,鼠标保持禁止状态
        • copyLink 允许复制和链接操作
        • copyMove 允许复制和移动操作
        • linkMove 允许链接和移动操作
        • all 什么操作都允许
    • DataTransfer.files
      • 拖拽的本地文件列表
      • 如果拖动操作不涉及拖动文件,则此属性为空列表
    • DataTransfer.items
      • 供DataTransferItemList对象,该对象是所有拖动数据的列表
      • 只读
    • DataTransfer.types
      • dragstart事件中设置数据格式,返回的是一个字符串数组
      • 只读
  • 4个标准方法
    • DataTransfer.getData(format)
      • getData 快捷获取拖拽的内容
      • 获取纯文本:text/plain
      • 实际开发中,一般直接使用 text
    • DataTransfer.setData(format, data)
      • 重新设置被拖拽的内容
      • 可以重置原生的拖拽内容,或者用来参数传递
      • 实际开发拖拽具体模块元素,此时setData()多用来传递拖拽元素的id
    • DataTransfer.clearData([format])
      • clearData() 只能用在 dragstart 事件中
      • 作用:清除所有数据,即执行clearData()后getData()只能活到到空字符串
      • 注意:
        • clearData() 只能用在 dragstart 事件中
        • 当我们需要使用setData()自定义拖拽数据的时,为了避免原生拖拽数据干扰,可以先执行一次clearData()方法
    • DataTransfer.setDragImage(img, offsetX, offsetY)
      • setDragImage() 只能用在 dragstart 事件中
      • 作用:设置拖拽时候有个图片跟在后面
      • 参数
        • img:表示图表DOM元素对象
        • offsetX:距离鼠标的水平偏移距离
        • offsetY:距离鼠标的垂直偏移距离

(6) scrollTop,offsetTop,scrollHeight,clientHeight,offsetHeight

  • scrollTop 和 offsetTop
    • scrollTop
      • 表示有滚动条时,滚动条 ( 向下垂直滚动的距离 )
    • offsetTop
      • ( 当前元素顶部 ) 距离 ( 最近父元素顶部 ) 的距离
      • offsetTop和有没有滚动条无关,是一个固定值,滚动时值不会改变
      • 当元素 display:none 时,offsetTop = 0
  • clientHeight 和 offsetHeight
    • clientHeight
      • 包括:( padding )
      • 不包括:( border ),( margin ),( 水平滚动条 )
    • offsetHeight
      • 包括:( padding ),( border ),( 水平滚动条 )
      • 不包括:( margin )
      • 所以:offsetHeight = client Height + border
  • scrollHeight
    • scrollHeight = clientHeight + scrollTop

image.png

image.png

(7) JSON - review

(7.1) 基本概念

  • 类型和格式的要求
    • 原始类型的值
      • 只能是 number string boolean null
      • 不能是 ( undefined )
    • 复合类型的值
      • 只能是 对象和数组
      • 不能是 ( 函数,日期对象,正则对象 )
    • 字符串必须是双引号,对象的key必须有双引号
    • 对象和数组的最后一个属性不能有 ,
  • 空数组,空对象,null,都是合法的json数据
  • 静态方法,一共有两个静态方法
    • JSON.stringify()
    • JSON.parse()

(7.2) JSON.stringify()

  • JSON.stringify(value[, replacer [, space]])
  • 作用
    • 用于将一个转为 JSON 字符串
    • 该字符串符合 JSON 格式,并且可以被JSON.parse方法还原
  • 对于原始类型的字符串,转换结果会带双引号
JSON.stringify('abc') === 'abc' // false
JSON.stringify('abc') === "abc" // false
JSON.stringify('abc') === "'abc'" // false,注意单引号和双引号的顺序,上面的就是返回false

JSON.stringify('abc') === '"abc"' // true

JSON.stringify('abc') === "\"abc\"" // true
// 这是因为将来还原的时候,内层双引号可以让 JavaScript 引擎知道,这是一个字符串,而不是其他类型的值
// 请再看下面的例子
JSON.stringify(false) // "false"
JSON.stringify('false') // ""false""
// 上面代码中,如果不是内层的双引号,将来还原的时候,引擎就无法知道原始值是布尔值还是字符串。
  • JSON.stringify() 会忽略 ( 对象的不可遍历属性 - 即不可枚举属性 )
  • 如果 ( 对象的属性 ) 是 ( undefined 或 函数 ),则会被 JSON.stringify() 过滤
  • 如果 ( 数组的成员 ) 是 ( undefined 或 函数 ),则会被 JSON.stringify() 转成 null
  • undefined或function -> 对象过滤,数组转null
对象
---
const obj = {
  a: undefined,
  b: function () {},
};
Object.defineProperties(obj, {
  c: {
    value: "ccc",
    enumerable: true, // 可枚举
  },
  d: {
    value: "ddd",
    enumerable: false, // 不可枚举 
  },
});

JSON.stringify(obj); // '{"c":"ccc"}'
// 1. 如果对象的属性是 ( undefined  function ) 则会被 JSON.stringify() 过滤
// 2. JSON.stringify() 会忽略对象的 ( 不可枚举的属性,不可遍历的属性 )
数组
---
var arr = [undefined, function () {}];
JSON.stringify(arr) // "[null,null]"
// 如果数组的成员是  ( undefined  function ) 则会被 JSON.stringify() 转成 null
  • 参数
    • 第二个参数
      • 如果接受一个数组,指定需要转成字符串的属性。
      • 如果接受一个函数,用来更改JSON.stringify返回值
    • 第三个参数
      • JSON.stringify还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性
      • 如果是数字,表示每个属性前面添加的空格(最多不超过10个)
      • 如果是字符串,(不超过10个字符),则该字符串会添加在每行前面。

(7.3) JSON.parse()

  • 作用
    • JSON.parse方法用于将 JSON 字符串转换成对应的值
    • 如果传入的字符串不是有效的 JSON 格式,JSON.parse方法将报错
  • 参数
    • 第二个参数
      • 接受一个处理函数,用来更改返回值


(一) 实现拖动 - 方法一

  • 并不是所有元素都是默认可以拖动的
    • 图片是默认可以拖动的 - 所以一般如果图表不要拖动的话,会设置draggable=false
    • draggable=true 在元素标签上添加上该属性后,元素就可以被拖动
  • 案例
    • 方块:绿色小方块,蓝色小方块,红色大方块
    • 移动性:绿色小方块可以移动,而蓝色小方块不能移动
    • 需求
      • 绿色小方块可以拖动,蓝色块不能拖动
      • 移入
        • 绿色小方块移入红色大方块中时,变成红色大方块的最后一个子元素
        • 文字提示:当前方块名
      • 移出:
        • 绿色小方块移出红色大方块时,红色块清除绿色块,绿色块复原位
        • 文字提示复原
实现代码
---
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .wrap {
        border: 1px solid black;
      }
      .destination-content {
        width: 100px;
        height: 100px;
        border: 1px solid red;
      }
      .origin1,
      .origin2 {
        width: 50px;
        height: 50px;
        border: 1px solid green;
      }
      .origin2 {
        border: 1px solid blue;
      }
    </style>
  </head>
  <body>
    <div class="wrap">
      <p class="destination-title">当前元素 -></p>
      <div class="destination-content" />
    </div>
    <div class="origin2" data-name="blue_box"></div>
    <div class="origin1" data-name="green_box" draggable="true"></div>

    <script>
      let name = null;
      let dragDOM = null;
      const wrapDOM = document.querySelector("div.wrap");
      const destinationContent = document.querySelector(
        "div.destination-content"
      );
      const destinationTitle = document.querySelector("p.destination-title");
      console.log(`destinationTitle`, destinationTitle);

      document.addEventListener(
        "dragstart",
        (e) => {
          name = e.target.getAttribute("data-name");
          dragDOM = e.target;
        },

        false
      );

      document.addEventListener(
        "drag",
        (e) => {
          e.target.style.background = "green";
          destinationContent.style.background = "red";
        },
        false
      );

      document.addEventListener(
        "dragend",
        (e) => {
          e.target.style.background = "#fff";
          destinationContent.style.background = "#fff";
        },
        false
      );

      destinationContent.addEventListener(
        "dragenter",
        (e) => {
          e.target.style.background = "black";
          destinationTitle.innerHTML = "当前元素 ->" + " " + name;
        },
        false
      );

      destinationContent.addEventListener("dragover", (e) => {
        e.preventDefault();
        // 注意:
        // 1. 这里一定要添加 dragover 事件
        // 2. 也一定要阻止 默认行为
        // 不然,下面的 drop 方法将没有效果
      });

      destinationContent.addEventListener(
        "dragleave",
        (e) => {
          e.target.removeChild(dragDOM);
          wrapDOM.appendChild(dragDOM);
          destinationTitle.innerHTML = "当前元素 ->";
        },
        false
      );

      destinationContent.addEventListener(
        "drop", // drop 方法是绑定在 destinationContent 上的,这点一定要注意
        (e) => {
          console.log(`555`, 555);
          e.preventDefault(); // 必须阻止默认行为,后面的添加child代码才会生效显示在页面上
          console.log(`6666`, dragDOM);
          e.target.appendChild(dragDOM);
          destinationTitle.innerHTML = "当前元素 ->" + " " + name;
        },
        false
      );
    </script>
  </body>
</html>

image.png image.png image.png

(二) 实现拖动 - 验证码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .content {
        position: relative;
      }
      .small {
        position: absolute;
        left: 0;
        top: 90px;
        cursor: move;
        transform: scale(0.9);
      }
    </style>
  </head>
  <body>
    <div class="verification">
      <div class="title">验证码</div>
      <div class="content">
        <!-- 这里两张图片的 draggable=false 表示图片不会被拖动,即防止被拖动,我们这里不是用drag属性和事件去实现的 -->
        <img src="./images/big.jpg" alt="b" draggable="false" class="big" />
        <img src="./images/small.png" alt="s" draggable="false" class="small" />
      </div>
    </div>
    <script>
      // 验证码
      const title = document.querySelector("div.title");
      const big = document.querySelector("img.big");
      const small = document.querySelector("img.small");

      const bigLeft = big.offsetLeft; // big图片左上角 距离 父元素的左上角 的 ( 水平位移 ) 距离 ------------------------------ 该值固定的 - 无论是小方块移动或者静止,所以可以用任意时刻的其他值计算,所以可以用const
      const bigTop = big.offsetTop; //  big图片左上角 距离 父元素的左上角 的 ( 垂直位移 ) 距离 ------------------------------- 该值固定的 - 无论是小方块移动或者静止,所以可以用任意时刻的其他值计算,所以可以用const
      // const smallLeft = small.offsetLeft; //  small图片左上角 距离 父元素(big)的左上角 的 ( 水平位移 ) 距离 ------------------ 该值鼠标点击时固定,小方块移动时不固定
      // const smallTop = small.offsetTop; //  small图片左上角 距离 父元素(big)的左上角 的 ( 垂直位移 ) 距离 -------------------- 该值鼠标点击时固定,小方块移动时不固定

      small.addEventListener(
        "mousedown",
        (e) => {
          const mouseToSmallLeft = e.pageX - bigLeft - small.offsetLeft; // 鼠标点击位置 距离 small 的水平位移 ------------------- 该值固定的 - 无论是小方块移动或者静止,所以可以用任意时刻的其他值计算,所以可以用const
          const mouseToSmallTop = e.pageY - bigTop - small.offsetTop; // 鼠标点击位置 距离 small 的垂直位移 ---------------------- 该值固定的 - 无论是小方块移动或者静止,所以可以用任意时刻的其他值计算,所以可以用const

          const updateUi = (e) => {
            if (
              e.pageX > 447 &&
              e.pageX < 492 &&
              e.pageY > 174 &&
              e.pageY < 218
            ) {
              title.innerHTML = "验证码验证成功";
              title.style.background = "green";
              title.style.width = 568 + "px";
              title.style.color = "white";
            } else {
              title.innerHTML = "验证码";
              title.style.background = "";
            }
          };

          const moving = (e) => {
            const mouseLeft = e.pageX; // 鼠标 距离 页面的 ( 水平位移 )
            const mouseTop = e.pageY; // 鼠标 距离 页面的 ( 垂直位移 )
            small.style.left = mouseLeft - bigLeft - mouseToSmallLeft + "px";
            small.style.top = mouseTop - bigTop - mouseToSmallTop + "px";
            updateUi(e);
          };

          // 注意:这里是 document 监听的 mousemove
          document.addEventListener("mousemove", moving, false);
          document.addEventListener(
            "mouseup",
            (e) => {
              document.removeEventListener("mousemove", moving, false);
            },
            false
          );
        },
        false
      );
    </script>
  </body>
</html>

image.png image.png image.png

鼠标点击位置距离黄块的距离 = 鼠标距离页面的距离pageX - 绿色块距离页面的距离 - 黄色块距离绿色块的距离
---
最终要计算的是 - 白色的offsetLeft
---
白色的offsetLeft = 鼠标位置 - 黑色offsetLeft - 鼠标距离黄色块的距离

(三) 实现拖动 - 文字拖动

  • 需求
    • 拖动文字,先修改被选中的要拖动的文字,再去掉除数字外的其他字符
    • 拖动时,显示一张图片
  • 关键点
    • e.dataTransfer.getData('text')
    • e.dataTransfer.setData('text', '替换成什么文字')
    • drop事件 需要在监听函数中 阻止默认行为,通过 e.preventDefault()
    • drop 和 dragover 一般都要设置e.preventDefault()
  • 过程
    • 1.选中文字的p标签,使之可以拖动
    • 2.将p标签拖入input框,监听input框的 drop 事件
    • 3.在drop事件中获取到 e.dataTransfer.getData('text')
      • 即可获取到 ( 选中部分的文字 )
      • 注意:是选中部分的文字,不是所有的文字,在第1步中有操作选中哪些文字
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p>138-8888-9999-文字</p>
    <input type="text" />
    <script>
      // 文字拖动
      // https://www.zhangxinxu.com/wordpress/2018/09/drag-drop-datatransfer-js/
      const input = document.querySelector("input");

      const img = new Image();
      img.src = "./images/small.png";

      // 1
      // DataTransfer.setData()
      // DataTransfer.setDragImage(img, offsetX, offsetY)
      document.addEventListener("dragstart", (e) => {
        e.dataTransfer.setData("text", "133-4444-5555-文字"); // 将 ( 138-8888-9999-文字 ) 替换成 ( 133-4444-5555-文字 )
        e.dataTransfer.setDragImage(img, 10, 10); // 拖拽时显示一张图片
      });

      // 2
      // DataTransfer.getData()
      input.addEventListener("drop", function (e) {
        // 注意是:input 的 drop 事件
        e.preventDefault(); // drop 和 dragover 需要 e.preventDefault() 后面的代码才会生效
        var selectedText = e.dataTransfer.getData("text"); // 获取拖拽纯文本内容(被选取部分)
        input.value = selectedText.replace(/\D/g, ""); // 清除所有非数字字符,将其替换成 ''
      });
    </script>
  </body>
</html>

资料

JS拖动对象那些事 juejin.cn/post/684490…
1小时搞定卡片拖拽 juejin.cn/post/684490…
张鑫旭 www.zhangxinxu.com/wordpress/2…