The Jerusalem PostLondon plane lands in Warsaw-Modlin airport after explosive device reported on boardPunchSokoto pledges stronger security partnershipsDaily MaverickADMINISTRATIVE CHAOS: Athletics SA moves rapidly to implement World Athletics leadership overhaul directiveBollywood HungamaKaran Tacker to begin shooting for Bhay: The Gaurav Tiwari Mystery Season 2 in MarchInquirer EntertainmentKatrina Johnson nixes idea of Miss International ‘archetype’SoompiWoo Davi And Kim Hyun Jin’s Upcoming 2026 KBS Drama Special “Sound:Track” Unveils Intriguing First PosterSky TG24Elezioni Midterm 2026, oggi il voto di metà mandato negli Stati Uniti. DIRETTAZDF heuteAktuelle Pressemitteilungen des ZDFسكاي نيوز عربيةضربات أميركية على قشم.. وطهران تقول إن الاتفاق بعيدNumeramaPrès d’un jeu sur deux sorti sur Steam en octobre 2026 a eu recours à l’IA générativeTagesschauLiveblog zu den Midterms: ++ US-Bürger wählen neuen Kongress ++Il Fatto QuotidianoDietro le quinte de “La porta magica” con Andrea Delogu, tra storie, emozioni e confessioni: “Questo programma mi ha salvata”
The Daily Newsstand · Free, Always
Tuesday, November 3, 2026

Vue DnD Kit 3.0: переписал свою dnd-либу для Vue с нуля

Translate

Я уже писал тут про Vue DnD Kit, свою либу для drag and drop во Vue. С тех пор вышла 3.0, и от прошлой версии там мало что осталось: переписал все с нуля. Новое API, анимации из коробки, канбан и деревья без танцев с бубном. Ну и граблей по дороге собрал прилично, про них тоже расскажу.

Что было не так со второй версией

Вторая версия работала, ей пользовались, в issues писали. Но проблема у нее была одна и большая: она давала только низкоуровневые makeDraggable и makeDroppable, а все остальное вы собирали сами.

Нужен обычный сортируемый список? Прокидываешь в каждый элемент индекс и весь массив, ловишь onDrop, зовешь suggestSort, пишешь результат, оборачиваешь все в <TransitionGroup>, чтобы хоть как-то анимировалось. Плейсхолдер рисуешь сам. Канбан это еще больше кода, дерево вообще отдельный квест.

По issues было видно, чего не хватает: анимаций из коробки и нормальных готовых списков, канбана и деревьев.

Аудит

Прежде чем что-то переписывать, я прогнал полный аудит v2: код, доку и примеры, и сверил, что обещает дока и что реально делает код. Вылезло:

  • 57 багов;

  • около 30 мест, где дока и код расходятся

  • 24 фичи, которые v2 умела, а в моих первых планах на v3 их не было (тот же автоскролл в том виде в котором он щас есть). Хорошо, что это всплыло до начала работы.

Из любимого:

  • в доке задержка активации в миллисекундах, а код считал ее в секундах

  • CJS-сборка сломана: .cjs.js в пакете с "type": "module"

  • любой mouseup на странице затирал user-select и touch-action, даже если вы ставили их сами

  • типы жили в рукописных .d.ts с потерянными импортами, и часть из них тихо превращалась в any

  • поиск цели под курсором за O(N²).

Короче, латать это смысла не было, решил переписывать.

Начал с RFC. Главный вопрос был такой: строить v3 поверх @dnd-kit/dom (как раз вышел официальный @dnd-kit/vue) или писать свой движок. Выбрал свой. У официальной обертки нету мульти-драга и выделения рамкой, канбан и деревья собираются руками по примерам, а результат применялся вручную через move(). Мне же хотелось, чтобы все работало из коробки и ощущалось как обычный Vue, а не как обертка над чужими классами.

При чем тут Claude

Большую часть работы я делал в паре с Claude Code, скрывать не буду.

