Перетаскивание (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
- Начало перетаскивания:
- Процесс начинается, когда пользователь нажимает на элемент и удерживает кнопку мыши.
- Перетаскивание элемента:
- По мере движения мыши элемент следует за курсором по экрану.
- Сброс элемента:
- Элемент освобождается, когда пользователь отпускает кнопку мыши, помещая его на новое место.
Понимание 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>Этот скрипт делает лампочку перетаскиваемой с помощью мыши и реагирует, когда вы сбрасываете её на тёмную область. Вот что делает каждая часть:
- Инициализация: Скрипт получает ссылки на иконку лампочки и тёмную область, а также объявляет переменные для отслеживания перетаскивания (
active, начальная позиция мыши, текущее смещение). - Начало перетаскивания (
mousedown): При нажатии кнопки мыши на лампочке скрипт записывает начальную позицию курсора (с учётом смещения от предыдущего перетаскивания) и устанавливаетactive = true. Смещение важно — без него иконка каждый раз возвращалась бы к исходной точке при начале нового перетаскивания. - Перемещение (
mousemove): Покаactiveравноtrue, скрипт вычисляет, насколько переместился курсор, и применяет это как трансформациюtranslate3d, чтобы иконка следовала за указателем. Еслиactiveравноfalse, обработчик немедленно выходит и ничего не делает. - Сброс (
mouseup): При отпускании кнопки скрипт сохраняет финальную позицию, устанавливаетactive = falseи вызываетisInside, чтобы определить, попала ли лампочка в тёмную область. Если да — и область, и лампочка становятся жёлтыми; иначе они сбрасываются к своим цветам по умолчанию. - Проверка попадания (
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>Ключевые события мыши:
mousedown: Это событие срабатывает, когда пользователь нажимает кнопку мыши на иконке лампочки. Оно отмечает начало перетаскивания и записывает начальную позицию курсора.mousemove: Это событие срабатывает при движении мыши. Если перетаскивание активно (то есть кнопка мыши ещё нажата), оно вычисляет новую позицию иконки на основе движения курсора и обновляет положение лампочки на экране.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 — для переноса элементов между зонами. Какой бы способ вы ни выбрали, обязательно предусмотрите клавиатурную альтернативу, чтобы любой пользователь мог выполнить ту же задачу.