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 相关文章推荐
prototype 1.5相关知识及他人笔记
Dec 16 Javascript
Javascript中 关于prototype属性实现继承的原理图
Apr 16 Javascript
解析页面加载与js函数的执行 onload or ready
Dec 12 Javascript
IE8利用自带的setCapture和releaseCapture解决iframe的拖拽事件方法
Oct 25 Javascript
jQuery电话号码验证实例
Jan 05 Javascript
使用Node.js实现简易MVC框架的方法
Aug 07 Javascript
微信小程序tabBar模板用法实例分析【附demo源码下载】
Nov 28 Javascript
Vue 开发音乐播放器之歌手页右侧快速入口功能
Aug 08 Javascript
详解Vue结合后台的列表增删改案例
Aug 21 Javascript
详解Node.js amqplib 连接 Rabbit MQ最佳实践
Jan 24 Javascript
node.js爬虫框架node-crawler初体验
Oct 29 Javascript
Vue中的nextTick作用和几个简单的使用场景
Jan 25 Vue.js
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 Http_Template_IT类库进行模板替换
2009/03/19 PHP
MongoDB在PHP中的常用操作小结
2014/02/20 PHP
js处理json以及字符串的比较等常用操作
2013/09/08 Javascript
使用jquery解析XML示例代码
2014/09/05 Javascript
js计算任意值之间随机数的方法
2015/01/16 Javascript
详谈javascript异步编程
2016/02/21 Javascript
JS Array创建及concat()split()slice()的使用方法
2016/06/03 Javascript
关于JavaScript数组你所不知道的3件事
2016/08/24 Javascript
JS控件bootstrap suggest plugin使用方法详解
2017/03/25 Javascript
python爬取安居客二手房网站数据(实例讲解)
2017/10/19 Javascript
微信小程序实现YDUI的ScrollTab组件
2018/02/02 Javascript
vue打包的时候自动将px转成rem的操作方法
2018/06/20 Javascript
vue.js 添加 fastclick的支持方法
2018/08/28 Javascript
vue引入axios同源跨域问题
2018/09/27 Javascript
vuedraggable+element ui实现页面控件拖拽排序效果
2020/07/29 Javascript
JavaScript如何把两个数组对象合并过程解析
2019/10/10 Javascript
Weex开发之地图篇的具体使用
2019/10/16 Javascript
python解决Fedora解压zip时中文乱码的方法
2016/09/18 Python
python爬虫中get和post方法介绍以及cookie作用
2018/02/08 Python
对python opencv 添加文字 cv2.putText 的各参数介绍
2018/12/05 Python
django框架CSRF防护原理与用法分析
2019/07/22 Python
python内存监控工具memory_profiler和guppy的用法详解
2019/07/29 Python
使用Python快乐学数学Github万星神器Manim简介
2019/08/07 Python
使用 pytorch 创建神经网络拟合sin函数的实现
2020/02/24 Python
django xadmin 管理器常用显示设置方式
2020/03/11 Python
Python如何自动获取目标网站最新通知
2020/06/18 Python
详解PyQt5中textBrowser显示print语句输出的简单方法
2020/08/07 Python
Python 里最强的地图绘制神器
2021/03/01 Python
美国大城市最热门旅游景点门票:CityPASS
2016/12/16 全球购物
Annoushka英国官网:英国奢侈珠宝品牌
2018/10/20 全球购物
社会实践自我鉴定
2013/11/07 职场文书
设备售后服务承诺书
2014/05/30 职场文书
保险专业求职信
2014/07/07 职场文书
会计学习心得体会
2014/09/09 职场文书
2014国庆节餐厅促销活动策划方案
2014/09/16 职场文书
2016教师国培研修感言
2015/12/08 职场文书