文档/API

DnDProvider

掌管拖拽的组件——为渲染在它内部的所有内容负责输入、浮层、选中集合、自动滚动和实时区域。

编辑此页

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

const DnDProvider: Component<DnDProviderProps>;

管理在它内部渲染的所有内容的拖拽:输入、浮层、选中集合、自动滚动和屏幕阅读器播报(见 DnDProviderProps)。

App.vue
<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 需要时才读取:改变其中一个,下一次拖拽就会使用新值。

Live

正在加载演示…

<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>,它位于页面上所有其他内容之上。关闭开关,你拖动的行就会消失在对话框下面。浮层指南说明了什么时候需要它。

Live

正在加载演示…

<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-startDnDEvent拖拽已开始(在激活之后)。
drag-moveDnDEvent拖拽期间指针移动了(每帧最多一次)。
drag-overDnDEvent拖拽下方的目标发生了变化。
drag-endDnDEndEvent放置已完成并已写入。
drag-cancelDnDEvent拖拽被取消,或放置被拒绝。
drag-resumeDnDEventonBeforeDrop 返回了 'continue':没有发生放置,拖拽继续进行。
drag-settleDnDEndEvent 或 DnDEvent拖拽的浮层已经落位:参数是 drag-end 或 drag-cancel 收到的那个事件。

同样的事件,再加上 onBeforeDrop,可以通过 useDnDMonitor 在 provider 之下的任何地方使用。它们的载荷见事件页面。

#overlay 插槽

为每一次没有自己浮层组件的拖拽渲染跟随指针的内容(参见浮层指南)。什么都不渲染时会退回到克隆。

浮层显示的内容:provider 的 #overlay 插槽和浮层组件(可拖拽元素、可排序项、列表和树的 overlay 选项)的 props。

Active

这次拖拽。

DragItem[]

所有被拖拽的项,主项在最前:整个多项拖拽只用一个浮层。

Over | null

拖拽下方的放置目标。已确定的放置执行动画期间,是它要落到的目标(保持放置那一刻的状态);被取消的拖拽飞回原处时为 null。

boolean

over 是否接收被拖拽的项;下方没有目标时为 false。

DnDStatus

'dragging',然后在浮层落位(或飞回原处)时为 'dropping'。

DragMode

此刻放置会做什么(见 DnDEvent.mode):例如复制时显示一个加号。

DropDestination | null

现在放置会把被拖拽的项放到可排序列表或树中的哪里:例如按树拖拽将进入的文件夹给浮层着色。在所有目标之外,是空位所在的地方,dropOutside: 'placeholder'(默认值)会把它们留在那里。当放置不会把它们放进任何列表时为 null:在普通放置目标上方、在拒绝它们的列表上方、在 dropOutside: 'cancel' 或 'remove' 时位于目标之外。已确定的放置执行动画期间,是它落到的位置。

键盘

键盘拖拽(provider 的 keyboard):它们的按键,以及自由拖拽的步长。

{ start?: string[]; drop?: string[]; cancel?: string[] }

用于拿起(start)、放下和取消的按键,以 KeyboardEvent.code('Space'、'KeyW')或 .key('Enter'、'w')表示。默认:Space 和 Enter 拿起和放下,Escape 取消。方向键始终用于移动。

number

自由(非可排序)拖拽中每按一次方向键移动的像素数。默认 25。

自动滚动

拖拽靠近容器边缘时容器如何滚动(provider 的 autoScroll)。

number

边缘区域,以容器尺寸的比例表示。默认 0.2(限制在 24–96px 之间)。

number

最大速度,单位为每 16ms 帧的像素数。

number

指针位于容器边缘时的速度,单位为每帧 px:在边缘区域内更慢,越过边缘更快,最高到 maxSpeed。默认 10。

触感反馈

触摸拖拽何时振动。只有由触摸发起的拖拽会振动;在浏览器无法振动(iOS Safari)或阻止振动(页面上还没有过点击)的地方,什么都不会发生。

HapticPattern

拖拽在长按之后开始时。默认 true。

HapticPattern

拖拽进入一个接收它的放置目标(列表、区域)时。默认 false。

HapticPattern

放置生效时。默认 false。

HapticPattern

放置被拒绝(被目标或 onBeforeDrop)、项飞回原处时。默认 false。

一次振动(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 | undefined

onBeforeDrop 返回了 'continue':拖拽继续。

播报所涉及的、事件之外的信息:供你自己的文本(其他语言、其他措辞)表达内置文本所表达的内容。

AnnouncedPosition

在可排序列表或树中:被拖拽项所在的位置。开始时,是它们被拿起的位置;移动时,是它们将要落到的位置(在列表之外时没有);结束时,是它们落到的位置,或返回的位置。

{ key: Key; label?: string; position: AnnouncedPosition }

交换:与被拖拽项交换的那个项,以及它现在的位置。

number

这次拖拽携带了多少项。

boolean

这次拖拽是复制(group.pull: 'clone'、复制键)。

可排序列表或树(其可见行)中的一个位置:index 从 0 开始计数,总数为 length。

Key

列表或树的 id。

string

列表的 label,即人们对它的称呼。

number

位置,从 0 开始:内置文本播报的是 index + 1。

number

列表共有多少个位置。