拖拽可以被固定在一个轴上、限制在一个盒子内、吸附到网格,或按你自己的规则调整。这些规则就是修饰器:它们在每一帧调整拖拽的偏移。可以把它们设置在一个可拖拽元素、一个列表或页面的一部分上,浮层、放置目标和放置都会遵循它们。
锁定轴
时间线上的片段沿着它们的轨道滑动。关闭锁定后,它们也可以换到别的轨道。
正在加载演示…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Timeline from './Timeline.vue';
</script>
<template>
<DnDProvider>
<Timeline />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.clip.id,
axis: () => (props.locked ? 'x' : undefined),
});
axis: 'x' 只沿水平方向移动,'y' 只沿竖直方向移动。makeDraggable、makeSortableList、makeSortableTree、makeSortableItem 和 makeDragArea 都接受它:
makeSortableList(listRef, { items: tracks, axis: 'y' });
- 锁定在所有修饰器之后运行,所以无论它们做什么,锁定都成立:吸附或边界无法把项推离它的直线。
- 这条直线在内容里,而不在屏幕上:它随拖拽开始时所在的列表(或父元素)移动。拖拽中途滚动页面,项仍留在它的那一行。
- 项自己的
axis优先于它所在列表或树的,可拖拽元素(或它所在列表)的又优先于它所在拖拽区域的。 - 它在拖拽开始时读取,所以可以像上面那样用 getter 切换它。
- 锁定到
'y'的树仍然可以嵌套:此时深度来自悬停在某一行的中间。
axis,而不是 restrictToVerticalAxis 和 restrictToHorizontalAxis 修饰器。它们做的是同一件事,但作为普通修饰器,它们在链条中间运行,排在它们之后的修饰器(同时吸附两个轴的网格)可能会再次把项移离该轴。修饰器
修饰器在每一帧调整拖拽的偏移。它的结果既移动浮层,也是查找目标所用的点,因此列表按锁定、限制或吸附后的位置排序,放置的 position 就是盒子最终所在的位置。
| 修饰器 | 作用 |
|---|---|
restrictToParent | 把浮层限制在拖拽开始时所在的容器内:可排序列表,或普通可拖拽元素的父元素。 |
restrictToElement(target) | 把浮层限制在一个元素内:模板引用、getter 或元素本身。 |
restrictToWindowEdges | 把浮层限制在窗口内。 |
restrictToFirstScrollableAncestor | 把浮层限制在被拖拽元素的第一个可滚动祖先内。 |
snapToGrid(size) | 把浮层吸附到网格(见下文)。 |
snapCenterToCursor | 让浮层居中于指针下方,而不是保持抓取时的偏移。 |
restrictToVerticalAxis | 只沿竖直方向移动。优先使用 axis: 'y'。 |
restrictToHorizontalAxis | 只沿水平方向移动。优先使用 axis: 'x'。 |
modifiers 是 provider、拖拽区域、列表和树,以及可拖拽元素和可排序项的选项。除 provider 的以外,其余的都还接受 ref 或 getter,在拖拽开始时读取。
限制在内部
视频通话中的自己画面始终留在屏幕上。切换边界,然后把它远远拖出去:
正在加载演示…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import CallScreen from './CallScreen.vue';
</script>
<template>
<DnDProvider>
<CallScreen />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: 'self-view',
modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
restrictToParent把它固定在屏幕上,也就是它的父元素。对于可排序项,“父元素”是它所在的列表,无论列表位于 DOM 中的哪里。restrictToWindowEdges允许它离开屏幕,但不能离开窗口。放在屏幕之外时,它不在放置目标上,所以会飞回原处。- 没有边界时,它可以去任何地方。
边界以浮层当前的尺寸把它限制在内部:使用 overlayFit: 'slot' 时,在较宽项的列表上方变大的盒子会以新的尺寸被限制在内部。
吸附到网格
流程图的节点吸附到画布网格上。切换单元格大小和平滑滑动:
正在加载演示…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Flowchart from './Flowchart.vue';
</script>
<template>
<DnDProvider>
<Flowchart />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.node.id,
modifiers: () => [snapToGrid({ x: props.grid, y: props.grid, smooth: props.smooth })],
});
snapToGrid(24) 把两个轴都吸附到 24 px 的单元格。传入对象可以设置更多:
| 选项 | 含义 |
|---|---|
x、y | 每个轴的单元格大小,单位为像素。省略其中一个,就只吸附另一个轴:{ y: 15 } 把事件吸附到每刻钟,同时让它在各天之间自由移动。 |
origin | 'container'(默认):浮层的左上角落在从容器左上角画出的网格线上——容器是可排序列表,或普通可拖拽元素的父元素。'delta':对偏移本身取整,所以一开始就不在网格上的项会一直不在网格上,并从原来的位置按整格移动。适用于放在任意位置(自由画布)、需要保持自身对齐的项。 |
smooth | true 或 { duration, easing }:浮层在单元格之间平滑滑动(默认 80 ms)。目标立即使用吸附后的位置。 |
使用 origin: 'container' 时,从容器的 border box 开始画网格:上面的画布用 box-shadow 而不是 border 作为边框,所以它的网格线从最边缘开始。
自定义修饰器
幻灯片上的方框靠近其他方框时,会跳到它们的边缘和中心上,粉色参考线显示它们所对齐的线:
正在加载演示…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Slide from './Slide.vue';
</script>
<template>
<DnDProvider>
<Slide />
</DnDProvider>
</template>import type { Modifier } from '@vue-dnd-kit/core';
/** Near a line, the box jumps onto it. */
const magnet: Modifier = ({ transform, overlayRect, containerRect, active }) => {
if (!containerRect) return transform;
const id = String(active.id);
const left = overlayRect.left - containerRect.left;
const top = overlayRect.top - containerRect.top;
return {
x: transform.x + pull(left, overlayRect.width, lines(id, 'x')),
y: transform.y + pull(top, overlayRect.height, lines(id, 'y')),
};
};
// In order: the magnet pulls, then the slide's edges hold.
const modifiers = [magnet, restrictToParent];
修饰器接收 transform,即经过前面的修饰器处理后、相对拖拽起点的偏移,并返回一个新的偏移。它还会收到计算时可能需要的信息:
| 参数 | 是什么 |
|---|---|
transform | 到目前为止的偏移。 |
overlayRect | 按 transform 移动后的浮层盒子,尺寸为它当前的尺寸。 |
activeRect | 拖拽开始时的被拖拽元素。 |
containerRect | 拖拽开始时所在的容器(可排序列表或父元素)的当前位置:它会跟随滚动。 |
windowRect | 窗口。 |
origin | 拖拽开始时的指针。 |
active、over | 拖拽,以及它下方的目标。 |
修饰器在每一帧运行:让它们保持快速、没有副作用。上面的参考线是在监听器的 onDragMove 中根据拖拽的 delta 画出的,delta 是经过所有修饰器之后的偏移。类型见修饰器。
顺序
来自多个地方的修饰器依次运行,每一个都基于前一个的结果:
- provider 的
modifiers; - 拖拽开始时所在的拖拽区域的
modifiers; - 被拖拽项自己的
modifiers,没有时取它所在列表或树的; - 拖拽区域的
restrictToArea; axis锁定。
在同一个数组中,它们按顺序运行,所以把吸附和磁吸放在边界之前:放在最后的边界无论其他修饰器做了什么,都能把盒子留在内部。上面的幻灯片运行的是 [magnet, restrictToParent];如果顺序反过来,磁吸可能会把一个被固定在幻灯片边缘的方框拉出边缘几个像素。
项的 modifiers 会替换它所在列表的,而不是追加。区域的边界在第 1–3 步的吸附之后生效,所以吸附后的项仍然留在内部,而轴锁定在所有步骤的最后。
有边界的列表能到达两端
一个图层面板:行只能上下移动,永远不会离开面板。把最上面一行拖到最底部:
正在加载演示…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Layers from './Layers.vue';
</script>
<template>
<DnDProvider>
<Layers />
</DnDProvider>
</template>const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });
指针继续移动时,边界会把浮层固定在盒子的边缘。一旦指针沿列表轴向离开了浮层,列表就改为按指针排序,所以无论你怎样握住项,第一个和最后一个位置都始终可以到达。网格仍按浮层排序:固定在边缘时,它已经覆盖了边缘的单元格。
页面某一部分的规则
两个区域,两套规则。Layout 中的贴纸吸附到它的网格上,并且不能离开它;Sketch 中的贴纸可以去任何地方,也可以进入 Layout。拖拽保持它开始时所在区域的规则,规则正在生效的区域会高亮。
正在加载演示…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider, type Coordinates, type Key } from '@vue-dnd-kit/core';
import DragArea from './DragArea.vue';
const stickers = ref([
{ id: 'logo', text: 'Logo', color: '#42b883', area: 'sketch', x: 20, y: 24 },
{ id: 'photo', text: 'Photo', color: '#3b82f6', area: 'sketch', x: 110, y: 96 },
{ id: 'quote', text: 'Quote', color: '#ec4899', area: 'sketch', x: 36, y: 150 },
{ id: 'title', text: 'Title', color: '#f59e0b', area: 'layout', x: 24, y: 24 },
{ id: 'button', text: 'Button', color: '#a855f7', area: 'layout', x: 120, y: 144 },
]);
const place = (area: string, id: Key, at: Coordinates) => {
const sticker = stickers.value.find((item) => item.id === id);
if (!sticker) return;
sticker.area = area;
sticker.x = Math.round(at.x);
sticker.y = Math.round(at.y);
};
</script>
<template>
<DnDProvider>
<div class="studio">
<DragArea
id="sketch"
title="Sketch"
rules="no rules"
:stickers="stickers.filter((sticker) => sticker.area === 'sketch')"
@place="place"
/>
<DragArea
id="layout"
title="Layout"
rules="snapToGrid(24) · restrictToArea"
strict
:stickers="stickers.filter((sticker) => sticker.area === 'layout')"
@place="place"
/>
</div>
</DnDProvider>
</template>
<style scoped>
.studio {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px;
width: 100%;
max-width: 560px;
margin: 0 auto;
}
</style><script setup lang="ts">
const area = useTemplateRef<HTMLElement>('area');
const { isDragging } = makeDragArea(area, {
modifiers: () => (props.strict ? [snapToGrid(24)] : []),
restrictToArea: () => props.strict,
});
</script>
<template>
<div ref="area" class="area" :class="{ busy: isDragging }">
<Sticker v-for="sticker in stickers" :key="sticker.id" :sticker="sticker" />
</div>
</template>
makeDragArea 为在其元素内部开始的拖拽设置规则——普通可拖拽元素和可排序项都一样,它们因此不再需要自己的规则。整个应用保留一个 DnDProvider,给画板、画布或信息流单独一个区域。
| 选项 | 作用 |
|---|---|
axis | 把拖拽锁定到一个轴。可拖拽元素自己的 axis 或它所在列表的优先。 |
modifiers | 在 provider 的之后、可拖拽元素自己的之前运行。 |
restrictToArea | true:拖拽留在区域的元素内。 |
autoScroll | 区域滚动容器(该元素,或它最近的可滚动祖先)的自动滚动。false 在那里关闭它。 |
activation | 按指针类型设置的激活,比如在滚动的信息流中使用更长的触摸按下。可拖拽元素自己的优先;provider 的最后。 |
disabled | 区域内以及其内部的区域中都不会开始拖拽。它的可拖拽元素会报告 aria-disabled。 |
每个选项都接受值、ref 或 getter,在按下或拖拽开始时读取。区域返回 isDragging(一个从这里开始的拖拽正在进行,就像上面高亮的边框显示的那样)和 isAutoScrolling(自动滚动正在滚动它的容器)。完整参考:makeDragArea。
区域如何组合
- 拖拽遵循它开始时所在的区域,即使离开了它也一样。从某个区域上方经过不会改变任何东西,除了它的
autoScroll:它适用于每一个滚动其容器的拖拽。 - 嵌套的区域不会合并:最近的那个胜出。
disabled是例外,它会被继承:关闭的区域也会关闭其内部的区域。 - 碰撞检测、键盘按键和播报仍在 provider 范围内生效。
DnDProvider?provider 之间相互独立:拖拽永远不会从一个跨到另一个,所以画板中的项永远无法离开它。只有当页面的某一部分需要自己的 provider 级设置,并且其中的项留在其内时,才嵌套 provider。见 provider。放在哪就留在哪
上面的时间线、通话、流程图、幻灯片和区域,都会把每个项留在它被放置的地方。每个拖拽事件都带有 position:被拖拽盒子在目标内部的左上角,从目标的内边距边缘量起,并加上它的滚动距离——也就是能让绝对定位的项正好停在那里的 left 和 top。它在修饰器之后计算,所以吸附后的节点会落在网格上。
makeDroppable(canvas, { id: 'canvas' });
useDnDMonitor({
onDragEnd({ active, over, position }) {
const node = nodes.value.find((item) => item.id === active.id);
if (!node || over?.id !== 'canvas' || !position) return;
node.x = Math.round(position.x);
node.y = Math.round(position.y);
},
});
更多关于自由拖拽和放置目标的内容,见拖放。