likes
comments
collection
share

SortableJS实现拖动排序

作者站长头像
站长
· 阅读数 44

概述

实际项目中,偶尔会遇到关于拖动相关的业务,比较常见的就是拖动调整顺序,前端比较知名的拖动库Sortable,封装的很完善,包括位置计算,过渡转化等,这里记录一期使用Sortable实现拖动排序。

最终效果

SortableJS实现拖动排序

代码

框架使用vue3

<template>
  <div class="sortjs-demon">
    <div class="drag-content">
      <div class="draggle-box" ref="okuabriginBox">
        <div
          :class="['origin-box', 'box-item']"
          v-for="(item, index) in originData"
          :key="item.id"
          ref="boxItem"
          :data-id="item.id"
        >
          {{ item.label }}
          <div class="my-handle"></div>
          <div class="ignore-elements">禁止拖动</div>
        </div>
      </div>
      <div
        :class="['draggle-box', 'target-box', enterActive ? 'active' : '']"
        ref="targetBox"
      >
        <div
          class="box-item"
          v-for="item in targetData"
          :data-id="item.id"
          :key="item.id"
        >
          {{ item.label }}
          <div class="my-handle"></div>
          <div class="ignore-elements">禁止拖动</div>
        </div>
      </div>
    </div>
    <div class="text-content">
      <div class="box">originData: {{ stringFy(originData) }}</div>
      <div class="box">targetData: {{ stringFy(targetData) }}</div>
    </div>
    <!-- <button @click="handleAdd">添加</button> -->
  </div>
</template>

<script lang="ts" setup>
import Sortable from "sortablejs";
import { nextTick, onMounted, ref, watch } from "vue";

const backupList = [
  {
    id: 1,
    label: "第一个",
  },
  {
    id: 2,
    label: "第二个",
  },
  {
    id: 3,
    label: "第三个",
  },
  {
    id: 4,
    label: "第四个",
  },
];
let originData = ref(backupList);
let targetData: any = ref([]);
const originBox = ref<HTMLElement>();
const targetBox = ref<HTMLElement>();
const enterActive = ref(false);
const stringFy = JSON.stringify;
watch(originData, () => {
  console.log("listDataChange", originData.value);
});
watch(targetData, () => {
  console.log("targetListChange", targetData.value);
});
function initDraggle() {
  const option = {
    sort: true, //拖动过程中是否调整对应排列顺序
    disabled: false, //是否禁止脱拽
    animation: 300, //动画延时
    group: {
      name: "shared", //用于在多个列表之间拖动分组名(相同的才能互相拖拽到对应列表
      //   pull: "clone",//pull===>默认为移动过去,拷贝过去则设置true
      put: true, //是否允许添加到这个列表
    },
    handle: ".my-handle", //拖动句柄,设置之后,只有拖动对应项中的设置当前类的元素才能生效
    filter: ".ignore-elements", //添加当前类的元素会忽略拖动
    preventOnFilter: true,
    ghostClass: "ghostClass", //占位元素类名
    chosenClass: "chosenClass", //选中元素类名
    dragClass: "dragClass", //拖拽中元素
    invertSwap: false, //是否反转交换
    forceFallback: true, //TODO:禁止h5拖拽出的阴影(内部本质上模拟了拖拽出的阴影部分,克隆的一份原来的dom元素进行定位)
    fallbackClass: "fallbackClass", //使用forceFallback时克隆的DOM元素的类名(等同于dragClass)
    removeCloneOnHide: true, //当克隆元素不显示时删除它,而不是仅仅隐藏它
    // direction: 'vertical',
    // easing: "cubic-bezier(0.55, 0, 1, 0.45)",//动画曲线
    // setData: function (dataTransfer: any, dragEl: any) {
    //   console.log(dataTransfer, dragEl);
    // },
    // 按下
    // onChoose: function (evt: any) {
    //   console.log("onChoose", evt);
    // },
    // 松开按下
    // onUnchoose: function (evt: any) {
    //   console.log("onUnChoose", evt);
    // },
    // 脱拽开始==>dragstart
    // onStart: function (evt: any) {
    //   console.log("onStart", evt);
    // },
    // 脱拽结束
    // onEnd: function (evt: any) {
    //   // var itemEl = evt.item;  //当前脱拽的元素
    //   // evt.to;    // 进入到的目标列表
    //   // evt.from;  // 原来的脱拽列表
    //   // evt.oldIndex;  // 脱拽元素在原来的位置的索引
    //   // evt.newIndex;  // 脱拽元素在当前位置的索引
    //   console.log("onEnd", evt);
    // },
    // 元素从另一个列表中拖放到列表中
    onAdd: function (evt: any) {
      if (evt.to === originBox.value) {
        updateData(
          originBox.value!,
          originData,
          targetData,
          Number(evt.item.dataset.id)
        );
      } else if (evt.to === targetBox.value) {
        updateData(
          targetBox.value!,
          targetData,
          originData,
          Number(evt.item.dataset.id)
        );
      }
    },
    // TODO:脱拽后改变原来dom结构位置的时候(可以做响应式数据交换,脱拽排序更新等)===>同一个列表变化
    onUpdate: function (evt: any) {
      console.log("onUpdate", evt);
      if (evt.from === evt.to && evt.from === originBox.value) {
        const oldIndex = evt.oldIndex;
        const newIndex = evt.newIndex;
        const temp = originData.value[oldIndex];
        originData.value[oldIndex] = originData.value[newIndex];
        originData.value[newIndex] = temp;
      } else if (evt.from === evt.to && evt.from === targetBox.value) {
        const oldIndex = evt.oldIndex;
        const newIndex = evt.newIndex;
        const temp = targetData.value[oldIndex];
        targetData.value[oldIndex] = targetData.value[newIndex];
        targetData.value[newIndex] = temp;
      }
    },
    // 通过对列表的任何更改(添加/更新/删除)调用
    // onSort: function (evt: any) {
    //   console.log("onSort", evt);
    // },
    // element从列表中移除到另一个列表中
    // onRemove: function (evt: any) {
    //   console.log("onRemove", evt);
    // },
    // 尝试拖动已过滤的元素(配置过filter属性的元素)
    // onFilter: function (evt: any) {
    //   console.log("onFilter", evt);
    // },
    // 当您在列表中或在列表之间移动项目时调用
    onMove: function (evt: any, originalEvent: any) {
      evt.dragged; // 当前拖拽元素
      evt.draggedRect; //拖拽元素的位置信息(本质上调用getBoundingClientRect())
      evt.related; // 拖拽关联的元素
      evt.relatedRect; // 拖拽关联元素的位置信息
      evt.willInsertAfter; // Boolean 如果Sortable默认会在目标后插入拖动元素,则为true
      originalEvent.clientY; // 当前鼠标Y位置
      originalEvent.clientX; // 当前鼠标X位置
      // return false; — for cancel//取消位置交换
      // return -1; — insert before target//插入到目标元素前面
      // return 1; — insert after target//插入目标元素后面
      // return true; —根据方向保留默认插入点
      // return void; —跟返回true一样
      // console.log("onMove", evt, originalEvent);
    },
    // 克隆元素时候调用(forceFallback:true,或者拖拽到另外一个列表时候调用)
    // onClone: function (evt: any) {
    //   console.log("onClone", evt);
    // },
    //当拖动元素改变位置时调用
    // onChange: function (/**Event*/ evt: any) {
    //   console.log("onChange", evt);
    // },
  };
  Sortable.create(originBox.value, option);
  Sortable.create(targetBox.value, option);
}

