文档/开始

从 v2 迁移

把 v2 应用迁移到 v3——改了什么以及为什么、升级步骤、每个 v2 API 到 v3 替代项的对照、常见场景的前后代码,以及容易踩的坑。

编辑此页

本页由 AI 翻译,可能存在不准确之处。 阅读英文原文

v3 是对 v2 的重写。模型保持不变:一个 provider,加上在你自己的元素上调用的组合式函数。但大多数名称和选项都变了,排序移进了库里,你为每个列表写的放置处理函数也不再需要。本页把 v2 的每个部分对应到 v3,然后展示常见场景的前后对比。

改了什么

  • 排序是内置的。在 v2 中,列表是一个放置区,它的行是带 [index, items] 载荷的可拖拽元素,你在 onDrop 中用 e.helpers.suggestSort() 排序。在 v3 中,由 makeSortableList 和 makeSortableItem 完成:列表渲染你的数组的一份草稿,它在拖拽时重新排序,放置时把新数组写进你的 ref。大多数 v2 的放置处理函数直接删除,而不是移植。
  • 东西会自己动。行滑到新位置(FLIP),浮层在放置时落到它的位置上,取消时飞回原处。<TransitionGroup> 不再需要。
  • 仍然只有一个包。@vue-dnd-kit/core 包含列表、树、多项拖拽、修饰器、传感器和碰撞检测函数。Nuxt 模块保留;@vue-dnd-kit/actions(程序化拖拽)和 @vue-dnd-kit/devtools 是新增的可选包。
  • 真正的浮层。它被传送到 body,显示一个深度克隆,尺寸与它将落入的位置一致。浮层组件以 props 的形式获得拖拽信息,而不是读取 provider 的内部状态。
  • 用 id,而不是元素。选中集合、事件和拖拽状态都以你数据中的 id 为键。不再需要读取 DOM 元素的映射表。
  • 类型和 accept 规则替代了普通可拖拽元素和放置目标上的 groups。列表按 group 名称互相交换项。
  • 放置之前做决定。onBeforeDrop 接受、拒绝、让拖拽继续,或只接收部分项。onDrop 只负责应用。
  • 传感器。指针和键盘输入都是传感器。激活按指针类型分别设置,触摸拖拽通过长按开始,所以列表在手指下仍能滚动。
  • 区域。makeDragArea 取代了 makeConstraintArea 和 makeAutoScroll,自动滚动无需代码就能在每个滚动容器中工作。
  • 无障碍默认开启:ARIA 属性、放置后焦点会回来,以及屏幕阅读器播报。

升级

npm i @vue-dnd-kit/core@^3
包v2v3
@vue-dnd-kit/core2.4.x3.x
@vue-dnd-kit/nuxt1.x3.x
@vue-dnd-kit/actions—1.x,可选
@vue-dnd-kit/devtools—3.x,可选
  • package.json 中写着 "@vue-dnd-kit/core": "^2…" 的应用会一直停留在 v2,直到版本范围改为 ^3。
  • Vue 3.5 或更新版本。v2 没有依赖;v3 有一个:@vueuse/core(14 或 15)。
  • 浏览器:Chrome 和 Edge 104+、Safari 15.4+、Firefox 104+。
  • v2 和 v3 共用同一个包名,所以应用要一次性迁移到 v3。没有兼容层。v2 的文档仍可从页面顶部的版本菜单访问。

推荐的工作顺序:

  1. 升级包,并重命名 provider 的 props(见下文)。
  2. 把每个可排序列表改成 makeSortableList 和 makeSortableItem,并删除它的放置处理函数。
  3. 移植普通的可拖拽元素和放置区。
  4. 把自定义预览移植成浮层组件,或 provider 的 #overlay 插槽。
  5. 用 makeDragArea 替换 makeConstraintArea 和 makeAutoScroll,或者什么都不用。
  6. 删除 v2 时代的变通写法:可拖拽元素上的 tabindex 和 aria-grabbed、整个项上的 touch-action: none、行外面的 <TransitionGroup>、预览组件外面的 markRaw。

新旧对照

表格中的无对应项表示 v3 中没有这个名称的东西;单元格里写明了改用什么。

一览

