Learn javascript drag and drop

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

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Эта статья представляет собой продолжение главы Мышь: Drag’n’Drop более глубоко. Она посвящена более гибкой и расширяемой реализации переноса.

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

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

  1. Перетаскиваются элементы из зоны переноса dragZone в зону-цель dropTarget . При этом сама зона не переносится. Например – два списка, нужен перенос элемента из одного в другой. В этом случае один список является зоной переноса, второй – зоной-целью. Возможно, что перенос осуществляется внутри одного и того же списка. При этом dragZone == dropTarget .
  2. На странице может быть несколько разных зон переноса и зон-целей.
  3. Обработка завершения переноса может быть асинхронной, с уведомлением сервера.
  4. Должно быть легко добавить новый тип зоны переноса или зоны-цели, а также расширить поведение существующей.
  5. Фреймворк для переноса должен быть расширяемым с учётом сложных сценариев.

Всё это вполне реализуемо. Но для этого фреймворк, описанный в статье Мышь: Drag’n’Drop более глубоко, нужно отрефакторить, и разделить на сущности.

Основные сущности

DragZone Зона переноса. С неё начинается перенос. Она принимает нажатие мыши и генерирует аватар нужного типа. DragAvatar Переносимый объект. Предоставляет доступ к информации о том, что переносится. Умеет двигать себя по экрану. В зависимости от вида переноса, может что-то делать с собой в конце, например, самоуничтожаться. DropTarget Зона-цель, на которую можно положить. В процессе переноса аватара над ней умеет рисовать на себе предполагаемое «место приземления». Обрабатывает окончание переноса. dragManager Единый объект, который стоит над всеми ними, ставит обработчики mousedown/mousemove/mouseup и управляет процессом. В терминах ООП, это не класс, а объект-синглтон, поэтому он с маленькой буквы.

На макете страницы ниже возможен перенос студентов из левого списка – вправо, в одну из команд или в «корзину»:

Здесь левый список является зоной переноса ListDragZone , а правые списки – это несколько зон-целей ListDropTarget . Кроме того, корзина также является зоной-целью отдельного типа RemoveDropTarget .

Пример

В этой статье мы реализуем пример, когда узлы дерева можно переносить внутри него. То есть, дерево, которое является одновременно TreeDragZone и TreeDropTarget .

Структура дерева будет состоять из вложенных списков с заголовком в SPAN :

  • Для аватара нужно клонировать заголовок узла, на котором было нажатие.
  • Узлы, на которые можно положить, при переносе подсвечиваются красным.
  • Нельзя перенести узел сам в себя или в своего потомка.
  • Дерево само поддерживает сортировку по алфавиту среди узлов.
  • Обязательна расширяемость кода, поддержка большого количества узлов и т.п.

dragManager

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

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

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

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

var dragManager = new function() < var dragZone, avatar, dropTarget; var downX, downY; var self = this; function onMouseDown(e) < if (e.which != 1) < // не левой кнопкой return false; >dragZone = findDragZone(e); if (!dragZone) < return; >// запомним, что элемент нажат на текущих координатах pageX/pageY downX = e.pageX; downY = e.pageY; return false; > function onMouseMove(e) < if (!dragZone) return; // элемент не зажат if (!avatar) < // элемент нажат, но пока не начали его двигать if (Math.abs(e.pageX - downX) < 3 && Math.abs(e.pageY - downY) < 3) < return; >// попробовать захватить элемент avatar = dragZone.onDragStart(downX, downY, e); if (!avatar) < // не получилось, значит перенос продолжать нельзя cleanUp(); // очистить приватные переменные, связанные с переносом return; >> // отобразить перенос объекта, перевычислить текущий элемент под курсором avatar.onDragMove(e); // найти новый dropTarget под курсором: newDropTarget // текущий dropTarget остался от прошлого mousemove // *оба значения: и newDropTarget и dropTarget могут быть null var newDropTarget = findDropTarget(e); if (newDropTarget != dropTarget) < // уведомить старую и новую зоны-цели о том, что с них ушли/на них зашли dropTarget && dropTarget.onDragLeave(newDropTarget, avatar, e); newDropTarget && newDropTarget.onDragEnter(dropTarget, avatar, e); >dropTarget = newDropTarget; dropTarget && dropTarget.onDragMove(avatar, e); return false; > function onMouseUp(e) < if (e.which != 1) < // не левой кнопкой return false; >if (avatar) < // если уже начали передвигать if (dropTarget) < // завершить перенос и избавиться от аватара, если это нужно // эта функция обязана вызвать avatar.onDragEnd/onDragCancel dropTarget.onDragEnd(avatar, e); >else < avatar.onDragCancel(); >> cleanUp(); > function cleanUp() < // очистить все промежуточные объекты dragZone = avatar = dropTarget = null; >function findDragZone(event) < var elem = event.target; while (elem != document && !elem.dragZone) < elem = elem.parentNode; >return elem.dragZone; > function findDropTarget(event) < // получить элемент под аватаром var elem = avatar.getTargetElem(); while (elem != document && !elem.dropTarget) < elem = elem.parentNode; >if (!elem.dropTarget) < return null; >return elem.dropTarget; > document.ondragstart = function() < return false; >document.onmousemove = onMouseMove; document.onmouseup = onMouseUp; document.onmousedown = onMouseDown; >;

