JavaScript 实现拖拽效果组件功能(兼容移动端)


Posted in Javascript onNovember 11, 2020

页面元素拖拽是一种非常实用的前端效果,基于元素拖拽可以实现很多不同的功能,增加客户端许多操作的便捷性,大大提高用户体验。日常生活中大家多多少少都见过这种效果,所以就不废话了,直接开干吧。

预期目标

实现一个 Class 类,通过该 Class,可以将任意 DOM 元素(比如 div)一键变为可拖拽状态,也可以恢复成原来的状态,例如这样:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #box1 {
      height: 50px;
      width: 50px;
      background-color: cadetblue;
    }

    #box2 {
      height: 50px;
      width: 50px;
      background-color: blue;
    }

    #box3 {
      height: 50px;
      width: 50px;
      background-color: red;
    }
  </style>
</head>

<body>
  <div id="box1">1</div>
  <a id="box2">2</a>
  <div id="box3">3</div>
</body>
<script type="module">
	// 我们要完成的目标 Class
  import DragElement from './DragElement.js'
  // 使 3 个元素可拖拽
  let box1 = new DragElement(document.querySelector("#box1"))
  let box2 = new DragElement(document.querySelector("#box2"))
  let box3 = new DragElement(document.querySelector("#box3"))
  // box2 解除拖拽效果,恢复为原来的样子
  // box2.dragRelease()
</script>
</html>

原本的样子

JavaScript 实现拖拽效果组件功能(兼容移动端)

随意拖放

JavaScript 实现拖拽效果组件功能(兼容移动端)

一、算法思路

1.1 拖拽的行为描述

我们先思考如何描述拖拽这一行为。我的思路是这样的:

  • 先对拖拽这一行为进行定义:在指定的元素上,若保持鼠标按下状态,则该元素将会跟随鼠标移动。当鼠标松开,该元素将不再跟随鼠标移动。如果是移动端的话,鼠标的角色改为触摸(touch)即可。

根据定义,我们可以确定几个关键信息:

  • 鼠标移动,是拖拽算法本身的作用范围。
  • 鼠标按下,开启拖拽
  • 鼠标松开,关闭拖拽

可以看到,完整的拖拽功能分为 3 个部分,分别是开启、运行与关闭。分别对应鼠标的按下、运行、松开事件。 因此我们至少需要设计相应的 3 个函数,作为事件的回调。在这里我分别命名为 dragStart()、dragMoving()、dragEnd()。

这里就出现了第一个重点:如何描述拖拽功能的状态变化?

显然,鼠标的按下与松开,将会决定DOM 元素是否能够被拖拽,这是一种 “状态” 的变化。这种状态的变化,在编码上,可以通过一个变量来实现,也可以通过不断地添加 or 移除回调函数来实现。如果通过变量的话,在鼠标没有按下时,鼠标移动事件也会触发进行状态判断,这其实是没有必要的,因此方案上我们选择后者,鼠标按下与松开时,分别添加和移除实现拖拽的函数。

以上是拖拽本身的行为,此外,由于我们需要 DOM 元素能够在原本的状态和可拖拽状态之间进行转换,因此我们还需要 2 个函数,一个用于将 DOM 元素变为可拖拽状态,另一个用于卸载这些状态。前者我称为 dragActive(),后者我称为 dragRelease()。它们做的事情,就是添加和解除事件监听。

现在第一个问题解决了,我们来解决第二个问题,那就是:拖拽函数怎么实现?

1.2 拖拽的实现

首先看核心的,拖拽本身应该怎么计算,如何让元素跟着鼠标走。

同样的,我们继续想象实际的场景。鼠标按下时,我们假设鼠标的坐标处于(x0, y0) 点,鼠标移动,假设移动到了(x1, y1) 点。那么该元素,相对自身初始位置便移动了(x1-x0, y1-y0) 的距离。这种相对于自身移动的,在 CSS 上可以通过相对定位,也可以通过 transform: translate 或 translate3d 来实现,由于定位在布局中很常用,我们也不知道指定的 DOM 元素到底是什么样式,为了尽量不影响原来的布局,所以我们采用 transform。

再回到具体计算上,鼠标的位置 x 和 y,可以通过事件回调函数传入的参数 event 得到,在 PC 端是 event.clientX 和 event.clientY,移动端是 event.changedTouches[0].pageX 和 event.changedTouches[0].pageY。而 mousemove 事件是连续触发的,我们的拖动也要让元素跟着鼠标连续运动,因此需要不停更新 (x0, y0),(x1, y1) 的值,在每个细小的运动中都进行差值计算,就像微积分一样。为了方便记录和更新,我们不妨把拖动中需要的变量用一个对象表示,称为 dragInfo,挂载到 document 元素上,这样在不同的函数、对象之间都可以访问。

class DragElement {
  constructor(element) {
    this.element = element
    document.dragInfo = {
      element: this.element,
      x0: 0,
      y0: 0,
      x1: 0,
      y1: 0
    }
  }
}

