Drag-and-drop — это технология, которая сделала небольшой переворот в веб-разработке и помогла многим начинающим веб-мастерам собственноручно собирать сайты без знания программирования. Те, кто хоть немного знаком с веб-разработкой, понимают, о чем речь.
Drag-and-drop (читается по-русски «драг энд дроп») дословно переводится как «перетяни и кинь» или «бери и брось». По сути, это способ манипулировать элементами на экране устройства. Манипуляции происходят при помощи «мыши», которая «захватывает» элемент и переносит его в нужную часть экрана. «Захват» элемента происходит при помощи клика и удержания левой кнопки мыши в момент наведения курсора на элемент. Потом в состоянии «нажатого клика» элемент переносится в нужную часть экрана и в момент «отжатия клика» становится на новое место.
Drag-and-drop нужно для простого действия — перенести объект на экране. В роли этого объекта может быть что угодно:
картинка на каком-либо сайте;
панель управления в операционной системе;
ярлыки на рабочем столе;
блоки веб-страницы в конструкторе сайтов;
и др.
Drag-and-drop — что это такое?
Как мы уже писали, метод Drag-and-drop основывается на «переносе объектов по экрану» при помощи мыши.
Современный стандарт HTML5 поддерживает перенос объектов, основываясь на двух технологиях:
при помощи специальных событий;
при помощи события мыши.
К специальным событиями относятся такие события, как:
«dragstart»,
«drag»,
«dragenter»,
«dragleave»,
«dragover»,
«drop»,
«dragend».
Каждое из событий поддерживается современными версиями браузера и по—своему хорошо. Их реализация бывает необходима в очень специфических ситуациях, например, когда нужно организовать «перетаскивание» файла с рабочего стола в окно браузера. Но в большинстве случаев, когда нужно осуществить перетаскивание в пределах окна браузера, специальные события имеют ряд ограничений. Например, нет возможности при помощи них ограничить перенос:
только в горизонтальной плоскости;
только в вертикальной плоскости;
в пределах ограниченной зоны;
и другие задачи, которые невозможно реализовать.
Поэтому самой популярной технологией на сегодня остается «драг энд дроп» при помощи отслеживания событий мыши. Именно на событиях мыши организованы популярные конструкторы веб-сайтов, на которых пользователи просто переносят нужные блоки на экран, не задумываясь, что из себя представляет перетаскиваемый блок на программном уровне.
Drag-and-drop: алгоритм действий
Drag-and-drop-технология на основе отслеживания событий мыши строится по следующему алгоритму:
Налаживают отслеживание нажатия кнопки мыши на компоненте, который нужно «перенести» при помощи события «mousedown».
Как только произошло нажатие, нужно подготовить компонент к перемещению.
Потом происходит отслеживание мыши при помощи «mousemove», чтобы переместить компонент на новое место расположения, сменив его координаты при помощи «left и top», а также «position:absolute».
Отслеживается событие «mouseup», которое сигнализирует о том, что кнопка мыши была «отжата». Как только событие произошло, нужно будет завершить технологию «drag-and-drop», остановив перенос компонента и зафиксировав его в новых координатах.
Drag-and-drop: пример в коде
Мы не будем сейчас разрабатывать полноценный конструктор сайта, но пример простого переноса элемента мы покажем. Давайте организуем перенос картинки в пустой прямоугольник. Реализуется это при помощи HTML и JavaScript.
Код будет следующим:
<!DOCTYPE HTML>
<html>
<head>
<script>
function allowDragAndDrop(elem) {
elem.preventDefault();
}
function drag(elem) {
elem.dataTransfer.setData(«text», elem.target.id);
}
function drop(elem) {
elem.preventDefault();
var data = elem.dataTransfer.getData(«text»);
elem.target.appendChild(document.getElementById(data));
}
</script>
</head>
<body>
<div id=»myFigure» ondrop=»drop(event)» ondragover=»allowDrop(event)»></div>
<img id=»myImage» src=»codernet.gif» draggable=»true» ondragstart=»drag(event)» width=»340″ height=»70″>
</body>
</html>
Важные пояснения по коду:
Чтобы элемент можно было «перетянуть», ему нужно задать атрибут «draggable» со значением «true»;
Важно указать, что должно произойти, когда элемент перетягивается на странице. За это отвечает функция «drag(elem)». За вызов этой функции отвечает атрибут «ondragstart».
Событие «ondragover» показывает, куда можно «перетянуть и бросить» компонент. Один компонент может быть «скинут» не в другой компонент, а только в «пустое» место на экране.
Атрибут «ondrop» отвечает за «скидывание», поэтому он вызывает функцию «drop(elem)», которая и осуществляет «скидывание».
Заключение
Теперь вы знаете, что такое drag-and-drop — это технология, которая помогает облегчить взаимопонимание между пользователем и компьютером или программированием. В последнее время «перетаскивание» блоков стало очень популярным в веб-разработке. Большинство пользователей слышали про такие сервисы, как «Wix» или «Tilda». Они позволяют создавать сайты без знания программирования. О чем говорить, если даже в такой популярной профессиональной CMS, как WordPress, при помощи плагинов также организована drag-and-drop-технология постройки сайтов. А более мелких реализаций «драг энд дроп» пруд пруди.
Интерфейс Drag&Drop
Содержание Основные технологические принципы ОС Windows Возможности интерфейса Drag&Drop в Delphi Участники операции Drag&Drop Этапы операции Drag&Drop Свойства компонентов, участвующих в операции Drag&Drop События компонентов, участвующих в операции Drag&Drop Методы компонентов, участвующих в операции Drag&Drop
Основные технологические принципы ОС Windows Plug and Play Point and Click Drag and Drop WYSIWYG (What You See Is What You Get ) Технология OLE (Object Linking and Embedding ) Объектно-ориентированная технология. Оконная технология
Возможности интерфейса Drag&Drop в Delphi позволяет компонентам обмениваться данными путем «перетаскивания» их мышью объекты можно перемещать в пределах формы или в другую прикладную программу
Участники операции источник (или перемещаемый объект). Источником может быть элемент управления (кнопка, изображение, метка и т. д.) или выбранная часть какого-либо объекта (например, строка из TListBox); приемник (объект, на который будет опущен источник). Приемником может быть любой элемент управления.
элемент управления Элемент интерфейса — примитив графического интерфейса пользователя, имеющий стандартный внешний вид и выполняющий стандартные действия.
Этапы операции Drag&Drop Начало перетаскивания. Проверка готовности приемника принять перетаскиваемый объект. Сбрасывание перетаскиваемого объекта (источника). Окончание процесса перетаскивания
Свойства компонентов, участвующих в операции Drag&Drop [1] DragMode: TDragMode; TDragMode может быть: dmManual dmAutomatic определяет, как будет выполняться весь комплекс действий, связанных с Drag&Drop.
Если DragMode = dmManual, то все события перетаскивания должны определяться вручную (т. е. программистом по ходу выполнения программы). Перетаскивание начинается только после вызова специальных методов. Если DragMode = dmAutomatic, то все события перетаскивания определяются автоматически, перетаскивание начинается сразу после нажатия кнопки мыши пользователем.
Свойства компонентов, участвующих в операции Drag&Drop [2] DragCursor определяет вид курсора в момент, когда над компонентом «перетаскиваются данные». DragCursor= crDrag, если компонент готов принять данные (курсор принимает вид прямоугольника со стрелкой). DragCursor= crNoDrag, если компонент не готов принять данные (курсор — перечеркнутый круг). Только в случае DragMode = dmAutomatic
События компонентов, участвующих в операции Drag&Drop [1] OnStartDrag Происходит в начале операции перетаскивания. Возникает у перетаскиваемого объекта. Не является обязательным для выполнения. Операция перетаскивания может быть произведена и без обработки этого события. Не все компоненты генерируют данное событие. Заголовок обработчика события : procedure TForm1.<имя_компонента>StartDrag (Sender: TObject; var DragObject: TDragObject); Параметры: Sender — содержит информацию о перетаскиваемом объекте. DragObject — используется для того, чтобы определить вид курсора или вид рисунка при перетаскивании объекта. От этого параметра процедура получает информацию об объекте, создаваемом данным событием
События компонентов, участвующих в операции Drag&Drop [2] OnDragOver Проверка готовности приемника принять перетаскиваемый объект Возникает в момент перемещения указателя мыши «с грузом» над компонентом. Заголовок обработчика: procedure TForm1.<имя_компонента>DragOvег (Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
События компонентов, участвующих в операции Drag&Drop [2] OnDragOver Параметры: Sender указывает на компонент, над которым перемещается объект. Source содержит информацию о компоненте — отправителе груза. X и Y координаты указателя мыши, выраженные в пикселях относительно компонента Sender. State указывает состояние перемещаемого объекта относительно Sender. TDragState = (dsDragEnter, dsDragLeave, dsDragMove); dsDragEnter показывает, что Source только что появился над Sender. dsDragLeave, Source только что покинул Sender либо была отпущена кнопка мыши. dsDragMove Source перемещается над Sender
События компонентов, участвующих в операции Drag&Drop [2] OnDragOver Параметры: Accept сообщает, готов ли Sender принять перетаскиваемые данные. Если Accept имеет значение True, то Sender готов принять перетаскиваемый объект (если пользователь «сбросил» перетаскиваемый объект (отпустил кнопку мыши в данной точке), то приложение вызовет событие обработки операции по сбрасыванию объектов). Значение False сообщает, что Sender не может принять перетаскиваемый объект (если пользователь отпустит кнопку мыши, то ничего не произойдет). В обработчике OnDragOver события главное — определить значение параметра Accept.
События компонентов, участвующих в операции Drag&Drop [3] OnDragDrop Возникает если в обработчике события OnDragOver значение параметра Accept=True и Вы попытались сбросить объект. возникает у компонента, на который объект был сброшен в его обработчике необходимо выполнить все действия над перетаскиваемым объектом по «сбрасыванию» Заголовок обработчика: procedure TForm1.<имя_компонента>DragDrop (Sender, Source: TObject; X, Y: Integer); Значения параметров этого обработчика события совпадают со значениями одноименных параметров обработчика события OnDragOver.
События компонентов, участвующих в операции Drag&Drop [4] OnDragEnd возникает при завершении перетаскивания (вне зависимости от того, приняты данные или нет) возникает для перетаскиваемого объекта также происходит при отмене перетаскивания не является обязательным для выполнения. Операция перетаскивания может быть произведена и без обработки этого события. не все компоненты генерируют данное событие.
События компонентов, участвующих в операции Drag&Drop [4] OnDragEnd Заголовок обработчика события: procedure TForml.<имя_компонента>EndDrag (Sender, Target: TObject; X, Y: Integer); Параметры: Sender получает информацию о перетаскиваемом объекте. Target содержит информацию об объекте, который получил данные. Если перетаскиваемый объект не был принят, то Target= Nil объект. X, Y — координаты указателя мыши в момент отпускания левой кнопки.
Методы компонентов, участвующих в операции Drag&Drop [1] BeginDrag применяется для того, чтобы начать операцию перетаскивания. понадобится в случае, когда свойство DragMode перетаскиваемого объекта установлено в значение dmManual. чтобы перетаскивание началось, необходимо инициализировать метод BeginDrag у объекта, который надо перетащить. Удобнее всего это делать при обработке событий мыши данного объекта.
Методы компонентов, участвующих в операции Drag&Drop [1] BeginDrag После применения метода с объектами будут происходить все те же события, рассмотренные выше для значения свойства DragMode, равного dmAutomatic. Обычно вызов метода BeginDrag осуществляется в обработчике события OnMouseDown перетаскиваемого объекта. пользователь сам должен позаботиться о проверке корректности начала операции перетаскивания, а именно, перетаскивание должно начинаться только при нажатии левой кнопки мыши (значение Button должно быть равно mbLeft).
Методы компонентов, участвующих в операции Drag&Drop [1] BeginDrag Описание procedure BeginDrag(Immediate: Boolean; Threshold: Integer=-1); Параметр Immediate может принимать два значения. True, перетаскивание начинается немедленно. False, перетаскивание начинается при смещении курсора мыши в любом направлении на количество пикселей, определенное параметром Threshold. Лучше изначально Immediate = False, так как в этом случае можно обрабатывать нажатие кнопки мыши, не начиная операцию перетаскивания. Для того чтобы начать процесс перетаскивания, можно просто в ходе программы в нужном месте присвоить свойству DragMode перетаскиваемого объекта значение dmAutomatic.
Методы компонентов, участвующих в операции Drag&Drop [2] EndDrag используется для того, чтобы остановить операцию перетаскивания, начатую вызовом метода BeginDrag. procedure EndDrag(Drop: Boolean); Drop=True, приводит к завершению операции перетаскивания и сбрасыванию объекта. Drop=False отменяет процесс перетаскивания.
Выводы Программирование операции Drag&Drop заключается в выполнения следующих действий: инициализация метода BeginDrag перетаскиваемого объекта (источника), если значение его свойства DragMode равно dmManual; создание обработчика события OnDragOver компонента-приемника, чтобы определить, где можно «сбрасывать» перетаскиваемый объект; создание обработчика события OnDragDrop компонента-приемника, чтобы определить, какие действия должны выполняться при «сбрасывании» перетаскиваемого объекта; создание обработчика события OnDragEnd компонента-источника. Если два предыдущих шага необходимы для любой операции перетаскивания, то последний шаг выполняется лишь тогда, когда надо выполнить некоторые действия в исходном компоненте при завершении процесса перетаскивания.
В
Windows-приложениях существует возможность
перетаскивания различных объектов при
помощи мыши, для чего используется
технология Drag-and-Drop (перетащить и
оставить). Это позволяет создавать
приложения с более удобным интерфейсом,
более интересные программы. Элемент,
который перемещается, называется
источником (source), а объект, в котором он
будет после этого находиться (курсор
мыши в момент окончания находится в его
пределах), носит название адресат
(target). Например: рисунок двигаем по форме:
рисунок — источник; если курсор мыши в
момент отпускания находится на форме,
то форма приемник, если на другом объекте,
то этот объект приемник.
Чтобы
переместить объект, надо на него навести
указатель мыши, нажать левую клавишу
мыши и, удерживая ее, установить объект
в нужное место. Перемещение объекта не
происходит автоматически. Необходимо
правильно использовать определенные
свойства, события, методы.
Свойства,
события и методы технологии Drag-and-Drop
Название |
Описание |
DragDrop |
Событие, |
DragOver |
Событие, |
DragIcon |
Свойство, |
DragMode |
Свойство, 0 1 |
Drag |
Метод, |
Рассмотрим
сначала автоматический
режим.
Упражнение1
Установите
на форме рисунок, в Окне Свойств задайте
ему автоматический режим перемещения.
Теперь запустите программу и попробуйте
переместить рисунок. Вы увидите движущуюся
рамку, но при отпускании мышки рисунок
на прежнем месте. А для того, чтобы он
переместился, надо использовать событие
DragDrop (событие, возникающее при отпускании
мыши). В этот момент надо задать рисунку
новые координаты, равные текущим
координатам курсора. Рисунок перемещаем
по форме, поэтому используем событие
DragDrop.
Private
Sub Form_DragDrop(Source As Control, X As Single, Y As Single)
Source
– это наш рисунок, X, Y – координаты
курсора в момент события.
Мы
хотим, чтобы объект, который двигаем,
остался в той позиции, где заканчиваем
перемещение, т.е. его координаты должны
быть – параметры X и Y события DragDrop.
Следовательно, пишем:
Source.Left
= X
Source.Top
– Y
Запустите
программу. Проверьте ее работу. А чтобы
было красивее, допустим, что курсор
мышки встает на середине объекта, и
делаем поправку.
Source.Left
= X – Source.Width/2
Source.Top
= Y– Source.Height/2
Теперь,
если установить на форме другие рисунки,
надо только задать им автоматический
режим перемещения, а в программе ничего
добавлять уже не надо. Параметр Source
означает именно тот объект, который мы
перемещаем. Попробуйте задать свойство
DragIcon, (файл, который при этом вызывается,
должен иметь расширение .ico) и тогда при
перемещении объекта его изображение
изменится.
Рассмотрим
теперь ручной
режим
Не
всегда удобно пользоваться автоматическим
режимом перемещения. Если установлен
DragMode=1, то объекты не реагируют на события
левой клавиши, к тому же не всегда
удается поставить точно объект. Разберем
теперь ручной режим перемещения,
используя метод Drag. Метод имеет параметр
Action, определяющий то или иное состояние
выполняемого перетаскивания.
Константа |
Значение |
Описание |
VbCancel |
0 |
Отмена операции |
vbBeginDrag |
1 |
Начало |
VbEndDrag |
2 |
Окончание |
Вызов
метода ИмяПеремещаемогоОбъекта.Drag(Action)
Например,
когда мы хотим начать перемещение
рисунка, надо написать img1.Drag(1)
Упражнение2
В
объекте Папка установлен ручной режим
перемещения. Для того чтобы папку можно
было передвигать, надо в событии
imgF_MouseDown, если нажата левая клавиша,
вызвать метод Drag. If
Button = vbLeftButton Then imgF.Drag (1). Это
начало перемещения, а конец у нас уже
запрограммирован в событии Form_DragDrop.
Запустите программу, проверьте ее
работу. Обратите внимание, что стрелка
мыши после окончания перемещения всегда
стоит посередине объекта, и если мы
вначале поставим стрелку не посередине,
то перемещение будет неточным. Этого
можно избежать при ручном режиме.
Задайте
две контейнерные константы, например
xx и уу. В событии imgF_MouseDown одновременно
с вызовом метода Drag напишите xx=X, yy=Y.
Переменные X и Y в этом событии равняются
координатам мышки относительно левого
верхнего угла объекта. А при окончании
перемещения сделайте поправку именно
на эти величины. Тогда мышка всегда
будет стоять относительно объекта
одинаково.
Задание1.
На форме установить объект Корзина.
Изначально она пустая, если в нее
что-нибудь положат (переместят), она
становится полной, а объекты, которые
в нее переместили, исчезают. При двойном
щелчке по корзине, она становится пустой,
а все объекты из нее видимые. Надо
использовать события корзины DragDrop и
DblClick
Домашнее
задание:
-
Создать
программу «Мозаика» или иную
придуманную Вами программу, где
используется перетаскивание объекта. -
Дополнить
проект «Корзина»:
а) саму корзину
тоже можно перемещать;
б)
когда мышка показывает на левый нижний
угол папки, она принимает вид двойной
стрелки, и если дальше двигаем нажатую
мышку, то папка соответственно меняет
размеры.
Контрольные
вопросы:
-
Назовите
основные события мыши. В каких случаях
они происходят? -
Технология
Drag-and-Drop, ее смысл, принципы работы. -
Параметр
Source, для чего служит и в каких событиях. -
Различия
автоматического и ручного способа
перемещения объектов, преимущества и
недостатки. -
Какие
свойства объектов можно и нужно
использовать при перемещении объектов?
УРОК
13
Цель
урока. Изучение новых объектов
Shape, Line, ComboBox. Ознакомление с разными
способами и функциями задания цвета.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
При работе с объектами пользователь получает возможность делать с ними то, что ему необходимо для более удобного обращения с компьютером (например, копировать объект, удалить его, создать новый и т.д.).
в начало |
Скачать материал
Скачать материал
- Сейчас обучается 419 человек из 64 регионов
- Сейчас обучается 252 человека из 63 регионов
Описание презентации по отдельным слайдам:
-
1 слайд
Интерфейс Drag&Drop
-
2 слайд
Содержание
Основные технологические принципы ОС Windows
Возможности интерфейса Drag&Drop в Delphi
Участники операции Drag&Drop
Этапы операции Drag&Drop
Свойства компонентов, участвующих в операции Drag&Drop
События компонентов, участвующих в операции Drag&Drop
Методы компонентов, участвующих в операции Drag&Drop -
3 слайд
Основные технологические принципы ОС Windows
Plug and Play
Point and Click
Drag and Drop
WYSIWYG (What You See Is What You Get )
Технология OLE (Object Linking and Embedding )
Объектно-ориентированная технология.
Оконная технология -
4 слайд
Возможности интерфейса Drag&Drop в Delphi
позволяет компонентам обмениваться данными путем «перетаскивания» их мышью
объекты можно перемещать в пределах формы или в другую прикладную программу -
5 слайд
Участники операции
источник (или перемещаемый объект). Источником может быть элемент управления (кнопка, изображение, метка и т. д.) или выбранная часть какого-либо объекта (например, строка из TListBox);
приемник (объект, на который будет опущен источник). Приемником может быть любой элемент управления. -
6 слайд
элемент управления
Элемент интерфейса — примитив графического интерфейса пользователя, имеющий стандартный внешний вид и выполняющий стандартные действия. -
7 слайд
Этапы операции Drag&Drop
Начало перетаскивания.
Проверка готовности приемника принять перетаскиваемый объект.
Сбрасывание перетаскиваемого объекта (источника).
Окончание процесса перетаскивания -
8 слайд
Свойства компонентов, участвующих в операции Drag&Drop
[1] DragMode: TDragMode;
TDragMode может быть:
dmManual
dmAutomatic
определяет, как будет выполняться весь комплекс действий, связанных с Drag&Drop. -
9 слайд
Если DragMode = dmManual, то все события перетаскивания должны определяться вручную (т. е. программистом по ходу выполнения программы). Перетаскивание начинается только после вызова специальных методов.
Если DragMode = dmAutomatic, то все события перетаскивания определяются автоматически, перетаскивание начинается сразу после нажатия кнопки мыши пользователем. -
10 слайд
Свойства компонентов, участвующих в операции Drag&Drop
[2] DragCursor
определяет вид курсора в момент, когда над компонентом «перетаскиваются данные».
DragCursor= crDrag, если компонент готов принять данные (курсор принимает вид прямоугольника со стрелкой).
DragCursor= crNoDrag, если компонент не готов принять данные (курсор — перечеркнутый круг). Только в случае DragMode = dmAutomatic -
11 слайд
События компонентов, участвующих в операции Drag&Drop
[1] OnStartDrag
Происходит в начале операции перетаскивания.
Возникает у перетаскиваемого объекта.
Не является обязательным для выполнения. Операция перетаскивания может быть произведена и без обработки этого события.
Не все компоненты генерируют данное событие.
Заголовок обработчика события :
procedure TForm1.<имя_компонента>StartDrag (Sender: TObject; var DragObject: TDragObject);
Параметры:
Sender — содержит информацию о перетаскиваемом объекте.
DragObject — используется для того, чтобы определить вид курсора или вид рисунка при перетаскивании объекта.
От этого параметра процедура получает информацию об объекте, создаваемом данным событием -
12 слайд
События компонентов, участвующих в операции Drag&Drop
[2] OnDragOverПроверка готовности приемника принять перетаскиваемый объект
Возникает в момент перемещения указателя мыши «с грузом» над компонентом.
Заголовок обработчика:
procedure TForm1.<имя_компонента>DragOvег (Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); -
13 слайд
События компонентов, участвующих в операции Drag&Drop
[2] OnDragOverПараметры:
Sender указывает на компонент, над которым перемещается объект.
Source содержит информацию о компоненте — отправителе груза.
X и Y координаты указателя мыши, выраженные в пикселях относительно компонента Sender.
State указывает состояние перемещаемого объекта относительно Sender.
TDragState = (dsDragEnter, dsDragLeave, dsDragMove);
dsDragEnter показывает, что Source только что появился над Sender.
dsDragLeave, Source только что покинул Sender либо была отпущена кнопка мыши.
dsDragMove Source перемещается над Sender -
14 слайд
События компонентов, участвующих в операции Drag&Drop
[2] OnDragOverПараметры:
Accept сообщает, готов ли Sender принять перетаскиваемые данные.
Если Accept имеет значение True, то Sender готов принять перетаскиваемый объект (если пользователь «сбросил» перетаскиваемый объект (отпустил кнопку мыши в данной точке), то приложение вызовет событие обработки операции по сбрасыванию объектов).
Значение False сообщает, что Sender не может принять перетаскиваемый объект (если пользователь отпустит кнопку мыши, то ничего не произойдет).
В обработчике OnDragOver события главное — определить значение параметра Accept. -
15 слайд
События компонентов, участвующих в операции Drag&Drop
[3] OnDragDropВозникает если в обработчике события OnDragOver значение параметра Accept=True и Вы попытались сбросить объект.
возникает у компонента, на который объект был сброшен
в его обработчике необходимо выполнить все действия над перетаскиваемым объектом по «сбрасыванию»Заголовок обработчика:
procedure TForm1.<имя_компонента>DragDrop (Sender, Source: TObject; X, Y: Integer);
Значения параметров этого обработчика события совпадают со значениями одноименных параметров обработчика события OnDragOver. -
16 слайд
События компонентов, участвующих в операции Drag&Drop
[4] OnDragEndвозникает при завершении перетаскивания (вне зависимости от того, приняты данные или нет)
возникает для перетаскиваемого объекта
также происходит при отмене перетаскивания
не является обязательным для выполнения. Операция перетаскивания может быть произведена и без обработки этого события.
не все компоненты генерируют данное событие. -
17 слайд
События компонентов, участвующих в операции Drag&Drop
[4] OnDragEndЗаголовок обработчика события:
procedure TForml.<имя_компонента>EndDrag (Sender, Target: TObject; X, Y: Integer);
Параметры:
Sender получает информацию о перетаскиваемом объекте.
Target содержит информацию об объекте, который получил данные.
Если перетаскиваемый объект не был принят, то Target= Nil объект.
X, Y — координаты указателя мыши в момент отпускания левой кнопки. -
18 слайд
Методы компонентов, участвующих в операции Drag&Drop
[1] BeginDrag
применяется для того, чтобы начать операцию перетаскивания.
понадобится в случае, когда свойство DragMode перетаскиваемого объекта установлено в значение dmManual.
чтобы перетаскивание началось, необходимо инициализировать метод BeginDrag у объекта, который надо перетащить. Удобнее всего это делать при обработке событий мыши данного объекта. -
19 слайд
Методы компонентов, участвующих в операции Drag&Drop
[1] BeginDrag
После применения метода с объектами будут происходить все те же события, рассмотренные выше для значения свойства DragMode, равного dmAutomatic.
Обычно вызов метода BeginDrag осуществляется в обработчике события OnMouseDown перетаскиваемого объекта.
пользователь сам должен позаботиться о проверке корректности начала операции перетаскивания, а именно, перетаскивание должно начинаться только при нажатии левой кнопки мыши (значение Button должно быть равно mbLeft). -
20 слайд
Методы компонентов, участвующих в операции Drag&Drop
[1] BeginDrag
Описание
procedure BeginDrag(Immediate: Boolean; Threshold: Integer=-1);
Параметр Immediate
может принимать два значения.
True, перетаскивание начинается немедленно.
False, перетаскивание начинается при смещении курсора мыши в любом направлении на количество пикселей, определенное параметром Threshold. Лучше изначально Immediate = False, так как в этом случае можно обрабатывать нажатие кнопки мыши, не начиная операцию перетаскивания.
Для того чтобы начать процесс перетаскивания, можно просто в ходе программы в нужном месте присвоить свойству DragMode перетаскиваемого объекта значение dmAutomatic. -
21 слайд
Методы компонентов, участвующих в операции Drag&Drop
[2] EndDrag
используется для того, чтобы остановить операцию перетаскивания, начатую вызовом метода BeginDrag.
procedure EndDrag(Drop: Boolean);
Drop=True, приводит к завершению операции перетаскивания и сбрасыванию объекта.
Drop=False отменяет процесс перетаскивания. -
22 слайд
Выводы
Программирование операции Drag&Drop заключается в выполнения следующих действий:
инициализация метода BeginDrag перетаскиваемого объекта (источника), если значение его свойства DragMode равно dmManual;
создание обработчика события OnDragOver компонента-приемника, чтобы определить, где можно «сбрасывать» перетаскиваемый объект;
создание обработчика события OnDragDrop компонента-приемника, чтобы определить, какие действия должны выполняться при «сбрасывании» перетаскиваемого объекта;
создание обработчика события OnDragEnd компонента-источника.
Если два предыдущих шага необходимы для любой операции перетаскивания, то последний шаг выполняется лишь тогда, когда надо выполнить некоторые действия в исходном компоненте при завершении процесса перетаскивания.
Найдите материал к любому уроку, указав свой предмет (категорию), класс, учебник и тему:
6 106 993 материала в базе
- Выберите категорию:
- Выберите учебник и тему
- Выберите класс:
-
Тип материала:
-
Все материалы
-
Статьи
-
Научные работы
-
Видеоуроки
-
Презентации
-
Конспекты
-
Тесты
-
Рабочие программы
-
Другие методич. материалы
-
Найти материалы
Другие материалы
- 26.12.2020
- 135
- 0
- 17.12.2020
- 434
- 0
- 20.09.2020
- 223
- 0
- 24.08.2020
- 194
- 0
- 07.07.2020
- 462
- 1
- 21.06.2020
- 165
- 0
- 19.06.2020
- 81
- 0
- 17.06.2020
- 303
- 1
Вам будут интересны эти курсы:
-
Курс профессиональной переподготовки «Маркетинг: теория и методика обучения в образовательной организации»
-
Курс профессиональной переподготовки «Управление персоналом и оформление трудовых отношений»
-
Курс повышения квалификации «Методика написания учебной и научно-исследовательской работы в школе (доклад, реферат, эссе, статья) в процессе реализации метапредметных задач ФГОС ОО»
-
Курс профессиональной переподготовки «Организация и предоставление туристских услуг»
-
Курс профессиональной переподготовки «Клиническая психология: организация реабилитационной работы в социальной сфере»
-
Курс повышения квалификации «Введение в сетевые технологии»
-
Курс повышения квалификации «Применение MS Word, Excel в финансовых расчетах»
-
Курс повышения квалификации «Страхование и актуарные расчеты»
-
Курс профессиональной переподготовки «Уголовно-правовые дисциплины: теория и методика преподавания в образовательной организации»
-
Курс профессиональной переподготовки «Организация деятельности по водоотведению и очистке сточных вод»
-
Курс профессиональной переподготовки «Организация и управление процессом по предоставлению услуг по кредитному брокериджу»
-
Курс профессиональной переподготовки «Стандартизация и метрология»
Придя впервые к технологии DRAG and DROP столкнулся с очень тяжелым её описанием (Это мое субъективное мнение. Прошу с ним не соглашаться, а перечитать все что только можно и посмотреть на этот вопрос с многих сторон). И решил написать пару статей, нацеленных на начинающих разработчиков, кто хочет познать дзен.
Статья будет состоять из двух частей:
- Метод создания DRAG and DROP эффектов.
- Практическое применение полученных знаний для создание сортировки при помощи DRAG and DROP
Параграф №1 Метод создания DRAG and DROP эффекта
Перед началом глубокого разбора, посмотрим поверхностно. Представьте себя в роли грузчика, вам необходимо перемесить коробку с одного места на другое. Для грузчика это «Ну взял, ну перенес. Готово!», а для программиста “Подошел к коробке, наклонился, взял коробку, поднял коробку, прошел N шагов, наклонился, отпустил коробку.”. Я это к тому, что перед началом работы проиграйте все в голове, по шагам и вы станете гораздо ближе к истине.
Я хочу оговориться, что методов создания данного эффекта несколько и обязательно прочтите о всех. Я воспользуюсь тем, который на данный момент считаю самым удобным.
При создании DRAG and DROP первым шагом необходимо объекту, который мы будем перемещать, присвоить значение draggable=’true’.
<html>
<head>
<style>
.ddd {
display:block;
float:left;
padding:10px;
border:1px solid #000;
}
</style>
</head>
<body>
<div id='d1' class='ddd' draggable='true'>Pervii 1</div>
</body>
</html>
,
На первом этапе я хочу показать сам процесс, а после мы его распространим на все объекты. Мы сейчас работаем в JS и как вам известно, в браузере существуют различные события, к которым мы можем привязать свои последовательности действий. Давайте разберем необходимые события для создания DRAG and DROP:
dragstart — происходит, когда пользователь начинает перетаскивать элемент.
drag — происходит, когда элемент перетаскивается.
dragend — происходит, когда пользователь закончил перетаскивание элемента.
dragenter — происходит, когда перетаскиваемый элемент попадает в целевой объект.
dragleave — происходит, когда перетаскиваемый элемент покидает целевой объект.
dragover — происходит, когда перетаскиваемый элемент находится над целью.
drop — происходит, когда перетаскиваемый элемент падает на целевой объект.
Теперь очень важная информация! События делятся на две группы. Для перемещаемого элемента (тот кого мы перетаскиваем): dragstart, Drag, Dragend. Для принимающего элемента (куда перетаскиваем): Dragenter, Dragleave, Dragover, Drop. И эти события не могут работать наоборот, но они могут работать друг без друга.
К примеру: Необходимо переместить объект и оставить его там, где мы отпустили кнопку мыши. Эта задача не требует принимающей части.
<html>
<head>
<style>
.ddd {
display:block;
float:left;
padding:10px;
border:1px solid #000;
}
</style>
</head>
<body>
<div id='d1' class='ddd' draggable='true'>Pervii 1</div>
<div id='d2' class='ddd'>Vtoroy 2</div>
<div id='d3' class='ddd'>Tretii 3</div>
<div id='d4' class='ddd'>Chetverty 4</div>
<div id='d5' class='ddd'>Pyatii 5</div>
<script>
var d1 = document.getElementById('d1');
var startCursorX;
var startCursorY;
var startX;
var startY;
d1.addEventListener('dragstart',function() {
startCursorX = event.pageX;//Начальная позиция курсора по оси X
startCursorY = event.pageY;//Начальная позиция курсора по оси Y
startX = d1.style.marginLeft.replace('px','')*1; // Нам нужны только цыфры без PX
startY = d1.style.marginTop.replace('px','')*1;
});
d1.addEventListener('dragend',function() {
d1.style.position = 'absolute';//CSS теперь элемент "Блуждающий" :)
d1.style.marginLeft = startX + event.pageX-startCursorX; //позиция элемента + позиция курсора - позиция курсоа в начале перетаскивания
d1.style.marginTop = startY + event.pageY-startCursorY; // Так же как и в предыдущем случае, только по другой оси
});
</script>
</body>
</html>
Бесспорно, пример сделан на коленке, но он замечательно иллюстрирует не обязательность принимающего объекта.
Я считаю примеры с отдельными событиями ни к чему, так как если в строчке d1.addEventListener(‘dragstart’,function() { вы замените ‘dragstart’ на любое другое событие, сами сможете с ним поиграть и получить интересные результаты. Давайте даже так, поиграйте и то что вам показалось необычным и интересным покажите в комментариях, так каждый узнает много нового и сам сможет это повторить.
Параграф №2. Не работает DROP в DRAG and DROP
Когда вы попробуете все события, вы обнаружите что drop не работает. Это разработчики данного метода делают атата тем, кто решил «И это всё… Хух, ерунда».
Ну тут все просто, перед событием drop необходимо на этот же элемент повесить событие
d2.addEventListener('dragover',function() {
event.preventDefault();
});
Это примите как факт, без него работать не будет.
- Основная логика drag and drop
-
Координаты и кнопка мыши
- Демо
-
Отслеживаем клик на объекте переноса
- Демо
- Drag and drop контроллер
-
Визуальное перемещение элемента
- Начало движения: сохранение позиции курсора в элементе
- Демо
-
Опускаем элемент
- Демо
- Индикация переноса над объектом
-
Отделяем начало drag’n’drop от простого клика
- Демо
-
Оптимизация
-
Смена способа вычисления координат
-
document.elementFromPoint(x,y)
- Вычисление по известным размерам
-
- Убрать элемент из-под курсора
- Упростить объект переноса
-
Смена способа вычисления координат
-
Рефакторинг
-
DragObject
-
DropTarget
-
dragMaster
-
-
Итоговый Drag’n’Drop фреймворк
- Обычный перенос
- Вложенные акцепторы
-
Некоторые дополнительные рецепты
- Перемещения акцепторов в процессе переноса
- Drag and drop «между».
- Анимация отмены переноса
- Проверка прав
- Резюме
В свое время приходилось реализовывать кучу drag and drop’ов под самым разным соусом.
Эта статья представляет собой учебник-выжимку о том, как организовать drag’n’drop в javascript, начиная от основ и заканчивая готовым фреймворком.
Кроме того, почти все javascript-библиотеки реализуют drag and drop так, как написано (в статье дано несколько разных вариантов, не факт что ваш фреймворк использует лучший). Зная, что и как, вы сможете поправить и адаптировать существующую библиотеку под себя.
Drag’n’drop в свое время был замечательным открытием в области интерфейсов, которое позволило упростить большое количество операций.
Одно из самых очевидных применений drag’n’drop — переупорядочение данных. Это могут быть блоки, элементы списка, и вообще — любые DOM-элементы и их наборы.
Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через drag’n’drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п.
Основная логика drag and drop
Организовать перенос элементов по странице — довольно просто. Для этого нужно:
- При помощи события
mouseDown
отследить клик на переносимом элементе - При каждом движении мыши в обработчике события
mouseMove
передвигать переносимый элемент по странице. - При отпускании кнопки мыши, то есть наступлении события
mouseUp
— остановить перенос элемента и произвести все действия, связанные с окончанием drag and drop.
Координаты и кнопка мыши
При обработке событий, связанных с мышью, нужен кроссбраузерный способ получения координат курсора из события в обработчике. Кроме того, необходимо знать нажатую кнопку мыши.
Для этого будем использовать свойства which
и pageX/pageY
, полное описание и механизмы кросс-браузерной реализации которых есть в статье по свойствам объекта событие.
which
- кнопка мыши — 1: левая, 2: средняя, 3: правая
pageX/pageY
- координаты курсора относительно верхнего-левого угла документа (с учетом прокрутки)
Кроссбраузерно ставить эти свойства на объект будет функция fixEvent
(по статье свойства объекта событие):
function fixEvent(e) { // получить объект событие для IE e = e || window.event // добавить pageX/pageY для IE if ( e.pageX == null && e.clientX != null ) { var html = document.documentElement var body = document.body e.pageX = e.clientX + (html && html.scrollLeft || body && body.scrollLeft || 0) - (html.clientLeft || 0) e.pageY = e.clientY + (html && html.scrollTop || body && body.scrollTop || 0) - (html.clientTop || 0) } // добавить which для IE if (!e.which && e.button) { e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) ) } return e }
В этом коде e.which
проходит кросс-браузерную обработку, чтобы корректно отражать нажатую кнопку мыши. Вы можете подробно прочитать об этом в статье Свойства объекта событие.
Демо
На демке ниже обработчик mouseMove
отслеживает координаты курсора мыши относительно левого-верхнего угла страницы, используя кроссбраузерную обертку fixEvent
.
document.onmousemove = mouseMove function mouseMove(event){ event = fixEvent(event) document.getElementById('mouseX').value = event.pageX document.getElementById('mouseY').value = event.pageY }
Координата X:
Координата Y:
Отслеживаем клик на объекте переноса
Чтобы начать перенос элемента, мы должны отловить нажатие кнопки мыши на объекте.
Для этого нам пригодится событие mousedown
. Повесим обработчик на те элементы, которые хотим сделать доступными для переноса.
Пока этот обработчик будет запоминать объект в глобальной переменной dragObject
element.onmousedown = function(e){ // запомнить переносимый объект // в переменной dragObject dragObject = this // остановить обработку события return false }
Остановить обработку события return false
очень важно — иначе браузер может запустить свои механизмы перетаскивания элементов и все нам поломать.
В случае с отпусканием кнопки мыши все проще — объект мы уже знаем, так что можно повесить один обработчик onmouseup
на document
.
document.onmouseup = function() { // опустить переносимый объект dragObject = null }
Демо
При нажатии на элемент он запоминается и выделяется.
Выделение(запоминание) действует на все время, когда нажата кнопка мыши, в том числе при перемещении курсора.
Остается добавить визуальное перемещение элемента — и drag and drop заработает.
Оптимизация onmousedown
Иногда бывает, что объектов, которые могут быть перенесены, много. Например, это ячейка таблицы или длинный список, или дерево статей с массой узлов.
Тогда время инициализации можно сильно сократить, если назначать обработчик onmousedown
не на каждый объект переноса, а на контейнер. И уже в самом обработчике по event.target
определять, где произошел клик.
Drag and drop контроллер
Перед дальнейшим развитием проведем реорганизацию кода.
Используем способ описания объекта без new
(описан здесь как фабрика объектов), чтобы объявить объект dragMaster
, предоставляющий необходимый функционал и отслеживающий перенос.
var dragMaster = (function() { // private методы и свойства var dragObject function mouseDown(e) { клик на переносимом элементе: начать перенос } function mouseMove(e){ if (dragObject) { отобразить перенос объекта } } function mouseUp(e){ if (dragObject) { конец переноса } } // public методы и свойства return { init: function() { // инициализовать контроллер document.onmousemove = mouseMove document.onmouseup = mouseUp }, makeDraggable: function(element){ // сделать элемент переносимым element.onmousedown = mouseDown } } }())
При таком способе задания объекта dragMaster
получает публичные свойства (например, makeDraggable
), которые имеют доступ к приватным переменным и методам mouse*
, dragObject
, так как являются вложенными функциями.
Полученный код:
- Не загрязняет глобальную область видимости переменными типа
dragObject
. - Дает единый объект-синглтон dragMaster, управляющий переносом.
- Приватные переменные хорошо сжимаются javascript-компрессором, что убыстряет и уменьшает код.
Последний пункт очень важен, так как обработчики mouseMove
, mouseUp
вызываются при каждом передвижении мыши и поднятии кнопки соответственно. Если mouseMove
будет работать медленно, то передвижение курсора станет рваным, заметно тормозным.
На это натыкались многие писатели drag’n’drop приложений. Мы же будем изначально закладывать производительность во все критические участки кода.
Визуальное перемещение элемента
Для того, чтобы перенести элемент, ему нужно поставить значение CSS-свойства position
в absolute
. Тогда он будет позиционироваться относительно верхнего-левого угла документа (точнее говоря, относительно ближайшего родителя, у которого position — relative/absolute, но у нас таких нет), и установка CSS-свойств left
и top
в координаты курсора мыши поместит левый-верхний угол элемента непосредственно под указатель.
Вот так:
Для перемещения элемента нам достаточно всего-лишь обновлять значения left/top
при каждом движении мыши mousemove
!
Начало движения: сохранение позиции курсора в элементе
Посетитель обычно кликает не в левый-верхний угол, а куда угодно на элементе.
Поэтому чтобы элемент не прилипал к курсору верхним-левым углом, к позиции элемента необходимо добавить смещение мыши на момент клика.
На рисунке ниже mouseX/mouseY
— координаты курсора мыши, а positionX/positionY
— координаты верхнего-левого угла элемента, которые легко получить из DOM:
Отсюда легко получаем смещение курсора мыши:
mouseOffsetX = mouseX - positionX mouseOffsetY = mouseY - positionY
Это изначальное смещение мы запоминаем при клике, прибавляем его при начале движения и сохраняем в дальнейшем.
Тогда позиция элемента относительно курсора мыши будет все время одной и той же.
var mouseOffset element.onmousedown = function(e) { e = fixEvent(e) ... var pos = getPosition(element) mouseOffset= { x: e.pageX - pos.x, y: e.pageY - pos.y } ... } document.onmousemove = function(e) { e = fixEvent(e) ... element.style.left = e.pageX - mouseOffset.x + 'px' element.style.top = e.pageY - mouseOffset.y + 'px' ... }
Демо
Код контроллера:
var dragMaster = (function() { var dragObject var mouseOffset // получить сдвиг target относительно курсора мыши function getMouseOffset(target, e) { var docPos = getPosition(target) return {x:e.pageX - docPos.x, y:e.pageY - docPos.y} } function mouseUp(){ dragObject = null // очистить обработчики, т.к перенос закончен document.onmousemove = null document.onmouseup = null document.ondragstart = null document.body.onselectstart = null } function mouseMove(e){ e = fixEvent(e) with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } return false } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return dragObject = this // получить сдвиг элемента относительно курсора мыши mouseOffset = getMouseOffset(this, e) // эти обработчики отслеживают процесс и окончание переноса document.onmousemove = mouseMove document.onmouseup = mouseUp // отменить перенос и выделение текста при клике на тексте document.ondragstart = function() { return false } document.body.onselectstart = function() { return false } return false } return { makeDraggable: function(element){ element.onmousedown = mouseDown } } }()) function getPosition(e){ var left = 0 var top = 0 while (e.offsetParent){ left += e.offsetLeft top += e.offsetTop e = e.offsetParent } left += e.offsetLeft top += e.offsetTop return {x:left, y:top} }
В коде появилась новая функция getPosition(элемент)
— она получает абсолютные координаты верхнего-правого угла элемента. Функция это стандартная и много где используемая.
Кроме того, при начале переноса останавливается выделение и перенос текста браузером:
document.ondragstart = function() { return false } document.body.onselectstart = function() { return false }
Если этого не сделать, то движение курсора мыши при нажатой кнопке будет не только перемещать элемент, но и, например, выделять текст под собой (стандартная функция выделения текста на странице).
Иногда красивее и удобнее — визуально перемещать не сам элемент, а его клон или макет.
Например, переносимый объект очень сложен, и его передвижение целиком тормозит браузер и выглядит громоздко/неэстетично.
Сам элемент при этом скрывается display/visibility='none'
или просто остается на месте, в зависимости от логики интерфейса.
Переносимый клон инициализуется в начале переноса и уничтожается в конце.
Опускаем элемент
Когда иконка опущена, нам необходимо определить, куда.
В другое хранилище — переместить. В корзину — удалить, и т.п.
Существенная техническая проблема заключается в том, что событие mouseup
сработает не на корзине, а на переносимом элементе, т.к. курсор мыши находится именно над ним.
Поэтому в событии будет информация об элементе непосредственно под курсором. На картинке выше event.target = сердечко
, а корзина в объекте события event
не присутствует.
Определить, что иконка опущена на корзину, можно, сравнив координаты корзины с коорданатами мыши на момент события.
Демо
В момент опускания на корзину выводится сообщение, перемещаемая иконка — в переменной dragObject
, цель переноса (корзина, объект-акцептор) — в переменной currentDropTarget
.
В коде контроллера: функции, тело которых заменено на «…», остались без изменения с прошлого примера.
var dragMaster = (function() { var dragObject var mouseOffset var dropTargets = [] function mouseUp(e){ e = fixEvent(e) for(var i=0; i<dropTargets.length; i++){ var targ = dropTargets[i] var targPos = getPosition(targ) var targWidth = parseInt(targ.offsetWidth) var targHeight = parseInt(targ.offsetHeight) if( (e.pageX > targPos.x) && (e.pageX < (targPos.x + targWidth)) && (e.pageY > targPos.y) && (e.pageY < (targPos.y + targHeight))){ alert("перенесен объект dragObject на акцептор currentDropTarget") } } dragObject = null removeDocumentEventHandlers() } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return dragObject = this mouseOffset = getMouseOffset(this, e) addDocumentEventHandlers() return false } function removeDocumentEventHandlers() { document.onmousemove = null document.onmouseup = null document.ondragstart = null document.body.onselectstart = null } function addDocumentEventHandlers() { document.onmousemove = mouseMove document.onmouseup = mouseUp document.ondragstart = function() { return false } document.body.onselectstart = function() { return false } } function getMouseOffset(target, e) {...} function mouseMove(e) {...} return { makeDraggable: function(element){...}, addDropTarget: function(dropTarget){ dropTargets.push(dropTarget) } } }())
var dragMaster2 = (function() { var dragObject var mouseOffset var dropTargets = [] function mouseUp(e){ e = fixEvent(e) for(var i=0; i<dropTargets.length; i++){ var targ = dropTargets[i] var targPos = getPosition(targ) var targWidth = parseInt(targ.offsetWidth) var targHeight = parseInt(targ.offsetHeight) if( (e.pageX > targPos.x) && (e.pageX < (targPos.x + targWidth)) && (e.pageY > targPos.y) && (e.pageY < (targPos.y + targHeight))){ alert("dragObject was dropped onto currentDropTarget!") } } dragObject = null removeDocumentEventHandlers() } function removeDocumentEventHandlers() { document.onmousemove = null document.onmouseup = null document.ondragstart = null document.body.onselectstart = null } function getMouseOffset(target, e) { var docPos = getPosition(target) return {x:e.pageX - docPos.x, y:e.pageY - docPos.y} } function mouseMove(e){ e = fixEvent(e) with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } return false } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return dragObject = this mouseOffset = getMouseOffset(this, e) addDocumentEventHandlers() return false } function addDocumentEventHandlers() { document.onmousemove = mouseMove document.onmouseup = mouseUp // отменить перенос и выделение текста при клике на тексте document.ondragstart = function() { return false } document.body.onselectstart = function() { return false } } return { makeDraggable: function(element){ element.onmousedown = mouseDown }, addDropTarget: function(dropTarget){ dropTargets.push(dropTarget) } } }()) function getPosition(e){ var left = 0; var top = 0; while (e.offsetParent){ left += e.offsetLeft; top += e.offsetTop; e = e.offsetParent; } left += e.offsetLeft; top += e.offsetTop; return {x:left, y:top}; }
Основных изменений всего три.
Добавлен массив dropTargets
и функция addDropTarget
, которая добавляет в него элементы, на которые можно дропать.
Измененный обработчик mouseUp
теперь проходит в цикле по возможным таким объектам и проверяет, не находится ли курсор внутри ограничивающего объект прямоугольника.
Если да, то демка всего лишь выводит сообщение. Реально приложение, конечно, может сделать более сложные действия.
Кроме того, установка и удаление обработчиков событий для document
выделены в отдельные функции — просто в целях лучшей читаемости.
Индикация переноса над объектом
В удобном интерфейсе мы, скорее всего, захотим как-то показывать посетителю, над каким объектом он сейчас находится.
Единственно место, где это можно сделать — обработчик mouseMove
. Сама проверка, над чем курсор сейчас находится, полностью аналогична mouseUp
.
Однако, так как mouseMove
выполняется при каждом передвижении мыши, его надо максимально оптимизировать.
Функция getPosition
— довольно медленная: она работает с DOM, и ей надо пройти по всей цепочке offsetParent
. Выполнять ее каждый раз при движении мыши для поиска текущего акцептора — все равно что нажать на большой-большой тормоз.
Стандартным выходом в такой ситуации является кеширование координат акцепторов, а точнее — их ограничивающих прямоугольников, так чтобы код mouseMove
был максимально прост.
Пронеси меня над корзиной
Код уже стал довольно длинным, поэтому в листинге ниже повторяющиеся фрагменты заменены на троеточие «…»
var dragMaster = (function() { var dragObject var mouseOffset var dropTargets = [] /* кеш прямоугольников границ акцепторов */ var dropTargetRectangles /* текущий акцептор, над которым объект в данный момент */ var currentDropTarget function cacheDropTargetRectangles() { dropTargetRectangles = /* сделать кеш прямоугольников */ } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return /* начать перенос */ dragObject = this mouseOffset = getMouseOffset(this, e) /* закешировать прямоугольники при начале переноса */ cacheDropTargetRectangles() addDocumentEventHandlers() return false } function getCurrentTarget(e) { var dropTarget = /* взять из кеша прямоугольник, в котором мышь */ return dropTarget /* null, если мы не над акцепторами */ } function mouseMove(e){ /* визуально показать перенос объекта */ with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } /* newTarget = над каким акцептором сейчас объект */ var newTarget = getCurrentTarget(e) /* если ушли со старого акцептора */ if (currentDropTarget && currentDropTarget != newTarget) { /* убрать выделение currentDropTarget */ } /* пришли на новый акцептор (возможно null) */ currentDropTarget = newTarget /* если новый акцептор существует (не null) */ if (newTarget) { /* выделить newTarget */ } return false; } function mouseUp(ev){ if (currentDropTarget) { alert("перенесен объект dragObject на акцептор currentDropTarget") /* убрать выделение с currentDropTarget */ } /* конец операции переноса */ dragObject = null removeDocumentEventHandlers() } function getMouseOffset(target, e) {...} function addDocumentEventHandlers() {...} function removeDocumentEventHandlers() {...} return { ... } }())
Полностью рабочий вариант с кешем и т.п. — здесь:
var dragMaster = (function() { var dragObject var mouseOffset var dropTargets = [] var dropTargetRectangles var currentDropTarget function cacheDropTargetRectangles() { dropTargetRectangles = [] for(var i=0; i<dropTargets.length; i++){ var targ = dropTargets[i]; var targPos = getPosition(targ); var targWidth = parseInt(targ.offsetWidth); var targHeight = parseInt(targ.offsetHeight); dropTargetRectangles.push({ xmin: targPos.x, xmax: targPos.x + targWidth, ymin: targPos.y, ymax: targPos.y + targHeight, dropTarget: targ }) } } function mouseUp(ev){ if (currentDropTarget) { alert("перенесен объект dragObject на акцептор currentDropTarget") showRollOff(currentDropTarget) } dragObject = null removeDocumentEventHandlers() } function getCurrentTarget(e) { for(var i=0; i<dropTargetRectangles.length; i++){ var rect = dropTargetRectangles[i]; if( (e.pageX > rect.xmin) && (e.pageX < rect.xmax) && (e.pageY > rect.ymin) && (e.pageY < rect.ymax)){ return rect.dropTarget } } return null } function mouseMove(e){ e = fixEvent(e) with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } var newTarget = getCurrentTarget(e) if (currentDropTarget && currentDropTarget != newTarget) { showRollOff(currentDropTarget) } currentDropTarget = newTarget if (newTarget) { showRollOn(newTarget) } return false; } function showRollOn(elem) { elem.className = 'uponMe' } function showRollOff(elem) { elem.className = '' } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return dragObject = this mouseOffset = getMouseOffset(this, e) cacheDropTargetRectangles() addDocumentEventHandlers() return false } function getMouseOffset(target, e) {...} function addDocumentEventHandlers() {...} function removeDocumentEventHandlers() {...} return { ... } }())
var dragMaster3 = (function() { var dragObject var mouseOffset var dropTargets = [] var dropTargetRectangles var currentDropTarget function cacheDropTargetRectangles() { dropTargetRectangles = [] for(var i=0; i<dropTargets.length; i++){ var targ = dropTargets[i]; var targPos = getPosition(targ); var targWidth = parseInt(targ.offsetWidth); var targHeight = parseInt(targ.offsetHeight); dropTargetRectangles.push({ xmin: targPos.x, xmax: targPos.x + targWidth, ymin: targPos.y, ymax: targPos.y + targHeight, dropTarget: targ }) } } function mouseUp(ev){ if (currentDropTarget) { alert("droped dragObject into curTarget") showRollOff(currentDropTarget) } dragObject = null removeDocumentEventHandlers() } function removeDocumentEventHandlers() { document.onmousemove = null document.onmouseup = null document.ondragstart = null document.body.onselectstart = null } function getMouseOffset(target, e) { var docPos = getPosition(target) return {x:e.pageX - docPos.x, y:e.pageY - docPos.y} } function getCurrentTarget(e) { for(var i=0; i<dropTargetRectangles.length; i++){ var rect = dropTargetRectangles[i]; if( (e.pageX > rect.xmin) && (e.pageX < rect.xmax) && (e.pageY > rect.ymin) && (e.pageY < rect.ymax)){ return rect.dropTarget } } return null } function mouseMove(e){ e = fixEvent(e) with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } var newTarget = getCurrentTarget(e) if (currentDropTarget && currentDropTarget != newTarget) { showRollOff(currentDropTarget) } currentDropTarget = newTarget if (newTarget) { showRollOn(newTarget) } return false; } function showRollOn(elem) { elem.className = 'uponMe' } function showRollOff(elem) { elem.className = '' } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return dragObject = this mouseOffset = getMouseOffset(this, e) cacheDropTargetRectangles() addDocumentEventHandlers() return false } function addDocumentEventHandlers() { document.onmousemove = mouseMove document.onmouseup = mouseUp // отменить перенос и выделение текста при клике на тексте document.ondragstart = function() { return false } document.body.onselectstart = function() { return false } } return { makeDraggable: function(element){ element.onmousedown = mouseDown }, addDropTarget: function(dropTarget){ dropTargets.push(dropTarget) } } }()) function getPosition(e){ var left = 0; var top = 0; while (e.offsetParent){ left += e.offsetLeft; top += e.offsetTop; e = e.offsetParent; } left += e.offsetLeft; top += e.offsetTop; return {x:left, y:top}; }
Отделяем начало drag’n’drop от простого клика
Следуя общему принципу отделения мух от котлет — лучше отделить простой клик на объекте от начала drag and drop.
Еще одна причина — дорогая инициализация drag & drop: нужно прокешировать все возможные акцепторы. Совершенно не обязательно это делать на mousedown
, если имеем простой клик.
Как отделить? Очень просто:
- При
mousedown
запомнить координаты и объект, но пока не начинать перенос - Если произошло событие
mouseup
— это был всего лишь клик, сбросить координаты - В
mousemove
проверить: если есть запомненные координаты и курсор отошел от них хотя бы на 2 пикселя — начать перенос
Демо
В коде этой демки стоит расстояние не 2, а 25 пикселей, в целях наглядности происходящего.
До перемещения курсора на 25 пикселей вверх или вниз перенос не начнется.
Отрыв от земли — после перемещения на 25 или больше пикселей
Код демо:
var dragMaster = (function() { ... var mouseDownAt function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return mouseDownAt = { x: e.pageX, y: e.pageY, dragObject: this } addDocumentEventHandlers() return false } function mouseMove(e){ e = fixEvent(e) if (mouseDownAt) { if (Math.abs(mouseDownAt.x-e.pageX)<25 && Math.abs(mouseDownAt.y-e.pageY)<25) { // слишком близко, возможно это клик return } // курсор нажатой мыши отвели далеко - начинаем перенос dragObject = mouseDownAt.dragObject mouseOffset = getMouseOffset(dragObject, mouseDownAt.x, mouseDownAt.y) cacheDropTargetRectangles() // запомненные координаты нам больше не нужны mouseDownAt = null } showDrag(e) // показать перенос return false; } function mouseUp(ev){ if (!dragObject) { // ничего не начали нести, был просто клик mouseDownAt = null } else { // чего-то несем - обрабатываем конец переноса if (currentDropTarget) { alert("перенесен объект dragObject на акцептор currentDropTarget") showRollOff(currentDropTarget) } dragObject = null } // (возможный) drag and drop завершен removeDocumentEventHandlers() } function showDrag(e) { // перенести объект with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } // подсветить акцептор var newTarget = getCurrentTarget(e) if (currentDropTarget && currentDropTarget != newTarget) { showRollOff(currentDropTarget) } currentDropTarget = newTarget if (newTarget) { showRollOn(newTarget) } } function getMouseOffset(target, x, y) { // для удобства поменяли синтаксис: x/y вместо event var docPos = getPosition(target) return {x:x - docPos.x, y:y - docPos.y} } function cacheDropTargetRectangles() {...} function removeDocumentEventHandlers() {...} function getCurrentTarget(e) {...} function showRollOn(elem) {...} function showRollOff(elem) {...} function addDocumentEventHandlers() {...} return { ... } }())
var dropMove2 = (function() { var dragObject var mouseOffset var mouseDownAt var dropTargets = [] var dropTargetRectangles var currentDropTarget function cacheDropTargetRectangles() { dropTargetRectangles = [] for(var i=0; i<dropTargets.length; i++){ var targ = dropTargets[i]; var targPos = getPosition(targ); var targWidth = parseInt(targ.offsetWidth); var targHeight = parseInt(targ.offsetHeight); dropTargetRectangles.push({ xmin: targPos.x, xmax: targPos.x + targWidth, ymin: targPos.y, ymax: targPos.y + targHeight, dropTarget: targ }) } } function mouseUp(ev){ if (!dragObject) { mouseDownAt = null } else { if (currentDropTarget) { alert("перенесен объект dragObject на акцептор currentDropTarget") showRollOff(currentDropTarget) } dragObject = null } removeDocumentEventHandlers() } function removeDocumentEventHandlers() { document.onmousemove = null document.onmouseup = null document.ondragstart = null document.body.onselectstart = null } function getMouseOffset(target, x, y) { var docPos = getPosition(target) return {x:x - docPos.x, y:y - docPos.y} } function getCurrentTarget(e) { for(var i=0; i<dropTargetRectangles.length; i++){ var rect = dropTargetRectangles[i]; if( (e.pageX > rect.xmin) && (e.pageX < rect.xmax) && (e.pageY > rect.ymin) && (e.pageY < rect.ymax)){ return rect.dropTarget } } return null } function mouseMove(e){ e = fixEvent(e) if (mouseDownAt) { if (Math.abs(mouseDownAt.x-e.pageX)<25 && Math.abs(mouseDownAt.y-e.pageY)<25) { return } dragObject = mouseDownAt.dragObject mouseOffset = getMouseOffset(dragObject, mouseDownAt.x, mouseDownAt.y) cacheDropTargetRectangles() mouseDownAt = null } showDrag(e) return false; } function showDrag(e) { with(dragObject.style) { position = 'absolute' top = e.pageY - mouseOffset.y + 'px' left = e.pageX - mouseOffset.x + 'px' } var newTarget = getCurrentTarget(e) if (currentDropTarget && currentDropTarget != newTarget) { showRollOff(currentDropTarget) } currentDropTarget = newTarget if (newTarget) { showRollOn(newTarget) } } function showRollOn(elem) { elem.className = 'uponMe' } function showRollOff(elem) { elem.className = '' } function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return mouseDownAt = { x: e.pageX, y: e.pageY, dragObject: this } addDocumentEventHandlers() return false } function addDocumentEventHandlers() { document.onmousemove = mouseMove document.onmouseup = mouseUp // отменить перенос и выделение текста при клике на тексте document.ondragstart = function() { return false } document.body.onselectstart = function() { return false } } return { makeDraggable: function(element){ element.onmousedown = mouseDown }, addDropTarget: function(dropTarget){ dropTargets.push(dropTarget) } } }()) function getPosition(e){ var left = 0; var top = 0; while (e.offsetParent){ left += e.offsetLeft; top += e.offsetTop; e = e.offsetParent; } left += e.offsetLeft; top += e.offsetTop; return {x:left, y:top}; }
Оптимизация
Бывает, что возможных акцепторов очень много. Тогда код, кеширующий прямоугольники при начале переноса, будет тормозить.
Визуально это проявляется как задержка от клика на объекте до его фактического переноса — потому что долго, с 100% поеданием одного ядра CPU обрабатывается mousedown
.
Речь тут идет о 100 акцепторах или больше — например, при переносе между длинными списками или деревьями. Хотя какие-то тормоза могут быть заметны и от 50.
Принципиальных решений здесь два.
Смена способа вычисления координат
document.elementFromPoint(x,y)
Этот малоизвестный метод работает во всех браузерах и возвращает элемент по координатам на странице.
Firefox/IE используют для этого clientX/Y
, а Opera, Chrome и Safari — pageX/Y
.
Возвращенный элемент является самым глубоко вложенным на точке с координатами (x,y)
. Это может быть текстовый узел в том числе.
Следуя по цепочке родителей, легко найти нужного акцептора.
На время вызова elementFromPoint
необходимо спрятать переносимый элемент, чтобы он не закрывал акцептора.
Псевдокод будет выглядеть так:
function getCurrentTarget(e) { dragObject.style.display = 'none' // спрятать if (navigator.userAgent.match('MSIE') || navigator.userAgent.match('Gecko')) { // IE || FF var elem = document.elementFromPoint(e.clientX,e.clientY) } else { var elem = document.elementFromPoint(e.pageX,e.pageY) } dragObject.style.display = '' // показать побыстрее while (elem!= null) { if (elem является акцептором) { return elem } elem = elem.parentNode } // не нашли return null }
Вычисление по известным размерам
Если структура объекта переноса нам известна — возможно, кешировать каждый элемент ни к чему?
Например, мы переносим объект в список акцепторов:
<ul class="articles-list"> <li>...</li> <li>...</li> <li>...</li> </ul>
И мы знаем, что каждый элемент имеет фиксированную высоту и отступы:
.articles-list li { height: 20px; margin: 0px; padding: 0px; }
В таком случае, можно вычислить номер LI, разделив общую высоту контейнера на высоту акцептора.
function getCurrentTarget(e) { var rect = /* ограничивающий прямоугольник для UL */ // (ulX, ulY) - координаты относительно верхнего-левого угла контейнера var ulX= e.pageX - rect.xmin var ulY = e.pageY - rect.ymin if ( ulX < 0 || ulX > rect.xmax-rect.xmin || ulY < 0 || ulY > rect.ymax-rect.ymin) { /* событие за границами списка */ return null } /* по 20px на каждого ребенка-акцептора */ var childNum = ulY / 20 return rect.dropTarget.childNodes[childNum] }
Конечно, такая оптимизация возможна не всегда и зависит от конкретной задачи.
Убрать элемент из-под курсора
В ряде задач допустимо не сохранять переносимый элемент под курсором, а передвинуть его на несколько пикселей в сторону, то mouseUp
и mouseMove
будут срабатывать уже на акцепторе, который станет возможным получить из event.target
.
Да, получится не так красиво, но это может быть единственным выходом.
Упростить объект переноса
Можно переносить не сам объект, а его «аватар», схематическое изображение.
Это может быть полезно:
- для ускорения отрисовки
- например, при переносе сложного блока можно переносить такого же размера серый прямоугольник
- чтобы убрать элемент из-под курсора
- переносить сам объект в стороне от мыши — может быть некрасиво. А обозначить перенос небольшой аватаркой рядом с курсором — уже другое дело.
Рефакторинг
Код dragMaster
‘а на текущий момент сочетает весь функционал по отслеживанию переноса, отображению и опусканию на акцептор.
Целесообразно его разделить на три компоненты: переносимые объекты, цели переноса (акцепторы) и менеджер, который следит, что куда переносится.
DragObject
Объект DragObject
— обобщенный переносимый объект, который привязывается к DOM-элементу. Он представлен на следующем листинге.
function DragObject(element) { element.dragObject = this dragMaster.makeDraggable(element) var rememberPosition var mouseOffset this.onDragStart = function(offset) { var s = element.style rememberPosition = {top: s.top, left: s.left, position: s.position} s.position = 'absolute' mouseOffset = offset } this.hide = function() { element.style.display = 'none' } this.show = function() { element.style.display = '' } this.onDragMove = function(x, y) { element.style.top = y - mouseOffset.y +'px' element.style.left = x - mouseOffset.x +'px' } this.onDragSuccess = function(dropTarget) { } this.onDragFail = function() { var s = element.style s.top = rememberPosition.top s.left = rememberPosition.left s.position = rememberPosition.position } this.toString = function() { return element.id } }
Использование:
new DragObject(element)
Достаточно одного вызова new
, т.к функция DragObject
добавляет конструируемый объект к element
в свойство element.dragObject
:
... element.dragObject = this ...
Методы:
- onDragStart(offset)
- Вызывается при начале переноса. В текущей реализации отрывает объект «от земли» и запоминает текущую позицию в
rememberPosition
и сдвиг курсора мыши от левого-верхнего угла объекта вmouseOffset
.В другой реализации может показывать перенос как-то по-другому, например создавать «переносимый клон» объекта.
- onDragMove(x, y)
- Вызывается при переносе объекта в координаты (x,y). Отображает перенос.
- onDragFail()
- Обрабатывает неудачный перенос. В текущей реализации возвращает объект на старые координаты.
Вообще говоря, можно делать это с анимацией, показывая как объект «перелетает» на старое место.
- onDragSuccess(dropTarget)
- Обрабатывает успешный перенос. В текущей реализации обработка успешного переноса целиком сосредоточена у принимающего объекта
DropTaget
, поэтому эта функция пустая. - show/hide()
- Показать/спрятать переносимый объект — вспомогательные методы
DropTarget
DropTarget
— обобщенный объект-акцептор, потенциальная цель переноса. Может быть большим контейнером или маленьким элементом — не важно.
Как показано дальше, поддерживаются вложенные DropTarget
: объект будет положен туда, куда следует, вне зависимости от степени вложенности.
function DropTarget(element) { element.dropTarget = this this.canAccept = function(dragObject) { return true } this.accept = function(dragObject) { this.onLeave() dragObject.hide() alert("Акцептор '"+this+"': принял объект '"+dragObject+"'") } this.onLeave = function() { element.className = '' } this.onEnter = function() { element.className = 'uponMe' } this.toString = function() { return element.id } }
Методов у DropTarget
поменьше, чем у DragObject
. Еще бы, акцептору ведь не нужно анимировать собственный перенос.
- canAccept(dragObject)
- При проносе объекта над
DropTarget
,dragMaster
спросит у акцептора, может ли он принятьdragObject
. Если нет —dragMaster
проигнорирует этот акцептор.В текущей реализации всегда возвращает
true
, то есть положить можно. Вообще говоря, может проверять класс переносимого объекта:this.canAccept = function(dragObject) { // могу принять только объекты типа TreeNodeDragObject return dragObject instanceof TreeNodeDragObject }
- accept(dragObject)
- Принимает переносимый объект. Объект может быть перемещен(в другой каталог) или уничтожен(корзина) — зависит от вашей логики обработки переноса.
В конкретном приложении стоит посмотреть особо, какую часть логики конца переноса стоит поместить
DragObject#onDragSuccess
, а какую — вDropTarget#accept
. Как правило, основную логику переноса удобно сосредоточить уDropTarget
. - onLeave/onEnter
- Анимация возможности положить объект на акцептор. Как правило, акцептор при этом подсвечивается. Эти методы будут вызваны только если акцептор может принять (
canAccept
) переносимый объект.
dragMaster
Ну и, наконец, похудевший и лишенный большинства обязанностей dragMaster
.
var dragMaster = (function() { var dragObject var mouseDownAt var currentDropTarget function mouseDown(e) { e = fixEvent(e) if (e.which!=1) return mouseDownAt = { x: e.pageX, y: e.pageY, element: this } addDocumentEventHandlers() return false } function mouseMove(e){ e = fixEvent(e) // (1) if (mouseDownAt) { if (Math.abs(mouseDownAt.x-e.pageX)<5 && Math.abs(mouseDownAt.y-e.pageY)<5) { return false } // Начать перенос var elem = mouseDownAt.element // текущий объект для переноса dragObject = elem.dragObject // запомнить, с каких относительных координат начался перенос var mouseOffset = getMouseOffset(elem, mouseDownAt.x, mouseDownAt.y) mouseDownAt = null // запомненное значение больше не нужно, сдвиг уже вычислен dragObject.onDragStart(mouseOffset) // начали } // (2) dragObject.onDragMove(e.pageX, e.pageY) // (3) var newTarget = getCurrentTarget(e) // (4) if (currentDropTarget != newTarget) { if (currentDropTarget) { currentDropTarget.onLeave() } if (newTarget) { newTarget.onEnter() } currentDropTarget = newTarget } // (5) return false } function mouseUp(){ if (!dragObject) { // (1) mouseDownAt = null } else { // (2) if (currentDropTarget) { currentDropTarget.accept(dragObject) dragObject.onDragSuccess(currentDropTarget) } else { dragObject.onDragFail() } dragObject = null } // (3) removeDocumentEventHandlers() } function getMouseOffset(target, x, y) { var docPos = getOffset(target) return {x:x - docPos.left, y:y - docPos.top} } function getCurrentTarget(e) { // спрятать объект, получить элемент под ним - и тут же показать опять if (navigator.userAgent.match('MSIE') || navigator.userAgent.match('Gecko')) { var x=e.clientX, y=e.clientY } else { var x=e.pageX, y=e.pageY } // чтобы не было заметно мигание - максимально снизим время от hide до show dragObject.hide() var elem = document.elementFromPoint(x,y) dragObject.show() // найти самую вложенную dropTarget while (elem) { // которая может принять dragObject if (elem.dropTarget && elem.dropTarget.canAccept(dragObject)) { return elem.dropTarget } elem = elem.parentNode } // dropTarget не нашли return null } function addDocumentEventHandlers() { document.onmousemove = mouseMove document.onmouseup = mouseUp document.ondragstart = document.body.onselectstart = function() {return false} } function removeDocumentEventHandlers() { document.onmousemove = document.onmouseup = document.ondragstart = document.body.onselectstart = null } return { makeDraggable: function(element){ element.onmousedown = mouseDown } } }())
- mouseDown(e)
- Метод не изменился. Он запоминает позицию, на которой произошло нажатие кнопки мыши
mouseDownAt
и добавляет остальные обработчики слежения за переносомaddDocumentEventHandlers()
. - mouseMove(e)
- Этот обработчик присоединяется к
document
в моментmouseDown
.-
Если в момент его срабатывания есть запомненное нажатие и курсор отошел больше чем на 5 пикселей — значит начался перенос.
DragObject'
у это сообщается вызовомonDragStart
с передачей текущего сдвига курсора относительно левого-верхнего углаmouseOffset
. - Объект информируется о текущих координатах переноса.
- Вычисляем текущий акцептор при помощи модифицированного метода
getCurrentTarget
(см. выше в разделе «Оптимизация»). - Обработать нового акцептора и уход со старого акцептора.
- Вернуть
false
для блокирования действий браузера и всплывания событияmouseMove
-
Если в момент его срабатывания есть запомненное нажатие и курсор отошел больше чем на 5 пикселей — значит начался перенос.
- mouseUp()
- Этот обработчик завершает (возможный) перенос. Обратите внимание — само событие в данном случае не нужно. Весь процесс переноса отслеживается событием
mouseMove
.- Если объект переноса не установлен, значит перед этим было простое нажатие
mouseDown
, элемент не отнесли на 5 пикселей в сторону — это не drag’n’drop. - При наличии акцептора — перенос успешно завершается, если акцептора нет — отменяется.
- В любом случае в конце все обработчики с документа снимаются
- Если объект переноса не установлен, значит перед этим было простое нажатие
Функция определения позиции элемента getPosition
заменена на более точный вариант getOffset
, описанный в статье про определение координат.
Итоговый Drag’n’Drop фреймворк
Скачать пакет из итогового фреймворка и демок можно здесь.
В комплекте — пара демок. На этой странице обе подключены в iframe’ах.
Обычный перенос
Открыть в отдельном окне: demo.html
Вложенные акцепторы
В этой демке акцепторами являются два вложенных DIV’а.
Открыть в отдельном окне: nested.html
Некоторые дополнительные рецепты
Перемещения акцепторов в процессе переноса
В процессе переноса акцепторы объекты могут сдвигаться, освобождая место.
Если вы используете кеш координат акцепторов, то при этом производится соответствующее обновление кеша, но вместо полного перевычисления обновляются только координаты сдвинувшихся объектов.
Например, при раздвижении списка вниз — увеличиваются Y-координаты всех сдвинувшихся LI.
При этом для удобного обновления кеш делается не массивом, а объектом с доступом по ID элемента, так чтобы можно было легко обновить именно нужные координаты.
Drag and drop «между».
Иногда, например, при смене позиции элемента в списке, объект переносится не на акцептор, а между акцепторами. Как правило, «между» — имеется в виду по высоте.
Для этого логику определения currentDropTarget
нужно поменять. Возможно два варианта:
- Допустим перенос как между, так и над
- В этом случае акцептор делится на 3 части по высоте
clientHeight
: 25% — 50% — 25%, и определяется попадание координаты события на нужную часть. - Перенос только между
- Акцептор делится на две части: 50% — 50%
Кроме того, в дополнение к текущему акцептору currentDropTarget
добавляется флаг, обозначающий, куда относительно акцептора происходит перенос.
Индикацию «переноса между» удобнее всего делать либо раздвижением элементов, либо показом полосы-индикатора border-top/border-bottom
, как показано на рисунке ниже:
Анимация отмены переноса
Обычно при переносе объекта куда-либо посетитель может просто отпустить его в любом месте.
При этом drag and drop фреймворк анимирует отмену переноса. Один из частых вариантов — скольжение объекта обратно к исходному месту, откуда его взяли.
Конечно, для этого исходное место необходимо запомнить.
Если перетаскивается аватарка(клон), то можно его и просто уничтожить.
Проверка прав
Совершенно не факт, что любой объект можно перенести на любой аксептор.
Как правило, все с точностью наоборот.
Акцептор может быть недоступен по двум причинам:
- либо это несовпадение типов, для этого drag and drop должен предусматривать различные типы объектов/акцепторов и проверку их соответствия
- либо у посетителя недостаточно для этого прав, в рамках наложенных CMS ограничений
При переносе над недоступным акцептором — getCurrentTarget
просто возвращает null.
Иногда проверку прав и результата переноса необходимо делать на сервере. Как правило, такую проверку выполняют только при mouseUp
, чтобы не нагружать сервер излишними запросами во время mouseMove
.
Здесь используется два подхода
- Синхронный XmlHttpRequest
- Запрос отправляется синхронно, чтобы не нарушать общий поток выполнения. Все хорошо, вот только браузер видимо подвисает при отпускании кнопки мыши. Да и другие недостатки у синхронного запроса есть.
- Отложенная отмена переноса
- Более продвинутый и удобный вариант. Основан на «оптимистичном» сценарии, по которому перенос, как правило, проходит успешно.
- Посетитель кладет объект туда, куда он хочет.
- Фреймворк перемещает объект, и затем «в фоне» делает асинхронный запрос к серверу. На запрос вешается
callback
, содержащий информацию, откуда был перенесен объект. - Сервер обрабатывает перенос и возвращает ответ, все ли в порядке.
- Если нет —
callback
выводит ошибку и объект возвращается туда, откуда был перенесен.
Резюме
Вы узнали основные принципы и особенности реализации drag and drop в яваскрипт.
Рассмотрели ряд оптимизаций и различные организации переноса на уровне UI.
Все современные javascript-библиотеки используют описанную схему и какие-то (не все) из оптимизаций.
Пакет из итогового фреймворка и демок доступен здесь.