Документация/Руководства
Оверлей
Что следует за указателем во время драга — клон по умолчанию и как его стилизовать, компоненты оверлея, которые меняются по ходу драга, несколько элементов сразу, его размер и где он рендерится.
Страница переведена ИИ и может содержать неточности. Читать английский оригинал
Пока вы перетаскиваете, за указателем следует блок — оверлей. Сам перетаскиваемый элемент остаётся на месте, как слот, в который он приземлится. При броске оверлей летит в этот слот и исчезает.
Оверлей телепортируется в body, за пределы ваших компонентов. По умолчанию он показывает
клон перетаскиваемого элемента.
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Expenses from './Expenses.vue';
</script>
<template>
<DnDProvider>
<Expenses />
</DnDProvider>
</template>Стандартный клон
Клон снимается при захвате, до того как что-либо сдвинется. Это глубокая копия элемента:
- Значения полей форм и пиксели
<canvas>копируются. - Вычисленные стили, которые элемент потерял бы вне своего списка, встраиваются инлайн:
унаследованные шрифты и цвета, стили, которые применяются через предков (
.list li { … }), ширины ячеек таблицы. Строка таблицы оборачивается в собственную таблицу. idи атрибутыdata-dnd-*удаляются, а клон получаетaria-hidden.- Рябь и другие эффекты нажатия UI-китов (Vuetify, Quasar, PrimeVue, Naive UI) не копируются.
В таблице выше строка, в которую вы что-то ввели, едет со своим текстом, суммой, отметкой и графиком — в собственной таблице, ячейки которой сохраняют ширины настоящих.
Помечайте всё остальное, что не должно ехать с клоном, атрибутом data-dnd-no-clone:
кнопку меню, тултип, отметку выделения. Кнопка удаления в каждой строке остаётся на месте:
<tr ref="el" class="row">
…
<td><button type="button" aria-label="Remove" data-dnd-no-clone @click="emit('remove')">×</button></td>
</tr>
Фон не угадывается
Клон показывает элемент как есть. Строка без собственного фона, потому что его рисует список
(<li> в белом <ul>, v-list-item, строка таблицы), даёт прозрачный клон. Задайте ему
фон списка в CSS, здесь — на ячейках:
<style scoped>
[data-dnd-overlay] .row td {
background: color-mix(in srgb, currentColor 4%, Canvas);
}
</style>
Клон сохраняет классы и атрибуты элемента, включая scope-атрибут Vue, поэтому правило работает и в scoped-стилях строки, и в глобальной таблице стилей. Другой выход — фон на самой строке. Иконки и плитки, которые должны парить, остаются прозрачными намеренно.
Стилизация блока
Блок — это элемент с data-dnd-overlay. Он position: fixed, поверх всего
(z-index: 9999), pointer-events: none и inert, а значение атрибута говорит, что он
делает:
data-dnd-overlay | Когда |
|---|---|
"dragging" | Следует за указателем. |
"dropping" | Приземляется в свой слот, летит обратно или ждёт асинхронного onBeforeDrop. |
Приподнятый вид во время драга, который исчезает при приземлении:
[data-dnd-overlay='dragging'] .row td {
box-shadow: 0 14px 20px -16px rgb(0 0 0 / 0.7);
}
Блок двигается через transform и получает размер как width и height: стилизуйте то, что
внутри него, а не сам блок. Клон заполняет его, с width: 100%, height: 100% и
transform: none на своём корне: поворачивайте и увеличивайте его свойствами rotate и scale, которые
анимируют и переходы, и ключевые кадры. Рецепты — в анимациях.
Компоненты оверлея
Чтобы показать что-то кроме клона, передайте компонент. Он рендерится внутри блока и получает драг в виде пропсов, которые меняются по ходу драга: где драг, принимает ли его это место, копирует ли бросок, и сам бросок.
Загружаем демо…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = ref([
{
id: 'Backlog',
tickets: [
{ id: 't1', title: 'Dark mode toggle', tag: 'UI', approved: false },
{ id: 't2', title: 'Retry failed uploads', tag: 'API', approved: true },
{ id: 't3', title: 'Keyboard shortcuts', tag: 'UI', approved: false },
],
},
{ id: 'Doing', tickets: [{ id: 't4', title: 'Faster search', tag: 'API', approved: true }] },
{ id: 'Done', tickets: [{ id: 't5', title: 'Onboarding tour', tag: 'UI', approved: true }] },
]);
</script>
<template>
<DnDProvider>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</DnDProvider>
</template>
<style scoped>
.board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
</style><script setup lang="ts">
import { computed } from 'vue';
import type { DnDStatus, DragItem, DragMode, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean; status: DnDStatus; mode: DragMode }>();
const ticket = computed(() => props.items[0]?.item as Ticket);
const state = computed(() => {
if (props.status === 'dropping') return props.over ? 'landed' : 'back'; // a cancel flies back
if (!props.over) return 'free';
return props.accepted ? 'ok' : 'refused';
});
</script>
<template>
<!-- Fills the overlay box, which takes the size of the slot. -->
<div class="ticket-overlay" :data-state="state">
{{ ticket.title }}
<span v-if="mode === 'copy'" class="copy">+ copy</span>
</div>
</template>
<style scoped>
.ticket-overlay { width: 100%; height: 100%; }
.ticket-overlay[data-state='refused'] { background: color-mix(in srgb, #ef4444 8%, Canvas); }
</style>
| Проп | Что в нём |
|---|---|
active | Драг (id, ids, data, type, …). |
items | Все перетаскиваемые элементы, взятый первым: { id, item, data, type }. item — элемент списка для сортируемых драгов. |
over | Цель под драгом; пока бросок приземляется — цель, на которую он приземляется. null над пустым местом и пока отклонённый или отменённый драг летит обратно. over.id сортируемого списка — id списка: колонки выше используют свои названия. |
accepted | Принимает ли over драг: окрашивайте оверлей в красный, если нет. |
status | 'dragging', затем 'dropping', пока он приземляется. |
mode | 'move', 'copy' (Alt зажат при драге из списка с clone или драг из палитры), 'swap' или 'insert' (обычный перетаскиваемый элемент идёт в список). |
to | Куда бросок положил бы элементы сейчас: { list, index }, а в дереве ещё parentKey и depth. null, когда он не положил бы их ни в какой список, например над обычной целью. См. куда пойдёт бросок. |
Пропсы — это OverlayProps; объявляйте только те, которые читаете. Передайте компонент
тому, что перетаскивается:
import TicketOverlay from './TicketOverlay.vue';
makeSortableList(listRef, { items: tickets, overlay: TicketOverlay }); // every item of the list
makeSortableItem(el, { item: () => props.ticket, overlay: TicketOverlay }); // this item
makeDraggable(el, { id: 'chip', overlay: TicketOverlay }); // a plain draggable
Компонент можно передать как есть, в shallowRef, чтобы его переключать (в том числе посреди
драга: оверлей меняется сразу), или функцией, которую Vue принимает за функциональный компонент:
overlay: (props) => <TicketOverlay {...props} compact /> в TSX. Никогда не кладите его в
глубокий ref или reactive: Vue сделает компонент реактивным и выдаст предупреждение.
Корень компонента оверлея должен заполнять блок (width: 100%; height: 100%): блок получает
размер слота и анимирует его при броске. Фиксированный размер на корне вместо этого сохраняет
этот размер внутри блока.
Слот #overlay провайдера
Для каждого драга без собственного компонента оверлей рендерит слот провайдера. Определите его один раз, рядом с корнем приложения, и его получат все списки и перетаскиваемые элементы приложения. Здесь у заметок свой призрак, а фото возвращаются к клону:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Moodboard from './Moodboard.vue';
import NoteGhost from './NoteGhost.vue';
</script>
<template>
<DnDProvider>
<Moodboard />
<!--
The overlay of every drag in the app that has none of its own. Notes get a ghost
of their own; for photos the slot renders nothing, so they fall back to the clone.
-->
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
</template><DnDProvider>
<RouterView />
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
Слот получает те же пропсы, что и компонент оверлея. Когда он ничего не рендерит (v-if
выше), драг возвращается к клону. Различайте драги по type: тип сортируемого элемента — имя
его группы, если элемент не задаёт собственный (makeSortableItem(el, { type })).
Цели, которые меняют оверлей
Цель может задать, что показывает оверлей, пока над ней драг, который она принимает.
makeDroppable, списки и деревья принимают overOverlay (overlay списка или дерева — для
его собственных элементов). Над папкой чип перетаскиваемых писем превращается в то, что
сделает бросок:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<!-- The mailbox reads the selection with `useDnD()`: it renders inside the provider. -->
<DnDProvider>
<Mailbox />
</DnDProvider>
</template>makeDroppable(el, {
id: () => props.folder.id,
data: () => props.folder,
overOverlay: MovePill,
onDrop: (event) => emit('move', event.active.ids),
});
Компонент цели получает те же пропсы, что и любой компонент оверлея, в том числе over, а
собственный оверлей драга (его компонент, слот провайдера или клон) — как свой слот по
умолчанию. MovePill опускает слот, поэтому заменяет чип:
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
// The folder under the drag: its `data`.
const folder = computed(() => props.over?.data as Folder | undefined);
</script>
<template>
<!-- Fills the box like any overlay component. -->
<div class="pill-box">
<span v-if="folder" class="pill">Move {{ items.length }} to {{ folder.name }}</span>
</div>
</template>
Отрендерите <slot />, чтобы сохранить собственный оверлей драга и обернуть его. Так делает
Done на доске из компонентов оверлея выше: над Done одобренный тикет
сохраняет собственный оверлей, Done ставит на него штамп, а тикет из другой колонки при броске
получает конфетти.
makeSortableList(listRef, {
items: tickets,
group: { name: 'release', put },
overlay: TicketOverlay, // the tickets' own, in every column
overOverlay: strict ? DoneOverlay : undefined, // Done's, around it
});
<template>
<div class="done">
<slot /> <!-- TicketOverlay -->
<span class="stamp">Shipped</span>
<span v-if="status === 'dropping' && arrives" class="confetti">…</span>
</div>
</template>
- Он показывается, только пока цель принимает драг, и остаётся, пока на неё приземляется бросок. Когда драг покидает цель, возвращается собственный оверлей драга.
- Смена идёт кроссфейдом. Как именно — задаёт
overlayTransitionпровайдера:falseменяет сразу, объект — это пропсы<Transition>вокруг содержимого (name,mode, JS-хуки для GSAP или motion). Безmode: 'out-in'на мгновение видны оба: дайте уходящемуposition: absoluteв.<name>-leave-active. - Бросок ждёт конфетти: CSS-анимации и переходы, которые идут в оверлее, пока он приземляется, доигрываются до конца, но не дольше двух секунд.
- У драга с
overlay: falseнет оверлея ни над какой целью.
Папки к тому же приземляют плашку по-своему — в иконку папки: у цели тоже может быть
overDropAnimation. См. анимации.
Куда пойдёт бросок: to
to сообщает оверлею, куда бросок положил бы элементы сейчас, — так же, как это скажет to
события броска: список и индекс в нём, а в дереве — родителя и глубину. Он меняется, когда
сдвигается слот. Страница этого меню сайта говорит, в какой раздел она попадёт и в какую
строку:
Загружаем демо…
<script setup lang="ts">
import { provide, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import MenuTree from './MenuTree.vue';
import { MENU, site } from './menu';
// Provided above the provider: its overlay renders under it, so the overlay sees it too.
provide(MENU, ref(site()));
</script>
<template>
<DnDProvider>
<MenuTree />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, DropDestination } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; to: DropDestination | null }>();
const page = computed(() => props.items[0]!.item as MenuNode);
// `parentKey` is the section the page would go into, `null` at the top level.
const where = computed(() => (props.to ? pathOf(menu.value, props.to.parentKey) : null));
</script>
<template>
<div class="move">
<div class="row">{{ page.title }}</div>
<p v-if="to && where" class="where">Move to <b>{{ where }}</b>, row {{ to.index + 1 }}</p>
</div>
</template>
Вне всех целей to — по-прежнему место слота: по умолчанию бросок там оставляет элементы на
месте слота (dropOutside: 'placeholder'). Он равен null, когда бросок не положил бы их ни в
какой список: над обычной целью, над списком, который их отклоняет, вне целей с
dropOutside: 'cancel' или 'remove'.
Что побеждает
Для каждого драга — первое, что подходит:
overOverlayцели (и списка или дерева тоже), пока драг над ней и она его принимает. То, что показалось бы иначе, становится её слотом по умолчанию.- Собственный
overlayэлемента (makeSortableItem,makeDraggable). overlayего списка или дерева.- Слот
#overlayпровайдера, если он что-то рендерит. - Стандартный клон. При мульти-драге — колонка или стопка клонов: см. мульти-драг.
Есть ли у драга оверлей вообще (overlay: false), решается в его начале. Какой компонент
показывается, читается на лету: его меняет ref, переключённый посреди драга, а также вход в
цель со своим оверлеем или выход из неё.
DragPreview: клон в вашей раскладке
DragPreview рендерит клон одного перетаскиваемого элемента, сделанный библиотекой. Компонент
оверлея может разложить несколько таких клонов или обрамить один. Кликайте по книгам, чтобы
выбрать их, затем перетащите одну: оверлей раскладывает настоящие клоны веером.
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
</script>
<template>
<DnDProvider>
<Shelf />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import { DragPreview, type DragItem } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[] }>();
const fan = computed(() =>
props.items.slice(0, 4).map((entry, index) => ({
entry,
angle: index === 0 ? 0 : (index % 2 ? -1 : 1) * Math.ceil(index / 2) * 10,
})),
);
</script>
<template>
<div class="fan">
<div v-for="{ entry, angle } in fan" :key="entry.id" class="card" :style="{ transform: `rotate(${angle}deg)` }">
<DragPreview :id="entry.id" class="face" :style="{ width: '100%', height: '100%' }">
<div class="blank" /> <!-- an item without a clone -->
</DragPreview>
</div>
<span v-if="items.length > 1" class="count">{{ items.length }}</span>
</div>
</template>
- Клоны есть для взятого элемента и других рядом с ним в порядке броска, столько, сколько
задаёт
overlayPreviewsпровайдера (по умолчанию 5,'all'— для всех), если они отрендерены в момент захвата. Для любого другого элементаDragPreviewрендерит свой слот по умолчанию. - Превью имеет размер своего элемента на момент захвата, заданный инлайн. Задавайте размер
через
style, который пробрасывается дальше (класс его не перекроет), как выше. - При сортируемом мульти-драге (
multiple: 'group'или'gaps') каждое превью при броске приземляется в слот своего элемента. Поворачивайте его обёртки черезtransform(а не свойствоrotate): приземление по пути его снимает. Внешний вид элемента (скругления, тень) задавайте на самом превью, как это делаетclass="face": обёртки остаются в блоке.
В руководстве по мульти-драгу есть оверлей, который показывает счётчик и заголовки вместо клонов.
Размер: overlayFit
Над списком с элементами другого размера блок принимает размер слота, в который приземлится.
Это переключает overlayFit провайдера; на странице DnDProvider есть
демо с узким и широким списками.
overlayFit (провайдер) | Блок |
|---|---|
'slot' (по умолчанию) | Принимает размер слота, в который приземлится, плавно подстраиваясь под него. Над широким списком он расширяется. |
'source' | Сохраняет размер перетаскиваемого элемента. |
С 'slot' блок меняет размер вокруг указателя, который остаётся в той же доле блока.
Содержимое никогда не масштабируется: оно заново раскладывается в новом размере, так что
flex-ряд, как у задач в том демо, выглядит правильно при любой ширине. Слот, стилизованный намного
меньше элемента, например тонкая линия, не сжимает блок по этой оси.
Бросок делает то же при любой настройке: блок летит к элементу, на который приземляется, и анимируется к его размеру, а содержимое по пути заново раскладывается. Когда элемент выглядит по-разному в двух списках (компактный чип в палитре, полная карточка на доске), клон источника заново раскладывается в размере цели и может выглядеть там сжатым или растянутым. Два способа сохранить его правильным:
- Дайте элементу раскладку, которая работает в обоих размерах, как у строк задач, или одинаковый размер во всех списках, куда он может приземлиться.
- Рендерите вид цели в компоненте оверлея. Он получает
over, поэтому может переключаться на вид того списка, над которым находится, и заполняет блок при любом размере.
Где он рендерится: overlayTo
Провайдер телепортирует оверлей в body. Модальный <dialog> (открытый через showModal())
находится в верхнем слое, над всем в body, поэтому оверлей там был бы скрыт под ним.
Направьте overlayTo на селектор или элемент внутри диалога. На
странице DnDProvider есть диалог, чтобы
попробовать с этим и без этого:
<script setup lang="ts">
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
// While the dialog is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value ? '#columns-dialog' : 'body'));
const show = () => {
dialog.value?.showModal();
open.value = true;
};
</script>
<template>
<DnDProvider :overlay-to="overlayTo">
<button type="button" @click="show">Edit columns</button>
<dialog id="columns-dialog" ref="dialog" @close="open = false">
<ColumnList v-model:columns="columns" />
</dialog>
</DnDProvider>
</template>
- Цель должна быть на странице, когда провайдер рендерит оверлей, то есть после его
монтирования. Закрытый
<dialog>на странице есть: он просто скрыт. Пока он закрыт, переключайтесь обратно наbody, чтобы оверлей других драгов был виден. - Блок —
position: fixed.transform,filterилиcontainна цели или её предках делают её системой отсчёта блока вместо вьюпорта, поэтому не ставьте их на диалог. - Стили клона вычисляются для того места, где он показан: в shadow root направьте
overlayToвнутрь него, и клон сохранит стили shadow root.
Без оверлея
У makeDraggable(el, { overlay: false }) блока нет вовсе: двигается сам элемент, через
возвращённый transform.
const { transform } = makeDraggable(el, { overlay: false });
const style = computed(() =>
transform.value ? { translate: `${transform.value.x}px ${transform.value.y}px` } : undefined,
);
См. drag and drop. Как приземляется драг с
блоком, задаёт его dropAnimation: цели (overDropAnimation), элемента (или его списка) или провайдера. См.
анимации.