v2v3
DnDProviderDnDProvider,带有新的 props 和一个 #overlay 插槽。
makeDraggable自由拖拽用 makeDraggable;列表的行用 makeSortableItem。
makeDroppable普通放置目标用 makeDroppable;列表和树用 makeSortableList 或 makeSortableTree。
makeSelectionAreamakeSelectionArea。
makeConstraintAreamakeDragArea。
makeAutoScroll不需要:自动滚动是自动的。用 makeDragArea 或列表的 autoScroll 调整它。
useDnDProvider状态用 useDnD,事件用 useDnDMonitor。
injectionKeyuseDnDContext() 和 provideDnDContext(app, context)。
DragPreviewDragPreview:现在是单个被拖拽项的克隆,用于你自己的浮层。
makeSnappedOverlayPosition、<DragPreview :grid>snapToGrid 修饰器。
e.helpers.suggestSort、suggestSwap、suggestCopy、suggestRemove内置在列表中:见排序。

Provider

v2v3
autoScrollViewport,不设置就关闭autoScroll:默认开启,作用于窗口和每个滚动容器。{ threshold, maxSpeed, acceleration } 或 false。
previewTo,默认渲染在 provider 内部overlayTo:选择器或元素,默认为 'body'。
#preview="{ preview }" 插槽#overlay 插槽,拖拽信息作为 props:items、active、over、accepted、status、mode、to。见浮层。
keyboard.keys.forDrag、forDrop、forCancel(在 useDnDProvider() 上设置):keyboard="{ keys: { start, drop, cancel } }"。
keyboard.step:keyboard="{ step }":默认 25 px,用于自由拖拽。
keyboard.keys.forMove、forMoveFaster、keyboard.moveFaster无对应项。方向键负责移动;:kbd{value="Shift"} 让自由拖拽的步长变为四倍。
collision.throttle无对应项。collision 决定如何找到目标:pointerFirst(默认)、pointerWithin、rectIntersection、closestCenter、closestCorners,或你自己的函数。
每页一个 provider,从不嵌套可以嵌套:最近的 provider 掌管一个项。整个应用一个 provider 仍然是目标。

在 v2 中没有对应项的新 props:activation、modifiers、sensors、animation、dropAnimation、animateChanges、haptics、copyModifier、overlayFit、overlayClass、overlayTransition、overlayPreviews、announcements、screenReaderInstructions 和 defaultStyles。provider 还会触发 drag-start、drag-move、drag-over、drag-end、drag-cancel、drag-resume 和 drag-settle。它们全都在 provider 指南中。

读取状态:useDnD

useDnDProvider()v3
state:'dragging'、'activating'、'selecting'、'pending' 或 nulluseDnD().status:'idle'、'activating'、'dragging'、'dropping' 或 'selecting'。'pending'(异步放置)对应 'dropping';null 对应 'idle'。
pointer.value.currentuseDnD().pointer,经过修饰器之后的值。每个事件还有 delta,即相对起点的偏移。
entities.draggingMapuseDnD().active:id、ids、items、data、type、element、rect。
entities.selectedSet(元素)useDnD().selection(id):has、add、delete、toggle、set、clear、ids。
hovered.droppable、hovered.draggableuseDnD().over:id、data、element、rect、accepted。
delay.progress、distanceProgressuseDnD().activation.progress,从 0 到 1。
preview.position、preview.size、preview.render、preview.to无对应项。库负责浮层的位置和尺寸。使用 overlayTo、#overlay 插槽或浮层组件。
entities.draggableMap、droppableMap、各个 visible…Set、scrollPosition无对应项。
—新增:useDnD().cancel()、position、isAutoScrolling。

事件移到了 useDnDMonitor:见事件与放置决定。完整结构见 useDnD API。

makeDraggable

v2v3
makeDraggable(ref, options, payload)makeDraggable(ref, options):没有第三个参数。列表的行使用 makeSortableItem(ref, { item })。
id,虚拟列表中需要id:从你的数据中以 getter 传入。默认值来自 useId(),每个组件实例都不同。
data: () => carddata:值、ref 或 getter。通过 active.data 和 active.items[i].data 读回。
groups: ['dev']type: 'dev',由目标的 accept 匹配。见分组。
dragHandle: '.handle'handle:选择器或元素 ref。
disableddisabled。
modifier: { keys, method }canDrag: (event) => event.altKey:每次按下时的检查。
activation: { distance, delay }按指针类型设置的 activation:{ mouse: { distance }, touch: { delay, tolerance }, pen }。同时设置 distance 和 delay 时,哪个先满足就从哪个开始拖拽。
activation.distance: { x, y, condition }无对应项。每种指针类型一个 distance。
render: markRaw(Preview)overlay: Preview:组件以 props 的形式获得拖拽信息。见浮层。
placementMargins无对应项。可排序项上的放置目标以它的中间部分(25% 到 75%)计算;树用 nesting: 'hover' 嵌套。见放进可排序项。
events.onSelfDragStart、onSelfDragMove、onSelfDragEnd、onSelfDragCancel可拖拽元素上无对应项:使用 useDnDMonitor 或 watch(isDragging)。见事件。
events.onHover、onLeave,以及经过它上方的拖拽的 onDragStart…在同一个元素上用 makeDroppable:onDragEnter、onDragLeave。
返回 isDragging、selected相同。
返回 isDragOver可排序项返回 indicator(配合 placeholder: 'indicator')和 isPlaceholder;放置目标返回 placement。
返回 isAllowed可拖拽元素上无对应项。放置目标返回 isAccepting。
—新增:label、axis、modifiers、keyboardMode、overlay: false 配合返回的 transform、dropAnimation、attributes、native。

