---
title: "轴、边界与网格"
description: "把拖拽锁定在一个轴上、限制在一个盒子内、吸附到网格或你自己的参考线——可用于单个项、一个列表或页面的整个部分。"
canonical_url: "https://zizigy.github.io/vue-dnd-kit/zh/docs/guides/constraints"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

拖拽可以被固定在一个轴上、限制在一个盒子内、吸附到网格，或按你自己的规则调整。这些规则就是**修饰器**：它们在每一帧调整拖拽的偏移。可以把它们设置在一个可拖拽元素、一个列表或页面的一部分上，浮层、放置目标和放置都会遵循它们。

## 锁定轴

时间线上的片段沿着它们的轨道滑动。关闭锁定后，它们也可以换到别的轨道。

<example height="22rem" hint="拖动一个片段；关闭锁定后把它拖到另一条轨道" name="axis-timeline">



</example>

```ts [TimelineClip.vue]
const { isDragging } = makeDraggable(el, {
  id: () => props.clip.id,
  axis: () => (props.locked ? 'x' : undefined),
});
```

`axis: 'x'` 只沿水平方向移动，`'y'` 只沿竖直方向移动。`makeDraggable`、`makeSortableList`、`makeSortableTree`、`makeSortableItem` 和 `makeDragArea` 都接受它：

```ts
makeSortableList(listRef, { items: tracks, axis: 'y' });
```

- 锁定在**所有修饰器之后**运行，所以无论它们做什么，锁定都成立：吸附或边界无法把项推离它的直线。
- 这条直线在内容里，而不在屏幕上：它随拖拽开始时所在的列表（或父元素）移动。拖拽中途滚动页面，项仍留在它的那一行。
- 项自己的 `axis` 优先于它所在列表或树的，可拖拽元素（或它所在列表）的又优先于它所在拖拽区域的。
- 它在拖拽开始时读取，所以可以像上面那样用 getter 切换它。
- 锁定到 `'y'` 的树仍然可以嵌套：此时深度来自悬停在某一行的中间。

<tip>

优先使用 `axis`，而不是 `restrictToVerticalAxis` 和 `restrictToHorizontalAxis` 修饰器。它们做的是同一件事，但作为普通修饰器，它们在链条中间运行，排在它们之后的修饰器（同时吸附两个轴的网格）可能会再次把项移离该轴。

</tip>

## 修饰器

修饰器在每一帧调整拖拽的偏移。它的结果既移动浮层，**也**是查找目标所用的点，因此列表按锁定、限制或吸附后的位置排序，放置的 `position` 就是盒子最终所在的位置。

<table>
<thead>
  <tr>
    <th>
      修饰器
    </th>
    
    <th>
      作用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        restrictToParent
      </code>
    </td>
    
    <td>
      把浮层限制在拖拽开始时所在的容器内：可排序列表，或普通可拖拽元素的父元素。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToElement(target)
      </code>
    </td>
    
    <td>
      把浮层限制在一个元素内：模板引用、getter 或元素本身。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToWindowEdges
      </code>
    </td>
    
    <td>
      把浮层限制在窗口内。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToFirstScrollableAncestor
      </code>
    </td>
    
    <td>
      把浮层限制在被拖拽元素的第一个可滚动祖先内。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        snapToGrid(size)
      </code>
    </td>
    
    <td>
      把浮层吸附到网格（见下文）。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        snapCenterToCursor
      </code>
    </td>
    
    <td>
      让浮层居中于指针下方，而不是保持抓取时的偏移。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToVerticalAxis
      </code>
    </td>
    
    <td>
      只沿竖直方向移动。优先使用 <code>
        axis: 'y'
      </code>
      
      。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToHorizontalAxis
      </code>
    </td>
    
    <td>
      只沿水平方向移动。优先使用 <code>
        axis: 'x'
      </code>
      
      。
    </td>
  </tr>
</tbody>
</table>

`modifiers` 是 provider、拖拽区域、列表和树，以及可拖拽元素和可排序项的选项。除 provider 的以外，其余的都还接受 ref 或 getter，在拖拽开始时读取。

### 限制在内部

视频通话中的自己画面始终留在屏幕上。切换边界，然后把它远远拖出去：

<example height="29rem" hint="分别使用每种边界，把自己画面拖出屏幕" name="bounds-pip">



</example>

```ts [SelfView.vue]
const { isDragging } = makeDraggable(el, {
  id: 'self-view',
  modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
```

