web架构师编辑器内容-图层拖动排序功能的开发

新的学习方法
  • 用手写简单方法实现一个功能
  • 然后用比较成熟的第三方解决方案
  • 即能学习原理又能学习第三方库的使用

从两个DEMO开始

列表排序的三个阶段
  • 拖动开始(dragstart)
    • 被拖动图层的状态变化
    • 会出一个浮层
  • 拖动进行中(dragmove)
    • 浮层会随着鼠标移动
    • 条目发生换位:当浮层下沿超过被拖条目二分之一的时候,触发换位。这是一个比较独特的需求
  • 松开鼠标阶段(drop)
    • 浮层消失
    • 被拖动图层状态复原
    • 数据被更新
拖动排序功能开发

第一阶段 Dragstart

  • 被拖动图层的状态变化 常规做法
    • 添加mouseDown事件,检查当前的target是那个元素,然后给他添加特定的状态
    • 添加mouseMove事件,创一个和被拖动元素一模一样的的浮层,将它的定位设置
      它的定位为绝对定位,并且随着鼠标的坐标更新。
  • 使用HTML的Drag特性
    • 文档地址:拖拽操作
    • 浏览器的默认拖拽行为:支持图象,链接和选择的文本
    • 其他元素默认情况是不可拖拽的。
    • 如果想可以拖拽可以设置为draggable = true
    • 使用dragstart事件监控拖动开始,并设置对应的属性

LayerList组件中添加draggable属性

js 复制代码
// LayerList.vue
<li
  class="ant-list-item"
  v-for="item in list" :key="item.id"
  :class="{ active: item.id === selectedId }"
  @click="handleClick(item.id)"
  draggable="true"
></li>

这样就可以有效果了:当拖动对应条目的时候,它会自动生成半透明的条目,并且跟随鼠标的移动。

接下来就开始使用dragstart事件监控拖动开始,并设置对应的属性

  • 给被拖动元素添加特定的状态:使用一系列的事件来监控拖动的进度,使用dragStart开始拖动操作
js 复制代码
// LayerList.vue
// html部分
<li
  class="ant-list-item"
  v-for="item in list" :key="item.id"
  :class="{ active: item.id === selectedId, ghost: dragData.currentDragging === item.id}"
  @click="handleClick(item.id)"
  @dragstart="onDragStart($event, item.id)"
  draggable="true"
></li>
// js部分(setup)
const dragData = reactive({
  currentDragging: ''
})
const onDragStart = (e: DragEvent, id: string ) => {
  dragData.currentDragging = id;
}
// css部分
.ant-list-item.ghost {
  opacity: 0.5;
}

完成出来的效果:

接下来就是在鼠标松开的时候,特定的状态消失:使用drop事件:

js 复制代码
<ul :list="list" class="ant-list-items ant-list-border" @drop="onDrop"></ul>
const onDrop = (e: DragEvent ) => {
  dragData.currentDragging = '';
}

但是这样做发现不起作用,后来发现是onDrog事件并没有触发,原因:
dragenter 或 dragover 事件的监听程序用于表示有效的放置目标,也就是被拖拽项目可能放置的地方。网页或应用程序的大多数区域都不是放置数据的有效位置。因此,这些事件的默认处理是不允许放置。
指定放置对象

因为网页大部分区域不是有效的放置位置,这些事件的默认处理都是不允许放置,所以这个行为并不会被触发。
如果你想要允许放置,你必须取消 dragenter 和 dragover 事件来阻止默认的处理。你可以在属性定义的事件监听程序返回 false,或者调用事件的 preventDefault() 方法来实现这一点。在一个独立脚本中的定义的函数里,可能后者更可行。

最终添加阻止默认行为事件:

js 复制代码
<ul :list="list" class="ant-list-items ant-list-border" @drop="onDrop" @dragover="onDragOver">

const onDragOver = (e: DragEvent) => {
  e.preventDefault()
}
处理松开鼠标时进行排序
  1. 修改dragData 添加一个当前索引的属性

    typescript 复制代码
        const dragData = reactive({
          currentDragging: '',
          currentIndex: -1,
        });
  2. @dragstart="onDragStart($event, item.id, index)" 方法中多添加一个index参数

    typescript 复制代码
        const onDragStart = (e: DragEvent, id: string, index: number) => {
          dragData.currentDragging = id;
          dragData.currentIndex = index;
        };

