拖拽功能实现

本文介绍在Vue框架中实现拖拽功能,通过监听鼠标按下、移动、弹起事件,利用绝对定位调整元素位置,实现元素列表顺序的拖拽改变。文章详细讲解了拖拽逻辑、元素定位技巧以及数据交换的解决办法,提供了完整示例代码。

因项目需要实现一个基本拖拽功能,实现一个容器中元素列表的顺序可根据拖拽改变,先看具体sample实现:
在这里插入图片描述
本例以在vue框架中举例,因为所用到的都是js本身的东西,其他框架类似。
拖拽功能的实现主要依靠鼠标按下、移动、弹起事件三个作为基础。在鼠标按下时识别选中的页面元素,对元素的起始点(横纵坐标)进行记录;鼠标移动时,将被移动元素进行绝对定位到目前鼠标移至的相对位置上;鼠标弹起时,通过位置计算出目标元素的位置,将起始位置元素与目标位置元素互换。
要实现拖拽效果,first thought可能会想用列表ul来实现,但ul中的li是不能实现我们想要的元素绝对定位的,所以需要用div来实现,外层div是父容器,里面div是需要拖拽的框框,采用绝对定位,具体来看:

  <div>
    <div id="mydrag" class="mydrag" :style="{'margin-left':'100px','margin-top':'100px',width:'120px',height:'190px',border:'1px solid red'}">
      <div class="ordereditem" v-for="(item, index) in orderedItems" :key="index" @mousedown="move" :style="{
          color: 'white',
          'background-color':'red',
          position: 'absolute',
          width: '100px',
          height: '20px',
          left: (containerLeft + 10) + 'px',
          top: (containerTop + 10 + index * 30) +'px',
          cursor: 'pointer'}"
      >{{ item }}</div>
    </div>
  </div>

这里面需要注意的,orderedItems是data中的属性,我们会将最终排序后的一个字符串数组放入其中,比如初始是这样的[‘item1’, ‘item2’, ‘item3’, ‘item4’, ‘item5’, ‘item6’];move方法是拖拽具体逻辑;left和top用于计算内部元素的位置,因为是绝对定位,需要找到父元素左部和上部在当前整个浏览器中的位置,再加可拖拽元素纵向排列的位置。

关于父元素位置这里还有一个技巧,因为组件的生命周期中父div和子div的加载时机是同步的,所以这里存在一个问题,在vue生命周期中我们无法等到父元素加载完成并确定在整个浏览器中的位置后,再来定位子元素的位置。想要这么做可以使用setTimeout方法达到脱离vue生命周期来实现元素绝对定位的目的:

    setTimeout(() => {
      const container = document.getElementById('mydrag')
      this.containerLeft = container.offsetLeft
      this.containerTop = container.offsetTop
    }, 0)

这个方法写在组件mounted中,通过setTimeout的机制,方法会在mounted之后执行,这时父组件已经渲染完毕其坐标已经确定,我们将它的左侧和上部位置赋给属性,这样子元素就能够通过这两个确定的属性来进行绝对定位了。(延时设置为0,但是setTimeout中仍然延迟执行)

本例的css除了行间以外只有一个规则,主要为了设置当我们拖拽时,元素的文字不要有选中的效果

