const DnDProvider: Component<DnDProviderProps>;管理在它内部渲染的所有内容的拖拽:输入、浮层、选中集合、自动滚动和屏幕阅读器播报(见 DnDProviderProps)。
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>
<template>
<DnDProvider :animation="{ duration: 200 }" @drag-end="save">
<RouterView />
<template #overlay="{ items, over, accepted }">
<!-- optional: a custom overlay for every drag -->
</template>
</DnDProvider>
</template>
每个组合式函数都必须在渲染于 provider 内部的组件中运行。目标是整个应用只用一个 provider:页面的各部分通过 makeDragArea 获得自己的规则。参见 provider 指南。
大多数 props 在 provider 需要时才读取:改变其中一个,下一次拖拽就会使用新值。
正在加载演示…
<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider, type ActivationOptions } from '@vue-dnd-kit/core';
import PropRow from './PropRow.vue';
import DensityList from './DensityList.vue';
const ACTIVATIONS: Record<string, ActivationOptions | undefined> = {
default: undefined,
'{ mouse: { delay: 300 } }': { mouse: { delay: 300, tolerance: 5 } },
};
const overlayFit = ref<'slot' | 'source'>('slot');
const activation = ref('default');
const keyboard = ref<'true' | 'false'>('true');
const copyModifier = ref<'Alt' | 'Shift' | 'null'>('Alt');
// The provider reads its props when it needs them: a change applies to the next drag.
const props = computed(() => ({
overlayFit: overlayFit.value,
activation: ACTIVATIONS[activation.value],
// `false` turns keyboard drags off; an object sets their keys and steps.
keyboard: keyboard.value === 'true' ? undefined : (false as const),
copyModifier: copyModifier.value === 'null' ? null : copyModifier.value,
}));
const compact = ref([
{ id: 'copy', title: 'Write copy' },
{ id: 'login', title: 'Fix login' },
]);
const roomy = ref([
{ id: 'sprint', title: 'Plan the sprint' },
{ id: 'review', title: 'Review the PR' },
]);
</script>
<template>
<DnDProvider v-bind="props">
<div class="demo">
<div class="props" role="group" aria-label="DnDProvider props">
<PropRow v-model="overlayFit" name="overlay-fit" :options="['slot', 'source']" />
<PropRow v-model="activation" name=":activation" :options="Object.keys(ACTIVATIONS)" />
<PropRow v-model="keyboard" name=":keyboard" :options="['true', 'false']" />
<PropRow v-model="copyModifier" name="copy-modifier" :options="['Alt', 'Shift', 'null']" />
</div>
<div class="lists">
<DensityList v-model="compact" title="Compact" compact />
<DensityList v-model="roomy" title="Roomy" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
/* Room for every row in one list: the lists grow downward and nothing moves under the pointer. */
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
min-height: 430px;
}
.props {
display: grid;
grid-template-columns: auto auto;
align-items: center;
gap: 6px 14px;
max-width: 100%;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
border-radius: 12px;
background: color-mix(in srgb, currentColor 3%, Canvas);
}
.lists {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 14px;
}
@media (max-width: 480px) {
.props {
grid-template-columns: minmax(0, 1fr);
}
}
</style>浮层渲染在哪里
overlayTo 把浮层移出 body:移进一个模态 <dialog>,它位于页面上所有其他内容之上。关闭开关,你拖动的行就会消失在对话框下面。浮层指南说明了什么时候需要它。
正在加载演示…
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import ColumnList from './ColumnList.vue';
const columns = ref(['Name', 'Status', 'Owner', 'Due date', 'Priority']);
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
const intoDialog = ref(true);
// A modal <dialog> sits on the top layer, above everything in <body>, the overlay
// included. While it is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value && intoDialog.value ? '#columns-dialog' : 'body'));
const show = () => {
dialog.value?.showModal();
open.value = true;
};
</script>
<template>
<DnDProvider :overlay-to="overlayTo">
<div class="sheet">
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in 2" :key="row">
<td v-for="column in columns" :key="column"><span class="skeleton" /></td>
</tr>
</tbody>
</table>
<button type="button" class="primary" @click="show">Edit columns</button>
</div>
<dialog id="columns-dialog" ref="dialog" @close="open = false">
<h3>Columns</h3>
<p>Drag to change their order.</p>
<ColumnList v-model:columns="columns" />
<footer>
<label class="switch">
<input v-model="intoDialog" type="checkbox" />
Overlay inside the dialog
</label>
<button type="button" class="primary" @click="dialog?.close()">Done</button>
</footer>
</dialog>
</DnDProvider>
</template>
<style scoped>
.sheet {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
}
table {
width: min(100%, 520px);
border-collapse: collapse;
font-size: 12px;
}
th {
padding: 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
font-weight: 600;
text-align: left;
white-space: nowrap;
}
td {
padding: 10px 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 8%, transparent);
}
.skeleton {
display: block;
height: 8px;
border-radius: 4px;
background: color-mix(in srgb, currentColor 10%, transparent);
}
button {
padding: 7px 14px;
border: 0;
border-radius: 8px;
font: 600 12px/1.2 system-ui, sans-serif;
cursor: pointer;
}
.primary {
background: #42b883;
color: #04130c;
}
button:focus-visible {
outline: 2px solid #42b883;
outline-offset: 2px;
}
/* No transform on the dialog: it would move the overlay's fixed position. */
dialog {
width: min(340px, calc(100vw - 32px));
box-sizing: border-box;
margin: auto;
padding: 18px;
border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
border-radius: 16px;
background: Canvas;
color: CanvasText;
box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.45);
}
h3 {
margin: 0;
font-size: 15px;
}
p {
margin: 4px 0 14px;
font-size: 12px;
opacity: 0.6;
}
footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
}
.switch input {
accent-color: #42b883;
}
</style>Props
DnDProvider 的 props。大多数在 provider 需要时读取:修改其中一个,下一次拖拽就会使用它。
CollisionDetection如何选择拖拽下方的目标。默认 pointerFirst;也可以用另一个内置的(closestCorners)或你自己的。
Modifier[]用于每次拖拽的修饰器,最先应用:拖拽区域、可拖拽元素和列表的修饰器在其后。
LayoutAnimation可排序列表的默认 FLIP 动画。
DropAnimation浮层在放置时如何落位、在取消时如何飞回原处,在没有更近的设置时使用:放置目标的 overDropAnimation 最先,然后是被拖拽项自己的(或它所在列表的),最后才是这个。
AutoAnimation可排序列表和树的默认 animateChanges:为非拖拽引起的项的变化添加动画。默认关闭。
boolean | AutoScrollOptions拖拽靠近容器和页面边缘时滚动它们。默认 true;选项用于调整,false 关闭。列表、树和拖拽区域可以覆盖它。
ActivationOptions按下在变成拖拽之前必须满足的条件,按指针类型设置(默认值见 ActivationOptions)。拖拽区域、列表、树、项和可拖拽元素可以覆盖它。
KeyboardOptions | false键盘拖拽:它们的按键和步长。false 关闭键盘拖拽;Escape 仍然可以取消任何拖拽。
Sensor[]由什么把输入转换为拖拽。默认为 [pointerSensor, keyboardSensor]:可以去掉其中一个、包装或替换它,或者添加你自己的(defineSensor)。键盘传感器还负责用 Escape 取消任何拖拽;keyboard: false 会关闭它的拖拽,但保留这一功能。这个列表随时可以变化:离开列表的传感器会停止,并取消它们的拖拽。
boolean | HapticsOptions触摸拖拽的振动。默认:拖拽在长按之后开始时短暂振动一次;传入对象可以添加其他振动(enter、drop、refuse);false 关闭全部振动。
string | null把移动变成复制的按键(KeyboardEvent.key)。默认为 'Alt';null 表示禁用。
string | HTMLElement浮层渲染到哪里:选择器或元素。默认 'body'。模态 <dialog> 位于页面其余部分之上:在其中拖拽时,浮层需要渲染到它里面。
'slot' | 'source'拖拽时浮层的尺寸。'slot'(默认):它跟随项将落入的位置,所以经过另一个列表时它会采用那个列表中项的尺寸,放置时也不需要调整尺寸。它围绕指针调整尺寸,指针始终位于盒子中相同的比例位置。'source':保持被拖拽元素的尺寸。默认浮层的内容会按新尺寸重新布局;它永远不会被缩放。
number | 'all'多项拖拽:拿起时有多少个被拖拽项获得克隆,即抓起的项以及按放置顺序与它相邻的其他项。默认 5;'all' 克隆每一个已渲染的项。默认浮层显示这些克隆,并为其余的项计数(+N);在你自己的布局中,DragPreview 为没有克隆的项渲染它的插槽。每个克隆都会飞:取消时飞回它的项,放置时飞到它的位置,放置进某个区域时依次飞入该区域。克隆是一起生成的,每次快照过程只为全部克隆做一次布局,但每个克隆都要读取它的样式:在慢速手机上留意很长的项。
boolean | TransitionProps拖拽过程中浮层从一个组件换成另一个组件的方式(放置目标的 overOverlay 接管,或 overlay ref 被切换):true(默认)在 150 ms 内交叉淡入淡出,false 立即替换,对象则是包裹内容根元素的 Vue <Transition> 的 props(name、mode,以及用于 GSAP 或 motion 的 JS 钩子)。没有 mode: 'out-in' 时两者会同时显示:给离开的那个设置 position: absolute(.<name>-leave-active),让它们在盒子里叠在一起。
string | Record<string, boolean> | Array<string | Record<string, boolean>>浮层内容([data-dnd-overlay-content])的 class,即填满浮层盒子的那个元素:库移动盒子并调整其尺寸时,要整体添加动画的就是它。它上面的过渡在拖拽拿起时从 @starting-style 开始运行,落位时反向运行([data-dnd-overlay='dropping']):放置会等待它完成。普通 CSS 要在 <style scoped> 之外(或用 :global(...))才能选中它,Tailwind 则用 starting: 和 in-data-[dnd-overlay=dragging]:。
Partial<Announcements> | false为部分事件提供自己的文本(参见 Announcements);false 让实时区域保持静默。
string如何用键盘拖拽,屏幕阅读器会为每个可拖拽元素读出(它的 aria-describedby)。默认:关于 Space、Enter、方向键和 Escape 的英文说明。
boolean注入默认外观:零优先级、分层的 CSS,让占位元素和多项拖拽留在原处的项变淡(opacity: 0.4)。默认 true;false 把外观交给你。拖拽所需的样式无论如何都会注入:拖拽期间禁止选中文本和滚动锚定(虚拟列表依赖于此)、拖拽光标(--dnd-cursor),以及 iOS 上长按时不弹出链接菜单。
事件
| 事件 | 载荷 | 触发时机 |
|---|---|---|
drag-start | DnDEvent | 拖拽已开始(在激活之后)。 |
drag-move | DnDEvent | 拖拽期间指针移动了(每帧最多一次)。 |
drag-over | DnDEvent | 拖拽下方的目标发生了变化。 |
drag-end | DnDEndEvent | 放置已完成并已写入。 |
drag-cancel | DnDEvent | 拖拽被取消,或放置被拒绝。 |
drag-resume | DnDEvent | onBeforeDrop 返回了 'continue':没有发生放置,拖拽继续进行。 |
drag-settle | DnDEndEvent 或 DnDEvent | 拖拽的浮层已经落位:参数是 drag-end 或 drag-cancel 收到的那个事件。 |
同样的事件,再加上 onBeforeDrop,可以通过 useDnDMonitor 在 provider 之下的任何地方使用。它们的载荷见事件页面。
#overlay 插槽
为每一次没有自己浮层组件的拖拽渲染跟随指针的内容(参见浮层指南)。什么都不渲染时会退回到克隆。
浮层显示的内容:provider 的 #overlay 插槽和浮层组件(可拖拽元素、可排序项、列表和树的 overlay 选项)的 props。
Active这次拖拽。
DragItem[]所有被拖拽的项,主项在最前:整个多项拖拽只用一个浮层。
Over | null拖拽下方的放置目标。已确定的放置执行动画期间,是它要落到的目标(保持放置那一刻的状态);被取消的拖拽飞回原处时为 null。
booleanover 是否接收被拖拽的项;下方没有目标时为 false。
DnDStatus'dragging',然后在浮层落位(或飞回原处)时为 'dropping'。
DragMode此刻放置会做什么(见 DnDEvent.mode):例如复制时显示一个加号。
DropDestination | null现在放置会把被拖拽的项放到可排序列表或树中的哪里:例如按树拖拽将进入的文件夹给浮层着色。在所有目标之外,是空位所在的地方,dropOutside: 'placeholder'(默认值)会把它们留在那里。当放置不会把它们放进任何列表时为 null:在普通放置目标上方、在拒绝它们的列表上方、在 dropOutside: 'cancel' 或 'remove' 时位于目标之外。已确定的放置执行动画期间,是它落到的位置。
键盘
键盘拖拽(provider 的 keyboard):它们的按键,以及自由拖拽的步长。
自动滚动
拖拽靠近容器边缘时容器如何滚动(provider 的 autoScroll)。
number边缘区域,以容器尺寸的比例表示。默认 0.2(限制在 24–96px 之间)。
number最大速度,单位为每 16ms 帧的像素数。
number指针位于容器边缘时的速度,单位为每帧 px:在边缘区域内更慢,越过边缘更快,最高到 maxSpeed。默认 10。
触感反馈
触摸拖拽何时振动。只有由触摸发起的拖拽会振动;在浏览器无法振动(iOS Safari)或阻止振动(页面上还没有过点击)的地方,什么都不会发生。
一次振动(navigator.vibrate):以 ms 为单位的时长,或一个开/关模式。true 是默认振动(10ms;refuse 为 [20, 40, 20]),false 或 0 表示不振动。
type HapticPattern = boolean | number | number[]播报
给屏幕阅读器的文本,按事件区分。返回 undefined 使用内置文本,返回 '' 则不播报。context 包含可排序列表的内置文本所播报的位置。
文本由 provider 的实时区域(live region)播报(移动要等拖拽停留 500 ms 之后);undefined 把该事件交给下一个来源(集成提供的文本,例如可排序列表的位置,然后是默认文本)。拿起时找到的目标不会播报移动:此时还没有任何东西移动。
(event: DnDEvent, context: AnnouncementContext) => string | undefined拖拽开始了。
(event: DnDEvent, context: AnnouncementContext) => string | undefined拖拽移到了另一个目标上方,或列表中的另一个位置(停留 500 ms 之后)。
(event: DnDEndEvent, context: AnnouncementContext) => string | undefined放置已生效,或拖拽在所有目标之外结束。
(event: DnDEvent, context: AnnouncementContext) => string | undefined拖拽被取消,或者它的放置被拒绝。
(event: DnDEvent, context: AnnouncementContext) => string | undefinedonBeforeDrop 返回了 'continue':拖拽继续。
播报所涉及的、事件之外的信息:供你自己的文本(其他语言、其他措辞)表达内置文本所表达的内容。