W3docs

Перетаскивание (Drag and Drop) в JavaScript

Изучите drag-and-drop в JavaScript двумя способами: через события мыши и нативный HTML5 Drag and Drop API с draggable, dragover, drop и DataTransfer.

В этом руководстве мы рассмотрим функциональность перетаскивания (drag-and-drop) в JavaScript — мощную возможность, которая повышает интерактивность веб-страниц. Существуют два способа её реализации: подход через события мыши (mousedown/mousemove/mouseup), дающий полный контроль над перемещением, и нативный HTML5 Drag and Drop API (draggable + события drag*), встроенный в браузер. Мы рассмотрим оба способа, выясним, где каждый из них уместен, а затем создадим практический пример, в котором иконка лампочки освещает тёмную область при перетаскивании в неё.

Что такое Drag-and-Drop?

Drag-and-drop — это взаимодействие в пользовательском интерфейсе, позволяющее пользователям захватывать объект и перемещать его в другое место на экране. Этот паттерн встречается повсюду: перетаскивание файлов в операционной системе, перестановка элементов в игре, загрузка фотографий путём их сброса на страницу или изменение порядка задач в списке. В схеме всегда присутствуют три роли:

  • Перетаскиваемый элемент (draggable) — элемент, который пользователь берёт.
  • Цель сброса (drop target или droppable) — область, которая может его принять.
  • Полезная нагрузка (payload) — необязательные данные, передаваемые от источника к цели (например, id перемещаемого элемента).

Выбор подхода

Оба метода допустимы; выбирайте тот, который соответствует вашим задачам.

  • События мыши (mousedown, mousemove, mouseup) — вы вручную отслеживаете указатель и перемещаете элемент самостоятельно. Используйте этот подход, когда элемент должен следовать за курсором попиксельно (слайдеры, свободные холсты, инструменты рисования, пользовательская физика). Этот же подход расширяется для сенсорных экранов с помощью touchstart/touchmove/touchend.
  • Нативный HTML5 Drag and Drop (draggable="true" + dragstart/dragover/drop) — браузер сам управляет «призрачным» изображением и жестом перетаскивания, предоставляя объект DataTransfer для передачи данных. Используйте этот подход для передачи чего-либо между зонами (загрузка файлов, изменение порядка списков, сброс элементов в корзину). На сенсорных устройствах он не работает из коробки.

В основном примере мы продемонстрируем подход через события мыши, а затем кратко опишем нативный API.

Ключевые концепции Drag-and-Drop в JavaScript

Алгоритм Drag'n'Drop

  1. Начало перетаскивания:
    • Процесс начинается, когда пользователь нажимает на элемент и удерживает кнопку мыши.
  2. Перетаскивание элемента:
    • По мере движения мыши элемент следует за курсором по экрану.
  3. Сброс элемента:
    • Элемент освобождается, когда пользователь отпускает кнопку мыши, помещая его на новое место.

Понимание droppable-зон

Droppable-зоны — это области, предназначенные для приёма перетаскиваемых элементов. Они определяют, когда перетаскиваемый объект находится над ними, и могут запускать определённые действия в ответ на это.

Информация

Убедитесь, что функциональность drag-and-drop поддерживает сенсорное управление. Пользователи мобильных устройств должны иметь возможность перетаскивать элементы с помощью жестов касания. Рассмотрите реализацию событий касания (touchstart, touchmove, touchend) или использование лёгкой библиотеки для кросс-устройственной совместимости.

Интерактивный пример: светлая и тёмная область

Давайте применим теорию на практике с помощью простого, но интерактивного примера. В качестве перетаскиваемого объекта мы используем иконку лампочки. Когда эта иконка перемещается над тёмной областью, область освещается, имитируя эффект включения света.

Настройка HTML и CSS

Сначала определим базовую структуру и стили. Мы включаем тёмный блок и иконку лампочки.