DragZone

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

Читайте также:  Php json send file

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

/** * Зона, из которой можно переносить объекты * Умеет обрабатывать начало переноса на себе и создавать "аватар" * @param elem DOM-элемент, к которому привязана зона */ function DragZone(elem) < elem.dragZone = this; this._elem = elem; >/** * Создать аватар, соответствующий зоне. * У разных зон могут быть разные типы аватаров */ DragZone.prototype._makeAvatar = function() < /* override */ >; /** * Обработать начало переноса. * * Получает координаты изначального нажатия мышки, событие. * * @param downX Координата изначального нажатия по X * @param downY Координата изначального нажатия по Y * @param event текущее событие мыши * * @return аватар или false, если захватить с данной точки ничего нельзя */ DragZone.prototype.onDragStart = function(downX, downY, event) < var avatar = this._makeAvatar(); if (!avatar.initFromEvent(downX, downY, event)) < return false; >return avatar; >;

TreeDragZone

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

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

function TreeDragZone(elem) < DragZone.apply(this, arguments); >extend(TreeDragZone, DragZone); TreeDragZone.prototype._makeAvatar = function() < return new TreeDragAvatar(this, this._elem); >;

DragAvatar

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

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

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

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

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

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

Читайте также:  Html код символ градуса

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

/** * "Аватар" - элемент, который перетаскивается. * * В простейшем случае аватаром является сам переносимый элемент * Также аватар может быть клонированным элементом * Также аватар может быть иконкой и вообще чем угодно. */ function DragAvatar(dragZone, dragElem) < /** "родительская" зона переноса */ this._dragZone = dragZone; /** * подэлемент родительской зоны, к которому относится аватар * по умолчанию - элемент, соответствующий всей зоне * может быть уточнен в initFromEvent */ this._dragZoneElem = dragElem; /** * Сам элемент аватара, который будет носиться по экрану. * Инициализуется в initFromEvent */ this._elem = dragElem; >/** * Инициализировать this._elem и позиционировать его * При необходимости уточнить this._dragZoneElem * @param downX Координата X нажатия мыши * @param downY Координата Y нажатия мыши * @param event Текущее событие мыши */ DragAvatar.prototype.initFromEvent = function(downX, downY, event) < /* override */ >; /** * Возвращает информацию о переносимом элементе для DropTarget * @param event */ DragAvatar.prototype.getDragInfo = function(event) < // тут может быть еще какая-то информация, необходимая для обработки конца или процесса переноса return < elem: this._elem, dragZoneElem: this._dragZoneElem, dragZone: this._dragZone >; >; /** * Возвращает текущий самый глубокий DOM-элемент под this._elem * Приватное свойство _currentTargetElem обновляется при каждом передвижении */ DragAvatar.prototype.getTargetElem = function() < return this._currentTargetElem; >; /** * При каждом движении мыши перемещает this._elem * и записывает текущий элемент под this._elem в _currentTargetElem * @param event */ DragAvatar.prototype.onDragMove = function(event) < this._elem.style.left = event.pageX - this._shiftX + 'px'; this._elem.style.top = event.pageY - this._shiftY + 'px'; this._currentTargetElem = getElementUnderClientXY(this._elem, event.clientX, event.clientY); >; /** * Действия с аватаром, когда перенос не удался * Например, можно вернуть элемент обратно или уничтожить */ DragAvatar.prototype.onDragCancel = function() < /* override */ >; /** * Действия с аватаром после успешного переноса */ DragAvatar.prototype.onDragEnd = function() < /* override */ >;

TreeDragAvatar

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

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