有了开始拖动的index之后,我们要知道drop的时候新的index,我们怎么在onDrop方法中拿到新的index呢?因为在onDrop中我们的参数是event,使用event.target可以拿到dom元素,把最新的index放到dom元素上面就可以了,使用:HTMLElement.dataset

  1. 使用 HTMLElement.dataset拿到最新的索引

HTMLElement.dataset属性允许无论是在读取模式和写入模式下访问在HTML 或 DOM中元素上设置的

所有自定义数据属性(data-*)集

它是一个DOMString的映射,每个自定义数据属性的一个条目。

请注意,dataset属性本身可以被读取,但不能直接写入,相反,所有的写入必股友是它的属性,这反过来

表示数据属性。

还要注意,一个HTML data-attribute 及其对应的DOM dataset.property 不共享相同的名称,但它

们总是相似的:

typescript 复制代码
       <li
         class="ant-list-item"
         :class="{
           active: item.id === selectedId,
           ghost: dragData.currentDragging === item.id,
         }"
         v-for="(item, index) in list"
         :key="item.id"
         @click="handleClick(item.id)"
         @dragstart="onDragStart($event, item.id, index)"
         :data-index="index"
         draggable="true"
       >
  1. 修改onDrop事件
ts 复制代码
const onDrop = (e: DragEvent) => {
  const currentEle = e.target as HTMLElement;
  if (currentEle.dataset.index) {
    const moveIndex = parseInt(currentEle.dataset.index);
    console.log(moveIndex);
  }
  dragData.currentDragging = "";
};

但是这样写moveIndex是不一定存在的,因为e.target是鼠标指向的元素,所以当在目标子元素上面进行释放的话,就会把目标当成子元素,比如如果释放到的元素是锁元素,则currentEle就是锁元素。所以这里需要一个方法来向上进行检索,找到符合条件的父元素。

js 复制代码
export const getParentElement = (element: HTMLElement, className: string) => {
  while (element) {
    if (element.classList && element.classList.contains(className)) {
      return element;
    } else {
      element = element.parentNode as HTMLElement;
    }
  }
  return null;
};
typescript 复制代码
    const onDrop = (e: DragEvent) => {
      const currentEle = getParentElement(
        e.target as HTMLElement,
        'ant-list-item'
      );
      if (currentEle && currentEle.dataset.index) {
        const moveIndex = parseInt(currentEle.dataset.index);
        // 使用第三方库arrayMove改变数组
        arrayMove.mutate(props.list, dragData.currentIndex, moveIndex);
      }
      dragData.currentDragging = '';
    };

array-move

最终实现的效果:

在拖动时完成排序:
ts 复制代码
    const onDragEnter = (e: DragEvent, index: number) => {
      // 这里的判断是为了避免完成转换后,触发新的一次dragEnter事件
      if (index !== dragData.currentIndex) {
        console.log('enter', index, dragData.currentIndex);
        arrayMove.mutate(props.list, dragData.currentIndex, index);
        dragData.currentIndex = index;
        end = index
      }
    };

这样就可以在拖动时完成排序了,onDrop里面就不需要进行同样的操作了,修改一下onDrop事件

ts 复制代码
let start = -1;
let end = -1;
const onDragStart = (e: DragEvent, id: string, index: number) => {
  dragData.currentDragging = id;
  dragData.currentIndex = index;
  start = index;
};
const onDrop = (e: DragEvent) => {
  context.emit('drop', { start, end})
  dragData.currentDragging = '';
};

现在就完成了可拖动排序的简单编码,主要掌握三个阶段:

  1. 排序开始:监控被拖拽的元素,添加特殊状态和UI
  2. 移动阶段:进入别的列表的时候完成数据的交换
  3. drop阶段:松开按钮的时候,将状态恢复原状,并且发送对应的事件。

使用第三方库进行排序:

使用Vue Draggable进行排序:

vue.draggable.next

bash 复制代码
npm i -S vuedraggable@next

将用draggable替换掉ul