makeDroppable

v2v3
makeDroppable(ref, options, () => items)makeDroppable(ref, options)。承载列表的区域就是一个 makeSortableList。
groupsaccept:'*'、一个类型、类型列表或函数。
groupMatch: 'every'、'some'acceptMode: 'all'、'some'。
events.onEnter、events.onLeaveonDragEnter、onDragLeave:独立的选项,没有 events 对象。
events.onDrop,可以返回 false 或 promiseonBeforeDrop 做决定,onDrop 负责应用。见放置决定。
events.onValidateonBeforeDrop,返回要保留的 id。
datadata,通过 over.data 读回。
disableddisabled。
返回 isAllowedisAccepting。
返回 isDragOver:{ top, right, bottom, left, center }isOver(布尔值),以及 placement:{ top, right, bottom, left },即指针所在的各个半边。没有 center。
—新增:id、label、priority、overOverlay、overDropAnimation、external(来自电脑的文件)。

放置事件

v2 IDragEventv3
e.draggedItemse.active.items:每一项都是 { id, item, data, label, type },抓起的项在最前。列表还会得到 e.items 和 e.keys。
e.draggedItems[0].datae.active.data。
e.draggedItems[i].index、.items列表:e.from,{ list, index }。
e.dropZone:items、placement、datae.over:id、data、element、rect、accepted。列表:e.to,{ list, index }。
e.hoveredDraggable无对应项:列表会自己算出 e.to.index。
e.helpers.suggestSort()列表负责写入放置结果;e.result 包含它改变的每个列表的新数组。
e.helpers.suggestSwap()在列表上设置 behavior: 'swap';e.partner 是与之交换的项。
e.helpers.suggestCopy()group: { name, pull: 'clone' } 或复制键(:kbd{value="Alt"}),配合列表的 clone;e.mode 为 'copy'。
e.helpers.suggestRemove()在放置目标中调用 e.source.remove(),或使用列表的 dropOutside: 'remove'。
result.sameListe.sameList。
e.helpers.insertAt、removeAt、removeIndexes、swapAt、getInsertSide、isAtZoneStart、isAtZoneEnd无对应项。列表不需要它们;对普通放置目标,按你的需要修改数据。
e.provideruseDnD()。
—新增:e.pointer、e.delta、e.position(放到画板上的项该放在哪里)、e.mode。

排序

v2v3
一个带 () => items 的 makeDroppable 列表makeSortableList(listRef, { items })。
一个带 () => [index, items] 的 makeDraggable 行makeSortableItem(el, { item: () => props.task })。
渲染你的数组渲染返回的 items:拖拽时重新排序的草稿。
suggestSort('vertical')、suggestSort('horizontal')strategy: 'vertical'(默认)、'horizontal' 或 'grid'。
用引用比较找出源数组和目标数组,再赋值 sourceItems 和 targetItems不需要:放置会写入它改变的每个列表。
<TransitionGroup>内置。animateChanges: true 为不经拖拽的变化(添加、删除、打乱)添加动画。
根据 isDragOver.top、.bottom 画插入线placeholder: 'indicator' 和项的 indicator.side。见插入线。
suggestSwap()behavior: 'swap'。
只复制的面板(suggestCopy() 加一个 copyOnly 标志)group: { name, pull: 'clone', put: false }、sort: false 和 clone。
用 suggestRemove() 的回收区makeDroppable(trash, { onDrop: (e) => e.source?.remove() }),或 dropOutside: 'remove'。
Pinia 或不可变数据:自己赋值结果把 items 写成 getter,然后在 onDrop 中调用 e.preventDefault() 并应用 e.result。见受控列表。
递归树:每个文件夹一个 droppable,再加 applyToTreemakeSortableTree:一个容器,行带着各自的深度扁平渲染。见树。
用 v-model 的嵌套分组(每个分组一个 droppable)列表套列表,全部在同一个 group 中,每个都用 items: defineModel()。
虚拟列表:每个可拖拽元素都要 id把草稿交给虚拟化器,在行上设置 id: () => props.item.id。见虚拟列表。

