Документация/Начало работы

Переход с 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
Пакетv2v3
@vue-dnd-kit/core2.4.x3.x
@vue-dnd-kit/nuxt1.x3.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 по-прежнему доступна в меню версий вверху страницы.

Хороший порядок работы:

  1. Обновите пакеты и переименуйте пропсы провайдера (ниже).
  2. Превратите каждый сортируемый список в makeSortableList и makeSortableItem и удалите его обработчик броска.
  3. Перенесите обычные перетаскиваемые элементы и зоны сброса.
  4. Перенесите свои превью в компоненты оверлея или в слот #overlay провайдера.
  5. Замените makeConstraintArea и makeAutoScroll на makeDragArea или ни на что.
  6. Удалите обходные решения v2: tabindex и aria-grabbed на перетаскиваемых элементах, touch-action: none на элементах целиком, <TransitionGroup> вокруг строк, markRaw вокруг компонентов превью.

Старое и новое

Нет аналога в таблице означает, что в v3 нет ничего с таким именем; в ячейке сказано, что использовать вместо этого.

Вкратце

v2v3
DnDProviderDnDProvider, с новыми пропсами и слотом #overlay.
makeDraggablemakeDraggable для свободного перетаскивания; makeSortableItem для строк списка.
makeDroppablemakeDroppable для обычных целей; makeSortableList или makeSortableTree для списков и деревьев.
makeSelectionAreamakeSelectionArea.
makeConstraintAreamakeDragArea.
makeAutoScrollНичего: автоскролл работает сам. Его настраивает makeDragArea или autoScroll списка.
useDnDProvideruseDnD для состояния, useDnDMonitor для событий.
injectionKeyuseDnDContext() и provideDnDContext(app, context).
DragPreviewDragPreview: теперь это клон одного перетаскиваемого элемента, для ваших собственных оверлеев.
makeSnappedOverlayPosition, <DragPreview :grid>Модификатор snapToGrid.
e.helpers.suggestSort, suggestSwap, suggestCopy, suggestRemoveВстроены в списки: см. сортировку.

Провайдер

v2v3
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' или nulluseDnD().status: 'idle', 'activating', 'dragging', 'dropping' или 'selecting'. 'pending' (асинхронный бросок) — это 'dropping'; null — это 'idle'.
pointer.value.currentuseDnD().pointer, после модификаторов. У каждого события есть ещё delta — смещение от начала.
entities.draggingMapuseDnD().active: id, ids, items, data, type, element, rect.
entities.selectedSet (элементы)useDnD().selection (id): has, add, delete, toggle, set, clear, ids.
hovered.droppable, hovered.draggableuseDnD().over: id, data, element, rect, accepted.
delay.progress, distanceProgressuseDnD().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

v2v3
makeDraggable(ref, options, payload)makeDraggable(ref, options): без третьего аргумента. Строки списка используют makeSortableItem(ref, { item }).
id, нужен в виртуальных спискахid: передавайте его из своих данных, геттером. Значение по умолчанию берётся из useId() и новое для каждого экземпляра компонента.
data: () => carddata: значение, ref или геттер. Читается обратно как active.data и active.items[i].data.
groups: ['dev']type: 'dev', с которым сверяется accept целей. См. группы.
dragHandle: '.handle'handle: селектор или ref элемента.
disableddisabled.
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

v2v3
makeDroppable(ref, options, () => items)makeDroppable(ref, options). Зона, в которой лежит список, — это makeSortableList.
groupsaccept: '*', тип, список типов или функция.
groupMatch: 'every', 'some'acceptMode: 'all', 'some'.
events.onEnter, events.onLeaveonDragEnter, onDragLeave: самостоятельные опции, без объекта events.
events.onDrop, который мог вернуть false или промисonBeforeDrop решает, onDrop применяет. См. решения о броске.
events.onValidateonBeforeDrop, возвращающий id, которые нужно оставить.
datadata, читается обратно как over.data.
disableddisabled.
Возвращает isAllowedisAccepting.
Возвращает isDragOver: { top, right, bottom, left, center }isOver (boolean) и placement: { top, right, bottom, left } — половины, в которых находится указатель. center нет.
—Новое: id, label, priority, overOverlay, overDropAnimation, external (файлы с компьютера).

Событие броска