element 表示拖拽的元素,x 和 y 分别为计算所需的变量。

获取鼠标位置的函数:

updateDragPosition = (event) => {
	return {
		x: event.clientX || (event.changedTouches ? event.changedTouches[0].pageX : document.dragInfo.x0),
		y: event.clientY || (event.changedTouches ? event.changedTouches[0].pageY : document.dragInfo.y0)
	}
}

或许有人会有疑问,为啥不直接 event.clientX || event.changedTouches[0].pageX,而是要用三元表达式。这是因为有些情况下,上述两者可能都不存在,比如当鼠标移到浏览器左边缘的时候,就无法获得位置:

JavaScript 实现拖拽效果组件功能(兼容移动端)

获取鼠标位置的函数写完后,就可以写拖拽的函数了:

dragMoving = (event) => {
	document.dragInfo.x1 = this.updateDragPosition(event).x - document.dragInfo.x0 + document.dragInfo.x1
	document.dragInfo.y1 = this.updateDragPosition(event).y - document.dragInfo.y0 + document.dragInfo.y1
	document.dragInfo.x0 = this.updateDragPosition(event).x
	document.dragInfo.y0 = this.updateDragPosition(event).y
	document.dragInfo.element.style.transform = 'translate3d(' + document.dragInfo.x1 + 'px, ' + document.dragInfo.y1 + 'px, 0)';
}

但此时问题就来了,由于 document 上只有一个 dragInfo,不同的组件之间坐标冲突如何解决?其实这个简单,只需要在 this.element 上添加一个对象记录每次拖拽后的位置即可,每当点击一个拖拽元素时,就将该元素的信息注入 document.dragInfo。

this.element.dragPostion = {
	x: 0,
	y: 0
}

综上,我们已经解决了最核心的流程描述与算法部分,接下来只要编码就可以了。

二、编码实现

请根据之前说的思路,自行阅读代码,整体逻辑还是非常清晰的,如果有一些细节不懂,可以在评论区提出,或者我有空了再补充。

class DragElement {
  constructor(element) {
    this.element = element
    document.dragInfo = {
      element: this.element,
      x0: 0,
      y0: 0,
      x1: 0,
      y1: 0
    }
    document.updateDragPosition = this.updateDragPosition
    this.dragActive()
  }

  // 更新鼠标位置
  updateDragPosition = (event) => {
    return {
      x: event.clientX || (event.changedTouches ? event.changedTouches[0].pageX : document.dragInfo.x0),
      y: event.clientY || (event.changedTouches ? event.changedTouches[0].pageY : document.dragInfo.y0)
    }
  }

  // 为元素配置相应的拖拽控制函数
  dragActive = () => {
    if (!this.element) return
    this.element.style.display = "block" 
    this.element.addEventListener('mousedown', this.dragStart, false)
    this.element.addEventListener('touchstart', this.dragStart, false)
    this.element.addEventListener('mouseup', this.dragEnd, false) // 释放
    this.element.addEventListener('touchend', this.dragEnd, false)
    this.element.addEventListener('touchcancel', this.dragEnd, false)
    // 为该元素添加一个对象,保存当前位置
    this.element.dragPostion = {
      x: 0,
      y: 0
    }
  }

  // 释放配置
  dragRelease = () => {
    this.element.removeEventListener('mousedown', this.dragStart)
    this.element.removeEventListener('touchstart', this.dragStart)
    this.element.removeEventListener('mouseup', this.dragEnd) // 释放
    this.element.removeEventListener('touchend', this.dragEnd)
    this.element.removeEventListener('touchcancel', this.dragEnd)
    this.element.style.display = ""
    return this.element
  }

  // 点击捕获拖拽元素,初始化相应信息
  dragStart = (event) => {
    document.dragInfo.element = this.element
    document.dragInfo.x0 = this.updateDragPosition(event).x
    document.dragInfo.y0 = this.updateDragPosition(event).y
    document.dragInfo.x1 = this.element.dragPostion.x
    document.dragInfo.y1 = this.element.dragPostion.y
    // 屏蔽默认行为
    event.preventDefault();

    // mousemove 绑定在 document 上,防止鼠标过快可能导致的元素跟丢
    document.addEventListener('mousemove', this.dragMoving, false)
    document.addEventListener('touchmove', this.dragMoving, false)
  }

  // 实时计算、更新相对位置变化
  dragMoving = (event) => {
    document.dragInfo.x1 = this.updateDragPosition(event).x - document.dragInfo.x0 + document.dragInfo.x1
    document.dragInfo.y1 = this.updateDragPosition(event).y - document.dragInfo.y0 + document.dragInfo.y1
    document.dragInfo.x0 = this.updateDragPosition(event).x
    document.dragInfo.y0 = this.updateDragPosition(event).y
    document.dragInfo.element.style.transform = 'translate3d(' + document.dragInfo.x1 + 'px, ' + document.dragInfo.y1 + 'px, 0)';
  }

