本篇是对 # Implement the Pragmatic drag and drop library的模仿
在当今不断发展的 Web 开发环境中,拖放功能已成为构建直观、交互式用户界面的关键组成部分。从文件上传到在看板中组织任务,拖放功能通过提供在网页上直观移动元素的方式,极大地提升了用户体验。
本指南全面介绍了 Pragmatic 拖放库,深入探讨其主要功能、优势以及丰富的使用场景。我们还将通过构建一个可排序列表应用,逐步学习如何在不依赖 Atlassian UI 组件的情况下,运用库的不同部分来实现功能。在本文中,您将了解如何让元素可拖动、定义放置目标、处理拖放事件,以及创建自定义的放置指示器以优化用户体验。
Pragmatic 拖放库概述
Pragmatic Drag and drop 是一个以性能为导向的拖放库,它基于原生 HTML 拖放 API,可在任意技术栈中实现无缝的拖放体验。凭借丰富的功能集,开发者可以快速实现流畅且美观的拖放效果。
在深入技术细节之前,我们先来看一看 Pragmatic 拖放库为何成为开发者首选的原因。
主要特点和优势
以下是 Pragmatic 拖放库的一些核心特性和主要优势:
- 轻量化:与其他市场上的拖放库相比,其核心包体积仅为 ~4.7kB。
- 框架无关:可与任何前端框架兼容。
- 支持多类型拖动:能够处理元素、文本、图像以及外部文件的拖动,与 react-beautiful-dnd 和 dnd-kit 等不支持文件放置的库形成鲜明对比。这意味着您只需一个库就能满足项目中各种拖放需求。
- 延迟加载:支持延迟加载核心包及可选包,从而进一步提升页面加载速度。
- 自定义功能:允许开发者自由定制可拖动元素的外观以及拖动时的预览效果。
- 跨窗口支持:利用原生拖放 API 实现跨浏览器窗口的拖动功能,这在大多数库中是无法实现的。
适用场景
Pragmatic 拖放库用途广泛,可应用于多个场景,以下是一些主要的使用案例:
- 文件上传
- 看板任务管理
- 可排序列表
- 虚拟列表
- 绘图交互
- 元素调整大小
与其他拖放库的比较
以下是 Pragmatic 拖放库与其他流行拖放库的功能对比:
| 拖放库 | Pragmatic drag and drop(element adapter) | React Beautiful Dnd | React DnD(+react-dnd-html5-backend) | DnD kit(+ @dnd-kit/modifiers + @dnd-kit/sortable) | Draggable(Shopify) |
|---|---|---|---|---|---|
| 大小 (gzip) | 4.7 kB | 31 kB | 24.8 kB | 26.9 kB | 11.8 kB |
| 大小 (minified) | 13.5 kB | 105 kB | 49.6 kB | 56.1 kB | 68.2kB |
| 延迟加载支持 | ✅ | ❌ | ❌ | ❌ | ✅ |
| 可访问性支持 | ✅(使用工具链) | ✅ | ❌ | ✅ | ❌ |
| 框架兼容性 | 任意 | React | React | React | 任意 |
| 文件拖放支持 | ✅ | ❌ | ✅ | ❌ | ❌ |
| 跨窗口拖动 | ✅ | ❌ | ❌ | ❌ | ❌ |
更详细的功能比较可以参考 此链接。
使用 Pragmatic 构建可排序列表
在本节中,我们将探索 Pragmatic 拖放库的核心概念,并通过构建一个可排序列表来了解其组件的协作方式。让我们开始吧!
设置 Vue 项目
为了快速入门,您可以使用预先创建的代码存储库,包含所有必要的代码。打开终端并运行以下命令以克隆存储库:
git clone git@github.com:BrendanEichDisciple/pragmatic-drag-and-drop-demo-list.git
克隆完成后,您将在 src/components 文件夹中找到所有组件代码,并在 src/constant.js 中找到模拟数据。
接下来,进入项目目录并安装依赖项:
cd pragmatic-drag-and-drop-demo-list
pnpm install
安装 Pragmatic 拖放包
运行以下命令安装 pragmatic-drag-and-drop 库及其所需依赖:
pnpm add @atlaskit/pragmatic-drag-and-drop @atlaskit/pragmatic-drag-and-drop-hitbox tiny-invariant
这些包包括:
- @atlaskit/pragmatic-drag-and-drop:核心拖放库。
- @atlaskit/pragmatic-drag-and-drop-hitbox:一个附加包,用于捕获放置目标的交互信息。
- tiny-invariant:轻量级库,用于开发过程中捕获潜在错误。
安装完成后,启动项目:
pnpm dev
然后,通过浏览器访问 http://localhost:5173:
目前,列表中的项尚不可拖动。在接下来的步骤中,我们将逐步实现其可拖动功能。
实现拖动功能
要让元素可拖动,我们需要使用库中提供的 draggable 方法。以下是实现步骤:
配置拖动行为
在 ListItem.vue 组件中,添加以下内容:
import { ref, onMounted, onUnmounted } from "vue";
import invariant from "tiny-invariant";
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
const { item } = defineProps({
item: {
type: Object,
required: true,
},
});
const itemRef = ref(null); // 创建一个引用
onMounted(() => {
const itemEl = itemRef.value;
invariant(itemEl); // 确保引用存在
const cleanup = draggable({
element: itemEl, // 为元素设置可拖动行为
getInitialData: () => ({ type: "item", itemId: item.id }), // 开始拖动时将数据附加到可拖动项目上
});
onUnmounted(() => {
cleanup(); // 清理拖动行为
});
});
</script>
<template>
<div class="list-item" ref="itemRef">
{{ item.label }}
</div>
</template>
现在,在我们的 ListItem 组件中,使用 ref 为元素创建一个引用。
然后,在 onMounted 生命周期钩子中,使用 invariant 函数确保 item 元素存在,再使其可拖动。
最后,调用 draggable 函数并提供具有以下键值对的对象参数:
- element — 对 item 元素的引用
- getInitialData — 在拖动开始时将 item 数据附加到可拖动项目的函数
draggable 函数返回一个 cleanup 函数,以便在组件卸载时删除其行为。这就是为什么我们从 onMounted 钩子返回它的原因。通过这些更改,我们的 ListItem 组件现在可以拖动了:
在下一节中,我们将通过向可拖动卡片添加淡入淡出效果来更进一步。
添加视觉拖动效果
为了提供视觉反馈,我们可以通过样式修改元素的不透明度。以下是实现步骤:
添加拖动状态
更新 ListItem.vue,引入一个状态变量 isDragging,并在拖动事件中对其进行控制:
<script setup>
// ... 之前的代码
const isDragging = ref(false); // 创建一个拖动状态
onMounted(() => {
const itemEl = itemRef.value;
invariant(itemEl);
const cleanup = draggable({
element: itemEl,
getInitialData: () => ({ type: "item", itemId: item.id }),
onDragStart: () => (isDragging.value = true), // 开始拖动时设置拖动状态
onDrop: () => (isDragging.value = false), // 结束拖动时清除拖动状态
});
onUnmounted(() => {
cleanup();
});
});
</script>
<template>
<!-- 当拖动时,isDragging 为 true,添加 .dragging 类 -->
<div class="list-item" ref="itemRef" :class="{ dragging: isDragging }">
{{ item.label }}
</div>
</template>
当您开始拖动卡片时,将触发 onDragStart 事件。此事件将名为 isDragging 的状态变量设置为 true。状态的这种变化有两件事:
- 向 item 添加拖动类
- 调整 item 的不透明度以创建视觉拖动效果
放下后,将触发 onDrop 事件。此事件将 isDragging 状态恢复为 false。结果如下:
- 从 item 中删除 dragging 类
- item 的原始样式会恢复
拖动类的样式在 style.css 文件中定义。
定义 item 的放置目标
要为 item 创建放置目标,我们首先需要设置可以放置 item 的放置目标。为此,pragmatic-drag-and-drop 库提供了 dropTargetForElements 函数,以使某个元素成为放置目标。
在我们的例子中,我们需要使 item 成为放置元素,即它自身既可以拖动元素,也可以成为放置目标。
定义 item 为放置目标
让我们首先将 item 设为放置目标。
首先,在 ListItem 组件中导入 dropTargetForElements 函数,并将其附加到 item 元素以将其设置为放置目标:
// 其它的导入
import {
draggable,
dropTargetForElements, // 新
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; // 新
import { attachClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; // 新
// ...之前的代码
const itemRef = ref(null);
const isDragging = ref(false);
onMounted(() => {
const itemEl = itemRef.value;
invariant(itemEl);
// 合并 draggable 和 dropTargetForElements 清理函数
// 返回单一清理函数
const cleanup = combine(
draggable({
element: itemEl,
getInitialData: () => ({ type: "item", itemId: item.id }),
onDragStart: () => (isDragging.value = true),
onDrop: () => (isDragging.value = false),
}),
// 添加 dropTargetForElements,使 item 元素成为放置目标
dropTargetForElements({
element: itemEl,
getData: ({ input, element }) => {
// 将 item 数据附加到拖放目标
const data = { type: "item", itemId: item.id };
// 将最靠近的边缘(顶部或底部)附加到数据对象
// 该数据将用于确定相对于目标 item下放 item 的位置
return attachClosestEdge(data, {
input,
element,
allowedEdges: ["top", "bottom"],
});
},
getIsSticky: () => true, // 使投放目标具有 "粘性"
onDragEnter: (args) => {
if (args.source.data.itemId !== item.id) {
console.log("onDragEnter", args);
}
},
})
);
onUnmounted(() => {
cleanup();
});
});
</script>
<template>
<div class="list-item" ref="itemRef" :class="{ dragging: isDragging }">
{{ item.label }}
</div>
</template>
我们对代码进行了一些更改,将 item 转换为放置目标。让我引导您完成每个步骤。
首先,我们使用 dropTargetForElements 函数通过附加 item 元素的 ref 使 item 成为放置目标。我们还添加了 getData 函数,用于将 item 和最近的边缘数据附加到放置目标,我们将使用它来确定可拖动项目放置到哪个 item 上。
接下来,我们添加了 getIsSticky 以使放置目标具有粘性,这有助于在放置目标之间移动时保持选择处于活动状态。
此外,我们还附加了 onDragEnter 事件,以检测可拖动项目何时进入放置目标区域。
为了有效地管理清理,我们使用库提供的 combine 函数组合了 draggable 和 dropTargetForElements 的清理函数。
要测试这些更改,只需将一条 item 拖到另一条 item 上即可。您将在控制台中看到详细的日志,包括有关拖动项目的数据和放置目标的信息。这是它的外观:
// 此日志包含我们使用 getData 和 getInitialData
// 函数附加的可拖动和下拉目标数据以及其他信息。
{
source: {...},
location: {...},
self: {...}
}
排列卡片
要排列卡片,我们可以使用库提供的 monitorForElements 函数。此函数监视诸如 onDrop 之类的拖放事件,并允许我们为移动 item 定义自定义拖放逻辑。
在我们详细了解拖放实现的细节之前,让我们先分解一下我们将处理的关键场景。这将使我们更清楚地了解该功能如何工作。
因为我们只有一列,且高度由内容决定,拖放功能的操作只有如下1种方式:
- 在列中重新排序—即将一条item拖动到同一列中的新位置
当你将一条 item 放到另一 item 上时,放置目标为 1,
- 您将鼠标悬停在其上的目标卡将充当放置目标(因为我们只在 ListItem 上绑定了可拖放属性,如果 ListView 上也绑定的话,拖放目标就为2了。你可以想象为事件的冒泡。)
现在我们已经清楚地了解了所有场景,让我们深入实施吧!
监控拖放事件
现在,打开 ListView.vue 文件,并调用 onMounted 内的 monitorForElements 钩子内的 monitorForElements 函数来监听下拉事件:
<script setup>
// ListView.vue
import { ref,
onMounted, // 新
onUnmounted // 新
} from "vue";
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; // 新
import { LIST_COLUMNS } from "../contstant.js";
import ListItem from "./ListItem.vue";
const items = ref(LIST_COLUMNS);
// 设置监听
onMounted(() => {
const cleanup = monitorForElements({
onDrop: ({ source, location }) => {
console.log(source, location);
},
});
onUnmounted(() => {
cleanup();
});
});
</script>
<template>
<div class="list">
<list-item v-for="item in items" :key="item.id" :item="item"></list-item>
</div>
</template>
现在,当您移动卡片时,您将看到一个控制台日志,其中包含有关拖动元素和放置目标的所有详细信息。 location.current.dropTargets.length 表示可放置目标的数量。
实现放置逻辑
现在我们已经设置了 monitorForElements 函数来监听 drop 事件,让我们实现处理 onDrop 事件的逻辑。
const cleanup = monitorForElements({
onDrop: ({ source, location }) => {
// 如果当前位置没有拖放目标,则提前返回
const destination = location.current.dropTargets.length;
if (!destination) {
return;
}
// 检查拖动源是否为 item,以处理 item 的特定逻辑
if (source.data.type === "item") {
// 获取被拖动 item 的 ID
const draggedItemId = source.data.itemId;
// 获取源列中被拖动 item 的索引
const draggedItemIndex = items.value.findIndex(
(item) => item.id === draggedItemId
);
}
},
});
情况 1:通过下拉到另一item上,在一列中重新排序 — 放置目标:1
为了处理这种情况,我们可以使用 getReorderDestinationIndex 函数。 我们将传递在 "使 item 成为下拉目标 "一节中附加的 closestEdgeOfTarget 数据,以便在拖动到下拉目标时知道最近的边缘是什么。 然后,我们可以使用 reorderCard 函数来重新排列卡片。
<script setup>
// 其它的引入
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; // 新
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"; // 新
import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; // 新
const reorderItem = ({ startIndex, finishIndex }) => {
// 调用重新排序功能,得到一个新的卡数组
// 包含被移动卡的新位置
items.value = reorder({
list: items.value,
startIndex,
finishIndex,
});
};
onMounted(() => {
const cleanup = monitorForElements({
onDrop: ({ source, location }) => {
const destination = location.current.dropTargets.length;
if (!destination) {
return;
}
if (source.data.type === "item") {
const draggedItemId = source.data.itemId;
const draggedItemIndex = items.value.findIndex(
(item) => item.id === draggedItemId
);
// 新
// 获取目标 item 的数据
const [destinationItemRecord] = location.current.dropTargets;
// 获取目标 item 的索引
const indexOfTarget = items.value.findIndex(
(item) => item.id === destinationItemRecord.data.itemId
);
// 确定目标 item 的最近边缘:顶部或底部
const closestEdgeOfTarget = extractClosestEdge(destinationItemRecord.data);
// 计算被拖动 item 的目标索引
const destinationIndex = getReorderDestinationIndex({
startIndex: draggedItemIndex,
indexOfTarget,
closestEdgeOfTarget,
axis: "vertical",
});
// 重新排列
reorderItem({
startIndex: draggedItemIndex,
finishIndex: destinationIndex,
});
}
},
});
onUnmounted(() => {
cleanup();
});
});
</script>
<template>
<div class="list">
<list-item v-for="item in items" :key="item.id" :item="item"></list-item>
</div>
</template>
现在,您将能够重新排列 item:
添加放置指示器
要改善用户体验,添加下拉指示器会非常有用。下拉指示器可以直观地显示拖动项目下拉后的位置。
要实现放置指示器,首先,在 components 文件夹中创建一个名为 DropIndicator.vue 的新组件。该组件将呈现一个可视化指示器,显示物品将被投放到何处:
<script setup>
import { reactive } from "vue";
const { edge, gap } = defineProps({
edge: {
type: String,
required: true,
},
gap: {
type: String,
required: true,
},
});
const classObject = reactive({
"edge-top": edge === "top",
"edge-bottom": edge === "bottom",
});
const styleObject = reactive({
"--gap": gap,
});
</script>
<template>
<div class="drop-indicator" :class="classObject" :style="styleObject"></div>
</template>
您可以在 style.css 文件中找到 DropIndicator 组件的样式。
现在,在 ListItem 组件中导入 DropIndicator 组件,并添加逻辑以在 item 被拖过时显示指示器:
<script setup>
// ListItem.vue
// 其它的导入
import {
attachClosestEdge,
extractClosestEdge, // 新
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import DropIndicator from "./DropIndicator.vue"; // 新
// 其它的代码
const closestEdge = ref(null); // 新
onMounted(() => {
// 其它的代码
const cleanup = combine(
draggable({
// ...
}),
dropTargetForElements({
element: itemEl,
getData: ({ input, element }) => {
// ...
},
getIsSticky: () => true,
// 新
onDragEnter: (args) => {
// 当可拖动项目进入下拉区域时,更新最近的边缘
if (args.source.data.itemId !== item.id) {
closestEdge.value = extractClosestEdge(args.self.data);
}
},
onDrag: (args) => {
// 在拖动到下拉区域时,持续更新最近的边缘
if (args.source.data.itemId !== item.id) {
closestEdge.value = extractClosestEdge(args.self.data);
}
},
onDragLeave: () => {
// 当可拖动项目离开下拉区域时,重置最近的边缘
closestEdge.value = null;
},
onDrop: (args) => {
// 在拖放可拖动项目时重置最近的边缘
closestEdge.value = null;
},
})
);
onUnmounted(() => {
cleanup();
});
});
</script>
<template>
<div class="list-item" ref="itemRef" :class="{ dragging: isDragging }">
{{ item.id }} - {{ item.label }}
<!-- 如果有最近边缘,则渲染 DropIndicator -->
<drop-indicator v-if="closestEdge" :edge="closestEdge" gap="8px" />
</div>
</template>
现在,当您将一条 item 拖到另一 item 上时,您应该会看到一个放置指示器,显示该item的放置位置:
结论
总之,Pragmatic 拖放库为在 Web 应用程序中实现拖放功能提供了强大而灵活的解决方案。它体积小、与任何前端框架兼容以及全面的功能集使其成为开发人员创建出色的拖放体验的绝佳选择。
希望本教程对您在 Vue 项目中使用 Pragmatic 拖放库有所帮助。但为什么要止步于此?我鼓励您制作可拖动的列,并在下面的评论中与我们分享您的实现方法!如果您有任何问题或反馈,请随时留言。编码快乐