v2 IDragEventv3
e.draggedItemse.active.items: у каждого { id, item, data, label, type }, взятый элемент первым. Списки получают e.items и e.keys.
e.draggedItems[0].datae.active.data.
e.draggedItems[i].index, .itemsСписки: e.from, { list, index }.
e.dropZone: items, placement, datae.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.sameListe.sameList.
e.helpers.insertAt, removeAt, removeIndexes, swapAt, getInsertSide, isAtZoneStart, isAtZoneEndНет аналога. Спискам они не нужны; у обычной цели меняйте данные как хотите.
e.provideruseDnD().
—Новое: e.pointer, e.delta, e.position (куда поставить элемент, брошенный на доску), e.mode.

Сортировка

v2v3
Список на makeDroppable с () => itemsmakeSortableList(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, .bottomplaceholder: '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. См. управляемые списки.
Рекурсивное дерево: зона сброса на каждую папку и applyToTreemakeSortableTree: один контейнер, строки рендерятся плоско со своей глубиной. См. деревья.
Вложенные группы с v-model (зона сброса на каждую группу)Список внутри списка, все в одной group, у каждого items: defineModel().
Виртуальные списки: id на каждом перетаскиваемом элементеПередайте черновик виртуализатору, id: () => props.item.id на строках. См. виртуальные списки.

Код до и после — ниже. Всё о списках — в сортируемых списках и списках с общими элементами.

Группы

В v2 была одна опция groups на всё. v3 разделяет её на две:

v2v3
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 у makeSelectionAreaaccept.

Список без group — сам себе группа и не обменивается элементами ни с каким другим списком. В v2 зона без групп принимала всё. См. какие драги принимает цель.

Оверлей

v2v3
Превью, отрендеренное в провайдере и телепортированное через previewToВсегда телепортируется: в body или в overlayTo.
Клон: тот же тег, его innerHTML и его размерГлубокий клон: значения полей форм, пиксели <canvas>, унаследованные стили и ячейки таблиц переносятся. То, что должно остаться, помечайте data-dnd-no-clone.
render: markRaw(Preview), который читает useDnDProvider().entitiesoverlay: Preview, который получает items, active, over, accepted, status, mode и to как пропсы. Без markRaw; чтобы переключать его, используйте shallowRef.
Слот #preview, позиционируемый по preview.positionСлот #overlay: библиотека сама размещает блок и задаёт ему размер. Если ничего не рендерить, используется клон.
useDnDProvider().preview.render.value = …Слот #overlay или shallowRef, переданный как overlay.
Новый вид над зоной, из onEnter и onLeaveoverOverlay у makeDroppable или списка, либо читайте over в компоненте оверлея. См. цели, которые меняют оверлей.
<DragPreview v-slot="{ draggingMap }">items в пропсах оверлея; <DragPreview :id="item.id" /> рендерит один клон.
<DragPreview :grid>, makeSnappedOverlayPositionМодификатор snapToGrid. Он привязывает к сетке и место, где ищутся цели, и position броска, а не только картинку.
<Transition> или AnimatePresence из motion вокруг DragPreviewoverlay-class с @starting-style для захвата, dropAnimation для приземления, overlayTransition между компонентами. См. подъём, наклон, посадку.
.dnd-kit-preview[data-dnd-overlay] ("dragging", "dropping") на блоке, [data-dnd-overlay-content] внутри него.
preview.size, который обновляли вручнуюБлок принимает размер слота, на который приземлится (overlayFit).
Превью исчезает при броскеОно приземляется на свой слот или летит обратно при отмене.

Подробнее — в руководстве по оверлею.

Выделение и мульти-драг

v2v3
selected, возвращаемый makeDraggableselected, возвращаемый makeDraggable и makeSortableItem.
entities.selectedSet, набор элементовuseDnD().selection, по id.
makeSelectionArea: по умолчанию нужно держать Ctrl (ControlLeft)По умолчанию никакой клавиши: нажатие на пустом месте области рисует рамку. modifier: { keys: ['Control'], method: 'every' } возвращает Ctrl.
modifier.keys как KeyboardEvent.codeKeyboardEvent.key ('Control', 'Shift') или .code ('ControlLeft').
strategy: 'toggle', 'select'То же самое.
groupsaccept.
events.onSelected(elements)onSelected(ids).
disabledТо же самое.
suggestSort(), перемещающий выделение пачкойЕго перемещают списки. multiple: 'group' (по умолчанию), 'gaps' или 'stack' говорит, что по пути делают остальные элементы.
onValidate для смешанных выделенийacceptMode: 'some' и onBeforeDrop, возвращающий id. См. принять только часть.

Подробнее — в мульти-драге и выделении.

Оси, границы и привязка

v2v3
makeConstraintArea(el, { axis: 'x' })makeDragArea(el, { axis: 'x' }), для драгов, которые начинаются внутри неё. axis работает и у перетаскиваемого элемента, списка, дерева или элемента списка.
axis: 'both'axis не задан.
restrictToArea: truemakeDragArea(el, { restrictToArea: true }) или модификаторы restrictToParent и restrictToElement(el).
Провайдер на каждую область, чтобы разделить правилаОдин провайдер и makeDragArea на каждую область. Её элементы по-прежнему могут перемещаться куда угодно.
<DragPreview :grid="20">, makeSnappedOverlayPositionsnapToGrid(20) или snapToGrid({ x, y, origin, smooth }), в modifiers у провайдера, области, списка или перетаскиваемого элемента.
—Новое: restrictToWindowEdges, restrictToFirstScrollableAncestor, snapCenterToCursor и ваши собственные модификаторы.

Подробнее — в осях, границах и сетках.

Автоскролл

v2v3
<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: trueautoScroll: false.
isScrollingisAutoScrolling, из makeDragArea или useDnD().

Клавиатура и доступность

v2v3
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 меняют формулировки.

Подробнее — в доступности.

События и решения о броске

v2v3
events.onSelfDragStart перетаскиваемого элементаuseDnDMonitor({ onDragStart }), с проверкой active.ids. Для класса достаточно isDragging.
onSelfDragMoveonDragMove монитора.
onSelfDragEnd, при броске или отменеonDragEnd монитора (бросок) и onDragCancel. onDragSettle слышит оба, когда оверлей приземлился.
onSelfDragCancelonDragCancel монитора.
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 событий.

Типы

v2v3
IDragEventDnDEvent (цели, мониторы), SortableDropEvent (списки), DnDEndEvent (onDragEnd).
IDragItemDragItem.
IDropZoneContextOver.
IHoveredDraggableContextНет аналога.
ISuggestSortResult, ISuggestSwapResult, ISuggestCopyResult, ISuggestRemoveResultSortableDropEvent: result, to, mode, partner.
IDropValidateEventDnDEvent, в onBeforeDrop.
IPlacementPlacement, без center.
ICoordinatesCoordinates.
IDragActivationOptionsActivationOptions.
IAutoScrollOptionsAutoScrollOptions.
TDnDStateDnDStatus.
IDnDProviderExternalDnDState — то, что возвращает 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 2Peer-зависимость @vue-dnd-kit/core 3; Nuxt 4.

modules: ['@vue-dnd-kit/nuxt'] в nuxt.config.ts остаётся как было. См. установку.

До и после

Перетаскиваемый элемент и зона сброса

v2

Card.vue
<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>
Zone.vue
<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

Card.vue
<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>
Zone.vue
<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

TaskList.vue
<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>
TaskItem.vue
<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

TaskList.vue
<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>
TaskItem.vue
<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

Board.vue
// 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

Column.vue
<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>
Board.vue
<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; теперь это список:

Palette.vue
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

TaskItem.vue
makeDraggable(el, {
  render: markRaw(TaskOverlay),
  data: () => props.task,
});
TaskOverlay.vue
<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

TaskItem.vue
makeDraggable(el, {
  id: () => props.task.id,
  overlay: TaskOverlay,
  data: () => props.task,
});
TaskOverlay.vue
<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

App.vue
<DnDProvider>
  <RouterView />
  <template #preview>
    <MyOverlay />  <!-- moved itself with useDnDProvider().preview.position -->
  </template>
</DnDProvider>

v3

App.vue
<DnDProvider>
  <RouterView />
  <template #overlay="{ items, over, accepted }">
    <MyOverlay :items="items" :over="over" :accepted="accepted" />
  </template>
</DnDProvider>

Появление, которое оборачивало DragPreview в <Transition>, становится overlay-class с @starting-style: см. подъём, наклон, посадку.

Бросок, который вы подтверждаете

v2

Zone.vue
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

Zone.vue
makeDroppable(el, {
  onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
  onDrop: (e) => place(e.active.items),
});

Список спрашивает так же, а промис позволяет открыть собственный диалог. Оверлей ждёт там, где его отпустили, пока промис не завершится:

Column.vue
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

Zone.vue
makeDroppable(el, {
  groups: ['frontend'],
  groupMatch: 'some',
  events: {
    onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
  },
});

v3

Zone.vue
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: список записывает каждый бросок в записываемый ref items после своего onDrop. Чтобы применить его самим, вызовите e.preventDefault() и используйте e.result.
  • false отклоняет бросок. В v2 onDrop, разрешившийся в 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).