Sortablejs之clone模式的避坑指南

687 阅读5分钟

在做单流程图管理的时候,因为想要流程图可以快速拖拽排序,且需求的流程是单线流程,对流程图样式要求也不高。于是在查阅各个流程图插件资料后,决定另辟蹊径,选择了用【Sortablejs的clone模式】来实现该功能,流程图样式就纯纯用伪元素来标上箭头→串联起来。因为中间遇到了很多和clone模式相关的小问题,于是特此记录下来。本文会先重点阐述一下三个卡点问题,最后再放上全流程代码

先附上相关文档

实现效果

卡点问题

  1. 列表1克隆到列表2后,手动同步修改列表2数据的时候,列表2出现了重复的DOM;
  2. 列表1克隆到列表2后,列表1被克隆的DOM失去了响应性。
  3. 克隆过去列表2的DOM,样式不跟着列表2改变

注:如果问题1、2同时存在,请先看问题2解决方案,可一并解决问题1。

问题1

因为Sortablejs的拖拽纯粹只是改变DOM,不会修改数据源,因此需要自己同步处理数据。但是克隆后,手动同步列表2的数据的时候,列表2多了一个DOM。

也能理解,列表2数据新增的时候,列表2相应也会渲染新增的数据对应的DOM;同时Sortablejs又克隆了一个DOM放进来,于是想增加一个数据,却新增了两个DOM。

那就直接移除克隆过来的 DOM就行了,让列表2的新增删除都通过响应数据渲染,同时这样也不用额外去处理克隆过来的DOM的样式了。

//列表2的初始化Sortablejs实例
function initList2Sortable() {
  let list2DOM = document.getElementById('list2')

  sortableList2.value = new Sortable(list2DOM , {
    ...
    onAdd: function (evt) {
      const { target, item, newIndex, oldIndex } = evt
      // 如果元素从列表1克隆到了列表2,手动新增列表2数据
      List2.value.splice(newIndex, 0, List1.value[oldIndex]
      // 移除Sortable.js自动添加的元素
      target.removeChild (item)
    }
  })
}

问题2

我给列表1做了个判断,当列表1的节点存在于列表2时,则给该节点加上disabled的类。

但把节点从列表1克隆到列表2的时候,左边的节点DOM没有响应。

尝试着先不移除列表2被克隆过来的DOM,发现它是具有响应性的,反而是列表1的DOM失去了响应性。

因此我推测:

sortablejs的克隆模式虽然看起来好像是从列表1复制了一个DOM到列表2,

但实际上是移动了 DOM 到列表2之后,再将DOM 浅拷贝 加回列表1,此时列表1的DOM就失去了响应性

知道了问题点,要解决起来也很简单,在克隆之后强行让列表1重新根据数据渲染一次即可,同时重新初始化列表1的Sortable实例。👈这是我原本的做法,但在编写文档的时候看到了这篇文章 SortableJS 的那些 "坑"前言 SortableJS 是一个功能强大的 JavaScript 拖拽库,但在使用 - 掘金 ,感觉这个方法更好些,即直接往列表1插入有响应性的 DOM ,再移除没有响应性的DOM

先补充个知识点👇

insertBefore():将一个节点插入到指定父节点的子节点中,并位于参考节点之前。

如果给定的节点已经存在于文档中,insertBefore() 会将其从当前位置移动到新位置。(也就是说,它会在附加到指定的新父节点之前自动从现有的父节点中移除。)

// 列表1的初始化Sortablejs实例
function initList1Sortable() {
  let list1DOM = document.getElementById('list1')
  sortableList1.value = new Sortable(list1DOM, {
    ...
    onEnd: function (/**Event*/ evt) {
      const { target, clone, item } = evt
      //插入带有响应性的DOM(此时列表2的item也被移除,问题1解决)
      target.insertBefore(item, clone)
      //移除失去响应性的DOM
      target.removeChild(clone)
    }
  })
}

问题3

虽然克隆到列表2的节点实际上已被我移除了,但是移动还没结束的时候会有个示意位置的DOM,可以看到是没有样式的,为了完美的效果,还是找了一下问题所在。

结果发现这和样式的隔离和穿透有关,这里的列表1和列表2被我分成了两个组件,并且在组件里都给style加上了scoped属性。scoped属性会给组件每个标签加上唯一标识,因为是两个组件,因此标识是不一样的。当DOM从组件1拖到组件2的时候,还保留着组件1的标识,样式隔离了。

解决方式:只要给组件2的节点样式加上deep来穿透就可以啦!

#link {
    :deep(.box) { 
        ...
    }
}

全代码

1.安装Sortablejs

init i Sortablejs

2.引入Sortable并初始化实例

import Sortable from 'sortablejs'

function initList1Sortable() {
  let list1DOM = document.getElementById('list1')
  sortableList1.value = new Sortable(list1DOM, {
     group: {
      name: 'shared',
      pull: 'clone',
      put: false // 不允许拖拽进这个列表
    },
    filter: '.disabled', //class有disabled的DOM不能拖拽
    animation: 150,
    onEnd: function (/**Event*/ evt) {
      const { target, clone, item, to, from } = evt
      //判断如果从列表1移到了列表2,才处理
      if (to !== from) {
        //插入带有响应性的DOM,此时列表2的item也被移除
        target.insertBefore(item, clone)
        //移除失去响应性的DOM
        target.removeChild(clone)
      }
    }
  })
}

function initList2Sortable() {
  let list2DOM = document.getElementById('list2')

  sortableList2.value = new Sortable(list2DOM , {
    group: 'shared',
    animation: 150,
    // 拖拽过程中跟随着鼠标移动的DOM的样式类
    dragClass: 'sortable-drag',
    // 拖动的手柄的对应的类
    handle: '.node-name',
    onEnd: function (/**Event*/ evt) {
      //判断确实移动了位置,才处理
      if (evt.oldIndex !== evt.newIndex) {
        let tempNode = tempLink.value.splice(evt.oldIndex, 1)
        tempLink.value.splice(evt.newIndex, 0, tempNode[0])
      }
    },
    onAdd: function (evt) {
      const { newIndex, oldIndex } = evt
      // 如果元素从列表1克隆到了列表2,手动新增列表2数据
      List2.value.splice(newIndex, 0, List1.value[oldIndex]
      // 上面的target.insertBefore(item, clone)已经移除了克隆过来的DOM,无需再移除
      // target.removeChild(item)
    }
  })
}

//初始化实例,要在列表渲染完成后
onMounted(() => {
    initList1Sortable()
    initList2Sortable()
})