前后代码见下文。关于列表的一切都在可排序列表和共享项的列表中。

分组

v2 用一个 groups 选项处理所有情况。v3 把它拆成两个:

v2v3
makeDraggable(el, { groups: ['dev'] })makeDraggable(el, { type: 'dev' })。
makeDroppable(el, { groups: ['dev'] })makeDroppable(el, { accept: 'dev' })。
放置区上的 groups: ['dev', 'ops']accept: ['dev', 'ops'],或一个函数:({ active, items, types }) => boolean。
groups: [],或不设置省略 type 或 accept。v2 的规则仍然成立:没有类型的可拖拽元素符合任何规则,没有 accept 的放置目标接受一切。
groupMatch: 'every'、'some'acceptMode: 'all'、'some'。
列表放置区及其行上的 groupsmakeSortableList 上的 group: 'board'。group: { name, pull, put } 说明什么可以离开、什么可以进入。
makeSelectionArea 上的 groupsaccept。

没有 group 的列表自成一组,不与任何其他列表交换项。在 v2 中,没有 groups 的放置区接受一切。见目标接收哪些拖拽。

浮层

v2v3
在 provider 中渲染、通过 previewTo 传送的预览总是被传送,到 body 或 overlayTo。
一个克隆:相同的标签、它的 innerHTML 和尺寸深度克隆:表单的值、<canvas> 的像素、继承的样式和表格单元格都会带上。用 data-dnd-no-clone 标记不需要带上的部分。
render: markRaw(Preview),读取 useDnDProvider().entitiesoverlay: Preview,以 props 的形式获得 items、active、over、accepted、status、mode 和 to。不需要 markRaw;切换它时用 shallowRef。
#preview 插槽,根据 preview.position 定位#overlay 插槽:库负责盒子的位置和尺寸。什么都不渲染时退回到克隆。
useDnDProvider().preview.render.value = …#overlay 插槽,或作为 overlay 传入的 shallowRef。
在放置区上方换一种外观,通过 onEnter 和 onLeavemakeDroppable 或列表上的 overOverlay,或在浮层组件中读取 over。见改变浮层的目标。
<DragPreview v-slot="{ draggingMap }">浮层 props 中的 items;<DragPreview :id="item.id" /> 渲染一个克隆。
<DragPreview :grid>、makeSnappedOverlayPositionsnapToGrid 修饰器。它吸附的不只是画面,还有查找目标的位置和放置的 position。
DragPreview 外面的 <Transition> 或 motion 的 AnimatePresence拿起用 overlay-class 配合 @starting-style,落位用 dropAnimation,组件之间用 overlayTransition。见抬起、倾斜、落定。
.dnd-kit-preview盒子上的 [data-dnd-overlay]("dragging"、"dropping"),它内部的 [data-dnd-overlay-content]。
preview.size,需要手动更新盒子取它将落入的位置的尺寸(overlayFit)。
预览在放置时消失它落到自己的位置上,或在取消时飞回原处。

更多内容见浮层指南。

选择与多项拖拽

v2v3
selected,由 makeDraggable 返回selected,由 makeDraggable 和 makeSortableItem 返回。
entities.selectedSet,元素的集合useDnD().selection,以 id 为键。
makeSelectionArea:默认需要按住 Ctrl(ControlLeft)默认不需要按键:在区域的空白处按下就会画出选框。modifier: { keys: ['Control'], method: 'every' } 可以恢复 Ctrl。
modifier.keys 使用 KeyboardEvent.codeKeyboardEvent.key('Control'、'Shift')或 .code('ControlLeft')。
strategy: 'toggle'、'select'相同。
groupsaccept。
events.onSelected(elements)onSelected(ids)。
disabled相同。
用 suggestSort() 批量移动选中集合由列表移动它。multiple: 'group'(默认)、'gaps' 或 'stack' 决定其余项在途中如何表现。
混合选中时用 onValidateacceptMode: 'some' 加上返回 id 的 onBeforeDrop。见只接受一部分。

更多内容见多项拖拽与选择。

轴、边界与吸附

