Документация/Руководства

Оси, границы и сетки

Фиксируйте драги по одной оси, удерживайте их внутри блока, привязывайте к сетке или к собственным направляющим — для одного элемента, списка или целой части страницы.

Редактировать страницу

Страница переведена ИИ и может содержать неточности. Читать английский оригинал

Драг можно удержать на одной оси, внутри блока, привязать к сетке или изменить собственным правилом. Правила — это модификаторы: они корректируют смещение драга на каждом кадре. Задайте их на одном перетаскиваемом элементе, на списке или на части страницы, и оверлей, цели и бросок будут им следовать.

Фиксация оси

Клипы на таймлайне скользят по своей дорожке. Выключите фиксацию, и они смогут менять и дорожку.

Live

Загружаем демо…

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

<template>
  <DnDProvider>
    <Timeline />
  </DnDProvider>
</template>
TimelineClip.vue
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 элемента важнее, чем у его списка или дерева, а axis перетаскиваемого элемента (или его списка) важнее, чем у его области перетаскивания.
  • Он читается в начале драга, поэтому геттер может его переключать, как выше.
  • Дерево, зафиксированное по 'y', всё равно вкладывает: глубина тогда берётся из наведения на середину строки.
Предпочитайте axis модификаторам restrictToVerticalAxis и restrictToHorizontalAxis. Они делают то же самое, но как обычные модификаторы работают в середине цепочки, и модификатор после них (сетка, привязывающая обе оси) может снова сдвинуть элемент с оси.

Модификаторы

Модификатор корректирует смещение драга на каждом кадре. Его результат двигает оверлей и служит точкой, в которой ищутся цели, поэтому список сортирует по зафиксированной, ограниченной или привязанной позиции, а position броска — там, где оказался блок.

МодификаторЧто делает
restrictToParentУдерживает оверлей внутри контейнера, в котором начался драг: сортируемого списка или родительского элемента обычного перетаскиваемого элемента.
restrictToElement(target)Удерживает оверлей внутри элемента: template ref, геттера или самого элемента.
restrictToWindowEdgesУдерживает оверлей внутри окна.
restrictToFirstScrollableAncestorУдерживает оверлей внутри первого прокручиваемого предка перетаскиваемого элемента.
snapToGrid(size)Привязывает оверлей к сетке (ниже).
snapCenterToCursorЦентрирует оверлей под указателем вместо сохранения смещения захвата.
restrictToVerticalAxisДвигает только по вертикали. Лучше axis: 'y'.
restrictToHorizontalAxisДвигает только по горизонтали. Лучше axis: 'x'.

modifiers — опция провайдера, областей перетаскивания, списков и деревьев, перетаскиваемых и сортируемых элементов. Все, кроме провайдера, принимают также ref или геттер, который читается в начале драга.

Удержать внутри

Собственное видео в видеозвонке остаётся на экране. Переключите границу и утащите его подальше:

Live

Загружаем демо…

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

<template>
  <DnDProvider>
    <CallScreen />
  </DnDProvider>
</template>
SelfView.vue
const { isDragging } = makeDraggable(el, {
  id: 'self-view',
  modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
  • restrictToParent держит его на экране, его родительском элементе. Для сортируемого элемента «родитель» — его список, где бы список ни находился в DOM.
  • restrictToWindowEdges позволяет ему покинуть экран, но не окно. Брошенное за пределами экрана, оно не над целью, поэтому летит обратно.
  • Без границы оно двигается куда угодно.

Граница удерживает внутри оверлей, в его текущем размере: с overlayFit: 'slot' блок, который растёт над списком более широких элементов, удерживается внутри в новом размере.

Привязка к сетке

Узлы блок-схемы привязываются к сетке холста. Переключите размер ячейки и плавность:

Live

Загружаем демо…

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

<template>
  <DnDProvider>
    <Flowchart />
  </DnDProvider>
</template>
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. Объект задаёт больше:

ОпцияЗначение
x, yРазмер ячейки в пикселях, для каждой оси. Не задайте одну, чтобы привязывать только другую: { y: 15 } привязывает событие к четвертям часа и даёт ему свободно двигаться по дням.
origin'container' (по умолчанию): левый верхний угол оверлея попадает на линии сетки, проведённые от левого верхнего угла контейнера — сортируемого списка или родителя обычного перетаскиваемого элемента. 'delta': округляется само смещение, так что элемент, начавший вне сетки, вне неё и остаётся и двигается на целые ячейки от того места, где был. Выбирайте его для элементов, размещённых где угодно (свободный холст), которые должны сохранять собственное выравнивание.
smoothtrue или { duration, easing }: оверлей плавно скользит между ячейками (по умолчанию 80 ms). Цели сразу используют привязанную позицию.

С origin: 'container' рисуйте сетку от border box контейнера: у холста выше рамка сделана через box-shadow, а не border, поэтому его линии начинаются у самого края.

Свой модификатор

Блоки на слайде прыгают на края и центры других, когда подходят близко, а розовые направляющие показывают линии, на которых они стоят:

Live

Загружаем демо…

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

<template>
  <DnDProvider>
    <Slide />
  </DnDProvider>
</template>
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 — смещение от начала драга в том виде, в каком его оставили предыдущие модификаторы, — и возвращает новое. Он также получает то, что может понадобиться для вычисления:

АргументЧто это
transformСмещение на данный момент.
overlayRectБлок оверлея, сдвинутый на transform, в его текущем размере.
activeRectПеретаскиваемый элемент в момент начала драга.
containerRectКонтейнер, в котором начался драг (сортируемый список или родительский элемент), там, где он сейчас: он следует за прокруткой.
windowRectОкно.
originУказатель в момент начала драга.
active, overДраг и цель под ним.

Модификаторы выполняются на каждом кадре: держите их быстрыми и без побочных эффектов. Направляющие выше рисуются по delta драга в onDragMove монитора — это смещение после всех модификаторов. Типы — в модификаторах.

Порядок

Модификаторы из нескольких мест выполняются один за другим, каждый от результата предыдущего:

  1. modifiers провайдера;
  2. modifiers области перетаскивания, в которой начался драг;
  3. собственные modifiers перетаскиваемого элемента или, если их нет, его списка или дерева;
  4. restrictToArea области перетаскивания;
  5. фиксация axis.

Внутри одного массива они выполняются по порядку, поэтому ставьте привязки и магниты перед границами: граница в конце удерживает блок внутри, что бы ни сделали остальные. Слайд выше выполняет [magnet, restrictToParent]; в обратном порядке магнит мог бы утянуть блок, удерживаемый у края слайда, на несколько пикселей за край.

modifiers элемента заменяют модификаторы его списка, а не добавляются к ним. Границы области идут после привязок шагов 1–3, поэтому привязанный элемент всё равно остаётся внутри, а фиксация оси идёт самой последней.

Ограниченные списки сохраняют свои края

Панель слоёв: строки двигаются только вверх и вниз и никогда не покидают панель. Перетащите верхнюю строку в самый низ:

Live

Загружаем демо…

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

<template>
  <DnDProvider>
    <Layers />
  </DnDProvider>
</template>
Layers.vue
const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });

