JavaScript Drag-and-Drop

Reading time ~15 minutes

Знакомство с техникой Drag-and-Drop и механизмом ее реализации с помощью JavaScript. Данный обзор не претендует на полноту покрытия материала. Задача статьи - познакомиться с созданием Drag-and-Drop на JavaScript. Понять сам принцип механизма и научиться применять основные инструменты для его реализации.

Определение Drag-and-Drop

Сам механизм Drag-and-Drop интуитивно понятен - “схватил-перетащил-бросил”. Преимущество внедрения Drag-and-Drop в интерфейсы заключается в упрощении реализации задач; в уменьшении количества пунктов меню типа “Copy-Paste”.

События Drag-and-Drop

Механизм Drag-and-Drop имеет в своем составе целую группу событий, с помощью которых можно контролировать процесс перетаскивания:

  • 1
    
    dragstart
    
    - пользователь начинает перетаскивание элемента
  • 1
    
    dragenter
    
    - перетаскиваемый элемент входит в область целевого объекта
  • 1
    
    dragover
    
    - перетаскиваемый элемент перемещается в области целевого объекта
  • 1
    
    dragleave
    
    - перетаскиваемый элемент покидает область целевого объекта
  • 1
    
    drag
    
    - момент начала процесса перетаскивания объекта
  • 1
    
    drop
    
    - момент, когда отпускается зажатая клавиша мыши (перетаскиваемый объект “роняется”)
  • 1
    
    dragend
    
    - момент завершения процесса перетаскивания объекта

Объект dataTransfer

Механизм Drag-and-Drop также имеет в своем составе объект dataTransfer, который служит для вспомогательных целей. В этом объекте хранится необходимая информация о событии перетаскивания. Помимо этого, в объект dataTransfer можно добавлять данные; а также считывать из него данные.

Свойства (наиболее важные) объекта dataTransfer:

  • 1
    
    dataTransfer.effectAllowed
    
    - задаем тип перетаскивания, которое пользователь может выполнять с элементом
  • 1
    
    dataTransfer.dropEffect
    
    - задаем внешний вид курсора мыши в соответствии с заданным типом перетаскивания

Методы (наиболее важные) объекта dataTransfer:

  • 1
    
    setData()
    
    - добавляет данные в нужном формате
  • 1
    
    clearData()
    
    - удаляет данные
  • 1
    
    setDragImage()
    
    - устанавливает изображение для перетаскивания с координатами курсора (0, 0 — левый верхний угол)
  • 1
    
    getData()
    
    - возвращает данные

Ниже будет рассматриваться практический пример реализации Drag-and-Drop на JavaScript.

HTML разметка

Базовая HTML-разметка будет простой:

<h2 id="dropStatus">application status</h2>
<h1 id="dropTitle">drop zone</h1>
<div id="dropZone"></div>
<div id="objectsZone">
    <div id="object1" class="objects">object 1</div>
    <div id="object2" class="objects">object 2</div>
    <div id="object3" class="objects">object 3</div>
</div>
<hr/>
<button type="button" id="readDropZone">get object data</button>

Что для чего служит в этой разметке?

Заголовок

1
id="dropStatus"
будет отображать текущее состояние процесса Drag-and-Drop. В него мы будет отправлять информацию о текущем состоянии Drag-and-Drop при помощи событий, о который говорилось выше.

Заголовок

1
id="dropTitle"
служит просто для декоративных целей.

Блок

1
id="dropZone"
является целевой областью - в нее мы будет перетаскивать объекты.

Объекты

1
id="object1"
,
1
id="object2"
,
1
id="object3"
- это перетаскиваемые объекты; их мы будем перемещать в область блока
1
id="dropZone"
.

Кнопка

1
id="readDropZone"
будет выводить информацию об перемещенных объектах.

В итоге разметка совместно со стилями будут выглядеть таким образом - JavaScript - Drag’n’Drop - Part 1.

JavaScript - разбираемся с событиями

Прежде чем детально рассматривать работу каждой из будущих функций по обработке событий, мне кажется, будет лучше просто понять, какие события и куда мы будем “вешать”.