typescript 复制代码
<template>
  <draggable
    :list="list"
    class="ant-list-items ant-list-bordered"
    ghost-class="ghost"
    handle=".handle"
    item-key="id"
  >
    <template #item="{ element }">
      <li
        class="ant-list-item"
        :class="{ active: element.id === selectedId }"
        @click="handleClick(element.id)"
      >
        <a-tooltip :title="element.isHidden ? '显示' : '隐藏'">
          <a-button
            shape="circle"
            @click.stop="
              handleChange(element.id, 'isHidden', !element.isHidden)
            "
          >
            <template v-slot:icon v-if="element.isHidden"
              ><EyeInvisibleOutlined />
            </template>
            <template v-slot:icon v-else><EyeOutlined /> </template>
          </a-button>
        </a-tooltip>
        <a-tooltip :title="element.isLocked ? '解锁' : '锁定'">
          <a-button
            shape="circle"
            @click.stop="
              handleChange(element.id, 'isLocked', !element.isLocked)
            "
          >
            <template v-slot:icon v-if="element.isLocked"
              ><LockOutlined/>
            </template>
            <template v-slot:icon v-else><UnlockOutlined  /> </template>
          </a-button>
        </a-tooltip>
        <inline-edit
          class="edit-area"
          :value="element.layerName"
          @change="
            (value) => {
              handleChange(element.id, 'layerName', value);
            }
          "
        ></inline-edit>
        <a-tooltip title="拖动排序">
          <a-button shape="circle" class="handle">
            <template v-slot:icon><DragOutlined /> </template
          ></a-button>
        </a-tooltip>
      </li>
    </template>
  </draggable>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import draggable from 'vuedraggable';
import {
  EyeOutlined,
  EyeInvisibleOutlined,
  LockOutlined,
  UnlockOutlined,
  DragOutlined,
} from '@ant-design/icons-vue';
import { ComponentData } from '../store/editor';
import InlineEdit from '../components/InlineEdit.vue';
export default defineComponent({
  props: {
    list: {
      type: Array as PropType<ComponentData[]>,
      required: true,
    },
    selectedId: {
      type: String,
      required: true,
    },
  },
  emits: ['select', 'change', 'drop'],
  components: {
    EyeOutlined,
    EyeInvisibleOutlined,
    LockOutlined,
    UnlockOutlined,
    InlineEdit,
    draggable,
    DragOutlined,
  },
  setup(props, context) {
    const handleClick = (id: string) => {
      context.emit('select', id);
    };
    const handleChange = (id: string, key: string, value: boolean) => {
      const data = {
        id,
        key,
        value,
        isRoot: true,
      };
      context.emit('change', data);
    };
    return {
      handleChange,
      handleClick,
    };
  },
});
</script>

<style scoped>
.ant-list-item {
  padding: 10px 15px;
  transition: all 0.5s ease-out;
  cursor: pointer;
  justify-content: normal;
  border: 1px solid #fff;
  border-bottom-color: #f0f0f0;
}
.ant-list-item.active {
  border: 1px solid #1890ff;
}
.ant-list-item.ghost {
  opacity: 0.5;
}

.ant-list-item:hover {
  background: #e6f7ff;
}
.ant-list-item > * {
  margin-right: 10px;
}
.ant-list-item button {
  font-size: 12px;
}

.ant-list-item .handle {
  cursor: move;
  margin-left: auto;
}
.ant-list-item .edit-area {
  width: 100%;
}
</style>
相关推荐
徐小夕6 小时前
我用 AI 撸了个开源"万能预览器":浏览器直接打开 Office、CAD 和 3D 模型
前端·vue.js·github
小码哥_常6 小时前
Flutter Android 延迟加载代码指南:提升应用性能的关键
前端
这是个栗子6 小时前
TypeScript(三)
前端·javascript·typescript·react
kvo7f2JTy6 小时前
基于机器学习算法的web入侵检测系统设计与实现
前端·算法·机器学习
北风toto6 小时前
前端CSS样式详细笔记
前端·css·笔记
nanfeiyan6 小时前
git commit
前端
【ql君】qlexcel8 小时前
Visual Studio Code的使用,VS code常用扩展
ide·vscode·编辑器·visual studio·扩展
前端精髓9 小时前
移除 Effect 依赖
前端·javascript·react.js
码云之上9 小时前
从一个截图函数到一个 npm 包——pdf-snapshot 的诞生记
前端·node.js·github
码事漫谈10 小时前
AI提效,到底能强到什么程度?
前端·后端