Drag and Drop API

Drag and Drop API — нативный браузерный API для реализации перетаскивания элементов между зонами на странице или из файловой системы в браузер, основанный на событиях dragstart, dragover, drop и объекте DataTransfer.

Зачем нужно

Нативный Drag and Drop API доступен без сторонних библиотек и обеспечивает встроенную поддержку перетаскивания файлов из ОС в браузер. Понимание его событийной модели и объекта DataTransfer необходимо для реализации Kanban-досок, сортируемых списков, загрузки файлов через drag, а также для работы со сторонними drag-библиотеками (react-dnd, dnd-kit), которые строятся поверх него.

Где используется

  • Kanban-доски: перемещение карточек между колонками
  • Файловая загрузка: drag файлов из Explorer/Finder в зону загрузки
  • Сортируемые списки: изменение порядка элементов
  • Конструкторы интерфейсов: перетаскивание виджетов на холст
  • Ячейки таблиц: перемещение строк

Основной контент

События Drag and Drop

Исходный элемент:  dragstart → drag → dragend
Целевой элемент:   dragenter → dragover → dragleave/drop

Базовый пример: перемещение карточки

<div class="column" id="todo">
  <div class="card" draggable="true" data-id="1">Задача 1</div>
  <div class="card" draggable="true" data-id="2">Задача 2</div>
</div>
<div class="column" id="done"></div>
let draggedElement = null;

// На перетаскиваемых элементах
document.querySelectorAll('.card').forEach(card => {
  card.addEventListener('dragstart', (e) => {
    draggedElement = card;
    e.dataTransfer.effectAllowed = 'move';
    // Передаём данные — доступны в зоне drop
    e.dataTransfer.setData('text/plain', card.dataset.id);
    card.classList.add('dragging');
  });

  card.addEventListener('dragend', () => {
    card.classList.remove('dragging');
    draggedElement = null;
  });
});

// На зонах-целях
document.querySelectorAll('.column').forEach(column => {
  // dragover нужен для разрешения drop — без preventDefault drop не сработает!
  column.addEventListener('dragover', (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    column.classList.add('drag-over');
  });

  column.addEventListener('dragleave', () => {
    column.classList.remove('drag-over');
  });

  column.addEventListener('drop', (e) => {
    e.preventDefault();
    column.classList.remove('drag-over');
    const id = e.dataTransfer.getData('text/plain');
    const card = document.querySelector(`[data-id="${id}"]`);
    if (card && card !== column) {
      column.appendChild(card);
    }
  });
});

Загрузка файлов через drag

const dropZone = document.getElementById('drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // разрешаем drop
  e.dataTransfer.dropEffect = 'copy';
  dropZone.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('drag-over');
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropZone.classList.remove('drag-over');

  const files = [...e.dataTransfer.files]; // FileList → Array

  files.forEach(async (file) => {
    console.log('Файл:', file.name, file.type, file.size);

    if (file.type.startsWith('image/')) {
      const url = URL.createObjectURL(file);
      const img = document.createElement('img');
      img.src = url;
      dropZone.appendChild(img);
    }

    // Загрузка на сервер
    const formData = new FormData();
    formData.append('file', file);
    await fetch('/api/upload', { method: 'POST', body: formData });
  });
});

DataTransfer — передача данных

// dragstart — записываем данные
e.dataTransfer.setData('text/plain', 'Простая строка');
e.dataTransfer.setData('application/json', JSON.stringify({ id: 1, type: 'card' }));

// drop — читаем данные
const text = e.dataTransfer.getData('text/plain');
const data = JSON.parse(e.dataTransfer.getData('application/json'));

// Типы MIME доступных данных
console.log([...e.dataTransfer.types]); // ['text/plain', 'application/json', ...]

// Файлы (только при перетаскивании из ОС)
const files = [...e.dataTransfer.files];

// Эффект перетаскивания (визуальный курсор)
e.dataTransfer.effectAllowed = 'move'; // или 'copy', 'link', 'copyMove', 'all'
e.dataTransfer.dropEffect = 'move';    // в dragover

Частые ошибки

  • Забыть e.preventDefault() в dragover: без него drop не сработает — браузер блокирует drop по умолчанию.
  • draggable="true" у всех вложенных элементов: атрибут нужен только на корневом перетаскиваемом элементе; у дочерних он создаёт конфликты.
  • DataTransfer вне обработчика событий: объект e.dataTransfer доступен только синхронно внутри обработчика drop — асинхронное чтение через await даст пустые данные.
  • iOS не поддерживает нативный D&D: для мобильных устройств нужны touch-события или специальные библиотеки (dnd-kit поддерживает touch).

Связанные темы

Ресурсы