v2v3
makeConstraintArea(el, { axis: 'x' })makeDragArea(el, { axis: 'x' }),作用于在它内部开始的拖拽。axis 也可用于可拖拽元素、列表、树或项。
axis: 'both'省略 axis。
restrictToArea: truemakeDragArea(el, { restrictToArea: true }),或修饰器 restrictToParent 和 restrictToElement(el)。
每个区域一个 provider,以隔开规则一个 provider,每个区域一个 makeDragArea。区域里的项仍然可以移动到任何地方。
<DragPreview :grid="20">、makeSnappedOverlayPositionsnapToGrid(20) 或 snapToGrid({ x, y, origin, smooth }),放在 provider、区域、列表或可拖拽元素的 modifiers 中。
—新增:restrictToWindowEdges、restrictToFirstScrollableAncestor、snapCenterToCursor,以及你自己的修饰器。

更多内容见轴、边界与网格。

自动滚动

v2v3
<DnDProvider :autoScrollViewport="true">,默认关闭默认开启,作用于窗口和每个滚动容器。:auto-scroll="false" 关闭它。
makeAutoScroll(el, { threshold, speed })不需要:任何 overflow: auto 或 scroll 的元素都会滚动。要调整某一个:makeDragArea(el, { autoScroll: { … } }) 或列表的 autoScroll。
threshold,单位 px,或按边设置threshold:容器尺寸的比例(0.2),介于 24 到 96 px 之间。按边设置的阈值无对应项。
speed,一个倍数maxSpeed(每帧 25 px)和 acceleration(10)。
disabled: trueautoScroll: false。
isScrollingisAutoScrolling,来自 makeDragArea 或 useDnD()。

键盘与无障碍

v2v3
每个可拖拽元素上的 tabindex="0"自动设置,同时设置 role、aria-roledescription、aria-describedby、aria-disabled 和 aria-pressed。你自己的属性优先。
手动写 :aria-grabbed="isDragging"删掉它:aria-pressed 会跟随拖拽。
在 useDnDProvider().keyboard.keys 上设置按键provider 的 keyboard prop。绑定匹配 event.code 或 event.key。
方向键按 keyboard.step 移动预览自由拖拽:每次 keyboard.step px。列表:按位置移动,并能移进分组中的下一个列表。keyboardMode: 'droppables' 在放置目标之间跳转。
重新绑定方向键(forMove、forMoveFaster)无对应项。
没有播报实时区域播报每一次拿起、移动、放置和取消。label 为事物命名;announcements 和 screenReaderInstructions 改写措辞。

更多内容见无障碍。

事件与放置决定

v2v3
可拖拽元素的 events.onSelfDragStartuseDnDMonitor({ onDragStart }),检查 active.ids。如果只是为了加类名,isDragging 就够了。
onSelfDragMove监听器的 onDragMove。
onSelfDragEnd,放置或取消时监听器的 onDragEnd(放置)和 onDragCancel。onDragSettle 两者都能收到,在浮层落位之后。
onSelfDragCancel监听器的 onDragCancel。
可拖拽元素的 onHover、onLeave,以及经过它上方的拖拽的 onDragStart…在该元素上用 makeDroppable:onDragEnter、onDragLeave。列表的行什么都不需要。
droppable 的 events.onEnter、onLeaveonDragEnter、onDragLeave。
droppable 的 events.onDroponBeforeDrop 做决定,然后 onDrop 负责应用。onDrop 是同步的,它的返回值会被忽略。
droppable 的 events.onValidateonBeforeDrop,配合 acceptMode: 'some'。
—新增:provider 的 @drag-start … @drag-settle,以及监听器的 onDragOver、onBeforeDrop 和 onDragResume。

v2 的 onDrop 可以返回一个值或一个值的 promise 来决定放置。在 v3 中,这是 onBeforeDrop 的工作,可用于列表、树、放置目标和监听器:

v2 onDrop 返回v3 onBeforeDrop 返回
什么都不返回,或 true:放置生效'accept'、true 或什么都不返回。
false:这次放置被拒,拖拽保持进行'continue'。现在 false 会拒绝放置。
被拒绝的 promise 或抛出异常:拖拽被取消'reject' 或 false。抛出异常也会拒绝。
一个 promise;期间 state 为 'pending'一个 promise;期间 status 为 'dropping',浮层带着 data-dnd-overlay="dropping" 等待。
onValidate 返回 IDragItem[]一个 id 数组:只有这些项会放置,其他的飞回原处。

监听器先回答,然后是目标。接着目标的 onDrop 运行,列表写入它的新数组,随后是 onDragEnd。更多内容见决定放置和事件 API。

类型