Граница удерживает оверлей у края блока, пока указатель движется дальше. Как только указатель покинул оверлей вдоль оси списка, список сортирует уже по указателю, так что первое и последнее место остаются достижимыми, как бы вы ни держали элемент. Сетки продолжают сортировать по оверлею: удерживаемый у края, он уже накрывает крайнюю ячейку.

Правила для части страницы

Две области, два набора правил. Стикеры в Layout привязываются к её сетке и не могут её покинуть; стикеры в Sketch ходят куда угодно, в том числе в Layout. Драг сохраняет правила области, в которой начался, а область, чьи правила действуют, подсвечивается.

Live

Загружаем демо…

<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>
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 на всё приложение, а доске, холсту или ленте дайте собственную область.

ОпцияЧто делает
axisФиксирует драги по одной оси. Собственный axis перетаскиваемого элемента или его списка важнее.
modifiersВыполняются после модификаторов провайдера и до собственных модификаторов перетаскиваемого элемента.
restrictToAreatrue: драги остаются внутри элемента области.
autoScrollАвтоскролл скролл-контейнера области (самого элемента или его ближайшего прокручиваемого предка). false отключает его там.
activationАктивация для каждого типа указателя, например более долгое нажатие касанием в прокручиваемой ленте. Собственная активация перетаскиваемого элемента важнее; активация провайдера — последняя.
disabledВ области не начинается ни один драг, как и в областях внутри неё. Её перетаскиваемые элементы сообщают aria-disabled.

Каждая опция принимает значение, ref или геттер, который читается в начале нажатия или драга. Область возвращает isDragging (идёт драг, начавшийся здесь, — это показывает подсвеченная рамка выше) и isAutoScrolling (автоскролл прокручивает её контейнер). Полный справочник: makeDragArea.

Как сочетаются области

  • Драг следует правилам области, в которой начался, даже после того как покинул её. Проход над областью ничего не меняет, кроме её autoScroll: он применяется к каждому драгу, который прокручивает её контейнер.
  • Вложенные области не сливаются: побеждает ближайшая. Исключение — disabled, и оно наследуется: выключенная область выключает и области внутри себя.
  • Определение цели, клавиши клавиатуры и объявления остаются общими для провайдера.
Почему не второй DnDProvider вокруг доски? Провайдеры независимы: драг никогда не переходит из одного в другой, так что элементы доски никогда не смогли бы её покинуть. Вкладывайте провайдер только тогда, когда части страницы нужны собственные настройки на уровне провайдера и её элементы остаются внутри неё. См. Провайдер.

Оставить там, где бросили

Таймлайн, звонок, блок-схема, слайд и области выше — все оставляют каждый элемент там, где его бросили. Каждое событие драга несёт position: левый верхний угол перетаскиваемого блока внутри цели, от края её padding и с учётом её прокрутки — те left и top, которые ставят абсолютно позиционированный элемент ровно туда. Он вычисляется после модификаторов, поэтому привязанный узел приземляется на сетку.

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);
  },
});

Подробнее о свободном перетаскивании и целях — в Drag and drop.