- `restrictToParent` 把它固定在屏幕上，也就是它的父元素。对于可排序项，“父元素”是它所在的列表，无论列表位于 DOM 中的哪里。
- `restrictToWindowEdges` 允许它离开屏幕，但不能离开窗口。放在屏幕之外时，它不在放置目标上，所以会飞回原处。
- 没有边界时，它可以去任何地方。

边界以**浮层**当前的尺寸把它限制在内部：使用 `overlayFit: 'slot'` 时，在较宽项的列表上方变大的盒子会以新的尺寸被限制在内部。

### 吸附到网格

流程图的节点吸附到画布网格上。切换单元格大小和平滑滑动：

<example height="29rem" hint="拖动一个节点；切换网格和平滑" name="snap-canvas">



</example>

```ts [FlowNode.vue]
const { isDragging } = makeDraggable(el, {
  id: () => props.node.id,
  modifiers: () => [snapToGrid({ x: props.grid, y: props.grid, smooth: props.smooth })],
});
```

`snapToGrid(24)` 把两个轴都吸附到 24 px 的单元格。传入对象可以设置更多：

<table>
<thead>
  <tr>
    <th>
      选项
    </th>
    
    <th>
      含义
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        x
      </code>
      
      、<code>
        y
      </code>
    </td>
    
    <td>
      每个轴的单元格大小，单位为像素。省略其中一个，就只吸附另一个轴：<code>
        { y: 15 }
      </code>
      
       把事件吸附到每刻钟，同时让它在各天之间自由移动。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        origin
      </code>
    </td>
    
    <td>
      <code>
        'container'
      </code>
      
      （默认）：浮层的左上角落在从容器左上角画出的网格线上——容器是可排序列表，或普通可拖拽元素的父元素。<code>
        'delta'
      </code>
      
      ：对偏移本身取整，所以一开始就不在网格上的项会一直不在网格上，并从原来的位置按整格移动。适用于放在任意位置（自由画布）、需要保持自身对齐的项。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        smooth
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
       或 <code>
        { duration, easing }
      </code>
      
      ：浮层在单元格之间平滑滑动（默认 80 ms）。目标立即使用吸附后的位置。
    </td>
  </tr>
</tbody>
</table>

使用 `origin: 'container'` 时，从容器的 border box 开始画网格：上面的画布用 `box-shadow` 而不是 `border` 作为边框，所以它的网格线从最边缘开始。

### 自定义修饰器

幻灯片上的方框靠近其他方框时，会跳到它们的边缘和中心上，粉色参考线显示它们所对齐的线：

<example height="26rem" hint="把一个方框拖到另一个旁边，或拖到幻灯片中间" name="magnet-guides">



</example>

```ts [Slide.vue]
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`，即经过前面的修饰器处理后、相对拖拽起点的偏移，并返回一个新的偏移。它还会收到计算时可能需要的信息：

<table>
<thead>
  <tr>
    <th>
      参数
    </th>
    
    <th>
      是什么
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        transform
      </code>
    </td>
    
    <td>
      到目前为止的偏移。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        overlayRect
      </code>
    </td>
    
    <td>
      按 <code>
        transform
      </code>
      
       移动后的浮层盒子，尺寸为它当前的尺寸。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activeRect
      </code>
    </td>
    
    <td>
      拖拽开始时的被拖拽元素。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        containerRect
      </code>
    </td>
    
    <td>
      拖拽开始时所在的容器（可排序列表或父元素）的当前位置：它会跟随滚动。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        windowRect
      </code>
    </td>
    
    <td>
      窗口。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        origin
      </code>
    </td>
    
    <td>
      拖拽开始时的指针。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        active
      </code>
      
      、<code>
        over
      </code>
    </td>
    
    <td>
      拖拽，以及它下方的目标。
    </td>
  </tr>
</tbody>
</table>