v2v3
IDragEventDnDEvent(放置目标、监听器)、SortableDropEvent(列表)、DnDEndEvent(onDragEnd)。
IDragItemDragItem。
IDropZoneContextOver。
IHoveredDraggableContext无对应项。
ISuggestSortResult、ISuggestSwapResult、ISuggestCopyResult、ISuggestRemoveResultSortableDropEvent:result、to、mode、partner。
IDropValidateEventDnDEvent,在 onBeforeDrop 中。
IPlacementPlacement,没有 center。
ICoordinatesCoordinates。
IDragActivationOptionsActivationOptions。
IAutoScrollOptionsAutoScrollOptions。
TDnDStateDnDStatus。
IDnDProviderExternalDnDState,即 useDnD() 的返回值。
IDraggableEvents、IDroppableEventsDraggableOptions、DroppableOptions 和 DnDMonitor 中的回调。

Nuxt

npm i @vue-dnd-kit/nuxt@^3 @vue-dnd-kit/core@^3
v2(@vue-dnd-kit/nuxt 1.x)v3(@vue-dnd-kit/nuxt 3.x)
自动导入 makeDraggable、makeDroppable、makeSelectionArea、makeConstraintArea、makeAutoScroll、useDnDProvider自动导入 makeDraggable、makeDroppable、makeSortableList、makeSortableItem、makeSortableTree、makeSelectionArea、makeDragArea、useDnD、useDnDMonitor。
不自动导入类型主要类型会自动导入(DnDEvent、SortableDropEvent、OverlayProps…)。通用类型(Key、Active、Over)需手动导入。
—修饰器、碰撞检测函数和传感器按需开启:vueDndKit: { autoImports: { modifiers: true, collision: true, sensors: true } }。
DnDProvider 和 DragPreview 全局注册相同,并多了一个 prefix 选项。
布局中、provider 内部的 <DragPreview />删掉它:浮层由 provider 渲染。DragPreview 接收一个 id,应放在浮层组件内部。
peer 依赖 @vue-dnd-kit/core 2peer 依赖 @vue-dnd-kit/core 3;Nuxt 4。

nuxt.config.ts 中的 modules: ['@vue-dnd-kit/nuxt'] 保持不变。见安装。

前后对比

一个可拖拽元素和一个放置区

v2

Card.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, {
  groups: ['task'],
  dragHandle: '.handle',
  data: () => props.card,
});
</script>

<template>
  <div ref="el" tabindex="0" :class="{ dragging: isDragging }">
    <span class="handle">⋮⋮</span>
    {{ card.title }}
  </div>
</template>
Zone.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';

const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragOver, isAllowed } = makeDroppable(el, {
  groups: ['task'],
  events: {
    onDrop: (e) => emit('drop', e.draggedItems.map((item) => item.data as Card)),
  },
});
</script>

<template>
  <div ref="el" :class="{ over: isDragOver && isAllowed }"><slot /></div>
</template>

v3

Card.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, {
  id: () => props.card.id,   // read back as `active.id`
  type: 'task',              // matched by the targets' `accept`
  handle: '.handle',
  data: () => props.card,
});
</script>

<template>
  <div ref="el" :class="{ dragging: isDragging }">
    <button type="button" class="handle" :aria-label="`Move ${card.title}`">⋮⋮</button>
    {{ card.title }}
  </div>
</template>
Zone.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';

const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isOver, isAccepting } = makeDroppable(el, {
  accept: 'task',
  onDrop: (e) => emit('drop', e.active.items.map((item) => item.data as Card)),
});
</script>

<template>
  <div ref="el" :class="{ over: isOver && isAccepting }"><slot /></div>
</template>
  • 卡片会自动获得 tabindex、role 和 aria-* 属性。<button> 手柄可以获得焦点,键盘拖拽从它开始。
  • groups 在可拖拽元素上变成了 type,在放置目标上变成了 accept。回调离开了 events 对象。
  • 有 isOver 而没有 isAccepting,说明这个目标拒绝这次拖拽:在那里放置会飞回原处。

一个可排序列表

v2

TaskList.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type IDragEvent } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');

makeDroppable(
  el,
  {
    events: {
      onDrop(e: IDragEvent) {
        const r = e.helpers.suggestSort('vertical');
        if (r) tasks.value = r.targetItems as Task[];
      },
    },
  },
  () => tasks.value,
);
</script>

<template>
  <div ref="el">
    <TransitionGroup name="list">
      <TaskItem v-for="(task, index) in tasks" :key="task.id" :task="task" :index="index" :tasks="tasks" />
    </TransitionGroup>
  </div>
</template>
TaskItem.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ task: Task; index: number; tasks: Task[] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, { id: props.task.id }, () => [props.index, props.tasks]);
</script>

<template>
  <div ref="el" :class="{ 'is-dragging': isDragging }">{{ task.title }}</div>
</template>

v3

TaskList.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');

const { items } = makeSortableList(el, { items: tasks });
</script>