Структура HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Lighting with Drag and Drop</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" />
<style>
  #darkArea {
    width: 300px;
    height: 300px;
    background-color: #333;
    position: relative;
    margin-top: 20px;
  }
  #lightIcon {
    font-size: 48px;
    color: #ccc;
    cursor: pointer;
    position: absolute;
  }
</style>
</head>
<body>
<div id="main">
  <div id="darkArea"></div>
  <i id="lightIcon" class="fas fa-lightbulb"></i>
</div>

<script>
// JavaScript will be added here.
</script>
</body>
</html>

Реализация JavaScript

Теперь добавим функциональность, чтобы сделать лампочку перетаскиваемой и реагирующей на тёмную область.

Пояснение к коду JavaScript

<script>
  // Get references to the light bulb icon and the dark area on the webpage
  var lightIcon = document.getElementById("lightIcon");
  var darkArea = document.getElementById("darkArea");

  // Variables to track whether the dragging is active and to store position data
  var active = false;
  var initialX, initialY, currentX, currentY, xOffset = 0, yOffset = 0;

  // Listen for the mouse down event on the light bulb icon
  lightIcon.addEventListener("mousedown", function(e) {
    // Record the starting position of the mouse and adjust by any existing offset
    initialX = e.clientX - xOffset;
    initialY = e.clientY - yOffset;
    // Set the active flag to true, indicating that dragging has started
    active = true;
  });

  // Listen for mouse movement across the entire document
  document.addEventListener("mousemove", function(e) {
    // If not dragging, don't do anything
    if (!active) return;
    // Calculate the new position of the mouse
    currentX = e.clientX - initialX;
    currentY = e.clientY - initialY;
    // Update the offset with the new position
    xOffset = currentX;
    yOffset = currentY;
    // Move the light bulb icon to the new position
    lightIcon.style.transform = "translate3d(" + currentX + "px, " + currentY + "px, 0)";
  });

  // Listen for the mouse up event across the entire document
  document.addEventListener("mouseup", function() {
    // Save the final position of the light bulb
    initialX = currentX;
    initialY = currentY;
    // Set the active flag to false, indicating dragging has ended
    active = false;
    // Check if the light bulb is inside the dark area
    if (isInside(darkArea, lightIcon)) {
      // Change the background color of the dark area to yellow
      darkArea.style.backgroundColor = "yellow";
      // Change the color of the light bulb to yellow
      lightIcon.style.color = "yellow";
    } else {
      // Revert the dark area's color to dark
      darkArea.style.backgroundColor = "#333";
      // Revert the light bulb's color to gray
      lightIcon.style.color = "#ccc";
    }
  });

  // Function to check if the light bulb is inside the dark area
  function isInside(container, element) {
    // Get the position of the container and the element
    var containerRect = container.getBoundingClientRect();
    var elementRect = element.getBoundingClientRect();
    // Return true if the element is within the container's boundaries
    return (
      elementRect.left >= containerRect.left &&
      elementRect.right <= containerRect.right &&
      elementRect.top >= containerRect.top &&
      elementRect.bottom <= containerRect.bottom
    );
  }
</script>

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

  1. Инициализация: Скрипт получает ссылки на иконку лампочки и тёмную область, а также объявляет переменные для отслеживания перетаскивания (active, начальная позиция мыши, текущее смещение).
  2. Начало перетаскивания (mousedown): При нажатии кнопки мыши на лампочке скрипт записывает начальную позицию курсора (с учётом смещения от предыдущего перетаскивания) и устанавливает active = true. Смещение важно — без него иконка каждый раз возвращалась бы к исходной точке при начале нового перетаскивания.
  3. Перемещение (mousemove): Пока active равно true, скрипт вычисляет, насколько переместился курсор, и применяет это как трансформацию translate3d, чтобы иконка следовала за указателем. Если active равно false, обработчик немедленно выходит и ничего не делает.
  4. Сброс (mouseup): При отпускании кнопки скрипт сохраняет финальную позицию, устанавливает active = false и вызывает isInside, чтобы определить, попала ли лампочка в тёмную область. Если да — и область, и лампочка становятся жёлтыми; иначе они сбрасываются к своим цветам по умолчанию.
  5. Проверка попадания (isInside): Этот вспомогательный метод сравнивает ограничивающие прямоугольники двух элементов с помощью getBoundingClientRect() и возвращает true только тогда, когда лампочка полностью находится внутри границ тёмной области.