function TreeDragAvatar(dragZone, dragElem) < DragAvatar.apply(this, arguments); >extend(TreeDragAvatar, DragAvatar); TreeDragAvatar.prototype.initFromEvent = function(downX, downY, event) < if (event.target.tagName != 'SPAN') return false; this._dragZoneElem = event.target; var elem = this._elem = this._dragZoneElem.cloneNode(true); elem.className = 'avatar'; // создать вспомогательные свойства shiftX/shiftY var coords = getCoords(this._dragZoneElem); this._shiftX = downX - coords.left; this._shiftY = downY - coords.top; // инициировать начало переноса document.body.appendChild(elem); elem.style.zIndex = 9999; elem.style.position = 'absolute'; return true; >; /** * Вспомогательный метод */ TreeDragAvatar.prototype._destroy = function() < this._elem.parentNode.removeChild(this._elem); >; /** * При любом исходе переноса элемент-клон больше не нужен */ TreeDragAvatar.prototype.onDragCancel = function() < this._destroy(); >; TreeDragAvatar.prototype.onDragEnd = function() < this._destroy(); >;

DropTarget

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

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

/** * Зона, в которую объекты можно класть * Занимается индикацией передвижения по себе, добавлением в себя */ function DropTarget(elem) < elem.dropTarget = this; this._elem = elem; /** * Подэлемент, над которым в настоящий момент находится аватар */ this._targetElem = null; >/** * Возвращает DOM-подэлемент, над которым сейчас пролетает аватар * * @return DOM-элемент, на который можно положить или undefined */ DropTarget.prototype._getTargetElem = function(avatar, event) < return this._elem; >; /** * Спрятать индикацию переноса * Вызывается, когда аватар уходит с текущего this._targetElem */ DropTarget.prototype._hideHoverIndication = function(avatar) < /* override */ >; /** * Показать индикацию переноса * Вызывается, когда аватар пришел на новый this._targetElem */ DropTarget.prototype._showHoverIndication = function(avatar) < /* override */ >; /** * Метод вызывается при каждом движении аватара */ DropTarget.prototype.onDragMove = function(avatar, event) < var newTargetElem = this._getTargetElem(avatar, event); if (this._targetElem != newTargetElem) < this._hideHoverIndication(avatar); this._targetElem = newTargetElem; this._showHoverIndication(avatar); >>; /** * Завершение переноса. * Алгоритм обработки (переопределить функцию и написать в потомке): * 1. Получить данные переноса из avatar.getDragInfo() * 2. Определить, возможен ли перенос на _targetElem (если он есть) * 3. Вызвать avatar.onDragEnd() или avatar.onDragCancel() * Если нужно подтвердить перенос запросом на сервер, то avatar.onDragEnd(), * а затем асинхронно, если сервер вернул ошибку, avatar.onDragCancel() * При этом аватар должен уметь "откатываться" после onDragEnd. * * При любом завершении этого метода нужно (делается ниже): * снять текущую индикацию переноса * обнулить this._targetElem */ DropTarget.prototype.onDragEnd = function(avatar, event) < this._hideHoverIndication(avatar); this._targetElem = null; >; /** * Вход аватара в DropTarget */ DropTarget.prototype.onDragEnter = function(fromDropTarget, avatar, event) <>; /** * Выход аватара из DropTarget */ DropTarget.prototype.onDragLeave = function(toDropTarget, avatar, event) < this._hideHoverIndication(); this._targetElem = null; >;

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

Читайте также:  Язык программирования питон литература

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

TreeDropTarget

TreeDropTarget содержит код, специфичный для дерева:

  • Индикацию переноса над элементом: методы _showHoverIndication и _hideHoverIndication .
  • Получение текущей точки приземления _targetElem в методе _getTargetElem . Ей может быть только заголовок узла дерева, причём дополнительно проверяется, что это не потомок переносимого узла.
  • Обработка успешного переноса в onDragEnd , вставка исходного узла avatar.dragZoneElem в узел, соответствующий _targetElem .

Итого

Реализация Drag’n’Drop оказалась отличным способом применить ООП в JavaScript.

Исходный код примера целиком находится в песочнице.

  • Синглтон dragManager и классы Drag* задают общий фреймворк. От них наследуются конкретные объекты. Для создания новых зон достаточно унаследовать стандартные классы и переопределить их.
  • Мини-фреймворк для Drag’n’Drop, который здесь представлен, является переписанным и обновлённым вариантом реальной библиотеки, на основе которой было создано много успешных скриптов переноса. В зависимости от ваших потребностей, вы можете расширить его, добавить перенос нескольких объектов одновременно, поддержку событий и другие возможности.
  • На сегодняшний день в каждом серьёзном фреймворке есть библиотека для Drag’n’Drop. Она работает похожим образом, но сделать универсальный перенос – штука непростая. Зачастую он перегружен лишней функциональностью, либо наоборот – недостаточно расширяем в нужных местах. Понимание, как это все может быть устроено, на примере этой статьи, может помочь в адаптации существующего кода под ваши потребности.

Источник

Оцените статью