<template>
  <ul ref="el">
    <TaskItem v-for="task in items" :key="task.id" :task="task" />
  </ul>
</template>
TaskItem.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';

const props = defineProps<{ task: Task }>();
const el = useTemplateRef<HTMLElement>('el');

const { isPlaceholder } = makeSortableItem(el, { item: () => props.task });
</script>

<template>
  <li ref="el" :class="{ slot: isPlaceholder }">{{ task.title }}</li>
</template>
  • 渲染 items,也就是草稿:它在拖拽时重新排序。放置时 tasks 得到新的顺序,defineModel 触发 update:modelValue。
  • 没有索引、没有载荷、没有 onDrop、没有 <TransitionGroup>:行会自己滑动。
  • 被拖拽的行作为它将落入的位置留在列表中。用 isPlaceholder 或 [data-dnd-placeholder] 给它加样式,而不是把它隐藏。

共享项的列表

v2

Board.vue
// Each column's makeDroppable calls this from its onDrop, with its array as the payload.
function applySort(e: IDragEvent) {
  const r = e.helpers.suggestSort('vertical');
  if (!r) return;

  const src = e.draggedItems[0]?.items;
  const tgt = e.hoveredDraggable?.items ?? e.dropZone?.items;

  if (src === todo.value) todo.value = r.sourceItems as Task[];
  else if (src === done.value) done.value = r.sourceItems as Task[];

  if (!r.sameList) {
    if (tgt === todo.value) todo.value = r.targetItems as Task[];
    else if (tgt === done.value) done.value = r.targetItems as Task[];
  }
}

v3

Column.vue
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const props = defineProps<{ column: { id: string; tasks: Task[] } }>();
const el = useTemplateRef<HTMLElement>('el');

const { items } = makeSortableList(el, {
  id: props.column.id,                   // `from.list` and `to.list` in drop events
  items: toRef(props.column, 'tasks'),   // writable: the parent passes a reactive column
  group: 'board',                        // same name: tasks move between these lists
});
</script>

<template>
  <ul ref="el">
    <TaskItem v-for="task in items" :key="task.id" :task="task" />
  </ul>
</template>
Board.vue
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';

const columns = reactive([
  { id: 'todo', tasks: [{ id: 't1', title: 'Write the docs' }] },
  { id: 'done', tasks: [] },
]);
</script>

<template>
  <Column v-for="column in columns" :key="column.id" :column="column" />
</template>

放置会写入两个数组:任务离开的那个和它进入的那个。TaskItem.vue 就是上面可排序列表中的那个。分发副本的面板过去靠 suggestCopy() 和一个 copyOnly 标志;现在它就是一个列表:

Palette.vue
makeSortableList(el, {
  items: blocks,
  group: { name: 'board', pull: 'clone', put: false },  // copies go out, nothing comes in
  sort: false,                                          // its own order stays
  clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});

对于 Pinia store 或其他你不直接写入的状态,把 items 写成 getter,在 onDrop 中调用 e.preventDefault() 并应用 e.result:见受控列表。

自定义浮层

v2

TaskItem.vue
makeDraggable(el, {
  render: markRaw(TaskOverlay),
  data: () => props.task,
});
TaskOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useDnDProvider } from '@vue-dnd-kit/core';

const { entities } = useDnDProvider();
const task = computed(() => {
  const [node] = entities.draggingMap.keys();
  return node ? (entities.draggableMap.get(node)?.data?.() as Task) : null;
});
</script>

<template>
  <div class="task-preview">{{ task?.title }}</div>
</template>

v3

TaskItem.vue
makeDraggable(el, {
  id: () => props.task.id,
  overlay: TaskOverlay,
  data: () => props.task,
});
TaskOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean }>();
const task = computed(() => props.items[0]!.data as Task);
</script>

<template>
  <!-- Fills the overlay box, which takes the size of the slot. -->
  <div class="task-preview" :class="{ refused: over && !accepted }">
    {{ task.title }}
    <span v-if="items.length > 1" class="count">{{ items.length }}</span>
  </div>
</template>

<style scoped>
.task-preview { width: 100%; height: 100%; }
</style>
  • 组件以 props 的形式获得拖拽信息:items(每一个被拖拽的项,抓起的项在最前)、over、accepted、status、mode 和 to。只声明你要读取的那些。对于列表的行,列表项是 items[0].item。
  • 原样传入它。用 shallowRef 切换它,绝不要用深层 ref。

provider 的插槽也以同样的方式改变,你的组件不再需要自己定位:

v2

