Документация/Начало работы
Переход с v2
Перенос приложения с v2 на v3 — что изменилось и почему, обновление, соответствие каждого API v2 его замене в v3, код до и после для типичных случаев и подводные камни.
Страница переведена ИИ и может содержать неточности. Читать английский оригинал
v3 — это переписанная v2. Модель осталась прежней: один провайдер и композаблы, которые вы вызываете на своих элементах. Но большинство имён и опций изменилось, сортировка переехала в библиотеку, а обработчик броска, который вы писали для каждого списка, больше не нужен. Эта страница сопоставляет каждую часть v2 с v3, а затем показывает типичные случаи до и после.
Что изменилось
- Сортировка встроена. В v2 список был зоной сброса, его строки — перетаскиваемыми
элементами с данными
[index, items], а сортировали вы вonDropчерезe.helpers.suggestSort(). В v3 это делаютmakeSortableListиmakeSortableItem: список рендерит черновик вашего массива, который перестраивается во время драга, а бросок записывает новый массив в ваш ref. Большинство обработчиков броска из v2 удаляются, а не переносятся. - Всё движется само. Строки плавно встают на новые места (FLIP), оверлей при броске
приземляется на свой слот, а при отмене летит обратно.
<TransitionGroup>больше не нужен. - По-прежнему один пакет. В
@vue-dnd-kit/coreесть списки, деревья, мульти-драг, модификаторы, сенсоры и функции определения коллизий. Nuxt-модуль остаётся;@vue-dnd-kit/actions(программное перетаскивание) и@vue-dnd-kit/devtools— новые и необязательные. - Настоящий оверлей. Он телепортируется в
bodyи показывает глубокий клон размером со слот, на который приземлится. Компоненты оверлея получают драг через пропсы, а не читают внутренности провайдера. - Id, а не элементы. Выделение, события и состояние драга привязаны к id из ваших данных. Никаких карт DOM-элементов читать не нужно.
- Типы и правила
acceptзаменяютgroupsдля обычных перетаскиваемых элементов и целей. Списки обмениваются элементами по имениgroup. - Решения до броска.
onBeforeDropпринимает бросок, отклоняет его, продолжает драг или берёт часть элементов.onDropтолько применяет. - Сенсоры. Ввод указателем и с клавиатуры — это сенсоры. Активация задаётся для каждого типа указателя, а драги касанием начинаются с долгого нажатия, поэтому списки продолжают прокручиваться под пальцем.
- Области.
makeDragAreaзаменяетmakeConstraintAreaиmakeAutoScroll, а автоскролл работает в любом скролл-контейнере без кода. - Доступность включена по умолчанию: ARIA-атрибуты, фокус, который возвращается после броска, и озвучивание для скринридеров.
Обновление
npm i @vue-dnd-kit/core@^3
yarn add @vue-dnd-kit/core@^3
pnpm add @vue-dnd-kit/core@^3
bun add @vue-dnd-kit/core@^3
| Пакет | v2 | v3 |
|---|---|---|
@vue-dnd-kit/core | 2.4.x | 3.x |
@vue-dnd-kit/nuxt | 1.x | 3.x |
@vue-dnd-kit/actions | — | 1.x, необязательный |
@vue-dnd-kit/devtools | — | 3.x, необязательный |
- Приложение, у которого в
package.jsonстоит"@vue-dnd-kit/core": "^2…", остаётся на v2, пока диапазон не станет^3. - Vue 3.5 или новее. У v2 не было зависимостей; у v3 одна —
@vueuse/core(14 или 15). - Браузеры: Chrome и Edge 104+, Safari 15.4+, Firefox 104+.
- У v2 и v3 одно имя пакета, поэтому приложение переходит на v3 целиком. Слоя совместимости нет. Документация v2 по-прежнему доступна в меню версий вверху страницы.
Хороший порядок работы:
- Обновите пакеты и переименуйте пропсы провайдера (ниже).
- Превратите каждый сортируемый список в
makeSortableListиmakeSortableItemи удалите его обработчик броска. - Перенесите обычные перетаскиваемые элементы и зоны сброса.
- Перенесите свои превью в компоненты оверлея или в слот
#overlayпровайдера. - Замените
makeConstraintAreaиmakeAutoScrollнаmakeDragAreaили ни на что. - Удалите обходные решения v2:
tabindexиaria-grabbedна перетаскиваемых элементах,touch-action: noneна элементах целиком,<TransitionGroup>вокруг строк,markRawвокруг компонентов превью.
Старое и новое
Нет аналога в таблице означает, что в v3 нет ничего с таким именем; в ячейке сказано, что использовать вместо этого.
Вкратце
| v2 | v3 |
|---|---|
DnDProvider | DnDProvider, с новыми пропсами и слотом #overlay. |
makeDraggable | makeDraggable для свободного перетаскивания; makeSortableItem для строк списка. |
makeDroppable | makeDroppable для обычных целей; makeSortableList или makeSortableTree для списков и деревьев. |
makeSelectionArea | makeSelectionArea. |
makeConstraintArea | makeDragArea. |
makeAutoScroll | Ничего: автоскролл работает сам. Его настраивает makeDragArea или autoScroll списка. |
useDnDProvider | useDnD для состояния, useDnDMonitor для событий. |
injectionKey | useDnDContext() и provideDnDContext(app, context). |
DragPreview | DragPreview: теперь это клон одного перетаскиваемого элемента, для ваших собственных оверлеев. |
makeSnappedOverlayPosition, <DragPreview :grid> | Модификатор snapToGrid. |
e.helpers.suggestSort, suggestSwap, suggestCopy, suggestRemove | Встроены в списки: см. сортировку. |
Провайдер
| v2 | v3 |
|---|---|
autoScrollViewport, выключен, пока не задан | autoScroll: включён по умолчанию, для окна и любого скролл-контейнера. { threshold, maxSpeed, acceleration } или false. |
previewTo, по умолчанию рендерится внутри провайдера | overlayTo: селектор или элемент, по умолчанию 'body'. |
Слот #preview="{ preview }" | Слот #overlay с драгом в пропсах: items, active, over, accepted, status, mode, to. См. оверлей. |
keyboard.keys.forDrag, forDrop, forCancel (задавались в useDnDProvider()) | :keyboard="{ keys: { start, drop, cancel } }". |
keyboard.step | :keyboard="{ step }": по умолчанию 25 px, для свободных драгов. |
keyboard.keys.forMove, forMoveFaster, keyboard.moveFaster | Нет аналога. Двигают стрелки; Shift делает шаги свободного драга в четыре раза больше. |
collision.throttle | Нет аналога. collision выбирает, как находится цель: pointerFirst (по умолчанию), pointerWithin, rectIntersection, closestCenter, closestCorners или ваша функция. |
| Один провайдер на страницу, никогда не вложенный | Вложение работает: элементом владеет ближайший провайдер. Цель по-прежнему — один провайдер на приложение. |
Новые пропсы, у которых нет аналога в v2: activation, modifiers, sensors, animation,
dropAnimation, animateChanges, haptics, copyModifier, overlayFit, overlayClass,
overlayTransition, overlayPreviews, announcements, screenReaderInstructions и
defaultStyles. Провайдер также эмитит drag-start, drag-move, drag-over, drag-end,
drag-cancel, drag-resume и drag-settle. Все они описаны в руководстве по провайдеру.
Чтение состояния: useDnD
useDnDProvider() | v3 |
|---|---|
state: 'dragging', 'activating', 'selecting', 'pending' или null | useDnD().status: 'idle', 'activating', 'dragging', 'dropping' или 'selecting'. 'pending' (асинхронный бросок) — это 'dropping'; null — это 'idle'. |
pointer.value.current | useDnD().pointer, после модификаторов. У каждого события есть ещё delta — смещение от начала. |
entities.draggingMap | useDnD().active: id, ids, items, data, type, element, rect. |
entities.selectedSet (элементы) | useDnD().selection (id): has, add, delete, toggle, set, clear, ids. |
hovered.droppable, hovered.draggable | useDnD().over: id, data, element, rect, accepted. |
delay.progress, distanceProgress | useDnD().activation.progress, от 0 до 1. |
preview.position, preview.size, preview.render, preview.to | Нет аналога. Библиотека сама размещает оверлей и задаёт ему размер. Используйте overlayTo, слот #overlay или компоненты оверлея. |
entities.draggableMap, droppableMap, наборы visible…Set, scrollPosition | Нет аналога. |
| — | Новое: useDnD().cancel(), position, isAutoScrolling. |
События переехали в useDnDMonitor: см. события и решения о броске.
Полные структуры — в API useDnD.
makeDraggable
| v2 | v3 |
|---|---|
makeDraggable(ref, options, payload) | makeDraggable(ref, options): без третьего аргумента. Строки списка используют makeSortableItem(ref, { item }). |
id, нужен в виртуальных списках | id: передавайте его из своих данных, геттером. Значение по умолчанию берётся из useId() и новое для каждого экземпляра компонента. |
data: () => card | data: значение, ref или геттер. Читается обратно как active.data и active.items[i].data. |
groups: ['dev'] | type: 'dev', с которым сверяется accept целей. См. группы. |
dragHandle: '.handle' | handle: селектор или ref элемента. |
disabled | disabled. |
modifier: { keys, method } | canDrag: (event) => event.altKey: проверка на каждое нажатие. |
activation: { distance, delay } | activation для каждого типа указателя: { mouse: { distance }, touch: { delay, tolerance }, pen }. Если заданы и distance, и delay, драг начинается по тому, что наступит раньше. |
activation.distance: { x, y, condition } | Нет аналога. Один distance на тип указателя. |
render: markRaw(Preview) | overlay: Preview: компонент получает драг через пропсы. См. оверлей. |
placementMargins | Нет аналога. Цель на сортируемом элементе засчитывается в его середине (от 25 до 75 %); деревья вкладывают через nesting: 'hover'. См. бросок в сортируемый элемент. |
events.onSelfDragStart, onSelfDragMove, onSelfDragEnd, onSelfDragCancel | Нет аналога на перетаскиваемом элементе: useDnDMonitor или watch(isDragging). См. события. |
events.onHover, onLeave и его onDragStart… для драгов над ним | makeDroppable на том же элементе: onDragEnter, onDragLeave. |
Возвращает isDragging, selected | То же самое. |
Возвращает isDragOver | Сортируемые элементы возвращают indicator (с placeholder: 'indicator') и isPlaceholder; цели возвращают placement. |
Возвращает isAllowed | Нет аналога у перетаскиваемых элементов. Цели возвращают isAccepting. |
| — | Новое: label, axis, modifiers, keyboardMode, overlay: false с возвращаемым transform, dropAnimation, attributes, native. |
makeDroppable
| v2 | v3 |
|---|---|
makeDroppable(ref, options, () => items) | makeDroppable(ref, options). Зона, в которой лежит список, — это makeSortableList. |
groups | accept: '*', тип, список типов или функция. |
groupMatch: 'every', 'some' | acceptMode: 'all', 'some'. |
events.onEnter, events.onLeave | onDragEnter, onDragLeave: самостоятельные опции, без объекта events. |
events.onDrop, который мог вернуть false или промис | onBeforeDrop решает, onDrop применяет. См. решения о броске. |
events.onValidate | onBeforeDrop, возвращающий id, которые нужно оставить. |
data | data, читается обратно как over.data. |
disabled | disabled. |
Возвращает isAllowed | isAccepting. |
Возвращает isDragOver: { top, right, bottom, left, center } | isOver (boolean) и placement: { top, right, bottom, left } — половины, в которых находится указатель. center нет. |
| — | Новое: id, label, priority, overOverlay, overDropAnimation, external (файлы с компьютера). |
Событие броска
v2 IDragEvent | v3 |
|---|---|
e.draggedItems | e.active.items: у каждого { id, item, data, label, type }, взятый элемент первым. Списки получают e.items и e.keys. |
e.draggedItems[0].data | e.active.data. |
e.draggedItems[i].index, .items | Списки: e.from, { list, index }. |
e.dropZone: items, placement, data | e.over: id, data, element, rect, accepted. Списки: e.to, { list, index }. |
e.hoveredDraggable | Нет аналога: списки сами вычисляют e.to.index. |
e.helpers.suggestSort() | Списки записывают бросок; e.result содержит новый массив каждого списка, который он меняет. |
e.helpers.suggestSwap() | behavior: 'swap' у списка; e.partner — элемент, с которым произошёл обмен. |
e.helpers.suggestCopy() | group: { name, pull: 'clone' } или клавиша копирования (Alt), вместе с clone списка; e.mode равен 'copy'. |
e.helpers.suggestRemove() | e.source.remove() в цели или dropOutside: 'remove' у списка. |
result.sameList | e.sameList. |
e.helpers.insertAt, removeAt, removeIndexes, swapAt, getInsertSide, isAtZoneStart, isAtZoneEnd | Нет аналога. Спискам они не нужны; у обычной цели меняйте данные как хотите. |
e.provider | useDnD(). |
| — | Новое: e.pointer, e.delta, e.position (куда поставить элемент, брошенный на доску), e.mode. |
Сортировка
| v2 | v3 |
|---|---|
Список на makeDroppable с () => items | makeSortableList(listRef, { items }). |
Строка на makeDraggable с () => [index, items] | makeSortableItem(el, { item: () => props.task }). |
| Рендерить свой массив | Рендерить возвращённый items: черновик, который перестраивается во время драга. |
suggestSort('vertical'), suggestSort('horizontal') | strategy: 'vertical' (по умолчанию), 'horizontal' или 'grid'. |
Сравнение по ссылке, чтобы найти исходный и целевой массивы, затем присваивание sourceItems и targetItems | Ничего: бросок записывает каждый список, который меняет. |
<TransitionGroup> | Встроено. animateChanges: true анимирует изменения, сделанные не драгом (добавление, удаление, перемешивание). |
Линия вставки по isDragOver.top, .bottom | placeholder: 'indicator' и indicator.side элемента. См. линию вставки. |
suggestSwap() | behavior: 'swap'. |
Палитра только для копирования (suggestCopy() и флаг copyOnly) | group: { name, pull: 'clone', put: false }, sort: false и clone. |
Зона-корзина с suggestRemove() | makeDroppable(trash, { onDrop: (e) => e.source?.remove() }) или dropOutside: 'remove'. |
| Pinia или неизменяемые данные: результат присваиваете сами | items как геттер, затем e.preventDefault() в onDrop и применение e.result. См. управляемые списки. |
Рекурсивное дерево: зона сброса на каждую папку и applyToTree | makeSortableTree: один контейнер, строки рендерятся плоско со своей глубиной. См. деревья. |
Вложенные группы с v-model (зона сброса на каждую группу) | Список внутри списка, все в одной group, у каждого items: defineModel(). |
Виртуальные списки: id на каждом перетаскиваемом элементе | Передайте черновик виртуализатору, id: () => props.item.id на строках. См. виртуальные списки. |
Код до и после — ниже. Всё о списках — в сортируемых списках и списках с общими элементами.
Группы
В v2 была одна опция groups на всё. v3 разделяет её на две:
| v2 | v3 |
|---|---|
makeDraggable(el, { groups: ['dev'] }) | makeDraggable(el, { type: 'dev' }). |
makeDroppable(el, { groups: ['dev'] }) | makeDroppable(el, { accept: 'dev' }). |
groups: ['dev', 'ops'] на зоне | accept: ['dev', 'ops'] или функция: ({ active, items, types }) => boolean. |
groups: [] или ничего | type или accept не заданы. Правила v2 сохраняются: перетаскиваемый элемент без типа подходит под любое правило, а цель без accept принимает всё. |
groupMatch: 'every', 'some' | acceptMode: 'all', 'some'. |
groups на зоне списка и его строках | group: 'board' у makeSortableList. group: { name, pull, put } говорит, что может уходить и входить. |
groups у makeSelectionArea | accept. |
Список без group — сам себе группа и не обменивается элементами ни с каким другим списком. В
v2 зона без групп принимала всё. См. какие драги принимает цель.
Оверлей
| v2 | v3 |
|---|---|
Превью, отрендеренное в провайдере и телепортированное через previewTo | Всегда телепортируется: в body или в overlayTo. |
Клон: тот же тег, его innerHTML и его размер | Глубокий клон: значения полей форм, пиксели <canvas>, унаследованные стили и ячейки таблиц переносятся. То, что должно остаться, помечайте data-dnd-no-clone. |
render: markRaw(Preview), который читает useDnDProvider().entities | overlay: Preview, который получает items, active, over, accepted, status, mode и to как пропсы. Без markRaw; чтобы переключать его, используйте shallowRef. |
Слот #preview, позиционируемый по preview.position | Слот #overlay: библиотека сама размещает блок и задаёт ему размер. Если ничего не рендерить, используется клон. |
useDnDProvider().preview.render.value = … | Слот #overlay или shallowRef, переданный как overlay. |
Новый вид над зоной, из onEnter и onLeave | overOverlay у makeDroppable или списка, либо читайте over в компоненте оверлея. См. цели, которые меняют оверлей. |
<DragPreview v-slot="{ draggingMap }"> | items в пропсах оверлея; <DragPreview :id="item.id" /> рендерит один клон. |
<DragPreview :grid>, makeSnappedOverlayPosition | Модификатор snapToGrid. Он привязывает к сетке и место, где ищутся цели, и position броска, а не только картинку. |
<Transition> или AnimatePresence из motion вокруг DragPreview | overlay-class с @starting-style для захвата, dropAnimation для приземления, overlayTransition между компонентами. См. подъём, наклон, посадку. |
.dnd-kit-preview | [data-dnd-overlay] ("dragging", "dropping") на блоке, [data-dnd-overlay-content] внутри него. |
preview.size, который обновляли вручную | Блок принимает размер слота, на который приземлится (overlayFit). |
| Превью исчезает при броске | Оно приземляется на свой слот или летит обратно при отмене. |
Подробнее — в руководстве по оверлею.
Выделение и мульти-драг
| v2 | v3 |
|---|---|
selected, возвращаемый makeDraggable | selected, возвращаемый makeDraggable и makeSortableItem. |
entities.selectedSet, набор элементов | useDnD().selection, по id. |
makeSelectionArea: по умолчанию нужно держать Ctrl (ControlLeft) | По умолчанию никакой клавиши: нажатие на пустом месте области рисует рамку. modifier: { keys: ['Control'], method: 'every' } возвращает Ctrl. |
modifier.keys как KeyboardEvent.code | KeyboardEvent.key ('Control', 'Shift') или .code ('ControlLeft'). |
strategy: 'toggle', 'select' | То же самое. |
groups | accept. |
events.onSelected(elements) | onSelected(ids). |
disabled | То же самое. |
suggestSort(), перемещающий выделение пачкой | Его перемещают списки. multiple: 'group' (по умолчанию), 'gaps' или 'stack' говорит, что по пути делают остальные элементы. |
onValidate для смешанных выделений | acceptMode: 'some' и onBeforeDrop, возвращающий id. См. принять только часть. |
Подробнее — в мульти-драге и выделении.
Оси, границы и привязка
| v2 | v3 |
|---|---|
makeConstraintArea(el, { axis: 'x' }) | makeDragArea(el, { axis: 'x' }), для драгов, которые начинаются внутри неё. axis работает и у перетаскиваемого элемента, списка, дерева или элемента списка. |
axis: 'both' | axis не задан. |
restrictToArea: true | makeDragArea(el, { restrictToArea: true }) или модификаторы restrictToParent и restrictToElement(el). |
| Провайдер на каждую область, чтобы разделить правила | Один провайдер и makeDragArea на каждую область. Её элементы по-прежнему могут перемещаться куда угодно. |
<DragPreview :grid="20">, makeSnappedOverlayPosition | snapToGrid(20) или snapToGrid({ x, y, origin, smooth }), в modifiers у провайдера, области, списка или перетаскиваемого элемента. |
| — | Новое: restrictToWindowEdges, restrictToFirstScrollableAncestor, snapCenterToCursor и ваши собственные модификаторы. |
Подробнее — в осях, границах и сетках.
Автоскролл
| v2 | v3 |
|---|---|
<DnDProvider :autoScrollViewport="true">, по умолчанию выключен | Включён по умолчанию, для окна и любого скролл-контейнера. :auto-scroll="false" выключает его. |
makeAutoScroll(el, { threshold, speed }) | Ничего: прокручивается любой элемент с overflow: auto или scroll. Чтобы настроить один: makeDragArea(el, { autoScroll: { … } }) или autoScroll списка. |
threshold в px или для каждой стороны | threshold: доля размера контейнера (0.2), от 24 до 96 px. Нет аналога для порогов по сторонам. |
speed, множитель | maxSpeed (25 px за кадр) и acceleration (10). |
disabled: true | autoScroll: false. |
isScrolling | isAutoScrolling, из makeDragArea или useDnD(). |
Клавиатура и доступность
| v2 | v3 |
|---|---|
tabindex="0" на каждом перетаскиваемом элементе | Ставится за вас, вместе с role, aria-roledescription, aria-describedby, aria-disabled и aria-pressed. Ваши собственные атрибуты важнее. |
:aria-grabbed="isDragging" вручную | Удалите: aria-pressed следует за драгом. |
Клавиши в useDnDProvider().keyboard.keys | Проп keyboard провайдера. Привязки сопоставляются с event.code или event.key. |
Стрелки двигают превью на keyboard.step | Свободные драги: на keyboard.step px. Списки: по слотам и в следующий список группы. keyboardMode: 'droppables' перескакивает между целями. |
Переназначение стрелок (forMove, forMoveFaster) | Нет аналога. |
| Никаких объявлений | Live-регион озвучивает каждый захват, перемещение, бросок и отмену. label даёт имена; announcements и screenReaderInstructions меняют формулировки. |
Подробнее — в доступности.
События и решения о броске
| v2 | v3 |
|---|---|
events.onSelfDragStart перетаскиваемого элемента | useDnDMonitor({ onDragStart }), с проверкой active.ids. Для класса достаточно isDragging. |
onSelfDragMove | onDragMove монитора. |
onSelfDragEnd, при броске или отмене | onDragEnd монитора (бросок) и onDragCancel. onDragSettle слышит оба, когда оверлей приземлился. |
onSelfDragCancel | onDragCancel монитора. |
onHover, onLeave перетаскиваемого элемента и его onDragStart… для драгов над ним | makeDroppable на элементе: onDragEnter, onDragLeave. Строкам списка ничего не нужно. |
events.onEnter, onLeave зоны сброса | onDragEnter, onDragLeave. |
events.onDrop зоны сброса | onBeforeDrop решает, затем onDrop применяет. onDrop синхронный, и то, что он возвращает, игнорируется. |
events.onValidate зоны сброса | onBeforeDrop, с acceptMode: 'some'. |
| — | Новое: @drag-start … @drag-settle провайдера и onDragOver, onBeforeDrop и onDragResume монитора. |
В v2 onDrop мог вернуть значение или промис значения, чтобы решить судьбу броска. В v3 это
работа onBeforeDrop — у списков, деревьев, целей и мониторов:
v2: onDrop возвращает | v3: onBeforeDrop возвращает |
|---|---|
Ничего или true: бросок проходит | 'accept', true или ничего. |
false: этот бросок отклонён, драг остаётся активным | 'continue'. Теперь false отклоняет бросок. |
| Отклонённый промис или исключение: драг отменяется | 'reject' или false. Исключение тоже отклоняет. |
Промис; тем временем state равен 'pending' | Промис; тем временем status равен 'dropping', а оверлей ждёт с data-dnd-overlay="dropping". |
onValidate возвращает IDragItem[] | Массив id: бросаются только они, остальные летят обратно. |
Сначала отвечают мониторы, затем цель. Потом выполняется onDrop цели, список записывает свои
новые массивы, и следует onDragEnd. Подробнее — в решении о броске
и в API событий.
Типы
| v2 | v3 |
|---|---|
IDragEvent | DnDEvent (цели, мониторы), SortableDropEvent (списки), DnDEndEvent (onDragEnd). |
IDragItem | DragItem. |
IDropZoneContext | Over. |
IHoveredDraggableContext | Нет аналога. |
ISuggestSortResult, ISuggestSwapResult, ISuggestCopyResult, ISuggestRemoveResult | SortableDropEvent: result, to, mode, partner. |
IDropValidateEvent | DnDEvent, в onBeforeDrop. |
IPlacement | Placement, без center. |
ICoordinates | Coordinates. |
IDragActivationOptions | ActivationOptions. |
IAutoScrollOptions | AutoScrollOptions. |
TDnDState | DnDStatus. |
IDnDProviderExternal | DnDState — то, что возвращает useDnD(). |
IDraggableEvents, IDroppableEvents | Колбэки в DraggableOptions, DroppableOptions и DnDMonitor. |
Nuxt
npm i @vue-dnd-kit/nuxt@^3 @vue-dnd-kit/core@^3
v2 (@vue-dnd-kit/nuxt 1.x) | v3 (@vue-dnd-kit/nuxt 3.x) |
|---|---|
Автоимпортирует makeDraggable, makeDroppable, makeSelectionArea, makeConstraintArea, makeAutoScroll, useDnDProvider | Автоимпортирует makeDraggable, makeDroppable, makeSortableList, makeSortableItem, makeSortableTree, makeSelectionArea, makeDragArea, useDnD, useDnDMonitor. |
| Типы не автоимпортируются | Основные типы автоимпортируются (DnDEvent, SortableDropEvent, OverlayProps…). Общие (Key, Active, Over) импортируются вручную. |
| — | Модификаторы, функции определения коллизий и сенсоры — по запросу: vueDndKit: { autoImports: { modifiers: true, collision: true, sensors: true } }. |
DnDProvider и DragPreview глобальные | То же самое, с опцией prefix. |
<DragPreview /> в лейауте, внутри провайдера | Удалите его: оверлей рендерит провайдер. DragPreview принимает id, и его место — внутри компонентов оверлея. |
Peer-зависимость @vue-dnd-kit/core 2 | Peer-зависимость @vue-dnd-kit/core 3; Nuxt 4. |
modules: ['@vue-dnd-kit/nuxt'] в nuxt.config.ts остаётся как было. См.
установку.
До и после
Перетаскиваемый элемент и зона сброса
v2
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
groups: ['task'],
dragHandle: '.handle',
data: () => props.card,
});
</script>
<template>
<div ref="el" tabindex="0" :class="{ dragging: isDragging }">
<span class="handle">⋮⋮</span>
{{ card.title }}
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';
const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragOver, isAllowed } = makeDroppable(el, {
groups: ['task'],
events: {
onDrop: (e) => emit('drop', e.draggedItems.map((item) => item.data as Card)),
},
});
</script>
<template>
<div ref="el" :class="{ over: isDragOver && isAllowed }"><slot /></div>
</template>
v3
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.card.id, // read back as `active.id`
type: 'task', // matched by the targets' `accept`
handle: '.handle',
data: () => props.card,
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">
<button type="button" class="handle" :aria-label="`Move ${card.title}`">⋮⋮</button>
{{ card.title }}
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';
const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
accept: 'task',
onDrop: (e) => emit('drop', e.active.items.map((item) => item.data as Card)),
});
</script>
<template>
<div ref="el" :class="{ over: isOver && isAccepting }"><slot /></div>
</template>
- Карточка сама получает
tabindex,roleи атрибутыaria-*. Ручка-<button>получает фокус, и драги с клавиатуры начинаются с неё. groupsсталtypeу перетаскиваемого элемента иacceptу цели. Колбэки ушли из объектаevents.isOverбезisAccepting— это цель, которая отказывает драгу: бросок туда летит обратно.
Сортируемый список
v2
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type IDragEvent } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');
makeDroppable(
el,
{
events: {
onDrop(e: IDragEvent) {
const r = e.helpers.suggestSort('vertical');
if (r) tasks.value = r.targetItems as Task[];
},
},
},
() => tasks.value,
);
</script>
<template>
<div ref="el">
<TransitionGroup name="list">
<TaskItem v-for="(task, index) in tasks" :key="task.id" :task="task" :index="index" :tasks="tasks" />
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ task: Task; index: number; tasks: Task[] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, { id: props.task.id }, () => [props.index, props.tasks]);
</script>
<template>
<div ref="el" :class="{ 'is-dragging': isDragging }">{{ task.title }}</div>
</template>
v3
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');
const { items } = makeSortableList(el, { items: tasks });
</script>
<template>
<ul ref="el">
<TaskItem v-for="task in items" :key="task.id" :task="task" />
</ul>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';
const props = defineProps<{ task: Task }>();
const el = useTemplateRef<HTMLElement>('el');
const { isPlaceholder } = makeSortableItem(el, { item: () => props.task });
</script>
<template>
<li ref="el" :class="{ slot: isPlaceholder }">{{ task.title }}</li>
</template>
- Рендерите
items, черновик: он перестраивается во время драга.tasksполучает новый порядок при броске, иdefineModelэмититupdate:modelValue. - Ни индекса, ни данных, ни
onDrop, ни<TransitionGroup>: строки встают на места сами. - Перетаскиваемая строка остаётся в списке как слот, на который приземлится. Стилизуйте её
через
isPlaceholderили[data-dnd-placeholder], а не прячьте.
Списки с общими элементами
v2
// Each column's makeDroppable calls this from its onDrop, with its array as the payload.
function applySort(e: IDragEvent) {
const r = e.helpers.suggestSort('vertical');
if (!r) return;
const src = e.draggedItems[0]?.items;
const tgt = e.hoveredDraggable?.items ?? e.dropZone?.items;
if (src === todo.value) todo.value = r.sourceItems as Task[];
else if (src === done.value) done.value = r.sourceItems as Task[];
if (!r.sameList) {
if (tgt === todo.value) todo.value = r.targetItems as Task[];
else if (tgt === done.value) done.value = r.targetItems as Task[];
}
}
v3
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';
const props = defineProps<{ column: { id: string; tasks: Task[] } }>();
const el = useTemplateRef<HTMLElement>('el');
const { items } = makeSortableList(el, {
id: props.column.id, // `from.list` and `to.list` in drop events
items: toRef(props.column, 'tasks'), // writable: the parent passes a reactive column
group: 'board', // same name: tasks move between these lists
});
</script>
<template>
<ul ref="el">
<TaskItem v-for="task in items" :key="task.id" :task="task" />
</ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'todo', tasks: [{ id: 't1', title: 'Write the docs' }] },
{ id: 'done', tasks: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
Бросок записывает оба массива: тот, который задача покинула, и тот, в который вошла.
TaskItem.vue — тот же, что в сортируемом списке выше. Палитра, которая раздаёт копии, была
suggestCopy() и флагом copyOnly; теперь это список:
makeSortableList(el, {
items: blocks,
group: { name: 'board', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
Для стора Pinia или другого состояния, которое вы не записываете напрямую, передайте items
геттером, вызовите e.preventDefault() в onDrop и примените e.result: см.
управляемые списки.
Свой оверлей
v2
makeDraggable(el, {
render: markRaw(TaskOverlay),
data: () => props.task,
});
<script setup lang="ts">
import { computed } from 'vue';
import { useDnDProvider } from '@vue-dnd-kit/core';
const { entities } = useDnDProvider();
const task = computed(() => {
const [node] = entities.draggingMap.keys();
return node ? (entities.draggableMap.get(node)?.data?.() as Task) : null;
});
</script>
<template>
<div class="task-preview">{{ task?.title }}</div>
</template>
v3
makeDraggable(el, {
id: () => props.task.id,
overlay: TaskOverlay,
data: () => props.task,
});
<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; accepted: boolean }>();
const task = computed(() => props.items[0]!.data as Task);
</script>
<template>
<!-- Fills the overlay box, which takes the size of the slot. -->
<div class="task-preview" :class="{ refused: over && !accepted }">
{{ task.title }}
<span v-if="items.length > 1" class="count">{{ items.length }}</span>
</div>
</template>
<style scoped>
.task-preview { width: 100%; height: 100%; }
</style>
- Компонент получает драг через пропсы:
items(все перетаскиваемые элементы, взятый — первым),over,accepted,status,modeиto. Объявляйте только те, которые читаете. Для строк списка элемент списка — этоitems[0].item. - Передавайте его как есть. Переключайте его через
shallowRef, никогда не через глубокийref.
Слот провайдера меняется так же, и ваш компонент больше не позиционирует себя сам:
v2
<DnDProvider>
<RouterView />
<template #preview>
<MyOverlay /> <!-- moved itself with useDnDProvider().preview.position -->
</template>
</DnDProvider>
v3
<DnDProvider>
<RouterView />
<template #overlay="{ items, over, accepted }">
<MyOverlay :items="items" :over="over" :accepted="accepted" />
</template>
</DnDProvider>
Появление, которое оборачивало DragPreview в <Transition>, становится overlay-class с
@starting-style: см. подъём, наклон, посадку.
Бросок, который вы подтверждаете
v2
makeDroppable(el, {
events: {
onDrop: (e) =>
new Promise<void | boolean>((resolve) => {
if (!window.confirm('Drop here?')) return resolve(false); // keep dragging
place(e.draggedItems);
resolve();
}),
},
});
v3
makeDroppable(el, {
onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
onDrop: (e) => place(e.active.items),
});
Список спрашивает так же, а промис позволяет открыть собственный диалог. Оверлей ждёт там, где его отпустили, пока промис не завершится:
makeSortableList(el, {
items: toRef(props.column, 'tasks'),
group: 'board',
// `ask` opens a dialog and resolves 'accept', 'continue' or 'reject'.
onBeforeDrop: (e) => (e.sameList ? 'accept' : ask(e)),
});
Зона, которая брала только подходящую часть смешанного выделения (groupMatch: 'some'):
v2
makeDroppable(el, {
groups: ['frontend'],
groupMatch: 'some',
events: {
onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
},
});
v3
import type { DragItem } from '@vue-dnd-kit/core';
const fits = (item: DragItem) => item.type.length === 0 || item.type.includes('frontend');
makeDroppable(el, {
accept: 'frontend',
acceptMode: 'some',
onBeforeDrop: (e) => e.active.items.filter(fits).map((item) => item.id), // the others fly back
onDrop: (e) => place(e.active.items.filter(fits)), // `active.items` still lists every dragged item
});
Списки делают это без кода: с acceptMode: 'some' элементы, которые принимает их group.put,
приземляются, а остальные летят обратно.
Подводные камни при обновлении
- Ставьте v3 с её диапазоном.
^3вpackage.json:npm updateоставляет^2на v2. - Рендерите черновик. Список рендерит
items, который возвращаетmakeSortableList, а не ваш массив. Если рендерить ваш массив, до броска ничего не двигается. - Не сортируйте дважды. Удалите из списков код с
suggestSort: список записывает каждый бросок в записываемый refitemsпосле своегоonDrop. Чтобы применить его самим, вызовитеe.preventDefault()и используйтеe.result. falseотклоняет бросок. В v2onDrop, разрешившийся вfalse, продолжал драг. В v3 верните'continue'изonBeforeDrop. Промис, возвращённый изonDrop, игнорируется.- Списки обмениваются только внутри
group. Список без неё не обменивается ни с каким другим списком. - Задавайте строкам ключи по id. С
:key="index"анимируются не те строки. Передавайте и id из своих данных: id по умолчанию новый для каждого экземпляра компонента. - Никакого
<TransitionGroup>и никакогоtransitionнаtransform. Библиотека двигает строки сама, и перемещения анимировались бы дважды. - Отмените обходное решение для касаний.
touch-action: noneна элементе целиком не даёт странице прокручиваться под пальцем; v3 вместо этого начинает драги касанием с долгого нажатия. Оставьте его на ручке, которая должна тащить сразу, сactivation: { touch: { distance: 4 } }. - Уберите устаревшие атрибуты. Библиотека ставит
tabindex,roleи нужные ейaria-*и никогда не перезаписывает ваши, так что оставшиесяroleилиaria-grabbedостанутся такими, какими вы их оставили. - Перестилизуйте оверлей.
.dnd-kit-previewбольше нет: стилизуйте то, что внутри[data-dnd-overlay='dragging'], но никогда неtransformблока. Дайте корню компонента оверлеяwidth: 100%; height: 100%и уберите<DragPreview />из своих лейаутов. - Автоскролл включён. Передайте
:auto-scroll="false"там, где v2 никогда не прокручивала. - Рамке не нужна клавиша.
makeSelectionAreaрисует от простого нажатия на пустом месте. Добавьтеmodifier, чтобы сохранить Ctrl из v2. - Один провайдер, области внутри. Элементы не могут переходить между провайдерами.
Странице, которая использовала провайдер на каждую область, чтобы разделить их правила, нужен
один провайдер и
makeDragAreaна каждую область. - Два аргумента.
makeDraggableиmakeDroppableпринимают ref и объект опций, без данных (payload).