vue列表拖拽排序功能实现

一、实现目标?

目标是输入一个数组,生成一个列表;通过拖拽排序,拖拽结束后输出一个经过排序的数组。

二、实现思路

1、是使用HTML5的drag功能来实现,每次拖拽时直接操作Dom节点排序,拖拽结束后再根据实际的dom节点遍历得出新的数组。

2、使用mousedown,mouseover等鼠标事件来实现,每次监听事件时,仅改动列表项的样式transform,而不操作实际的dom顺序。拖拽结束时,根据transform计算数组项顺序,得出新数组用vue数据驱动的方式重绘列表,重置所有样式。

总的来说就是可以通过不同的监听事件(drag、mouseover),按不同的顺序操作Dom(1.先操作实际dom,再添加动画,在输出数组;2。不操作实际dom,仅改变transfrom,得出新数组,用新数组生成新列表来更新节点)。

三、实际代码

第一种实现

html部分(被拖拽的元素需要设置draggable=true,否则不会有效果)

<div id="app">
  <ul
	  ref="parentNode"
	  @dragstart="onDragStart"
	  @dragover="onDragOver"
	  @dragend="onDragEnd">
	    <li
	      v-for="(item,index) in data"
	      :key="index"
	      class="item"
	      draggable="true"
	    >{{item}}</li>
  </ul>
</div>

拖拽事件有两个对象(被拖拽对象和目标对象)。dragstart 事件: 当拖拽元素开始被拖拽的时候触发的事件,此事件作用在被拖拽元素上。dragover事件:当拖拽元素穿过目标元素时候触发的事件,此事件作用在目标元素上。

在拖拽事件开始时,将本次拖拽的对象保存到变量中。每当dragover事件,将目标对象保存到变量中,添加判断当目标对象和拖拽对象为不同的列表项时,交换两个dom元素的先后顺序。

onDragStart(event) {
  console.log("drag start")
  this.draging = event.target;
},
onDragOver(event) {
  console.log('drag move')
  this.target = event.target;
  if (this.target.nodeName === "LI" && this.target !== this.draging) {
    if (this._index(this.draging) < this._index(this.target)) {
      this.target.parentNode.insertBefore(this.draging, this.target.nextSibling);
    } else {
      this.target.parentNode.insertBefore(this.draging, this.target);
    }
  }
},
onDragEnd(event) {
  console.log('drag end')
  let currentNodes = Array.from(this.$refs.parentNode.childNodes);

  let data = currentNodes.map((i, index) => {
    let item = this.data.find(c => c == i.innerText);
    return item
  });
  console.log(data)
},
_index(el) {
  let domData = Array.from(this.$refs.parentNode.childNodes);
  return domData.findIndex(i => i.innerText == el.innerText);
}

现在基本效果有了,然后是添加动画。添加动画的方式是通过transform实现。

因为每次拖拽排序触发时都会改变dom结构,为了实现移动的效果,可以在每次排序时先将dom节点恢复通过transform到原来的位置,使得表现上还是排序前的状态。然后添加transition,同时置空transform实现移动效果。(这里需要重绘才能触发效果,否则两次transform会直接抵消掉,可以使用setTimeout或者ele.offsetWidth来触发重绘),transform的偏移量可以通过改变节点顺序前后的距顶高度来获得。
完整代码:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      ul{
        list-style:none;
        padding-bottom:20px;
      }
      .item{
        cursor: pointer;
        height:24px;
        line-height:24px;
        background-color:#9c9c9c;
        border:1px solid #d9d9d9;
        border-radius:4px;
        color:#fff;
        padding:10px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <ul
        ref="parentNode"
        @dragstart="onDragStart"
        @dragover="onDragOver"
        @dragend="onDragEnd"
      >
        <li
        v-for="(item,index) in data"
        :key="index"
        class="item"
        draggable="true"
        >{{item}}</li>
      </ul>
    </div>
  </body>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        data:[1,2,3,4,5,6],
        draging:null,//被拖拽的对象
        target:null,//目标对象
      },
      mounted () {
        //为了防止火狐浏览器拖拽的时候以新标签打开,此代码真实有效
        document.body.ondrop = function (event) {
          event.preventDefault();
          event.stopPropagation();
        }
      },
      methods:{
        onDragStart(event){
          console.log("drag start")
          this.draging=event.target;
        },
        onDragOver(event){
          console.log('drag move')
          this.target=event.target;
          let targetTop=event.target.getBoundingClientRect().top;
          let dragingTop=this.draging.getBoundingClientRect().top;
          if (this.target.nodeName === "LI"&&this.target !== this.draging) {
            if (this.target) {
              if (this.target.animated) {
                return;
              }
            }
            if(this._index(this.draging)<this._index(this.target)){
              this.target.parentNode.insertBefore(this.draging,this.target.nextSibling);
            }else{
              this.target.parentNode.insertBefore(this.draging, this.target);
            }
            this._anim(targetTop,this.target);
            this._anim(dragingTop,this.draging);
          }
        },
        _anim(startPos,dom){
          let offset=startPos-dom.getBoundingClientRect().top;
          dom.style.transition="none";
          dom.style.transform=`translateY(${offset}px)`;
          //触发重绘
          dom.offsetWidth;
          dom.style.transition="transform .3s";
          dom.style.transform=``;
          //触发重绘
          // setTimeout(()=>{
          //     dom.style.transition="transform .3s";
          //     dom.style.transform=``;
          // },0)
          clearTimeout(dom.animated);
          dom.animated=setTimeout(()=>{
            dom.style.transition="";
            dom.style.transform=``;
            dom.animated=false;
          },300)
        },
        onDragEnd(event){
          console.log('drag end')
          let currentNodes=Array.from(this.$refs.parentNode.childNodes);
          let data=currentNodes.map((i,index)=>{
            let item=this.data.find(c=>c==i.innerText);
            return item
          });
          console.log(data)
        },
        _index(el){
          let domData=Array.from(this.$refs.parentNode.childNodes);
          return domData.findIndex(i=>i.innerText==el.innerText);
        }
      }
    })
  </script>