Итак, начнем с перетаскиваемых элементов

1
id="object1"
,
1
id="object2"
,
1
id="object3"
. На каждый из них мы повесим два события:

  • 1
    
    dragstart
    
    - событие начала процесса перетаскивания элемента
  • 1
    
    dragend
    
    - событие окончания процесса перетаскивания элемента

Для каждого из элементов, при возникновении на нем события, мы будем запускать соответствующую функцию

1
dragStart
или
1
dragEnd
:

var objects = document.querySelectorAll('#objectsZone > .objects');
...
if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

Обратим внимание на строку

1
el.setAttribute('draggable', 'true');
- здесь мы динамически добавляем для всех элементов с классом
1
.objects
атрибут
1
draggable="true"
, тем самым делая (благодаря HTML5) эти элементы доступными для перетаскивания.

На элемент

1
id="dropZone"
мы “повесим” гораздо больше событий:

  • 1
    
    dragenter
    
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    
    ) входит в область целевого объекта (
    1
    
    id="dropZone"
    
    )
  • 1
    
    dragleave
    
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    
    ) выходит из области целевого объекта (
    1
    
    id="dropZone"
    
    )
  • 1
    
    dragover
    
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    
    ) перемещается внутри области целевого объекта (
    1
    
    id="dropZone"
    
    )
  • 1
    
    drop
    
    - перетаскиваемый объект (например,
    1
    
    id="object1"
    
    ) помещается внутри целевого объекта (
    1
    
    id="dropZone"
    
    )

И конечно же, для каждого события будет своя функция. JavaScript-код в итоге будет выглядеть таким образом:

var dropZone = document.querySelector('#dropZone');
...
if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

Ну и на кнопку

1
id="readDropZone"
мы “повесим” обычный код с функцией
1
readZone
:

var dropButton = document.querySelector('#readDropZone');
...
if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Если суммировать все вышесказанное, то общий вид handler’ов в нашем случае будет выглядеть таким образом:

// LISTENERS

if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Далее будет детально останавливаться на каждой из функций - что она делает и для чего.

Функция dragStart (event)

Начнем с начала и запустим функцию для обработки старта события перетаскивания - события

1
dragstart
. Хочу сразу оговориться, что в процессе написания кода для обработки события перетаскивания важно четко представлять себе, какое событие и на каком элементе происходит.

В данном случае мы будем обрабатывать событие

1
dragstart
, которое возникает на перетаскиваемом элементе (
1
id="object1"
,
1
id="object2"
или
1
id="object3"
- не важно).

Событие

1
dragstart
в момент своего возникновения автоматически генерирует объект dataTransfer, который (как мне кажется) можно в общих чертах сравнить с событийным объектом Event; последний также хранит в себе множество данных о произошедшем событии. Некоторыми методами и свойствами объекта Event мы воспользуемся в нашем примере:

var dropStatus = document.querySelector('#dropStatus');
...
function dragStart (event) {
    dropStatus.innerHTML = 'Dragging the ' + event.target.getAttribute('id');
    event.dataTransfer.dropEffect = 'move';
    event.dataTransfer.setData('text', event.target.getAttribute('id'));
}

Функция

1
dragStart
при возникновении события “берет” элемент
1
dropStatus
и методом
1
innerHTML
“пихает” внутрь него строку, часть которой представляет из себя значение атрибута
1
id
элемента, на котором произошло событие (
1
event.target
).

Для объекта dataTransfer задается значение его свойства

1
dropEffect
-
1
move
.

В третьей строке для объекта dataTransfer с помощью метода

1
setData()
задается имя переменной
1
text
и значение для этой переменной - ID текущего элемента.

Функции dragEnter(), dragLeave(), dragOver()

Три функции, каждая из которых отслеживает событие, возникающее на элементе

1
dropZone
:

function dragEnter (event) {
    dropStatus.innerHTML = 'You are dragging over ' + event.target.getAttribute('id');
    this.classList.add('over');
}

function dragLeave (event) {
    dropStatus.innerHTML = 'You left the ' + event.target.getAttribute('id');
    this.classList.remove('over');
    this.removeAttribute('class');
}

