sortablejs + el-tree 在移动端的树形拖拽排序实现

612 阅读5分钟

演示

demo.gif

项目源码

简介

sortablejs 是一个功能强大的 JavaScript 拖拽库,支持鼠标拖拽与触屏拖拽。 官网Demo地址

el-treeelement-ui 的一个树形组件,本身是提供拖拽排序的,但是不支持移动端,因为 dragstart、dragover 等事件不支持触屏,需要用到 touchstart 等。不改变 el-tree 源码情况下,结合 sortablejs 实现移动端的拖拽排序。

安装

npm install sortablejs --save

引入项目

import Sortable from 'sortablejs'

vue2 使用示例

<template>
    <ul id="items">
	   <li>item 1</li>
	   <li>item 2</li>
	   <li>item 3</li>
    </ul>
</template>
 
<script>
import Sortable from 'sortablejs';
 
const el = document.getElementById('items');
const sortable = new Sortable(el);
// const sortable = Sortable.create(el);
 
</script>

配置项

const sortable = new Sortable(el, {
	group: "name",  // or { name: "...", pull: [true, false, 'clone', array], put: [true, false, array] }
	sort: true,  //是否可以在列表内排序,默认开启
	delay: 0, //拖拽延迟时间(通常用于移动端实现长按拖拽)
	delayOnTouchOnly: false, //只有当用户使用触摸时才会延迟
	touchStartThreshold: 0, // 在取消延迟拖动事件之前,应该移动多少像素(px)
	disabled: false, //如果设置为true,则禁用可排序。
	store: null,  // @see Store
	animation: 150,  // ms,排序时移动项目的动画速度,设置0则无过渡动画
	easing: "cubic-bezier(1, 0, 0, 1)", //渐变动画,默认为null,可访问https://easings.net/查看示例
	handle: ".my-handle",  // 需要绑定的元素(比如我们想长按某个盒子来拖拽其父元素,那么此时我们仅需要给这个盒子添加这里配置的类型即可)
	filter: ".ignore-elements",  //不需要进行拖动的元素(给元素添加这个选项配置的类名后,该元素将不会被拖动)
	preventOnFilter: true, // 当触发filter时调用event.preventDefault()
	draggable: ".item",  // 指定元素内的哪些项可以被拖动
	dataIdAttr: 'data-id', // HTML attribute that is used by the `toArray()` method
	ghostClass: "sortable-ghost",  //占位元素类名,可以对此类名进行配置以实现特定效果(拖拽时,sortablejs会生成一个占位元素占据元素的位置,此类名主要用于控制这个元素的样式)
	chosenClass: "sortable-chosen",  //所选项目的类名
	dragClass: "sortable-drag",  //拖动项的类名
	swapThreshold: 1, //交换区阈值
	invertSwap: false, //如果设置为true,将始终使用反向交换区
	invertedSwapThreshold: 1, //反转交换区的阈值(默认设置为swapThreshold值)
	direction: 'horizontal', // 可分拣方向(如果没有给出,将自动检测)
	forceFallback: false,  //忽略HTML5 行为并强制回退
	fallbackClass: "sortable-fallback",  //使用forceFallback时克隆的DOM元素的类名
	fallbackOnBody: false,  //将克隆的DOM元素附加到文档正文中
	fallbackTolerance: 0, //以像素为单位指定鼠标在被视为拖动之前应该移动多远。
	dragoverBubble: false,//拖拽经过时是否冒泡
	removeCloneOnHide: true, //当clone元素未显示时将其删除,而不是将其隐藏
	emptyInsertThreshold: 5, //px,鼠标距离必须为空可排序,才能将拖动元素插入其中
 
 
	setData: function (/** DataTransfer */dataTransfer, /** HTMLElement*/dragEl) {
		dataTransfer.setData('Text', dragEl.textContent); //HTML5 DragEvent的DataTransfer对象
	},
 
	//元素被选中时触发
	onChoose: function (/**Event*/evt) {
		evt.oldIndex;  //当前元素在父元素中的索引
	},
 
	//元素未被选中时(与onEnd事件相似)
	onUnchoose: function(/**Event*/evt) {
		// same properties as onEnd
	},
 
	//开始拖拽时
	onStart: function (/**Event*/evt) {
		evt.oldIndex;  //当前元素在父元素中的索引
	},
 
	//拖拽完成时
	onEnd: function (/**Event*/evt) {
		var itemEl = evt.item;  //被拖拽的元素
		evt.to;    //目标列表
		evt.from;  //初始列表
		evt.oldIndex;  //初始列表的初始索引(即拖拽改变之前的列表索引)
		evt.newIndex;  //元素在新父级中的新索引(即拖拽之后形成的列表的索引)
		evt.oldDraggableIndex; //元素在旧父元素内的旧索引,仅计算可拖动元素		 
        evt.newDraggableIndex; //元素在新父级中的新索引,仅计算可拖动元素
		evt.clone //克隆元素		
        evt.pullMode;  //当项目位于另一个可排序的位置时:如果是克隆,则为“clone”;如果是移 
        动,则为true
 
	//元素从另一个列表中删除到列表中
	onAdd: function (/**Event*/evt) {
		//与onEnd具有相同的属性
	},
 
	//更改列表中的排序
	onUpdate: function (/**Event*/evt) {
		//与onEnd具有相同的属性
	},
 
	//由列表的任何更改调用(添加/更新/删除)
	onSort: function (/**Event*/evt) {
		//与onEnd具有相同的属性
	},
 
	//元素从列表中删除到另一个列表中
	onRemove: function (/**Event*/evt) {
		//与onEnd具有相同的属性
	},
 
	//尝试拖动已过滤的元素
	onFilter: function (/**Event*/evt) {
		var itemEl = evt.item;  //HTMLElement接收到“mousedown”、“tapstart”事件。
	},
 
	//在列表中或列表之间移动项目的事件
	onMove: function (/**Event*/evt, /**Event*/originalEvent) {
		// Example: https://jsbin.com/nawahef/edit?js,output
		evt.dragged; // 被拖动元素
		evt.draggedRect; // DOMRect {left, top, right, bottom}
		evt.related; // HTMLElement on which have guided
		evt.relatedRect; // DOMRect
		evt.willInsertAfter; //布尔值,默认为真。为真的情况下Sortable将在目标后插入拖动元素
		originalEvent.clientY; //鼠标位置
		// return false; — for cancel
		// return -1; — 在目标之前插入
		// return 1; — 在目标之后插入
		// return true; —根据方向保留默认插入点
		// return void; —根据方向保留默认插入点
	},
 
	//创建元素克隆时调用
	onClone: function (/**Event*/evt) {
		var origEl = evt.item;
		var cloneEl = evt.clone;
	},
 
	//当拖动元素改变位置时调用
	onChange: function(/**Event*/evt) {
		evt.newIndex //使用此事件的最可能原因是获取拖动元素的当前索引
		// same properties as onEnd
	}
});

el-tree 移动端拖拽排序实现

html 代码

<template>
  <div class="app-container">
    <!-- default-expand-all 节点要默认展开,否则子节点初始不渲染就会导致获取不到dom元素 -->
    <el-tree :data="treeList" default-expand-all />
  </div>
</template>

js 代码

<script>
import Sortable from 'sortablejs'

export default {
  name: 'HelloWorld',
  data() {
    return {
      treeList: [{
        id: '1',
        label: '一级 1',
        children: [{id: '1-1', label: '二级 1-1'}, {id: '1-2', label: '二级 1-2'}, {id: '1-3', label: '二级 1-3'}]
      }, {
        id: '2',
        label: '一级 2',
        children: [{id: '2-1', label: '二级 2-1'}, {id: '2-2', label: '二级 2-2'}, {id: '2-3', label: '二级 2-3'}]
      }],
      defaultProps: {
        children: 'children',
        label: 'label'
      },

      // 树形数据的平铺(深度优先)
      flatDataList: [],

      // sortable 对象数组
      sortableObjList: []
    }
  },

  mounted() {
    this.$nextTick(() => {
      this.initSortable()
    })
  },

  methods: {
    initSortable() {
      // 树形数据展开
      const flatDataList = []
      this.flatTree(this.treeList, flatDataList)

      // 给 el-tree role=treeitem 节点添加属性 data-id = unitId
      document.querySelectorAll('div[role=treeitem]').forEach((el, i) => {
        el.setAttribute('data-id', flatDataList[i].id)
      })
      // el-tree 一级节点会有一个多余的dom干扰排序结果,这里 id 标记为 null
      document.querySelector('.el-tree .el-tree__drop-indicator')?.setAttribute('data-id', null)

      // 找到一级节点的父级 role=tree
      const fatherEls = document.querySelectorAll('div[role=tree]') || []
      // 找到全部下级的 el-tree role=group 节点(即 role=treeitem 的父节点)
      const childrenEls = document.querySelectorAll('div[role=group]') || []
      // 合并
      const els = [...fatherEls, ...childrenEls]

      // 创建拖拽对象
      const sortableList = []
      els.forEach((el, i) => {
        sortableList.push(
          new Sortable(el, {
            dataIdAttr: 'data-id',
            ghostClass: 'sortable-ghost',
            dragClass: 'sortable-drag',
            onEnd: (evt) => {
              // 拖拽结束,且位置发生变化
              if (evt.newIndex !== evt.oldIndex) {
                // 获取当前排序后的 id 列表
                const ids = sortableList[i].toArray().filter(t => t !== null)
                console.log('新的排序', ids)
              }
            }
          })
        )
      })
    },

    // 树形数据展开(深度优先)
    flatTree(treeList, result=[]) {
      treeList.forEach(t => {
        result.push(t)
        if (t.children && t.children.length) {
          this.flatTree(t.children, result)
        }
      })
    }
  }
}
</script>
<style>
.sortable-ghost {
  background-color: #C4E1FF;
}
.sortable-drag {
  background-color: #f5f7fa;
}
</style>

<style scoped>
.app-container{
  padding: 0 15px;
}
</style>