Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
171 lines (102 sloc) 13.2 KB

libs:

  • getCoords.js

Применяем ООП: Drag'n'Drop++

Эта статья представляет собой продолжение главы info:drag-and-drop-objects. Она посвящена более гибкой и расширяемой реализации переноса.

Рекомендуется прочитать указанную главу перед тем, как двигаться дальше.

В сложных приложениях Drag'n'Drop обладает рядом особенностей:

  1. Перетаскиваются элементы из зоны переноса dragZone в зону-цель dropTarget. При этом сама зона не переносится.

    Например -- два списка, нужен перенос элемента из одного в другой. В этом случае один список является зоной переноса, второй -- зоной-целью.

    Возможно, что перенос осуществляется внутри одного и того же списка. При этом dragZone == dropTarget.

  2. На странице может быть несколько разных зон переноса и зон-целей.

  3. Обработка завершения переноса может быть асинхронной, с уведомлением сервера.

  4. Должно быть легко добавить новый тип зоны переноса или зоны-цели, а также расширить поведение существующей.

  5. Фреймворк для переноса должен быть расширяемым с учётом сложных сценариев.

Всё это вполне реализуемо. Но для этого фреймворк, описанный в статье 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

Обязанность dragManager -- обработка событий мыши и координация всех остальных сущностей в процессе переноса.

Готовьтесь, дальше будет много кода с комментариями.

Следующий код должен быть очевиден по смыслу, если вы читали предыдущую статью. Объект взят оттуда, и из него изъята лишняя функциональность, которая перенесена в другие сущности.

Если вызываемые в нём методы onDrag* непонятны -- смотрите далее, в описание остальных объектов.

[js src="DragManager.js"]

DragZone

Основная задача DragZone -- создать аватар и инициализировать его. В зависимости от места, где произошел клик, аватар получит соответствующий подэлемент зоны.

Метод для создания аватара _makeAvatar вынесен отдельно, чтобы его легко можно было переопределить и подставить собственный тип аватара.

[js src="DragZone.js"]

TreeDragZone

Объект зоны переноса для дерева, по существу, не вносит ничего нового, по сравнению с DragZone.

Он только переопределяет _makeAvatar для создания TreeDragAvatar.

[js src="TreeDragZone.js"]

DragAvatar

Аватар создается только зоной переноса при начале Drag'n'Drop. Он содержит всю необходимую информацию об объекте, который переносится.

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

У аватара есть три основных свойства:

_dragZone : Зона переноса, которая его создала.

_dragZoneElem : Элемент, соответствующий аватару в зоне переноса. По умолчанию -- DOM-элемент всей зоны. Это подходит в тех случаях, когда зона перетаскивается только целиком. При инициализации аватара значение этого свойства может быть уточнено, например изменено на подэлемент списка, который перетаскивается.

_elem : Основной элемент аватара, который будет двигаться по экрану. По умолчанию равен _dragZoneElem, т.е мы переносим сам элемент.

При инициализации мы можем также склонировать `_dragZoneElem`, или создать своё красивое представление переносимого элемента и поместить его в `_elem`.

[js src="DragAvatar.js"]

TreeDragAvatar

Основные изменения -- в методе initFromEvent, который создает аватар из узла, на котором был клик.

Обратите внимание, возможно что клик был не на заголовке SPAN, а просто где-то на дереве. В этом случае initFromEvent возвращает false и перенос не начинается.

[js src="TreeDragAvatar.js"]

DropTarget

Именно на DropTarget ложится работа по отображению предполагаемой "точки приземления" аватара, а также, по завершению переноса, обработка результата.

Как правило, DropTarget принимает переносимый узел в себя, а вот как конкретно организован процесс вставки -- нужно описать в классе-наследнике. Разные типы зон делают разное при вставке: TreeDropTarget вставляет элемент в качестве потомка, а RemoveDropTarget -- удаляет.

[js src="DropTarget.js"]

Как видно, из кода выше, по умолчанию DropTarget занимается только отслеживанием и индикацией "точки приземления". По умолчанию, единственной возможной "точкой приземления" является сам элемент зоны. В более сложных ситуациях это может быть подэлемент.

Для применения в реальности необходимо как минимум переопределить обработку результата переноса в onDragEnd.

TreeDropTarget

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. Она работает похожим образом, но сделать универсальный перенос -- штука непростая. Зачастую он перегружен лишним функционалом, либо наоборот -- недостаточно расширяем в нужных местах. Понимание, как это все может быть устроено, на примере этой статьи, может помочь в адаптации существующего кода под ваши потребности.