function dragOver (event) {
    event.preventDefault();
}

Первые две функции -

1
dragEnter (event)
и
1
dragLeave (event)
очень похожи между собой. Каждая из них манипулирует содержимым заголовка
1
dropStatus
, сигнализируя о происходящем событии.

Третья функция

1
dragOver (event)
может показаться странной. Все ее назначение - это отмена действия по-умолчанию. Что это за действие по-умолчанию? Дело в том, что у браузеров имеется свой собственный (помимо HTML5) механизм реализации события перетаскивания Drag-and-Drop. И если его не отключить, то он не даст срабатывать нашему механизму.

Функция dragDrop (event)

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

1
dropZone
:

var droppedIN = false;
...
function dragDrop (event) {
    event.preventDefault();
    var elementID = event.dataTransfer.getData('text');
    var element = document.getElementById(elementID);
    event.target.appendChild(element);
    element.removeAttribute('draggable');
    element.classList.add('dragged');
    element.style.cursor = 'default';
    droppedIN = true;
    dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');
}

В строке

1
event.preventDefault();
мы снова отменяем действие по-умолчанию. На этот раз это касается самого перетаскиваемого элемента - ведь он может быть ссылкой и браузер выполнит переход по ней (действие по-умолчанию), что нам совсем не нужно.

В строке:

var elementID = event.dataTransfer.getData('text');

… мы из объекта dataTransfer получаем ID перетаскиваемого элемента. Вы же помните, что в функции

1
dragStart (event)
с помощью строки:

event.dataTransfer.setData('text', event.target.getAttribute('id'));

…мы его как раз получали?

Далее находим перетаскиваемый элемент по его ID:

var element = document.getElementById(elementID);

И помещаем его внутрь текущего активного элемента:

event.target.appendChild(element);

Далее убираем у перетаскиваемого элемента атрибут

1
draggable
- он больше не перетаскиваемый. Визуально сигнализируем об этом, изменив вид курсора мыши:

element.style.cursor = 'default';

И сообщаем об изменившемся статусе в заголовке:

dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');

Отдельного упоминания стоит строка

1
droppedIN = true;
. Это флаг, с помощью которого мы определяем, произошло ли событие
1
drop
или нет.

Может случиться так, что объект мы перетащили в область элемента

1
dropZone
, но передумали его помещать туда. И “отпустили” перетаскиваемый элемент за областью элемента
1
dropZone
. В результате событие
1
dragend
произошло, но событие
1
drop
не выполнилось.

Такую ситуацию обрабатывает функция

1
dragEnd()
:

function dragEnd() {
    if ( droppedIN === false ) {
        dropStatus.innerHTML = 'You let the ' + event.target.getAttribute('id') + ' to go!';
    }
    droppedIN = false;
}

Функция readZone ()

Последняя функция из нашего примера - это функция-счетчик. Ее задача - просто посчитать, сколько элементов на данный момент мы “бросили” в область

1
dropZone
:

function readZone () {
    var dropZoneChild = dropZone.children;
    for ( var i = 0; i < dropZoneChild.length; i++ ) {
        alert('Object ' + dropZoneChild[i].getAttribute('id') + ' is in ' + dropZone.getAttribute('id'));
    }
}

Нажимаем кнопку

1
dropButton
и alert’ом последовательно выводим все элементы, помещенные внутрь объекта
1
dropZone
.

Вот, в принципе, и все, что можно вкратце сказать. Осталось только взглянуть на готовый пример работы кода - JavaScript - Drag’n’Drop - Part 2.

На этом все. Здоровая критика и полезные замечания только приветствуются.


Этот скромный обзор не смог бы появиться, если бы не было двух полезных для меня ресурсов:

Есть более детальный обзор и более интересный пример задачи на JavaScript Drag-and-Drop размещен здесь:

VSC - explorer.compactFolders

В Visual Studio Code по умолчанию стоит настройка, которая отображает на владке Explorer вложенные папки таким образом:![VSC - Default Vi...… Continue reading

Flattering operators

Published on July 12, 2024

Оператор withLatestFrom

Published on July 03, 2024