.mydrag {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

在move方法中包含了整个拖拽的行为,如下代码流程展示了一次拖拽的逻辑

move (e) {
  // 鼠标点下时逻辑
  document.onmousemove = (e) => {
	// 鼠标移动时逻辑
  }
  document.onmouseup = (e) => {
	// 抬起鼠标时的逻辑
  }
}

当鼠标按下时:

  let odiv = e.target // 获取目标元素
  odiv.style.zIndex = 1 // 被移动的元素处于浮层最上
  // this.positionX = odiv.offsetLeft // 起始横坐标
  this.positionY = odiv.offsetTop // 起始纵坐标
  // let x = e.clientX - odiv.offsetLeft // 点击位置距元素左侧距离
  let y = e.clientY - this.positionY // 点击位置距元素上部距离
  this.startIndex = this.locateIndex(this.positionY - this.containerTop + 20 / 2) // 从被移动元素起始位置判断被移动元素的索引,参数计算的是元素的中心点纵坐标

offsetTop是当前点击元素的顶部在浏览器中纵坐标,e.clientY是鼠标点击位置在浏览器中的纵坐标,这里的y代表鼠标点击于元素内部的相对位置,用于在拖拽时,保持这个相对距离。至于横坐标,在我们的例子里因为只牵扯纵向拖动,横向不考虑。startIndex代表点击的是第几个元素,通过this.locateIndex这个方法获得,这个方法是通用的,当鼠标按下时能够获取点击的哪个元素,当鼠标抬起时,获取其被移动至位置是第几个。那么如何得知当前点击的是第几个元素呢?
可以采用如下思路:当按下或抬起鼠标时,这时能够获取到当前被点击元素的纵坐标中心点相对于父容器的位置,考虑将整个容器划分为从上到下6个区域(我们的例子里一共有6个元素),在这6各区域中间设置5个临界值,当鼠标按下或抬起时,通过比对中心点纵坐标和临界值,确定当前元素所处的位置,具体代码如下在mounted

    // 本例中有6个元素,需要5个分界点,[35, 65, 95, 125, 155],以此判断进行吸附
    let accumulatedY = 0
    accumulatedY += 10
    for (let i = 1; i < this.orderedItems.length; i++) {
      this.positions.push(accumulatedY + (20 + 10) * i - 10 / 2)
    }

临界点确定了,locateIndex方法中传入纵坐标中心点位置,能够确定当前被点击或移至的位置代表的元素索引

    locateIndex (offsetMiddle) {
      for (let i = 0; i < this.positions.length; i++) {
        if (offsetMiddle <= this.positions[i]) {
          return i
        }
      }
      return this.positions.length // 超过最后一个分界点的情况
    }

当鼠标移动时:

// let left = e.clientX - x // 元素随鼠标拖拽移动,元素左侧的位置
let top = e.clientY - y // 元素随鼠标拖拽移动,元素上部的位置
if (top <= this.containerTop + 10) {
  top = this.containerTop + 10
} else if (top >= this.containerTop + 160) {
  top = this.containerTop + 160
}
odiv.style.left = (this.containerLeft + 10) + 'px'
odiv.style.top = top + 'px'

这里逻辑很简单,横向没有管,纵向的话首先限定不要超出父容器界限,其次注意将鼠标点击的相对位置计算进去来决定当前元素被移动到哪里。

当鼠标抬起时:

this.endIndex = this.locateIndex(odiv.style.top.replace('px', '') - this.containerTop + 20 / 2) // 从被移动元素终点位置判断被移动到的位置索引,参数计算的是元素的中心点纵坐标
// 交换startIndex与endIndex上的元素,这里要进行深拷贝
let ary = this.orderedItems.slice()
const temp = ary[this.startIndex]
ary[this.startIndex] = ary[this.endIndex]
ary[this.endIndex] = temp
this.orderedItems = ary
odiv.style.zIndex = 0 // 恢复被移动的元素浮层级别
odiv.style.top = this.positionY + 'px' // 元素归位
document.onmousemove = null // 释放
document.onmouseup = null // 释放

被移动至元素索引计算前面已经描述。当实际鼠标抬起时,我们要做的首先要将元素归位到未移动之前的位置上,然后将绑定的数据位置交换就可以实现拖拽与吸附了。
关于元素位置交换,这里遇到一个坑,一开始一直尝试用改变起始那个数组的方式,一直有问题,现象是观察到数据已经改变了,但是页面渲染出来的结果,永远是item1到item6,这个很奇怪,后来尝试了下每次拖拽后,采用深拷贝将当前状态数组复制一份出来重新赋值交换,再将这个新数组赋值回去才得到解决。这个按照vue数据驱动的方式难以解释,读者可以尝试,欢迎告知原因。
github:
https://github.com/tanglingjia/exp/blob/master/src/components/DragAndDrop.vue

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值