Аудит v2 и разбор того, как устроены dnd-kit и библиотеки виртуализации, делали агенты, из этого и собрался RFC. Решения были за мной: что входит в 3.0, как называются опции, какое поведение по умолчанию. Обычно это выглядело как раунды вопросов: вот варианты, вот плюсы и минусы, выбирай. Код писали агенты, иногда несколько параллельно, дальше тесты, дальше я ручками все тыкал.

ИИ сильно ускоряет, но чего хотят пользователи, он не знает, это все равно решаешь сам. И баги он находит не все, много багов в ядре я нашел сам что в коде, что в примерах.

API стал проще

Сортируемый список во второй версии:

Пример кода
<!-- 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>

И то же самое в третьей:

Пример кода
<!-- 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>

Ни индексов, ни onDrop, ни TransitionGroup. Анимации, клавиатура, автоскролл и озвучка для скринридеров уже внутри.

Вся фишка в черновике. items из makeSortableList это черновик вашего массива: пока тащишь, библиотека переставляет элементы в нем, а ваши данные не трогает. На дропе пишет новый порядок в ваш ref. Если источник readonly (Pinia, данные с сервера), ничего не пишет, а отдает готовый результат:

const { items } = makeSortableList(listRef, {
  id: 'todo',
  group: 'board',
  items: () => store.lists.todo, // геттер, только для чтения
  onDrop: (e) => {
    e.preventDefault();
    for (const [listId, next] of e.result) store.setList(listId, next);
  },
});

Канбан это просто одинаковый group у колонок:

const { items } = makeSortableList(listRef, {
  items: toRef(props.column, 'tasks'),
  group: 'board',
});

Что нового

  • Анимации по умолчанию: соседи разъезжаются, элемент после дропа летит в свой слот, при отмене возвращается назад. Можно выключить или подсунуть свои на GSAP или motion. prefers-reduced-motion учитывается.

  • Копирование из палитры (pull: 'clone'), swap вместо сдвига, корзина, удаление дропом мимо списка.

  • Деревья: вложение сдвигом вбок или наведением на строку, лимит глубины.

  • Мульти-драг: выделяешь Ctrl/Cmd-кликом или рамкой, и все едет одним блоком.

  • Асинхронный onBeforeDrop: можно показать диалог и дождаться ответа. Принять, отклонить или вернуть 'continue', и драг продолжится.

  onBeforeDrop: async (e) => {
    const ok = await confirm(`Перенести ${e.items.length} задач в архив?`);
    return ok ? 'accept' : 'reject';
  },
  • Виртуализация с TanStack Virtual и VueUse, тысячи строк. В доке есть пример таблицы на Nuxt UI с поиском и виртуализацией.

  • Нативный drag and drop: файлы с рабочего стола, ссылки из соседней вкладки и текст из другого приложения работают как обычный драг, цели подсвечиваются, списки раздвигаются. Свои элементы можно вытащить за пределы страницы и даже в другое окно приложения.

  makeDroppable(zone, {
    external: { files: 'image/*' },
    onDrop: (e) => upload(e.external!.files),
  });
  • Клавиатура и скринридеры: пробелом берешь, стрелками двигаешь, каждое действие озвучивается. Тексты по умолчанию английские, можно подставить свои.

  • Никаких компонентов-оберток: элементы ваши, так что компоненты Nuxt UI, Vuetify или любого другого кита таскаются как есть. (Возможно возрожу пакет с компонентами)

  • DevTools: вкладка в Vue DevTools и Nuxt DevTools с живой мини-картой всех списков, логом драгов и правкой опций на лету. И джойстик, которым можно таскать элементы. Зачем? Потому что могу)

  • Модуль для Nuxt и пакет @vue-dnd-kit/actions, чтобы двигать элементы из кода: кнопки "поднять выше", меню, тесты.

А вот разрабатывать стало сложнее

Пользоваться стало проще, а внутри все сильно усложнилось. Ядро сейчас это 108 файлов и около 17 тысяч строк без тестов.