function updateData(
  dom: HTMLElement,
  updateListRef: any,
  originDataListRef: any,
  id: number
) {
  nextTick(() => {
    const targetChildren = [...dom.children] as HTMLElement[];
    const res: any = [];
    targetChildren.forEach((element) => {
      const findItem = backupList.find(
        (item) => item.id === Number(element.dataset.id)
      );
      if (findItem) {
        res.push(findItem);
      }
    });
    updateListRef.value = res;
    originDataListRef.value = originDataListRef.value.filter(
      (item: any) => item.id !== id
    );
  });
}
onMounted(() => {
  initDraggle();
});
</script>

<style lang="less">
.sortjs-demon {
  margin-top: 30px;

  .drag-content {
    display: flex;
    justify-content: space-around;
  }
  .text-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 30px;
  }
  .my-handle {
    width: 20px;
    height: 20px;
    background-color: #008c8c;
    position: absolute;
    right: 0;
    top: 0;
  }
  .ghostClass {
    border: 1px solid red;
  }
  .chosenClass {
    // background-color: #008c8c!important;
  }
  .dragClass {
    background-color: yellow !important;
  }
  .fallbackClass {
    background-color: purple !important;
  }
  .ignore-elements {
    width: 50px;
    height: 30px;
    text-align: center;
    line-height: 30px;
    font-size: 12px;
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    background-color: green;
  }
  * {
    margin: 0;
    padding: 0;
  }
  .origin-box.dragging {
    user-select: none;
    background-color: red;
  }
  .draggle-box {
    width: 500px;
    height: 500px;
    border: 1px solid #000;
    display: flex;
  }
  .target-box {
    .box-item {
      background-color: blue;
    }
  }
  .draggle-box.active {
    border-color: red;
  }
  .box-item {
    border-radius: 50%;
    margin-right: 3px;
    text-align: center;
    line-height: 100px;
    width: 100px;
    user-select: none;
    height: 100px;
    position: relative;
  }
  .box-item.is-drag {
    background-color: #008c8c;
  }
  .origin-box {
    background-color: pink;
  }
  .draggle-box-wrap {
    display: flex;
    justify-content: space-around;
    margin-top: 30px;
  }
}
</style>