文档/指南

Provider

DnDProvider 掌管它之下的每一次拖拽。它查找目标、启动拖拽、负责滚动,并把发生的一切告诉 useDnD 和 useDnDMonitor。

编辑此页

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

整个应用一个 provider

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

<template>
  <DnDProvider>
    <RouterView />
  </DnDProvider>
</template>

在较高的层级放一个 DnDProvider,比如 App.vue 或某个布局里,然后就不用再管它了。每个组合式函数(makeSortableList、makeDraggable、useDnD……)都会查找它所在组件上方最近的 provider。

组合式函数必须在渲染于 provider 内部的组件中运行。如果在自身模板包含 <DnDProvider> 的组件里调用,会抛出 No <DnDProvider> found above this component:Vue 的 inject 只能看到祖先组件。

它掌管什么

部分provider 做什么
输入把指针、触摸和键盘输入变成拖拽(sensors、activation),一次一个拖拽。
查找目标对拖拽下方的放置目标做命中测试(collision),并通知它们。
浮层渲染跟随指针的盒子,传送到 body,并在放置时让它落位。
选中集合一个选中 id 的集合,由它之下的所有列表和可拖拽元素共享。
自动滚动拖拽靠近容器或窗口边缘时滚动它们。
实时区域向屏幕阅读器播报拿起、移动、放置和取消。

除此之外它什么都不渲染:你的标记保持原样。

选项一览

每个选项都有可用的默认值,所以 provider 通常不需要传任何选项。provider 在需要时才读取它的 props:下面的演示会实时切换 collision、activation 和 autoScroll。

Prop默认值设置什么
collisionpointerFirst如何选出拖拽下方的目标。见下文。
activation鼠标 4 px,触摸 200 ms按下何时变成拖拽,按指针类型分别设置。见下文。
autoScroll开启边缘滚动:{ threshold, maxSpeed, acceleration } 或 false。见下文。
haptics触摸拿起时短暂振动触摸拖拽的振动,或 false。见下文。
copyModifier'Alt'拖拽时按住该键,移动就变成复制。
modifiers[]适用于每次拖拽的移动规则:见约束。
animation / dropAnimation开启排序时项的滑动 / 浮层落位:见动画。
animateChanges关闭列表和树的默认值:为非拖拽引起的数据变化添加动画。
keyboardSpace / Enter、Escape键盘按键和步长;false 关闭键盘拖拽。见无障碍。
sensors指针 + 键盘把输入变成拖拽的部件:见传感器。
overlayTo'body'浮层传送到哪里。
overlayFit'slot'浮层尺寸:它将落入的位置的尺寸,或源元素的尺寸。见浮层。
overlayTransition交叉淡入淡出拖拽过程中浮层从一个组件换成另一个组件的方式:false,或 <Transition> 的 props。见改变浮层的目标。
overlayClass—浮层内容([data-dnd-overlay-content])的类,它是作为整体添加动画的元素:拖拽拿起时抬起,落位时落定。见浮层:抬起、倾斜、落定。
overlayPreviews5多项拖拽:拿起时有多少个被拖拽项获得克隆,或 'all'。见默认浮层。
announcements内置屏幕阅读器文本,或 false。
screenReaderInstructions内置每个可拖拽元素用 aria-describedby 指向的文本。
defaultStylestrue为拖拽状态注入零优先级的 CSS:见样式。

包含所有类型结构的完整列表见 DnDProvider API。provider 还会触发 drag-start、drag-move、drag-over、drag-end、drag-cancel、drag-resume 和 drag-settle,与 useDnDMonitor 收到的事件相同。

读取拖拽:useDnD()

Live

正在加载演示…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Tasks from './Tasks.vue';
import StatePanel from './StatePanel.vue';
</script>

<template>
  <!-- One provider: the list drags, the panel reads the drag. Neither imports the other. -->
  <DnDProvider>
    <div class="demo">
      <Tasks />
      <StatePanel />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 24px;
}
</style>

useDnD() 以响应式状态返回当前拖拽,可在 provider 之下的任何组件中使用:

import { useDnD } from '@vue-dnd-kit/core';