Каждый драг это машина состояний: ожидание, активация, драг, дроп. У каждого выхода из нее (дроп, отмена, ошибка в вашем колбэке, свернутая вкладка, размонтированный провайдер) правило одно: убрать за собой все, что поменял, и отправить ровно одно финальное событие. Легко, пока не появляется мульти-драг между несколькими списками в виртуальном списке с анимациями.

В какой-то момент файл сортировки разросся до двух с лишним тысяч строк, с сорока ветками под разные режимы. Пришлось остановиться и все раскидать: отдельные стратегии под сортировку, swap, копирование и вставку, плюс правило "ни одного файла больше 500 строк". Рефакторинг без единой новой фичи, но без него дальше было не уехать.

Или вот: хотелось, чтобы следующий драг не ждал, пока предыдущий элемент долетит до места. Хватаешь следующую карточку сразу, а ту, что еще летит, можно поймать прямо в полете. Выглядит естественно, а внутри это пачка багов с "прыгающими" элементами при быстрых драгах подряд.

Тесты

  • 628 юнит-тестов в ядре на Vitest, плюс тесты actions, devtools и модуля для Nuxt.

  • Больше 600 e2e на Playwright в Chromium, Firefox и WebKit, плюс отдельный прогон с тачем на эмуляции мобилки.

  • Бюджет размера в CI: если импорт тянет лишний код (условно, простой draggable вдруг подтягивает сортировку), сборка падает.

Пара багов, которые так и нашлись.

В CI на WebKit упал тест автоскролла. Сначала решил, что очередной флакающий тест, а оказалось, реальный баг: если нагруженная машина успевала довести курсор до края контейнера уже к первому кадру драга, автоскролл не включался.

Второй я поймал сам, пока делал пример с таблицей на Nuxt UI. Тащишь строку, держишь курсор ниже последней строки, таблица скроллится, а место вставки стоит на месте. Сначала думал, что это решается модификаторами, но нет, баг ядра. Пофиксил в ядре, а не костылем в примере.

Ну и все примеры я еще руками перетыкал, тесты тестами.

Документация

Доку пересобрал с нуля на Nuxt (Nuxt UI + Nuxt Content):

  • 9 языков, русский тоже есть. Переводит ИИ, и если перевод отстал от английского оригинала, сайт об этом предупреждает.

  • API-справочник генерится из JSDoc типов, а в CI стоит проверка: если у публичной функции нет описания или она не показана ни на одной странице, сборка падает.

  • Больше сотни живых примеров, среди них целые приложения: канбан, календарь, файловый менеджер, блочный редактор, конструктор страниц и шахматы, в которые можно сыграть.

  • Любой пример открывается в StackBlitz в один клик.

  • Дока по v1 и v2 никуда не делась, она в legacy.

Минусы

  1. API сломан, v3 несовместима с v2. Есть гайд по миграции с таблицей старых и новых имен, но переписывать все равно придется. С ^2 в package.json вы остаетесь на второй версии, пока сами не обновитесь.

  2. Размер. Сортируемый список около 49 КБ gzip (с tree-shaking, без Vue), вся библиотека около 57 КБ, голые draggable и droppable около 28 КБ. Не 5 КБ, да. Это цена анимаций, доступности, виртуализации и нативного dnd, но если нужно подвигать один квадратик, будет перебор.

  3. Только свежие браузеры: Chrome и Edge 104+, Safari 15.4+, Firefox 104+.

  4. Мейнтейнер один, это я. Так что issues и PR очень помогают.

Попробовать

npm i @vue-dnd-kit/core

Для Nuxt:

npx nuxi module add @vue-dnd-kit/nuxt

Если пишете с ИИ-ассистентом (интересно кто не пишет), есть скилл, чтобы он знал API v3 и не путал его со второй версией:

npx skills add ZiZIGY/vue-dnd-kit

Ссылки:

Напоследок

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

Попробуете, напишите, что зашло, а что нет: чего не хватает, где неудобно, где сломалось. Ну и звездочка на гитхабе не помешает)

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.