Информация

Используйте translate3d в CSS-трансформациях для перетаскивания элементов. Это задействует аппаратное ускорение GPU, обеспечивая более плавное движение и снижая нагрузку на CPU, что критически важно для ресурсоёмких приложений.

Полный пример

Теперь посмотрим на всё в действии:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Interactive Lighting with Drag and Drop</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" />
    <style>
      #darkArea {
        width: 300px;
        height: 300px;
        background-color: #333;
        position: relative;
        margin-top: 20px;
      }
      #lightIcon {
        font-size: 48px;
        color: #ccc;
        cursor: pointer;
        position: absolute;
      }
    </style>
  </head>
  <body>
    <div id="main">
     <p>Move the light into the dark area to light it up!</p>
      <div id="darkArea"></div>
      <i id="lightIcon" class="fas fa-lightbulb"></i>
    </div>

    <script>
      // Get references to the light bulb icon and the dark area on the webpage
      var lightIcon = document.getElementById("lightIcon");
      var darkArea = document.getElementById("darkArea");

      // Variables to track whether the dragging is active and to store position data
      var active = false;
      var initialX,
        initialY,
        currentX,
        currentY,
        xOffset = 0,
        yOffset = 0;

      // Listen for the mouse down event on the light bulb icon
      lightIcon.addEventListener("mousedown", function (e) {
        // Record the starting position of the mouse and adjust by any existing offset
        initialX = e.clientX - xOffset;
        initialY = e.clientY - yOffset;
        // Set the active flag to true, indicating that dragging has started
        active = true;
      });

      // Listen for mouse movement across the entire document
      document.addEventListener("mousemove", function (e) {
        // If not dragging, don't do anything
        if (!active) return;
        // Calculate the new position of the mouse
        currentX = e.clientX - initialX;
        currentY = e.clientY - initialY;
        // Update the offset with the new position
        xOffset = currentX;
        yOffset = currentY;
        // Move the light bulb icon to the new position
        lightIcon.style.transform = "translate3d(" + currentX + "px, " + currentY + "px, 0)";
      });

      // Listen for the mouse up event across the entire document
      document.addEventListener("mouseup", function () {
        // Save the final position of the light bulb
        initialX = currentX;
        initialY = currentY;
        // Set the active flag to false, indicating dragging has ended
        active = false;
        // Check if the light bulb is inside the dark area
        if (isInside(darkArea, lightIcon)) {
          // Change the background color of the dark area to yellow
          darkArea.style.backgroundColor = "yellow";
          lightIcon.style.color = "yellow";
        } else {
          // Revert the dark area's color to dark
          darkArea.style.backgroundColor = "#333";
          // Revert the light bulb's color to gray
          lightIcon.style.color = "#ccc";
        }
      });

      // Function to check if the light bulb is inside the dark area
      function isInside(container, element) {
        // Get the position of the container and the element
        var containerRect = container.getBoundingClientRect();
        var elementRect = element.getBoundingClientRect();
        // Return true if the element is within the container's boundaries
        return elementRect.left >= containerRect.left && elementRect.right <= containerRect.right && elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
      }
    </script>
  </body>
</html>

Ключевые события мыши:

  1. mousedown: Это событие срабатывает, когда пользователь нажимает кнопку мыши на иконке лампочки. Оно отмечает начало перетаскивания и записывает начальную позицию курсора.
  2. mousemove: Это событие срабатывает при движении мыши. Если перетаскивание активно (то есть кнопка мыши ещё нажата), оно вычисляет новую позицию иконки на основе движения курсора и обновляет положение лампочки на экране.
  3. mouseup: Это событие происходит, когда пользователь отпускает кнопку мыши, отмечая конец перетаскивания. Оно проверяет, находится ли лампочка в границах тёмной области, и решает, нужно ли менять цвет фона этой области.