</html>
第二种实现

mousedown的时候记录下拖拽项和拖拽项初始位置,mouseover的时候将拖拽项和目标项交换位置,添加transform,mouseup的时候遍历出新数组来更新视图。这种方式就是动画不好加,个人瞎琢磨的,应该是思路错误了,放着看看吧。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      ul{
        list-style:none;
        padding-bottom:20px;
      }
      .item{
        cursor: pointer;
        height:24px;
        line-height:24px;
        background-color:#9c9c9c;
        border:1px solid #d9d9d9;
        border-radius:4px;
        color:#fff;
        padding:10px;
        user-select: none;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <ul
      ref="parentNode"
      @mouseover="onMouseOver"
      @mouseup="onMouseUp">
        <li
          ref="li"
          v-for="(item,index) in data"
          :key="index"
          class="item"
          @mouseDown="(event)=>{onMouseDown(event,index)}"
        >{{item}}</li>
      </ul>
    </div>
  </body>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        data:[1,2,3,4,5,6],
        isDonw:false,
        draging:null,
        dragStartPos:0
      },
      mounted () {
        //为了防止火狐浏览器拖拽的时候以新标签打开,此代码真实有效
        document.body.ondrop = function (event) {
          event.preventDefault();
          event.stopPropagation();
        }
        document.onmouseup=()=>{
          if(this.isDonw)
          this.onMouseUp()
        };
      },
      computed:{
        nodes(){
          return Array.from(this.$refs.parentNode.children)
        },
        itemHeight(){
          return this.nodes[0].offsetHeight;
        }
      },
      methods:{
        onMouseDown(event,index){
          this.isDonw=true;
          this.draging=this.$refs['li'][index];
          this.dragStartPos=this.draging.getBoundingClientRect().top;
        },
        onMouseOver(event){
          if(this.isDonw){
            let target=event.target;
            let drag=this.draging;
            let Index=this._index(target);
            if(target.nodeName!='UL' && target!=drag){
              let targetTop=target.getBoundingClientRect().top;
              let dragTop=drag.getBoundingClientRect().top;
              let targetOffset=targetTop-dragTop;
              let dragOffset=targetTop-this.dragStartPos;

              //样式变化
              let targetStyle= target.style.transform;
              let lastTransform=0;
              if(targetStyle){
                  lastTransform=this.getTransform(targetStyle);
              }
              drag.style.transform=`translateY(${dragOffset}px)`;
              target.style.transform=`translateY(${lastTransform-targetOffset}px)`;
            }
          }
        },
        onMouseUp(){
          this.isDonw=false;
          this.draging=null;
          this.dragStartPos=0;
          let res=[]
          for(let i=0;i<this.nodes.length;i++){
            let item=this.nodes[i];
            let transform=this.getTransform(item.style.transform);
            if(transform){
              res[i+transform/this.itemHeight]=this.data[i];
            }else{
              res[i]=this.data[i];
            }
            item.style.transform='';
            item.style.transition='';
          }
          this.data=[...res];
          console.log(res)
        },
        getTransform(style){
          if(style){
            let firstIndex=style.indexOf('(')+1;
            let lastIndex=style.indexOf(')')-2;
            return parseInt(style.substring(firstIndex,lastIndex))
          }
        },
        _index(el){
          let domData=Array.from(this.$refs.parentNode.childNodes);
          return domData.findIndex(i=>i.innerText==el.innerText);
        }
      }
    })
  </script>
</html>
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值