App.vue
<DnDProvider>
  <RouterView />
  <template #preview>
    <MyOverlay />  <!-- moved itself with useDnDProvider().preview.position -->
  </template>
</DnDProvider>

v3

App.vue
<DnDProvider>
  <RouterView />
  <template #overlay="{ items, over, accepted }">
    <MyOverlay :items="items" :over="over" :accepted="accepted" />
  </template>
</DnDProvider>

把 DragPreview 包在 <Transition> 中实现的弹出效果,改为 overlay-class 配合 @starting-style:见抬起、倾斜、落定。

需要确认的放置

v2

Zone.vue
makeDroppable(el, {
  events: {
    onDrop: (e) =>
      new Promise<void | boolean>((resolve) => {
        if (!window.confirm('Drop here?')) return resolve(false); // keep dragging
        place(e.draggedItems);
        resolve();
      }),
  },
});

v3

Zone.vue
makeDroppable(el, {
  onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
  onDrop: (e) => place(e.active.items),
});

列表也用同样的方式询问,而 promise 让你可以打开自己的对话框。在 promise 完成之前,浮层停在松开的地方等待:

Column.vue
makeSortableList(el, {
  items: toRef(props.column, 'tasks'),
  group: 'board',
  // `ask` opens a dialog and resolves 'accept', 'continue' or 'reject'.
  onBeforeDrop: (e) => (e.sameList ? 'accept' : ask(e)),
});

一个只接收混合选中集合中匹配部分的放置区(groupMatch: 'some'):

v2

Zone.vue
makeDroppable(el, {
  groups: ['frontend'],
  groupMatch: 'some',
  events: {
    onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
  },
});

v3

Zone.vue
import type { DragItem } from '@vue-dnd-kit/core';

const fits = (item: DragItem) => item.type.length === 0 || item.type.includes('frontend');

makeDroppable(el, {
  accept: 'frontend',
  acceptMode: 'some',
  onBeforeDrop: (e) => e.active.items.filter(fits).map((item) => item.id), // the others fly back
  onDrop: (e) => place(e.active.items.filter(fits)), // `active.items` still lists every dragged item
});

列表不用写代码就能做到这一点:设置 acceptMode: 'some' 后,它们的 group.put 接受的项会落位,其他的飞回原处。

升级时的坑

  • 用版本范围安装 v3。在 package.json 中写 ^3:npm update 会让 ^2 停留在 v2。
  • 渲染草稿。列表渲染 makeSortableList 返回的 items,而不是你的数组。如果渲染你的数组,在放置之前什么都不会动。
  • 不要排序两次。删掉列表中的 suggestSort 代码:列表会在它的 onDrop 之后把每次放置写进可写的 items ref。要自己应用,就调用 e.preventDefault() 并使用 e.result。
  • false 会拒绝放置。在 v2 中,resolve 为 false 的 onDrop 会让拖拽继续。在 v3 中,从 onBeforeDrop 返回 'continue'。从 onDrop 返回的 promise 会被忽略。
  • 列表只在同一个 group 内共享。没有 group 的列表不与任何其他列表交换项。
  • 用 id 作为行的 key。:key="index" 会让错误的行产生动画。也要从你的数据中传入 id:默认 id 对每个组件实例都不同。
  • 不要用 <TransitionGroup>,也不要在 transform 上加 transition。库会自己移动行,否则移动会被动画两次。
  • 撤掉触摸的变通写法。整个项上的 touch-action: none 会让页面无法在手指下滚动;v3 改为通过长按开始触摸拖拽。在需要立即拖动的手柄上保留它,并配合 activation: { touch: { distance: 4 } }。
  • 移除过时的属性。库会设置 tabindex、role 和它需要的 aria-*,并且从不覆盖你的属性,所以遗留的 role 或 aria-grabbed 会保持你留下的样子。
  • 重新设置浮层样式。.dnd-kit-preview 已经没有了:给 [data-dnd-overlay='dragging'] 内部的内容加样式,绝不要碰盒子的 transform。给浮层组件的根元素设置 width: 100%; height: 100%,并把 <DragPreview /> 从你的布局中移除。
  • 自动滚动是开启的。在 v2 从不滚动的地方传入 :auto-scroll="false"。
  • 选框不需要按键。makeSelectionArea 在它的空白处普通按下就会开始画框。添加一个 modifier 可以保留 v2 的 Ctrl。
  • 一个 provider,区域放在里面。项不能跨越 provider。过去用每个区域一个 provider 来隔开规则的页面,需要改为一个 provider 加每个区域一个 makeDragArea。
  • 两个参数。makeDraggable 和 makeDroppable 接收一个 ref 和一个选项对象,没有载荷。