Drag and Drop в JavaScript: Полное руководство от теории до практики

Drag and Drop в JavaScript: Полное руководство от теории до практики

Drag and Drop (перетаскивание) — это не просто эффектный UI-приём, а фундаментальный паттерн взаимодействия, который делает интерфейсы интуитивными и живыми. Реализовать его на чистом JavaScript проще, чем кажется, если понимать логику событий. В этом руководстве мы разберём механизм от A до Z, создадим рабочий пример и рассмотрим тонкости, о которых умалчивают в туториалах.

Как работает Drag and Drop: Механика событий

В основе нативного Drag and Drop API лежит последовательность событий, которые срабатывают как на перетаскиваемом элементе (drag source), так и на зоне сброса (drop target).

События для перетаскиваемого элемента

  1. dragstart — срабатывает в момент начала перетаскивания. Здесь устанавливаются данные.
  2. drag — происходит непрерывно во время движения элемента.
  3. dragend — финальное событие, когда действие завершено (успешно или отменено).

События для зоны сброса

  1. dragenter — когда перетаскиваемый объект входит в границы элемента.
  2. dragover — постоянно срабатывает, пока объект находится над элементом. По умолчанию браузер запрещает сброс, поэтому нужно предотвращать действие по умолчанию.
  3. dragleave — когда объект покидает границы элемента.
  4. drop — ключевое событие! Срабатывает при отпускании кнопки мыши. Здесь обрабатывается «сброс» данных.

Важно: Событие dragover должно содержать event.preventDefault(), чтобы событие drop могло сработать. Без этого «разрешения» зона сброса будет игнорировать попытку отпустить элемент.

Практическая реализация: Создаём доску задач

Давайте создадим простой канбан-борд с двумя колонками: «Задачи» и «В работе».

1. Разметка HTML

Создадим базовую структуру. Атрибут draggable="true" — ключевой для элементов, которые можно перетаскивать.

<div class="board">
  <div class="column" id="todo">
    <h3>Задачи</h3>
    <div class="task" draggable="true" id="task1">Написать код</div>
    <div class="task" draggable="true" id="task2">Протестировать</div>
  </div>
  <div class="column" id="progress">
    <h3>В работе</h3>
  </div>
</div>

2. JavaScript: Логика перетаскивания

Пишем скрипт, который свяжет все события в единую систему.

// Находим все задачи и колонки
const tasks = document.querySelectorAll('.task');
const columns = document.querySelectorAll('.column');

let draggedTask = null;

// События для ПЕРЕТАСКИВАЕМОГО ЭЛЕМЕНТА (задачи)
tasks.forEach(task => {
  task.addEventListener('dragstart', function(e) {
    draggedTask = this;
    this.classList.add('dragging');
    // Устанавливаем данные для передачи (например, ID задачи)
    e.dataTransfer.setData('text/plain', this.id);
    // Указываем допустимый эффект (move, copy, link)
    e.dataTransfer.effectAllowed = 'move';
  });

  task.addEventListener('dragend', function() {
    this.classList.remove('dragging');
    draggedTask = null;
  });
});

// События для ЗОНЫ СБРОСА (колонки)
columns.forEach(column => {
  column.addEventListener('dragover', function(e) {
    e.preventDefault(); // КРИТИЧЕСКИ ВАЖНО!
    e.dataTransfer.dropEffect = 'move';
    this.classList.add('over');
  });

  column.addEventListener('dragleave', function() {
    this.classList.remove('over');
  });

  column.addEventListener('drop', function(e) {
    e.preventDefault();
    this.classList.remove('over');
    
    // Если перетаскиваемый элемент существует и это не та же самая колонка
    if (draggedTask && this !== draggedTask.parentNode) {
      this.appendChild(draggedTask); // Перемещаем задачу в новую колонку
    }
  });
});

Производительность: Событие drag срабатывает очень часто (как mousemove). Не выполняйте в нём тяжёлые операции или манипуляции с DOM. Используйте его только для визуальных подсказок, если это необходимо.

Расширенные возможности и тонкости

Передача данных через dataTransfer

Объект DataTransfer — это мост между dragstart и drop. Вы можете передавать не только текст, но и файлы, ссылки, даже произвольные данные.

// При начале перетаскивания
e.dataTransfer.setData('application/json', JSON.stringify({id: 123, type: 'task'}));

// При сбросе
const rawData = e.dataTransfer.getData('application/json');
if (rawData) {
  const data = JSON.parse(rawData);
  console.log(data.id); // 123
}

Кастомизация внешнего вида

Используя CSS-классы (как .dragging и .over в примере выше), можно создавать сложные визуальные эффекты: полупрозрачный «призрак» элемента, подсветку зоны сброса, анимации.

.dragging {
  opacity: 0.5;
  transform: rotate(5deg);
}

.column.over {
  background-color: #f0f8ff;
  border: 2px dashed #4a90e2;
}

Ограничения и альтернативы

Нативное API имеет свои ограничения: сложный контроль над изображением-«призраком», проблемы с некоторыми мобильными браузерами. Для сложных интерфейсов (например, конструкторов) часто используют библиотеки (Sortable.js, Draggable.js) или реализуют логику на событиях мыши (mousedown, mousemove, mouseup), что даёт полный контроль, но требует больше кода.

FAQ: Часто задаваемые вопросы

Почему не срабатывает событие drop?

В 99% случаев причина в отсутствии event.preventDefault() в обработчике события dragover на целевом элементе. Браузер по умолчанию запрещает сброс на большинстве элементов.

Как передать сложные данные (объект, массив)?

Используйте setData с типом 'application/json' и передавайте строку, полученную через JSON.stringify(). На стороне приёма извлеките данные с помощью getData и JSON.parse().

Drag and Drop работает на мобильных устройствах?

Да, нативное API поддерживается в современных мобильных браузерах, но взаимодействие через touch-события имеет свои нюансы. Для кроссплатформенных решений иногда проще использовать библиотеки, которые абстрагируют эти различия.

Чем отличается effectAllowed от dropEffect?

effectAllowed задаётся в dragstart и определяет, какие операции (move, copy, link) разрешены для этого перетаскивания. dropEffect задаётся в dragover и показывает пользователю, какая операция произойдёт (например, иконка курсора). Они должны быть согласованы.

Можно ли перетаскивать файлы из системы в браузер?

Да! Это одна из самых мощных возможностей API. Если сделать зоной сброса элемент и обработать событие drop, в e.dataTransfer.files будет доступен список файлов. Так работают загрузчики файлов.