const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
字段内容
status'idle'、'activating'(按下后等待激活)、'dragging'、'dropping'(没有拖拽在进行,但仍有浮层在落位,或异步的 onBeforeDrop 正在做决定)、'selecting'(框选中)。
active被拖拽的内容:id、ids(所有被拖拽的 id,抓起的那个在最前)、items、data、type、element、rect、sensor。空闲时为 null。
over拖拽下方的目标:id、data、element、rect 和 accepted。下方没有目标时为 null。
pointer指针的视口坐标,已经过修饰器处理。
activation.progress按下等待激活期间从 0 到 1:可以用它画一个圆环。
isAutoScrolling自动滚动正在滚动某个元素。
selectionprovider 的选中集合:ids、has、add、delete、toggle、set、clear。见多项拖拽。
cancel()取消进行中的拖拽(项飞回原处)、正在蓄力的按下,或框选。
DropHint.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useDnD } from '@vue-dnd-kit/core';

const { status, over } = useDnD();
const refused = computed(() => status.value === 'dragging' && over.value?.accepted === false);
</script>

<template>
  <p v-if="refused" role="status">This list doesn't take that.</p>
</template>

放置一旦确定,active、over 和 pointer 就读到 null,即使浮层还在落位(status 读到 'dropping')。由它们派生的 UI 永远不会把旧的拖拽和新数据一起显示。在异步的 onBeforeDrop 等待期间,状态保持松开时的样子。

下一次拖拽不会等待落位:它立即开始,上一个浮层在它周围落位时,status 读到 'dragging'。在正在落位的浮层上按下,就能在半空中接住它:拖拽从浮层所在的位置继续。onDragSettle 告诉你一次拖拽的浮层何时落定。

监听每一次拖拽:useDnDMonitor()

import { useDnDMonitor } from '@vue-dnd-kit/core';

useDnDMonitor({
  onDragStart: ({ active }) => {},
  onDragMove: ({ pointer, delta }) => {},
  onDragOver: ({ over }) => {},                  // the target changed
  onBeforeDrop: (e) => 'accept',                 // or 'reject', 'continue', some ids; may be async
  onDragEnd: ({ active, over, dropped, accepted, position }) => {},
  onDragCancel: ({ active }) => {},
  onDragResume: ({ active }) => {},              // onBeforeDrop answered 'continue'
  onDragSettle: ({ active, over }) => {},        // its overlay has landed
});

监听器能收到其 provider 之下的每一次拖拽,无论拖拽从哪里开始:上面的面板无需导入列表,就能记录列表的拖拽。调用它的组件卸载时,监听器随之停止。

事件触发时机
onDragStart拖拽开始。
onDragMove拖拽移动。
onDragOver拖拽下方的目标改变。
onBeforeDrop拖拽被松开。它的返回值决定放置:见决定放置。
onDragEnd放置已完成。可排序列表已经运行了它们的 onDrop 并写入了新顺序。拖拽结束时下方没有目标,则 dropped 为 false。
onDragCancel按下 Escape、放到拒绝它的目标上,或 onBeforeDrop 返回了 'reject'。
onDragResumeonBeforeDrop 返回了 'continue':没有放置,拖拽继续。
onDragSettle拖拽的浮层已经落位(或已飞回原处,或被下一次拖拽接住),参数是 onDragEnd 或 onDragCancel 收到的那个事件。在此期间可能已有其他拖拽开始。

每个事件都带有 active、over、pointer、delta(相对起点的偏移)、position(被拖拽的盒子在 over 中的位置,见拖放),对来自可排序列表的项还带有 source。完整结构见事件 API。

onDragEnd 是做“放置之后”工作的地方:保存、清空选中集合、统计分析。列表自己的 onDrop 在写入之前运行,所以它看到的仍是旧顺序。

查找目标

选择一个函数,然后把图钉或卡片拖到各个目标上。徽标显示该函数在这一帧中对它们的排名;绿色的那个就是目标。

Live

正在加载演示…

<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
  DnDProvider,
  closestCenter,
  closestCorners,
  pointerFirst,
  pointerWithin,
  rectIntersection,
  type CollisionDetection,
  type DnDEndEvent,
  type Key,
} from '@vue-dnd-kit/core';
import Desk from './Desk.vue';
import { magnet } from './magnet';

const DETECTORS: Record<string, CollisionDetection> = {
  pointerFirst,
  pointerWithin,
  rectIntersection,
  closestCenter,
  closestCorners,
  magnet,
};
const names = Object.keys(DETECTORS);
const mode = ref('pointerFirst');

