Документация/Руководства
Drag and drop
Свободное перетаскивание с makeDraggable и цели с makeDroppable — типизированные цели, которые принимают одни драги и отклоняют другие, план комнаты, зоны удаления, папки внутри списка и обычные элементы, которые принимает список.
Страница переведена ИИ и может содержать неточности. Читать английский оригинал
Используйте makeDraggable и makeDroppable для всего, что не является сортируемым списком:
мусор в баки, мебель на плане комнаты, файлы в папки, корзина, шахматная доска.
Загружаем демо…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type Key } from '@vue-dnd-kit/core';
import Bin from './Bin.vue';
import { BINS, WASTE } from './waste';
// The tray takes everything back (no `accept`); the bins take what their rule allows.
const TRAY = { id: 'tray', name: 'To sort', color: '#42b883', accept: '*', rule: "'*'" };
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<DnDProvider>
<div class="recycling">
<Bin :bin="TRAY" :items="itemsIn('tray')" tray @drop="drop('tray', $event)" />
<div class="bins">
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.recycling {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 600px;
margin: 0 auto;
}
.bins {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
@media (max-width: 560px) {
.bins {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>Перетаскиваемый элемент
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ waste: { id: string; name: string; type: string } }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.waste.id, // read back as `active.id`
type: () => props.waste.type, // matched by the targets' `accept`
data: () => props.waste, // anything: `active.data`
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">{{ waste.name }}</div>
</template>
Передавайте id из ваших данных, геттером. Id по умолчанию берётся из useId() и новый для
каждого экземпляра компонента, поэтому остальному приложению он ни о чём не говорит. Опции
читаются тогда, когда они нужны, так что геттер или ref держат их актуальными.
| Опция | Что делает |
|---|---|
id, data, type | Кто перетаскивается, что он несёт и с чем сверяется accept целей. |
handle | Селектор или элемент внутри: только он начинает драг. См. ручки. |
disabled | Драг не начинается. |
overlay | true (по умолчанию): за указателем следует копия. Компонент: ваше собственное превью. false: элемент двигается сам (ниже). |
axis, modifiers | Зафиксировать драг по 'x' или 'y', привязать к сетке, удержать в границах: см. ограничения. |
keyboardMode | Как его двигают стрелки: по пикселям (по умолчанию) или от цели к цели (ниже). |
activation, canDrag | Когда нажатие становится драгом: см. сенсоры. |
attributes | role, tabindex и aria-* для клавиатуры и скринридеров (по умолчанию true). |
| Возвращает | Что это |
|---|---|
isDragging | Этот элемент перетаскивается. |
selected | Записываемый, связан с выделением провайдера. |
transform | Смещение драга, для overlay: false. |
id | Используемый id. |
Кнопки, поля ввода и другие интерактивные потомки не начинают драг указателем, поэтому внутри перетаскиваемого элемента они продолжают работать. Все опции — в API makeDraggable.
Цель
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type Key } from '@vue-dnd-kit/core';
import type { Bin, Waste } from './waste';
const props = defineProps<{ bin: Bin; items: Waste[] }>();
const emit = defineEmits<{ drop: [id: Key] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
id: props.bin.id,
accept: props.bin.accept, // 'glass', ['paper', 'cardboard'], a function…
onDrop: (event) => emit('drop', event.active.id),
});
</script>
<template>
<section ref="el" :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">
<WasteItem v-for="waste in items" :key="waste.id" :waste="waste" />
</section>
</template>
| Возвращает | true, когда |
|---|---|
isOver | Драг над этой целью, принимает она его или нет. |
isAccepting | Что-то перетаскивается, и эта цель это принимает: подсвечивайте подходящие цели. |
placement | Не булево значение: { top, right, bottom, left } — половины цели, в которых находится указатель, или null, когда над ней ничего нет. Используйте его для «перед / после этой карточки». |
isOver без isAccepting означает, что драг над целью, которая его отклоняет. Покажите это,
как баки выше: бросок туда отменяется, и элемент летит обратно. Баки, которые не могут
принять элемент, тускнеют, пока он движется; больше вариантов — в
стилизации.
| Опция | Что делает |
|---|---|
id, data | Читаются как over.id и over.data. |
accept, acceptMode | Какие драги она принимает (следующий раздел). |
disabled | Она ничего не принимает, и драги проходят над ней. |
priority | Побеждает большее, когда цели перекрываются без вложенности (по умолчанию 0). |
onDragEnter, onDragLeave | Драг вошёл или вышел. |
onBeforeDrop, onDrop | Решить судьбу броска, затем применить его (ниже). |
Какие драги принимает цель
type перетаскиваемого элемента сверяется с accept цели:
accept | Принимает |
|---|---|
не задан или '*' | Всё. |
'glass' | Перетаскиваемые элементы типа glass и элементы без типа. |
['paper', 'cardboard'] | Любой из двух типов и элементы без типа. |
({ active, items, types }) => boolean | То, что скажет ваша функция. |
Перетаскиваемый элемент без type подходит под любое строковое правило. type может быть и
массивом (['paper', 'large']): достаточно одного совпавшего типа. Функция решает по чему
угодно в драге. Бак для компоста выше читает данные элементов, а не их тип:
{
id: 'compost',
accept: ({ items }) => items.every((item) => (item.data as Waste).compostable),
}
accept читается вживую, пока драг ищет цель: передайте ref, чтобы его менять.
Когда перетаскиваются сразу несколько элементов, acceptMode говорит, сколько из них должны
подойти: 'all' (по умолчанию) или 'some'. С 'some' верните из onBeforeDrop id, которые
нужно оставить, а остальные полетят обратно. См. решение о броске.
Бросок
onDrop обычной цели — место, где данные меняете вы. Сама библиотека ничего не
перемещает:
<script setup lang="ts">
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</template>
| Поле события | Что в нём |
|---|---|
active | Драг: id, ids, items (каждый — { id, data, type, item }), data, type. |
over | Эта цель: id, data, element, rect, accepted. |
pointer, delta | Где указатель и насколько он сдвинулся от начала. |
position | Где перетаскиваемый блок находится в этой цели (следующий раздел). |
source | Задан, когда элементы пришли из сортируемого списка: { list, remove() }. |
active.ids содержит все перетаскиваемые id, взятый первым. Порядок такой: onBeforeDrop
(решить), onDrop (применить), затем drag-end провайдера. Чтобы слышать каждый драг
страницы в одном месте, используйте useDnDMonitor (см. провайдер).
Чтобы спросить перед броском, продолжить драг или принять только часть элементов, см.
решение о броске.
Оставить там, где бросили
План комнаты, холст, доска: элемент остаётся там, где вы его отпустили. Каждое событие несёт
position — left / top, которые ставят абсолютно позиционированный элемент туда, где его
бросили.
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Planner from './Planner.vue';
</script>
<template>
<DnDProvider>
<Planner />
</DnDProvider>
</template>const room = useTemplateRef<HTMLElement>('room');
makeDroppable(room, {
id: 'room',
onDrop: ({ active, position }) => {
const piece = find(active.id);
if (!piece || !position || !room.value) return;
// Clamped, so a piece let go half outside stays inside the walls.
piece.x = clamp(position.x, room.value.clientWidth - piece.width);
piece.y = clamp(position.y, room.value.clientHeight - piece.height);
piece.place = 'room';
},
});
<div ref="el" class="piece" :style="{ left: `${piece.x}px`, top: `${piece.y}px` }">…</div>
position — левый верхний угол перетаскиваемого блока в over, отсчитанный от края его
padding, с учётом прокрутки: рамка комнаты, её стены, не учитывается. Над пустым местом он
равен null.
active.rect и delta: так теряются рамка и прокрутка цели. Сетки,
границы и фиксация оси для таких досок — в ограничениях.Без оверлея
По умолчанию за указателем следует копия элемента, а сам элемент остаётся на месте: так
работает мебель на складе. С overlay: false копии нет, и элемент двигается сам, по
transform, который возвращает драг: так работает мебель в комнате.
const inRoom = computed(() => props.piece.place === 'room');
const { isDragging, transform } = makeDraggable(el, {
id: () => props.piece.id,
overlay: computed(() => !inRoom.value), // a value or a ref, read when the drag starts
});
const style = computed(() => ({
left: `${props.piece.x}px`,
top: `${props.piece.y}px`,
...(transform.value && { transform: `translate3d(${transform.value.x}px, ${transform.value.y}px, 0)` }),
}));
- Тогда
position— угол самого элемента, так что обработчик броска не меняется. - Элемент находится под указателем, пока движется; при поиске цели библиотека смотрит сквозь
него. Поднимите его над соседями, пока он перетаскивается (
z-index). - Он прокручивается вместе со страницей, а оверлей — нет:
transformвычитает то, на сколько сдвинулся его контейнер с начала драга, так что при прокрутке страницы он остаётся под указателем. - Он движется внутри своего контейнера: родитель с
overflow: hiddenего обрезает, а его контекст наложения решает, над чем он проходит. У оверлея нет ни той, ни другой проблемы.
Для своего превью вместо копии см. оверлей.
Зоны удаления
Драг, который начинается в сортируемом списке, несёт e.source. Вызовите remove(), и
список уберёт элементы:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Triage from './Triage.vue';
</script>
<template>
<DnDProvider>
<Triage />
</DnDProvider>
</template>const triage = (bin: 'done' | 'deleted') => (e: DnDEvent) => {
bins[bin].push(...e.active.items.map((entry) => entry.item as Notice));
e.source?.remove();
};
makeDroppable(doneZone, { id: 'done', onDrop: triage('done') });
makeDroppable(trashZone, { id: 'trash', onDrop: triage('deleted') });
e.active.items[i].item— сам элемент списка. Сохраните его, и отмена вернёт его на прежнее место.- Затем собственный
onDropсписка выполняется сto.index === -1, аe.resultсодержит список без этих элементов. - Оверлей сжимается в зону, потому что элемент больше нигде не показан.
- Список может и сам убирать то, что брошено вне всего: см.
dropOutside.
Delete для многих всё же быстрее: предложите
и их.Бросок в сортируемый элемент
Строка сортируемого списка тоже может быть целью: папка, которая принимает закладки, группа, которая принимает задачи. Поставьте droppable на элемент строки:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Bookmarks from './Bookmarks.vue';
</script>
<template>
<DnDProvider>
<Bookmarks />
</DnDProvider>
</template>const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.folder, type: 'folder' });
const { isOver } = makeDroppable(el, {
accept: 'link',
onDrop: (e) => {
emit('add', e.active.items.map((entry) => entry.item as Link)); // Bookmarks adds them
e.source?.remove(); // take them out of the list they came from
},
});
Открытая папка показывает свои ссылки списком той же группы, который принимает только ссылки: они сортируются там, и их можно вытащить обратно на верхний уровень или в другую папку.
Пока список сортировал бы драг, такая цель учитывается только в середине своего элемента (от 25 до 75 % по оси списка). Там бросок попадает в элемент, и список не двигается. У краёв список сортирует как обычно.
- Цель, чей
acceptотклоняет драг, пропускается: список сортирует над этим элементом, как над любым другим. Папки принимают только ссылки, поэтому перетаскиваемая папка сортируется мимо остальных. - Цель никогда не получает собственный драг.
- С клавиатуры стрелка сначала останавливается на папке; следующее нажатие в ту же сторону проходит мимо неё.
- После броска элемент больше не показан, поэтому оверлей сжимается в папку, а не летит обратно.
- Над папкой список оставляет слот перетаскиваемой строки на прежнем месте, чтобы под
указателем ничего не сдвигалось. Слот получает тогда
data-dnd-placeholder="into": бросок туда не попадёт, так что его можно скрыть ([data-dnd-placeholder='into'] { opacity: 0 }).
Файловый менеджер делает то же самое в сетке, с вложенными папками.
Обычные перетаскиваемые элементы в список
Сортируемый список принимает элементы, перетаскиваемые с makeDraggable, если знает, как
сделать из них элемент:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Agenda from './Agenda.vue';
import BlockChip from './BlockChip.vue';
const BLOCKS = [
{ title: 'Focus', minutes: 90, color: '#42b883' },
{ title: 'Meeting', minutes: 30, color: '#3b82f6' },
{ title: 'Lunch', minutes: 60, color: '#f59e0b' },
{ title: 'Gym', minutes: 45, color: '#ec4899' },
{ title: 'Break', minutes: 15, color: '#a1a1aa' },
];
</script>
<template>
<DnDProvider>
<div class="planner">
<aside class="blocks">
<h4>Blocks</h4>
<BlockChip v-for="block in BLOCKS" :key="block.title" :block="block" />
</aside>
<Agenda />
</div>
</DnDProvider>
</template>
<style scoped>
.planner {
display: flex;
justify-content: center;
gap: 24px;
}
.blocks {
display: flex;
flex: none;
flex-direction: column;
gap: 6px;
width: 140px;
}
h4 {
margin: 0 0 6px;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
@media (max-width: 480px) {
.blocks {
width: 112px;
}
}
</style>const { items } = makeSortableList(list, {
items: agenda,
createItem: (active) => ({ ...(active.data as Block), id: `entry-${++created}` }),
});
Пока перетаскиваемый элемент над списком, созданный элемент находится в черновике и
рендерится вашим шаблоном, как любая другая строка: время в расписании уже его учитывает.
Настоящим он становится при броске (mode: 'insert', from.list === null), и оверлей
приземляется на него.
- Верните
undefined, чтобы отказать. Списки безcreateItemигнорируют обычные перетаскиваемые элементы. - Дайте новому элементу собственный ключ: блок остаётся в палитре.
- С клавиатуры стрелка в сторону списка забирает перетаскиваемый элемент на его уровне, а следующие стрелки двигают его по слотам.
Файлы с компьютера
Файлы, перетащенные с рабочего стола или из файлового менеджера, — не драги библиотеки: их
несёт браузер. Цель принимает их с external, а бросок приносит их в e.external:
Загружаем демо…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import AvatarDrop from './AvatarDrop.vue';
import Attachments from './Attachments.vue';
</script>
<template>
<DnDProvider>
<div class="form">
<AvatarDrop />
<Attachments />
</div>
</DnDProvider>
</template>
<style scoped>
.form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
gap: 16px;
}
</style>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesчитается как<input accept>: MIME-типы (image/*,application/pdf) и расширения (.md).trueпринимает любой файл, функция решает по каждому файлу.linksиtextпринимают то, что перетаскивают другие приложения и вкладки,types— любой другой MIME-тип.- Драг идёт как драг провайдера:
isOver,isAccepting, события и объявления работают как для любого другого. Цели безexternalего игнорируют. Оверлея нет: перетаскиваемое изображение рисует браузер. - Пока файлы держат над страницей, браузер сообщает только их типы (
e.external.fileTypes). Имена, размеры и содержимое приходят с броском, и тогда же проверяются расширения. Папки пропускаются; сmultiple: falseдраг нескольких файлов отклоняется.
Списки тоже принимают файлы, ссылки и текст и освобождают для них место, пока их держат. Элементы могут и покидать страницу — в другое приложение или в другое ваше окно: см. файлы и другие приложения.
<input type="file">, как демо делает с кнопкой Browse.Клавиатура
Перетаскиваемый элемент в фокусе берётся клавишей Space или Enter, бросается теми же клавишами, а Escape отменяет драг. В промежутке его двигают стрелки:
keyboardMode | Стрелка |
|---|---|
'pixels' (по умолчанию) | Сдвигает его на 25 px, keyboard.step провайдера. С Shift — в четыре раза дальше. Так двигается мебель. |
'droppables' | Прыгает к ближайшей цели в этом направлении, которая его принимает. Мусор прыгает от бака к баку и пропускает баки, которые его отклоняют. |
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });
Клавиши можно переназначить на провайдере. Подробнее — в доступности.