修饰器在每一帧运行：让它们保持快速、没有副作用。上面的参考线是在监听器的 `onDragMove` 中根据拖拽的 `delta` 画出的，`delta` 是经过所有修饰器之后的偏移。类型见[修饰器](https://zizigy.github.io/docs/api/modifiers)。

## 顺序

来自多个地方的修饰器依次运行，每一个都基于前一个的结果：

1. provider 的 `modifiers`；
2. 拖拽开始时所在的拖拽区域的 `modifiers`；
3. 被拖拽项自己的 `modifiers`，没有时取它所在列表或树的；
4. 拖拽区域的 `restrictToArea`；
5. `axis` 锁定。

在同一个数组中，它们按顺序运行，所以把吸附和磁吸放在边界之前：放在最后的边界无论其他修饰器做了什么，都能把盒子留在内部。上面的幻灯片运行的是 `[magnet, restrictToParent]`；如果顺序反过来，磁吸可能会把一个被固定在幻灯片边缘的方框拉出边缘几个像素。

项的 `modifiers` 会**替换**它所在列表的，而不是追加。区域的边界在第 1–3 步的吸附之后生效，所以吸附后的项仍然留在内部，而轴锁定在所有步骤的最后。

## 有边界的列表能到达两端

一个图层面板：行只能上下移动，永远不会离开面板。把最上面一行拖到最底部：

<example height="26rem" hint="把 Heading 拖到底部，越过面板边缘" name="bounded-layers">



</example>

```ts [Layers.vue]
const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });
```

指针继续移动时，边界会把浮层固定在盒子的边缘。一旦指针沿列表轴向离开了浮层，列表就改为按指针排序，所以无论你怎样握住项，第一个和最后一个位置都始终可以到达。网格仍按浮层排序：固定在边缘时，它已经覆盖了边缘的单元格。

## 页面某一部分的规则

两个区域，两套规则。Layout 中的贴纸吸附到它的网格上，并且不能离开它；Sketch 中的贴纸可以去任何地方，也可以进入 Layout。拖拽保持它开始时所在区域的规则，规则正在生效的区域会高亮。

<example height="25rem" hint="在 Layout 中拖动一张贴纸，再把一张从 Sketch 拖进 Layout" name="drag-areas">



</example>

```vue [DragArea.vue]
<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`，给画板、画布或信息流单独一个区域。

<table>
<thead>
  <tr>
    <th>
      选项
    </th>
    
    <th>
      作用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        axis
      </code>
    </td>
    
    <td>
      把拖拽锁定到一个轴。可拖拽元素自己的 <code>
        axis
      </code>
      
       或它所在列表的优先。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        modifiers
      </code>
    </td>
    
    <td>
      在 provider 的之后、可拖拽元素自己的之前运行。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restrictToArea
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
      ：拖拽留在区域的元素内。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        autoScroll
      </code>
    </td>
    
    <td>
      区域滚动容器（该元素，或它最近的可滚动祖先）的自动滚动。<code>
        false
      </code>
      
       在那里关闭它。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        activation
      </code>
    </td>
    
    <td>
      按指针类型设置的<a href="/docs/guides/sensors#activation-constraints">
        激活
      </a>
      
      ，比如在滚动的信息流中使用更长的触摸按下。可拖拽元素自己的优先；provider 的最后。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        disabled
      </code>
    </td>
    
    <td>
      区域内以及其内部的区域中都不会开始拖拽。它的可拖拽元素会报告 <code>
        aria-disabled
      </code>
      
      。
    </td>
  </tr>
</tbody>
</table>

每个选项都接受值、ref 或 getter，在按下或拖拽开始时读取。区域返回 `isDragging`（一个从这里开始的拖拽正在进行，就像上面高亮的边框显示的那样）和 `isAutoScrolling`（自动滚动正在滚动它的容器）。完整参考：[makeDragArea](https://zizigy.github.io/docs/api/make-drag-area)。

### 区域如何组合

- 拖拽遵循它**开始时所在**的区域，即使离开了它也一样。从某个区域上方经过不会改变任何东西，除了它的 `autoScroll`：它适用于每一个滚动其容器的拖拽。
- 嵌套的区域不会合并：最近的那个胜出。`disabled` 是例外，它会被继承：关闭的区域也会关闭其内部的区域。
- 碰撞检测、键盘按键和播报仍在 provider 范围内生效。

<note>

为什么不在画板外面再包一个 `DnDProvider`？provider 之间相互独立：拖拽永远不会从一个跨到另一个，所以画板中的项永远无法离开它。只有当页面的某一部分需要自己的 provider 级设置，并且其中的项留在其内时，才嵌套 provider。见 [provider](https://zizigy.github.io/docs/guides/provider)。

</note>

## 放在哪就留在哪

上面的时间线、通话、流程图、幻灯片和区域，都会把每个项留在它被放置的地方。每个拖拽事件都带有 `position`：被拖拽盒子在目标内部的左上角，从目标的内边距边缘量起，并加上它的滚动距离——也就是能让绝对定位的项正好停在那里的 `left` 和 `top`。它在修饰器之后计算，所以吸附后的节点会落在网格上。

```ts [Flowchart.vue]
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);
  },
});
```

更多关于自由拖拽和放置目标的内容，见[拖放](https://zizigy.github.io/docs/guides/drag-and-drop)。

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
