Drag’n’Drop – это возможность захватить мышью элемент и перенести его. В своё время это было замечательным открытием в области интерфейсов, которое позволило упростить большое количество операций.
Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через Drag’n’Drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п.
Отличия от HTML5 Drag’n’Drop
В современном стандарте HTML5 есть поддержка Drag’n’Drop при помощи специальных событий.
Эти события поддерживаются всеми современными браузерами, и у них есть свои интересные особенности, например, можно перетащить файл в браузер, так что JS получит доступ к его содержимому. Они заслуживают отдельного рассмотрения.
Но в плане именно Drag’n’Drop у них есть существенные ограничения. Например, нельзя организовать перенос «только по горизонтали» или «только по вертикали». Также нельзя ограничить перенос внутри заданной зоны. Есть и другие интерфейсные задачи, которые такими встроенными событиями нереализуемы.
Поэтому здесь мы будем рассматривать Drag’n’Drop при помощи событий мыши.
Рассматриваемые приёмы, вообще говоря, применяются не только в Drag’n’Drop, но и для любых интерфейсных взаимодействий вида «захватить – потянуть – отпустить».
Алгоритм Drag’n’Drop
Основной алгоритм Drag’n’Drop выглядит так:
- Отслеживаем нажатие кнопки мыши на переносимом элементе при помощи события
mousedown
. - При нажатии – подготовить элемент к перемещению.
- Далее отслеживаем движение мыши через
mousemove
и передвигаем переносимый элемент на новые координаты путём сменыleft/top
иposition:absolute
. - При отпускании кнопки мыши, то есть наступлении события
mouseup
– остановить перенос элемента и произвести все действия, связанные с окончанием Drag’n’Drop.
В следующем примере эти шаги реализованы для переноса мяча:
var
ball =
document.
getElementById
(
'ball'
)
;
ball.
onmousedown
=
function
(
e
)
{
// 1. отследить нажатие
// подготовить к перемещению
// 2. разместить на том же месте, но в абсолютных координатах
ball.
style.
position =
'absolute'
;
moveAt
(
e)
;
// переместим в body, чтобы мяч был точно не внутри position:relative
document.
body.
appendChild
(
ball)
;
ball.
style.
zIndex =
1000
;
// показывать мяч над другими элементами
// передвинуть мяч под координаты курсора
// и сдвинуть на половину ширины/высоты для центрирования
function
moveAt
(
e
)
{
ball.
style.
left =
e.
pageX -
ball.
offsetWidth /
2
+
'px'
;
ball.
style.
top =
e.
pageY -
ball.
offsetHeight /
2
+
'px'
;
}
// 3, перемещать по экрану
document.
onmousemove
=
function
(
e
)
{
moveAt
(
e)
;
}
// 4. отследить окончание переноса
ball.
onmouseup
=
function
(
)
{
document.
onmousemove =
null
;
ball.
onmouseup =
null
;
}
}
Если запустить этот код, то мы заметим нечто странное. При начале переноса мяч «раздваивается» и переносится не сам мяч, а его «клон».
Это можно увидеть в действии внутри ифрейма:
Попробуйте перенести мяч мышкой и вы увидите описанное, довольно-таки странное, поведение.
Это потому, что браузер имеет свой собственный Drag’n’Drop, который автоматически запускается и вступает в конфликт с нашим. Это происходит именно для картинок и некоторых других элементов.
Его нужно отключить:
ball.
ondragstart
=
function
(
)
{
return
false
;
}
;
Теперь всё будет в порядке.
В действии (внутри ифрейма):
Ещё одна особенность правильного Drag’n’Drop – событие mousemove
отслеживается на document
, а не на ball
.
С первого взгляда кажется, что мышь всегда над мячом и обработчик mousemove
можно повесить на сам мяч, а не на документ.
Однако, на самом деле мышь во время переноса не всегда над мячом.
Вспомним, событие mousemove
возникает хоть и часто, но не для каждого пикселя. Быстрое движение курсора вызовет mousemove
уже не над мячом, а, например, в дальнем конце страницы.
Вот почему мы должны отслеживать mousemove
на всём document
.
Правильное позиционирование
В примерах выше мяч позиционируется в центре под курсором мыши:
self.
style.
left =
e.
pageX -
ball.
offsetWidth /
2
+
'px'
;
self.
style.
top =
e.
pageY -
ball.
offsetHeight /
2
+
'px'
;
Если поставить left/top
ровно в pageX/pageY
, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво. Поэтому мы сдвигаем его на половину высоты/ширины, чтобы был центром под мышью. Уже лучше.
Но не идеально. В частности, в самом начале переноса, особенно если мячик «взят» за край – он резко «прыгает» центром под курсор мыши.
Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся.
Где захватили, за ту «часть элемента» и переносим:

-
Когда человек нажимает на мячик
mousedown
– курсор сдвинут относительно левого-верхнего угла мяча на расстояние, которое мы обозначимshiftX/shiftY
. И нужно при переносе сохранить этот сдвиг.Получить значения
shiftX/shiftY
легко: достаточно вычесть из координат курсораpageX/pageY
левую-верхнюю границу мячика, полученную при помощи функции getCoords.При Drag’n’Drop мы везде используем координаты относительно документа, так как они подходят в большем количестве ситуаций.
Конечно же, не проблема перейти к координатам относительно окна, если это понадобится. Достаточно использовать
position:fixed
,elem.getBoundingClientRect()
для определения координат иe.clientX/Y
.// onmousedown
shiftX=
e.
pageX-
getCoords
(
ball)
.
left;
shiftY=
e.
pageY-
getCoords
(
ball)
.
top;
-
Далее при переносе мяча мы располагаем его
left/top
с учётом сдвига, то есть вот так:// onmousemove
ball.
style.
left=
e.
pageX-
shiftX+
'px'
;
ball.
style.
top=
e.
pageY-
shiftY+
'px'
;
Итоговый код с правильным позиционированием:
var
ball =
document.
getElementById
(
'ball'
)
;
ball.
onmousedown
=
function
(
e
)
{
var
coords =
getCoords
(
ball)
;
var
shiftX =
e.
pageX -
coords.
left;
var
shiftY =
e.
pageY -
coords.
top;
ball.
style.
position =
'absolute'
;
document.
body.
appendChild
(
ball)
;
moveAt
(
e)
;
ball.
style.
zIndex =
1000
;
// над другими элементами
function
moveAt
(
e
)
{
ball.
style.
left =
e.
pageX -
shiftX +
'px'
;
ball.
style.
top =
e.
pageY -
shiftY +
'px'
;
}
document.
onmousemove
=
function
(
e
)
{
moveAt
(
e)
;
}
;
ball.
onmouseup
=
function
(
)
{
document.
onmousemove =
null
;
ball.
onmouseup =
null
;
}
;
}
ball.
ondragstart
=
function
(
)
{
return
false
;
}
;
function
getCoords
(
elem
)
{
// кроме IE8-
var
box =
elem.
getBoundingClientRect
(
)
;
return
{
top
:
box.
top +
pageYOffset,
left
:
box.
left +
pageXOffset
}
;
}
В действии (внутри ифрейма):
Различие особенно заметно, если захватить мяч за правый-нижний угол. В предыдущем примере мячик «прыгнет» серединой под курсор, в этом – будет плавно переноситься с текущей позиции.
Итого
Мы рассмотрели «минимальный каркас» Drag'n'Drop
.
Его компоненты:
- События
ball.mousedown
→document.mousemove
→ball.mouseup
. - Передвижение с учётом изначального сдвига
shiftX/shiftY
. - Отмена действия браузера по событию
dragstart
.
На этой основе можно сделать очень многое.
- При
mouseup
можно обработать окончание переноса, произвести изменения в данных, если они нужны. - Во время самого переноса можно подсвечивать элементы, над которыми проходит элемент.
- При обработке событий
mousedown
иmouseup
можно использовать делегирование, так что одного обработчика достаточно для управления переносом в зоне с сотнями элементов.
Это и многое другое мы рассмотрим в статье про Drag’n’Drop объектов.
Комментарии
<code>
, для нескольких строк кода — тег<pre>
, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)