Как мы изучили в статье о событиях мыши, эти события являются основой создания интерактивной функциональности drag-and-drop, позволяя динамически перемещать элементы на веб-странице с помощью мыши. (Если вам нужно точно позиционировать элемент, в главе о координатах JavaScript объясняется разница между clientX/clientY, pageX/pageY и getBoundingClientRect().)

Нативный HTML5 Drag and Drop API

Метод на основе событий мыши отлично подходит, когда нужно, чтобы элемент точно следовал за курсором. Но когда настоящая цель — передать что-либо — перетащить карточку в колонку, товар в корзину, файл в зону загрузки — встроенный в браузер Drag and Drop API требует меньше кода и сам управляет жестом перетаскивания.

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

Любой элемент становится перетаскиваемым при установке атрибута draggable в значение true. Ссылки и изображения перетаскиваемы по умолчанию; всё остальное — нет.

<div id="item" draggable="true">Drag me</div>
<div id="dropzone">Drop here</div>

События Drag and Drop

API генерирует последовательность событий. Наиболее важные из них:

СобытиеСрабатывает наКогда
dragstartперетаскиваемом элементев момент начала перетаскивания
dragoverцели сбросанепрерывно, пока перетаскиваемый элемент находится над ней
dropцели сбросакогда пользователь отпускает над ней
dragendперетаскиваемом элементекогда перетаскивание завершается (сброс или отмена)

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

Каждое событие перетаскивания предоставляет event.dataTransfer — объект для прикрепления полезной нагрузки в dragstart и её получения в drop.

const item = document.getElementById("item");
const zone = document.getElementById("dropzone");

// 1. Attach a payload when the drag starts.
item.addEventListener("dragstart", (e) => {
  e.dataTransfer.setData("text/plain", item.id);
});

// 2. By default elements are NOT valid drop targets.
//    Prevent the default to allow a drop.
zone.addEventListener("dragover", (e) => {
  e.preventDefault();
});

// 3. Read the payload and move the element on drop.
zone.addEventListener("drop", (e) => {
  e.preventDefault();
  const id = e.dataTransfer.getData("text/plain");
  zone.appendChild(document.getElementById(id));
});
Внимание

Самая распространённая ошибка при работе с нативным API — забыть вызвать e.preventDefault() внутри обработчика dragover. Без этого браузер отклоняет элемент как цель сброса, и событие drop никогда не срабатывает. Данные, установленные через setData, становятся доступны только после срабатывания drop — по соображениям безопасности они не могут быть прочитаны во время dragover.

Пара setData(format, value) / getData(format) позволяет передавать простой текст, URL (text/uri-list), HTML или собственные строковые ключи. Для загрузки файлов читайте e.dataTransfer.files, который является FileList, как и у <input type="file">.

Сравнение событий мыши и нативного API

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

Чтобы глубже изучить базовую систему событий, обратитесь к статье Введение в события браузера, а для обеспечения доступности помните, что drag-and-drop всегда должен иметь клавиатурную альтернативу — см. Доступность DOM.

Заключение

Drag-and-drop делает интерфейсы прямыми и интуитивными. Теперь у вас есть два инструмента для его реализации: подход через события мыши, который перемещает элемент попиксельно под полным ручным управлением, и нативный HTML5 Drag and Drop API, позволяющий браузеру управлять жестом, пока вы передаёте данные через DataTransfer. Выбирайте события мыши для свободного перемещения и поддержки сенсорных экранов, а нативный API — для переноса элементов между зонами. Какой бы способ вы ни выбрали, обязательно предусмотрите клавиатурную альтернативу, чтобы любой пользователь мог выполнить ту же задачу.

Практика

Практика
Какие утверждения о функциональности drag and drop в JavaScript верны?
Какие утверждения о функциональности drag and drop в JavaScript верны?
Was this page helpful?