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 相关文章推荐
JQuery实现自定义对话框的代码
Jun 15 Javascript
网页加载时页面显示进度条加载完成之后显示网页内容
Dec 23 Javascript
javascript仿php的print_r函数输出json数据
Sep 13 Javascript
jQuery实现的动态伸缩导航菜单实例
May 07 Javascript
javascript实现密码验证
Nov 10 Javascript
js判断当前页面在移动设备还是在PC端中打开
Jan 06 Javascript
ui组件之input多选下拉实现方法(带有搜索功能)
Jul 14 Javascript
微信通过页面(H5)直接打开本地app的解决方法
Sep 09 Javascript
探索Vue高阶组件的使用
Jan 08 Javascript
Vue $mount实战之实现消息弹窗组件
Apr 22 Javascript
json数据格式常见操作示例
Jun 13 Javascript
解决vue单页面应用进入页面加载所有 js 的问题
Aug 12 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数组函数序列之in_array() 查找数组值是否存在
2011/10/29 PHP
PHP Warning: PHP Startup: Unable to load dynamic library \ D:/php5/ext/php_mysqli.dll\
2012/06/17 PHP
浅析php中抽象类和接口的概念以及区别
2013/06/27 PHP
不使用php api函数实现数组的交换排序示例
2014/04/13 PHP
ThinkPHP模板替换与系统常量及应用实例教程
2014/08/22 PHP
php正则表达式基本知识与应用详解【经典教程】
2017/04/17 PHP
thinkPHP5框架自定义验证器实现方法分析
2018/06/11 PHP
js+FSO遍历文件夹下文件并显示
2007/03/07 Javascript
简单漂亮的js弹窗可自由拖拽且兼容大部分浏览器
2013/10/22 Javascript
JS+CSS实现弹出全屏灰黑色透明遮罩效果的方法
2014/12/20 Javascript
15个jquery常用方法、小技巧分享
2015/01/13 Javascript
通过实例理解javascript中没有函数重载的概念
2015/06/03 Javascript
仿Angular Bootstrap TimePicker创建分钟数-秒数的输入控件
2016/07/01 Javascript
vue2 router 动态传参,多个参数的实例
2017/11/10 Javascript
jQuery实现form表单序列化转换为json对象功能示例
2018/05/23 jQuery
JavaScript事件对象event用法分析
2018/07/27 Javascript
从源码里了解vue中的nextTick的使用
2018/11/22 Javascript
微信小程序跨页面传递data数据方法解析
2019/12/13 Javascript
jquery html添加元素/删除元素操作实例详解
2020/05/20 jQuery
分析Python的Django框架的运行方式及处理流程
2015/04/08 Python
详解Django中的ifequal和ifnotequal标签使用
2015/07/16 Python
python魔法方法-属性转换和类的表示详解
2016/07/22 Python
Python数据分析之获取双色球历史信息的方法示例
2018/02/03 Python
用Python将一个列表分割成小列表的实例讲解
2018/07/02 Python
解决Pycharm界面的子窗口不见了的问题
2019/01/17 Python
Python使用numpy模块实现矩阵和列表的连接操作方法
2019/06/26 Python
django创建超级用户过程解析
2019/09/18 Python
python的mysql数据库建立表与插入数据操作示例
2019/09/30 Python
pytorch中 gpu与gpu、gpu与cpu 在load时相互转化操作
2020/05/25 Python
英国最大的婴儿监视器网上商店:Baby Monitors Direct
2018/04/24 全球购物
美国领先的在线邮轮旅游公司:CruiseDirect
2018/06/07 全球购物
英国办公家具网站:Furniture At Work
2019/10/07 全球购物
正隆泰信息技术有限公司上机题
2012/06/14 面试题
造型师求职自荐信
2013/09/27 职场文书
团队精神口号
2014/06/06 职场文书
消防安全主题班会
2015/08/12 职场文书