// The ranking of the current frame, best first: a wrapper around the chosen function
// records it, so the targets can show their rank.
const ranked = shallowRef<Key[]>([]);
const collision = computed<CollisionDetection>(() => {
  const detect = DETECTORS[mode.value]!;
  return (args) => (ranked.value = detect(args));
});

const result = ref('Drag the pin or the card over the targets');
const onEnd = ({ over }: DnDEndEvent) => {
  ranked.value = [];
  result.value = over ? `Dropped on ${over.id}` : 'Dropped on nothing';
};
const onCancel = () => {
  ranked.value = [];
  result.value = 'Cancelled';
};
</script>

<template>
  <DnDProvider :collision="collision" @drag-end="onEnd" @drag-cancel="onCancel">
    <div class="lab">
      <div class="modes" role="radiogroup" aria-label="Collision detection">
        <label v-for="name in names" :key="name" :class="{ on: mode === name }">
          <input v-model="mode" type="radio" :value="name" />
          {{ name }}
        </label>
      </div>
      <code class="call">&lt;DnDProvider :collision="{{ mode }}"&gt;</code>
      <Desk :ranked="ranked" />
      <p class="result" aria-live="polite">{{ result }}</p>
    </div>
  </DnDProvider>
</template>

<style scoped>
.lab {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

.modes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
  max-width: 100%;
  padding: 3px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.modes label {
  padding: 5px 9px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.call {
  max-width: 100%;
  padding: 6px 10px;
  overflow-x: auto;
  border-radius: 8px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  color: #42b883;
}

.result {
  margin: 0;
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.6;
}
</style>

在拖拽的每一帧,provider 把参与其中的放置目标交给 collision,它返回这些目标的 id,最佳的在前。库自带五个函数:

函数排名依据适用于
pointerFirst指针下方的目标;没有时,取被拖拽盒子所重叠的目标。默认值:指针下方精确,间隙处宽容。
pointerWithin只看指针下方的目标,最内层的在前。精确的目标:放在间隙里不会落到任何地方。
rectIntersection被拖拽盒子重叠的每个目标:先比 priority,再比嵌套深度,再比重叠面积。大的项拖到小的目标上,而指针位于项的边缘。
closestCenter所有目标,按盒子中心到目标中心的距离。总有一个目标胜出,即使离得很远:网格以及等尺寸项的列表。
closestCorners所有目标,按四个角之间的距离。并排的列(看板),高度差异很大的目标。

在指针下方,命中测试遵循用户所见:重叠的目标中只有最上层的那个会被命中(重叠处是 front,从不是 back),嵌套的目标中最内层的在前(先 subfolder,再 folder)。

自定义函数

magnet.ts
import type { CollisionDetection, Coordinates, Rect } from '@vue-dnd-kit/core';

/** How far the point is from the rect: 0 inside it. */
const gap = (rect: Rect, point: Coordinates) =>
  Math.hypot(
    Math.max(rect.left - point.x, 0, point.x - rect.right),
    Math.max(rect.top - point.y, 0, point.y - rect.bottom),
  );

/** What is under the pointer; in a gap, the nearest target within 40px. */
export const magnet: CollisionDetection = ({ hits, pointer, candidates }) => {
  if (hits.length) return hits;
  return candidates
    .filter((candidate) => gap(candidate.rect, pointer) <= 40)
    .sort((a, b) => gap(a.rect, pointer) - gap(b.rect, pointer))
    .map((candidate) => candidate.id);
};
参数内容
pointer指针,已经过修饰器处理。
overlayRect被拖拽盒子当前显示的样子,没有浮层时为 null。
candidates参与其中的每个目标:id、rect(它的可见部分,已被滚动容器裁剪)、priority、depth(顶层为 0,嵌套越深越大)。
hits指针下方的目标,来自命中测试,最佳的在前。

返回空数组表示“没有目标”。禁用的目标、位于被拖拽项内部的目标,以及被滚动容器遮住的目标,都不会传给该函数。拒绝该拖拽的目标仍然参与竞争:拖到它上方时,over.accepted 为 false,放置会被拒绝(位于可排序项内部的目标则会让开,让列表在它上方排序)。演示包装了所选的函数来记录它的排名:(args) => (ranked.value = detect(args))。

provider 会抑制函数请求的切换。拖拽刚离开的目标,要等指针移动 10 px 后才能再次胜出,因此边界不会闪烁。在嵌套于同一父元素的目标之间(一列卡片的间隙里),保留上一个目标。

按下何时变成拖拽

按下不会立刻开始拖拽。activation 按指针类型(mouse、touch、pen)设置要等待的条件:以 px 计的 distance、以 ms 计的 delay(外加一个 tolerance,超出它按下就放弃,让页面可以滚动),或两者中先满足的那个。

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
指针默认值
mouse、pen{ distance: 4 }:点击仍然是点击。在 handle 上按下鼠标会立刻开始,不移动的话仍然是点击。
touch{ delay: 200, tolerance: 5 }:长按,这样手指下的列表仍能滚动。

没有写出的类型保留默认值。可拖拽元素自己的 activation 优先于拖拽区域的,后者又优先于 provider 的。按下等待期间,useDnD().status 为 'activating',activation.progress 从 0 走到 1;按 Escape 或调用 useDnD().cancel() 会放弃它。在传感器中可以实时试验这些约束,并根据进度画出长按圆环。

自动滚动

把一个频道拖向列表的顶部或底部。绿色条带是边缘区域:越深入其中,列表滚动得越快。

Live

正在加载演示…

<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Channels from './Channels.vue';

const OPTIONS = {
  threshold: [0.1, 0.2, 0.4],
  maxSpeed: [10, 25, 50],
  acceleration: [4, 10, 25],
} as const;

const on = ref(true);
// The defaults: an edge zone of 20% of the container (24 to 96px), 10px a frame at the
// edge, up to 25px a frame past it.
const options = ref({ threshold: 0.2, maxSpeed: 25, acceleration: 10 });
const autoScroll = computed(() => (on.value ? options.value : false));
const call = computed(() =>
  on.value
    ? `:auto-scroll="{ threshold: ${options.value.threshold}, maxSpeed: ${options.value.maxSpeed}, acceleration: ${options.value.acceleration} }"`
    : ':auto-scroll="false"',
);
</script>

<template>
  <DnDProvider :auto-scroll="autoScroll">
    <div class="demo">
      <div class="switches">
        <label class="switch">
          <input v-model="on" type="checkbox" />
          autoScroll
        </label>
        <fieldset v-for="(values, name) in OPTIONS" :key="name" :disabled="!on">
          <legend>{{ name }}</legend>
          <label v-for="value in values" :key="value" :class="{ on: options[name] === value }">
            <input v-model="options[name]" type="radio" :value="value" />
            {{ value }}
          </label>
        </fieldset>
      </div>
      <code class="call">{{ call }}</code>
      <Channels :threshold="on ? options.threshold : 0" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.switches {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 14px;
}

fieldset {
  display: inline-flex;
  margin: 0;
  padding: 3px;
  border: 0;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

fieldset:disabled {
  opacity: 0.4;
}

legend {
  float: left;
  align-self: center;
  padding: 0 8px 0 6px;
  font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

fieldset label {
  padding: 5px 9px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

fieldset label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

fieldset label:has(input:focus-visible),
.switch:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

fieldset input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.switch input {
  accent-color: #42b883;
}

.call {
  max-width: 100%;
  padding: 6px 10px;
  overflow-x: auto;
  border-radius: 8px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  color: #42b883;
}
</style>

任何 overflow: auto 或 scroll 的元素都会自动滚动,窗口也一样。provider 先滚动指针下方的容器,再滚动它的祖先,最后滚动窗口。

选项默认值设置什么
threshold0.2边缘区域,为容器尺寸的比例,介于 24 到 96 px 之间。
acceleration10在最边缘处的速度,单位为 px/帧。它随深入区域的深度的平方增长,所以起步缓慢而精确。
maxSpeed25最高速度。越过边缘(指针在容器之外)后,速度继续增长,直到这个值。

:auto-scroll="false" 会关闭它。只有当指针已朝某个方向移动后,才会朝那个方向滚动,所以在边缘附近拿起的项不会自己滚动。列表、树和拖拽区域可以为自己的容器设置单独的 autoScroll:

makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });

键盘拖拽从不自动滚动:它会把项滚动到可见范围内。有内容在滚动时,isAutoScrolling(来自 useDnD() 或拖拽区域)为 true。

触感反馈与复制键

触摸拖拽开始时会短暂振动。:haptics="false" 关闭它;传入对象可以设置每一种振动:

<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
键振动时机默认值
start长按变成拖拽。true(10 ms)
enter拖拽进入接受它的目标。关闭
drop放置完成。关闭
refuse放置被拒绝,项飞回原处。关闭

每一项可以取 true(默认模式)、以 ms 计的时长、一个开/关模式数组,或 false。只有由触摸开始的拖拽才会振动,而 iOS Safari 完全无法振动。

copyModifier 是在列表之间拖拽时按住、把移动变成复制的那个键:默认为 'Alt',可以是任意 KeyboardEvent.key,或 null 表示不用。项所来自的列表需要一个 clone 函数;见共享项的列表。

多个 provider

大多数应用恰好只需要一个。把愿望清单放到它自己的 provider 下,然后试着把一本书移过去:它无法离开自己的 provider,外层的监听器也不再收到愿望清单的拖拽。

Live

正在加载演示…

<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
import EventLog from './EventLog.vue';

const reading = ref([
  { id: 'dune', title: 'Dune', hue: 32 },
  { id: 'emma', title: 'Emma', hue: 330 },
  { id: 'ulysses', title: 'Ulysses', hue: 210 },
]);
const wishlist = ref([
  { id: 'beloved', title: 'Beloved', hue: 140 },
  { id: 'solaris', title: 'Solaris', hue: 260 },
]);

const separate = ref(false);
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <div class="modes" role="radiogroup" aria-label="Wishlist">
        <label :class="{ on: !separate }"><input v-model="separate" type="radio" :value="false" />one provider</label>
        <label :class="{ on: separate }"><input v-model="separate" type="radio" :value="true" />wishlist in its own</label>
      </div>
      <div class="columns">
        <div class="column">
          <Shelf id="reading" v-model="reading" title="Reading" />
          <EventLog title="outer monitor" />
        </div>
        <!-- A provider inside a provider: its items belong to it, and drags never cross. -->
        <DnDProvider v-if="separate">
          <div class="column island">
            <span class="tag">&lt;DnDProvider&gt;</span>
            <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
            <EventLog title="inner monitor" />
          </div>
        </DnDProvider>
        <div v-else class="column">
          <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
        </div>
      </div>
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}

.modes {
  display: inline-flex;
  padding: 3px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.modes label {
  padding: 5px 10px;
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 230px));
  gap: 16px;
  max-width: 100%;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.island {
  position: relative;
  margin: -10px;
  padding: 10px;
  border: 1.5px dashed color-mix(in srgb, #42b883 60%, transparent);
  border-radius: 16px;
}

.tag {
  position: absolute;
  top: -9px;
  right: 14px;
  padding: 0 6px;
  background: Canvas;
  font: 10px/18px ui-monospace, Menlo, Consolas, monospace;
  color: #42b883;
}
</style>
<DnDProvider>
  <Shelf v-model="reading" />
  <DnDProvider>
    <Shelf v-model="wishlist" />
    <EventLog />            <!-- hears the inner provider's drags only -->
  </DnDProvider>
</DnDProvider>
  • provider 之间相互独立:拖拽永远不会从一个跨到另一个。
  • 允许嵌套 provider。最近的所有者胜出:项属于它上方最近的 provider,外层 provider 会忽略它。
  • 每个 provider 都有自己的选中集合、浮层、实时区域和监听器。

只有当页面的某一部分需要自己的 provider 级设置(碰撞检测、键盘按键、播报),并且其中的项从不离开它时,第二个 provider 才有意义。如果项无法在页面的两部分之间移动,检查它们是否处于两个 provider 之下。

针对页面某一部分的规则(吸附的画板、触摸按下时间更长的信息流、不能拖拽任何东西的面板)不需要单独的 provider:
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
其中的项仍属于应用的 provider,依然可以移动到任何地方。见约束。

挂载在别处的应用,无论是在 shadow root 中还是作为独立的 Vue 应用,都可以加入已有的 provider:在挂载之前把 useDnDContext() 传给 provideDnDContext(app, context)。命中测试和浮层可以跨越 shadow root 工作。

SSR 与 Nuxt

provider 对 SSR 是安全的。列表和项像其他标记一样在服务端渲染。浮层和实时区域是传送出去的,只在客户端挂载,因此永远不会导致激活不匹配。默认 id 来自 Vue 的 useId(),在服务端和客户端之间保持一致。

在 Nuxt 中,模块会全局注册 DnDProvider 并自动导入组合式函数:见安装。