  // 关闭拖拽
  dragEnd = () => {
    // 保存当前位置
    this.element.dragPostion.x = document.dragInfo.x1
    this.element.dragPostion.y = document.dragInfo.y1
    // 解绑
    document.removeEventListener('touchmove', this.dragMoving)
    document.removeEventListener('mousemove', this.dragMoving)
  }
}

export default DragElement

到此这篇关于JavaScript 实现拖拽效果组件功能(兼容移动端)的文章就介绍到这了,更多相关JavaScript 拖拽效果组件内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
幻宇的层模拟窗口效果-提供演示和下载
Jan 20 Javascript
jQuery中验证表单提交方式及序列化表单内容的实现
Jan 06 Javascript
jQuery中wrapInner()方法用法实例
Jan 16 Javascript
如何屏蔽防止别的网站嵌入框架代码
Aug 24 Javascript
原生JS获取元素集合的子元素宽度实例
Dec 14 Javascript
jQuery实现radio第一次点击选中第二次点击取消功能
May 15 jQuery
基于Vue框架vux组件库实现上拉刷新功能
Nov 28 Javascript
实例分析vue循环列表动态数据的处理方法
Sep 28 Javascript
详解js实时获取并显示当前时间的方法
May 10 Javascript
ES6 Promise对象的含义和基本用法分析
Jun 14 Javascript
原生JS实现顶部导航栏显示按钮+搜索框功能
Dec 25 Javascript
vue+element-ui JYAdmin后台管理系统模板解析
Jul 28 Javascript
vant 中van-list的用法说明
Nov 11 #Javascript
让Vue响应Map或Set的变化操作
Nov 11 #Javascript
vue项目中使用rem,在入口文件添加内容操作
Nov 11 #Javascript
VUE前端从后台请求过来的数据进行转换数据结构操作
Nov 11 #Javascript
Vue 防止短时间内连续点击后多次触发请求的操作
Nov 11 #Javascript
Vue 401配合Vuex防止多次弹框的案例
Nov 11 #Javascript
VUE-ElementUI 自定义Loading图操作
Nov 11 #Javascript
You might like
php获取字符串中各个字符出现次数的方法
2015/02/23 PHP
PHP7.3.10编译安装教程
2019/10/08 PHP
PHP filter_var() 函数, 验证判断EMAIL,URL等
2021/03/09 PHP
JavaScript DOM 添加事件
2009/02/14 Javascript
JS实现QQ图片一闪一闪的效果小例子
2013/07/31 Javascript
JS控制阿拉伯数字转为中文大写示例代码
2013/09/04 Javascript
js jq 单击和双击区分示例介绍
2013/11/05 Javascript
基于jquery扩展漂亮的CheckBox(自己编写)
2013/11/19 Javascript
alert出数组中的随即值代码
2014/09/25 Javascript
node.js中的fs.symlink方法使用说明
2014/12/15 Javascript
jQuery构造函数init参数分析
2015/05/13 Javascript
动态JavaScript所造成一些你不知道的危害
2016/09/25 Javascript
关于vue.js过渡css类名的理解(推荐)
2017/04/10 Javascript
JavaScript实现二叉树的先序、中序及后序遍历方法详解
2017/10/26 Javascript
AngularJs点击状态值改变背景色的实例
2017/12/18 Javascript
JavaScript new对象的四个过程实例浅析
2018/07/31 Javascript
[04:00]黄浦江畔,再会英雄——完美世界DOTA2 TI9应援视频
2019/07/31 DOTA
Python编程中用close()方法关闭文件的教程
2015/05/24 Python
浅谈python的dataframe与series的创建方法
2018/11/12 Python
python实现指定字符串补全空格、前面填充0的方法
2018/11/16 Python
python实现名片管理系统
2018/11/29 Python
Python安装selenium包详细过程
2019/07/23 Python
numpy创建单位矩阵和对角矩阵的实例
2019/11/29 Python
Python打印特殊符号及对应编码解析
2020/05/07 Python
Python基于jieba, wordcloud库生成中文词云
2020/05/13 Python
Python django框架 web端视频加密的实例详解
2020/11/20 Python
Pretty Green美国:英式摇滚服饰风格代表品牌之一
2019/01/23 全球购物
xml有哪些解析技术?区别是什么
2016/04/26 面试题
简述synchronized和java.util.concurrent.locks.Lock的异同
2014/12/08 面试题
C有"按引用传递"吗
2016/09/06 面试题
水电站项目建议书
2014/05/12 职场文书
应届生找工作求职信
2014/06/24 职场文书
师德师风个人自我剖析材料
2014/09/27 职场文书
2015年度质量工作总结报告
2015/04/27 职场文书
电力安全学习心得体会
2016/01/18 职场文书
在HTML中引入CSS的几种方式介绍
2021/12/06 HTML / CSS