libs:
- getCoords.js
Эта статья представляет собой продолжение главы info:drag-and-drop-objects. Она посвящена более гибкой и расширяемой реализации переноса.
Рекомендуется прочитать указанную главу перед тем, как двигаться дальше.
В сложных приложениях Drag'n'Drop обладает рядом особенностей:
-
Перетаскиваются элементы из зоны переноса
dragZone
в зону-цельdropTarget
. При этом сама зона не переносится.Например -- два списка, нужен перенос элемента из одного в другой. В этом случае один список является зоной переноса, второй -- зоной-целью.
Возможно, что перенос осуществляется внутри одного и того же списка. При этом
dragZone == dropTarget
. -
На странице может быть несколько разных зон переноса и зон-целей.
-
Обработка завершения переноса может быть асинхронной, с уведомлением сервера.
-
Должно быть легко добавить новый тип зоны переноса или зоны-цели, а также расширить поведение существующей.
-
Фреймворк для переноса должен быть расширяемым с учётом сложных сценариев.
Всё это вполне реализуемо. Но для этого фреймворк, описанный в статье info:drag-and-drop-objects, нужно отрефакторить, и разделить на сущности.
Всего будет 4 сущности:
DragZone
: Зона переноса. С нее начинается перенос. Она принимает нажатие мыши и генерирует аватар нужного типа.
DragAvatar
: Переносимый объект. Предоставляет доступ к информации о том, что переносится. Умеет двигать себя по экрану. В зависимости от вида переноса, может что-то делать с собой в конце, например, самоуничтожаться.
DropTarget
: Зона-цель, на которую можно положить. В процессе переноса аватара над ней умеет рисовать на себе предполагаемое "место приземления". Обрабатывает окончание переноса.
dragManager
: Единый объект, который стоит над всеми ними, ставит обработчики mousedown/mousemove/mouseup
и управляет процессом. В терминах ООП, это не класс, а объект-синглтон, поэтому он с маленькой буквы.
На макете страницы ниже возможен перенос студентов из левого списка -- вправо, в одну из команд или в "корзину":
Здесь левый список является зоной переноса ListDragZone
, а правые списки -- это несколько зон-целей ListDropTarget
. Кроме того, корзина также является зоной-целью отдельного типа RemoveDropTarget
.
В этой статье мы реализуем пример, когда узлы дерева можно переносить внутри него. То есть, дерево, которое является одновременно TreeDragZone
и TreeDropTarget
.
Структура дерева будет состоять из вложенных списков с заголовком в SPAN
:
<ul>
<li><span>Заголовок 1</span>
<ul>
<li><span>Заголовок 1.1</span></li>
<li><span>Заголовок 1.2</span></li>
...
</ul>
</li>
...
</ul>
При переносе:
- Для аватара нужно клонировать заголовок узла, на котором было нажатие.
- Узлы, на которые можно положить, при переносе подсвечиваются красным.
- Нельзя перенести узел сам в себя или в своего потомка.
- Дерево само поддерживает сортировку по алфавиту среди узлов.
- Обязательна расширяемость кода, поддержка большого количества узлов и т.п.
[iframe height=450 border=1 src="dragTree"]
Обязанность dragManager
-- обработка событий мыши и координация всех остальных сущностей в процессе переноса.
Готовьтесь, дальше будет много кода с комментариями.
Следующий код должен быть очевиден по смыслу, если вы читали предыдущую статью. Объект взят оттуда, и из него изъята лишняя функциональность, которая перенесена в другие сущности.
Если вызываемые в нём методы onDrag*
непонятны -- смотрите далее, в описание остальных объектов.
[js src="DragManager.js"]
Основная задача DragZone
-- создать аватар и инициализировать его. В зависимости от места, где произошел клик, аватар получит соответствующий подэлемент зоны.
Метод для создания аватара _makeAvatar
вынесен отдельно, чтобы его легко можно было переопределить и подставить собственный тип аватара.
[js src="DragZone.js"]
Объект зоны переноса для дерева, по существу, не вносит ничего нового, по сравнению с DragZone
.
Он только переопределяет _makeAvatar
для создания TreeDragAvatar
.
[js src="TreeDragZone.js"]
Аватар создается только зоной переноса при начале Drag'n'Drop. Он содержит всю необходимую информацию об объекте, который переносится.
В дальнейшем вся работа происходит только с аватаром, сама зона напрямую не вызывается.
У аватара есть три основных свойства:
_dragZone
: Зона переноса, которая его создала.
_dragZoneElem
: Элемент, соответствующий аватару в зоне переноса. По умолчанию -- DOM-элемент всей зоны. Это подходит в тех случаях, когда зона перетаскивается только целиком.
При инициализации аватара значение этого свойства может быть уточнено, например изменено на подэлемент списка, который перетаскивается.
_elem
: Основной элемент аватара, который будет двигаться по экрану. По умолчанию равен _dragZoneElem
, т.е мы переносим сам элемент.
При инициализации мы можем также склонировать `_dragZoneElem`, или создать своё красивое представление переносимого элемента и поместить его в `_elem`.
[js src="DragAvatar.js"]
Основные изменения -- в методе initFromEvent
, который создает аватар из узла, на котором был клик.
Обратите внимание, возможно что клик был не на заголовке SPAN
, а просто где-то на дереве. В этом случае initFromEvent
возвращает false
и перенос не начинается.
[js src="TreeDragAvatar.js"]
Именно на DropTarget
ложится работа по отображению предполагаемой "точки приземления" аватара, а также, по завершению переноса, обработка результата.
Как правило, DropTarget
принимает переносимый узел в себя, а вот как конкретно организован процесс вставки -- нужно описать в классе-наследнике. Разные типы зон делают разное при вставке: TreeDropTarget
вставляет элемент в качестве потомка, а RemoveDropTarget
-- удаляет.
[js src="DropTarget.js"]
Как видно, из кода выше, по умолчанию DropTarget
занимается только отслеживанием и индикацией "точки приземления". По умолчанию, единственной возможной "точкой приземления" является сам элемент зоны. В более сложных ситуациях это может быть подэлемент.
Для применения в реальности необходимо как минимум переопределить обработку результата переноса в onDragEnd
.
TreeDropTarget
содержит код, специфичный для дерева:
- Индикацию переноса над элементом: методы
_showHoverIndication
и_hideHoverIndication
. - Получение текущей точки приземления
_targetElem
в методе_getTargetElem
. Ей может быть только заголовок узла дерева, причем дополнительно проверяется, что это не потомок переносимого узла. - Обработка успешного переноса в
onDragEnd
, вставка исходного узлаavatar.dragZoneElem
в узел, соответствующий_targetElem
.
[js src="TreeDropTarget.js"]
Реализация Drag'n'Drop оказалась отличным способом применить ООП в JavaScript.
Исходный код примера целиком находится в песочнице.
-
Синглтон
dragManager
и классыDrag*
задают общий фреймворк. От них наследуются конкретные объекты. Для создания новых зон достаточно унаследовать стандартные классы и переопределить их. -
Мини-фреймворк для Drag'n'Drop, который здесь представлен, является переписанным и обновленным вариантом реальной библиотеки, на основе которой было создано много успешных скриптов переноса.
В зависимости от ваших потребностей, вы можете расширить его, добавить перенос нескольких объектов одновременно, поддержку событий и другие возможности.
-
На сегодняшний день в каждом серьезном фреймворке есть библиотека для Drag'n'Drop. Она работает похожим образом, но сделать универсальный перенос -- штука непростая. Зачастую он перегружен лишним функционалом, либо наоборот -- недостаточно расширяем в нужных местах. Понимание, как это все может быть устроено, на примере этой статьи, может помочь в адаптации существующего кода под ваши потребности.