Технология перетаскивания (drag and drop). Drag-and-drop: способ оперирования элементами интерфейса пользователя

Настройка WI-FI 23.12.2020
Настройка WI-FI

Проще всего что-то взять и положить, чем писать, что нужно взять и куда положить. Конечно без мышки, или подобного ей устройства, ничего не выберешь и ничего не укажешь, но даже в текущем положении вещей использование идеи «drag and drop» очень естественно и комфортно.

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

Описание идеи

Выбрать, переместить и положить - идея естественная и удобная. Просто удивительно, что она не родилась, когда мышка стала незаменимым аксессуаром компьютера.

Самый очевидный пример - выбор товара в интернет-магазине. Взять мышкой нужный товар и перетащить его в корзину для покупок - просто, естественно и удобно. Загрузка файлов: взять документ за пределами окна браузера и положить его на элемент страницы, инициировав тем самым передачу документа на сервер - также практичная идея.

Для разработчика идея «drag and drop» - это манипулирование элементами страницы без ручного пересчета координат и размеров тегов, возможность выбора нескольких элементов и выравнивания их, а также перемещения сторон блочных тегов.

HTML и CSS - отличные языки описания тегов и стилей их оформления, но когда разработчик имеет возможность интерактивно манипулировать элементами страницы без ручного пересчета координат и размеров, это делает работу более комфортной и эффективной.

Простой перенос файлов

«Drag and drop»: перевод с английского на русский буквально звучит «тащи и бросай». На практике это звучит и действует лучше: выбрал, перенес и отпустил - просто и естественно.

Реализовать на странице передачу файлов на страницу, на сервер или для иного использования очень просто.

В данном примере мышкой было выбрано несколько файлов на рабочем столе (левый рисунок). На выделении была нажата левая кнопка мышки и выбранное «поехало» в корзинку. Браузер сам показал, как это происходит, написал подсказку «копирование» и вокруг создал контуры перемещаемых файлов.

Когда мышка оказалась над корзинкой, посетитель отпустил левую кнопку мышки, событие «тащи и бросай» состоялось и на странице сайта (нижний рисунок) JavaScript-код смог получить и обработать все файлы, которые посетитель предоставил странице (сайту).

Описание реализации

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

Здесь интерфейс пользователя представлен двумя тегами: scPlaceFile (это сама корзинка, куда нужно положить файлы) и scPlaceFiles (это результат обработки файлов, в данном случае их список).

Логика работы страницы такова. При загрузке страницы в браузере в корзинке назначается обработчик события «ondrop» - положить, остальные события блокируются и не используются.

Страница работает в штатном режиме, но как только посетитель выберет файл (файлы) и перетащит их на картинку корзинки, то есть на тег scPlaceFile, будет запущена обработка события «файлы приехали».

Данный обработчик просто выводит список файлов. Их количество находится в event.dataTransfer.files.length, а информация о каждом файле в event.dataTransfer.files[i].name. Что делать с полученными данными, определяет разработчик, в данном случае просто формируется список полученных файлов.

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

DnD и внешние данные

Загрузка изображений на сервер в «drag and drop» - обычная практика применения этой технологии. Как правило, разработчик создает форму для загрузки файла (1), которая работает обычным образом (2). Посетитель может в обычном режиме выбирать файлы и загружать их.

Однако если посетитель на определенное место формы сделает «drag and drop», то поле имени файла (файлов) заполнится автоматически.

Это хорошее решение. Допустить, что на компьютере нет мышки, конечно, очень трудно. Но лучше разработать интерфейс пользователя в обычном варианте и в DnD-реализации.

DnD и внутренние данные

Забота об интересах посетителя всегда важна, но проблемы разработчика также имеют значение. Реализовать «drag and drop» можно не только стандартными средствами, но и посредством обработки событий мышки на элементах страницы.

Задача расчета значений координат тегов и их размеров возникает постоянно. Ручной расчет - хорошая практика, но интерактивный вариант более удобен. Все теги всегда имеют прямоугольную форму и, отслеживая «мышиные» события на сторонах элементов, можно создать возможность автоматического перемещения элементов в нужное место страницы, либо изменения их.

Обработка события нажатия кнопки мышки - запоминание координат места клика, например, одной из сторон элемента. Перемещение мышки - сторона перемещается в нужном направлении. Отпускание кнопки мышки - сторона останавливается, а ее координаты изменяются. Так можно изменить положение элемента или его размер.

Формально это не «drag and drop», но эффект аналогичный и практичный. Сделав универсальные обработчики для любого элемента страницы, можно получить хороший интерактивный результат, ускорить разработку и упростить код.

Визуальное и ручное программирование

Мышка на компьютере и пальцы на смартфоне - совершенно разные подходы к реализации интерфейса пользователя (посетитель, разработчик). Является вполне естественным и современным требование кроссбраузерности.

Все это в совокупности усложняет создание страниц, но применяя идею «drag and drop» в ее стандартной форме, используя ее события, совмещая эту идею с обычными событиями на элементах, можно реализовать механизм, при котором создание страницы будет происходить визуально.

Сейчас давайте рассмотрим выбор элемента или элементов. Факт выбора - появление контекстного меню, например, цель - выравнивание выбранного (слева, справа, по центру), или распределение элементов по вертикали или горизонтали с одинаковым шагом, или изменение их размеров (по минимальному, по максимальному).

Автоматический перерасчет координат и размеров предпочтительнее ручного. Меньше ошибок - быстрее достигается цель. Кроме того, можно сделать станицу в одном браузере, сохранить положение и размеры элементов. Открыв эту страницу на смартфоне, можно поправить значения координат и размеров и запомнить их для конкретной модели смартфона или версии браузера.

Так одна и та же страница без ручного соблюдения требования кроссбраузерности будет иметь различные данные для отображения на различных устройствах и в различных браузерах.

Если позволить посетителю выполнять эти процедуры самостоятельно, а также выбирать нужные элементы страницы из числа предусмотренных разработчиком, можно обеспечить кроссбраузерность и требуемую функциональность страницы с учетом мнения пользователя.

Использование технологии перетаскивания (drag and drop ) дает возможность пользователю перемещать различные объекты из одного в другой, например, элементы одного списка в другой. Для этого необходимо использовать два элемента управления: приемник и источник. Приемником считается тот объект, который примет объект источник (перемещаемый объект).

События, возникающие в процессе перемещения объектов, перечислены ниже в том порядке в каком они возникают.

OnStartDrag (тип TStartDragEvent) - в начале выполнения операции генерируется объектом-источником. Параметры, которые передаются обработчику событий: объект приемник DragObject (тип TDragObject), объект-источник Source (тип TObject).

OnDragOver (тип TDragOverEvent) - создает объект-приемник, когда над ним оказывается перемещаемый объект. Параметры, которые передаются обработчику событий:объект-приемник Sender (тип TObject), объект-источник Source (тип TObject), состояние перемещения State (тип TDragState), X и Y (тип integer) - текущие координаты указателя мыши, Accept (тип boolean) признак подтверждения операции перемещения. Состояние перемещения дает понять находиться ли перемещаемый объект в области приемника, передвигается ли в ней, покинул ее. Переданные параметры дают возможность объекту-приемнику принять или отклонить объект-источник. Параметр Accept устанавливается в значение Trye если операция перемещения принята, в противном случае - False.

onDragDrop (тип TDragDropEvent) - создается объектом-приемником, когда перемещаемый объект опускается на нем. Обработчику события передаются текущие координаты указателя мыши, объект приемник Sender (тип TObject), исходный объект перемещения Source (тип TObject).

onEndDrag (тип EndDragEvent) - создается при завершении операции перетаскивания. Передаются обработчику события координаты X и Y точки, где оказался объект-источник Sender и объект приемник Target.

Для создания перетаскивания достаточно реализовать два события: OnDragDrop и OnDragOver при установленном свойстве DragMode равном dmAutomatic. В противном случае начало операции перемещения, метод BeginDrag, необходимо программисту закодировать.

Для закрепления материала создадим следующее приложение. На форму поместим компонент Panel. В свойстве DragMode Инспектора Объектов установим значение dmAutomatic. Выделим объект форма и с помощью Инспектора Объектов создадим нижеследующие события:

Procedure TForm1.FormDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin if Source = Panel1 then Accept:= True else Accept:= False; end; procedure TForm1.FormDragDrop(Sender, Source: TObject; X, Y: Integer); begin Panel1.Left:= X; Panel1.Top:= Y; end;

Теперь запустив приложение и нажав над панелью кнопку мыши мы можем перемещать объект панель по всей форме.

Итог: мы познакомились с технологией перетаскивания (drag and drop) и использовали ее на практике.

Известный факт, что поле для загрузки файлов трудно стилизовать так, как хочется разработчику. Многие просто скрывают его и добавляют кнопку, которая открывает диалог выбора файлов. Однако, теперь у нас появился даже ещё более модный способ обработки выбора файлов: drag and drop.

Технически это уже было возможно сделать, потому что большинство (если не все) реализации поля выбора файлов позволяли перетаскивать файлы для их выбора, но это требовало от вас показывать элемент с типом file . Итак, давайте по-настоящему использовать API, которое даёт нам браузер, для реализации выбора файлов через drag-and-drop и их загрузки на сервер.

В этой статье мы будем использовать чистый ES2015+ JavaScript (без фреймворков или библиотек) для выполнения этого проекта, и это предполагает, что у вас есть опыт работы с JavaScript в браузере. Этот пример - помимо ES2015+ синтаксиса, который можно легко изменить на синтаксис ES5 или транспилировать с помощью Babel - должен быть совместим со всеми вечнозелёными браузерами + IE 10 и 11.

Ниже можете видеть пример того, что должно получиться:

Демонстрационная страница, на которой можно загрузить файлы с помощью drag and drop с немедленным предварительным просмотром изображений и демонстрацией индикатора прогресса загрузки. События Drag-and-Drop

Первое, что мы должны обсудить, это события связанные с перетаскиванием, потому что они движущая сила этого функционала. В общем, есть восемь событий, срабатывающих в браузере и связанных с перетаскиванием: drag , dragend , dragenter , dragexit , dragleave , dragover , dragstart и drop . Мы не будем проходиться по ним всем, потому что события drag , dragend , dragexit и dragstart срабатывают на элементе, который перетаскивают, а это не наш случай, мы будем перетаскивать файлы из нашей файловой системы вместо DOM-элементов, так что эти события никогда не сработают.

Если вам стало интересно почитать об этих событиях, то вы можете изучить документацию связанную с ними на MDN .

Как и следовало ожидать, вы можете навесить обработчики для нужных событий таким же образом, каким происходит регистрация обработчиков большинства браузерных событий: с помощью addEventListener .

Let dropArea = document.getElementById("drop-area")
dropArea.addEventListener("dragenter", handlerFunction, false)
dropArea.addEventListener("dragleave", handlerFunction, false)
dropArea.addEventListener("dragover", handlerFunction, false)
dropArea.addEventListener("drop", handlerFunction, false)

Ниже приведено небольшое описание того, что эти события делают. Используем dropArea из примера кода выше для того, чтобы сделать описание понятнее:

Событие dragenter  - перетаскиваемый объект перетаскивается над dropArea , делая dropArea целью события drop , если пользователь перетащит его туда. Событие dragleave - перетаскиваемый объект перетащили за пределы dropArea на другой элемент, делая его целью события drop вместо dropArea . Событие dragover срабатывает каждые несколько сотен миллисекунд, пока объект перетаскивают над dropArea . Событие drop  - пользователь отпустил кнопку мыши, перетаскиваемый объект перетащили на dropArea .

Стоит отметить, что при перетаскивании объекта над элементом, являющимся дочерним для dropArea , событие dragleave сработает над dropArea , а событие dragenter на дочернем элементе, потому что он становится target . Событие drop всплывёт до элемента dropArea (конечно, если до этого всплытие не остановит другой обработчик событий), таким образом событие сработает на dropArea , несмотря на то, что target у него будет другим.

Также обратите внимание, что для реализации пользовательского интерфейса с drag-and-drop, вам необходимо вызывать event.preventDefault() на каждом из слушателей этих событий. Если вы этого не сделаете, то браузер в конечном итоге откроет файл, который вы перетаскиваете, вместо того, чтобы отправить его в обработчик события drop .

Настраиваем нашу форму

До того как мы начнём добавлять функциональность drag-and-drop, нам надо добавить базовую форму со стандартным полем типа file . Технически это не обязательно, но рекомендуется предоставить такую альтернативу пользователям, чей браузер не поддерживает drag-and-drop API.



Загрузите изображения с помощью диалога выбора файлов или перетащив нужные изображения в выделенную область



Выбрать изображения

Довольно простая структура. Вы можете заметить обработчик события onchange на input . Посмотрим на него позже. Было бы также хорошей идеей добавить action к тегу form и кнопку submit , чтобы помочь людям, у которых выключен JavaScript. Затем можно использовать JavaScript для того, чтобы избавиться от них, почистить форму. В любом случае, вам понадобится серверный скрипт для загрузки файлов, неважно написан ли он собственными силами или вы используете сервис, такой как Cloudinary . Кроме этого, здесь нет ничего особенного, так что давайте набросаем стили:

#drop-area {
border: 2px dashed #ccc;
border-radius: 20px;
width: 480px;
font-family: sans-serif;
margin: 100px auto;
padding: 20px;
}
#drop-area.highlight {
border-color: purple;
}
p {
margin-top: 0;
}
.my-form {
margin-bottom: 10px;
}
#gallery {
margin-top: 10px;
}
#gallery img {
width: 150px;
margin-bottom: 10px;
margin-right: 10px;
vertical-align: middle;
}
.button {
display: inline-block;
padding: 10px;
background: #ccc;
cursor: pointer;
border-radius: 5px;
border: 1px solid #ccc;
}
.button:hover {
background: #ddd;
}
#fileElem {
display: none;
}

Многие из этих стилей пока не используются, но это нормально. Основным моментом является то, что поле file скрыто, а его подпись label стилизована так, чтобы выглядеть как кнопка, таким образом люди поймут, что кликнув по нему вызовется диалог выбора файлов. Кроме того, мы следуем соглашению, согласно которому область, куда следует перетащить файл, обозначается пунктирной линией.

Добавляем функциональность Drag-and-Drop

Теперь можем перейти к сладкому: drag and drop. Давайте напишем скрипт внизу страницы или в отдельном файле, смотря как вам больше нравится. Первое, что нам понадобится - это ссылка на область, куда предстоит тащить файл. Так мы сможем обрабатывать нужные нам события на ней:

Let dropArea = document.getElementById("drop-area")

Теперь давайте добавим сами события. Начнём с добавления обработчиков для всех событий, чтобы предотвратить поведение по умолчанию и остановить всплытие выше необходимого:

;["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false)
}) function preventDefaults (e) {
e.preventDefault()
e.stopPropagation()
}

Теперь давайте добавим индикатор, который позволит пользователям понять, что они действительно перетаскивали элементы над нужной областью, используем CSS для изменения цвета границы области для перетаскивания. Стили уже описаны выше для селектора #drop-area.highlight , так что давайте используем JavaScript для добавления и удаления класса highlight , когда это необходимо.

;["dragenter", "dragover"].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false)
}) ;["dragleave", "drop"].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false)
}) function highlight(e) {
dropArea.classList.add("highlight")
} function unhighlight(e) {
dropArea.classList.remove("highlight")
}

Мы использовали оба события dragenter и dragover для подсвечивания области для перетаскивания по причинам, о которых я говорил ранее. Если вы начинаете перетаскивать непосредственно над dropArea и затем перешли на дочерний элемент, то сработает событие dragleave и подсвечивание области пропадёт. Событие dragover сработает после событий dragenter и dragleave , так что подсветка вернётся обратно на dropArea до того, как мы увидим, что она пропала.

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

Теперь всё что нам нужно, это выяснить что делать, когда файлы будут перетащены:

DropArea.addEventListener("drop", handleDrop, false) function handleDrop(e) {
let dt = e.dataTransfer
let files = dt.files handleFiles(files)
}

Код выше не приближает нас к цели, но делает две важные вещи:

  • Демонстрирует, как получить данные о файлах, которые перетащили.
  • Приводит нас в то же место, что и поле input с типом file и обработчиком на событие onchange: handleFiles .
  • Помните о том, что files это не массив, а FileList . Таким образом, при реализации handleFiles , нам нужно преобразовать FileList в массив, чтобы более легко было его итерировать:


    ([...files]).forEach(uploadFile)
    }

    Это было скучно (That was aniticlimactic ). Перейдём к uploadFile , где будут действительно крутые штуки (real meaty stuff ).

    Function uploadFile(file) {

    method: "POST",
    body: formData
    })
    .then(() => { /* Готово. Информируем пользователя */ })
    .catch(() => { /* Ошибка. Информируем пользователя */ })
    }

    Здесь мы используем FormData  - встроенный браузерный API для создания форм с данными для отправки на сервер. Для этого мы используем fetch API, чтобы действительно отправить изображения на сервер. Убедитесь, что вы изменили URL для работы с вашим сервером или сервисом, с помощью formData.append можете добавить к форме любые дополнительные данные, которые могут потребоваться для работы с вашим сервером. Как альтернатива, если вы хотите поддерживать Internet Explorer, вы можете захотеть использовать XMLHttpRequest , это значит, что ваш uploadFile будет выглядеть так:

    Function uploadFile(file) {

    xhr.open("POST", url, true) xhr.addEventListener("readystatechange", function(e) {
    // Готово. Информируем пользователя
    }
    // Ошибка. Информируем пользователя
    }
    xhr.send(formData)
    }

    В зависимости от настроек вашего сервера, вы можете проверять различные значения status , отличные от 200 , но для наших нужд это сработает.

    Дополнительные возможности

    Это вся базовая функциональность, но часто требуется её расширить. Конкретно в этом руководстве, мы добавим панель предпросмотра, где будут показаны выбранные изображения, также добавим индикатор прогресса, который будет показывать пользователю статус загрузки. Итак, давайте начнём с предпросмотра изображений.

    Предпросмотр изображений

    Есть несколько способов сделать это: вы можете ждать пока изображения загрузятся и запросить у сервера URL для картинок, но это означает что вам придётся ждать пока выполняется загрузка, а временами изображения могут быть довольно большими. Альтернатива, которую мы будет исследовать сегодня - это использовать FileReader API с данными файлов, которые мы получили из события drop . Это работает асинхронно, но вы можете использовать синхронную альтернативу FileReaderSync , но пользователи могут попробовать прочитать несколько больших файлов подряд, таким образом, это может заблокировать поток выполнения на длительное время и по-настоящему испортить впечатления пользователя от сервиса. Что же, давайте создадим функцию previewFile и посмотрим как это работает:

    Function previewFile(file) {
    let reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onloadend = function() {
    let img = document.createElement("img")
    img.src = reader.result
    document.getElementById("gallery").appendChild(img)
    }
    }

    Здесь мы создали new FileReader и вызвали метод readAsDataURL для объекта File . Как уже упоминалось, это работает асинхронно, поэтому нужно добавить обработчик события onloadend для обработки результата чтения файла. После этого используем base64 URL для атрибута src нового элемента и добавляем его в элемент gallery . Есть только две вещи, которые надо сделать, чтобы всё было готово и работало: добавить элемент gallery и вызов функции previewFile .

    Во-первых, добавим HTML, который приведен ниже, сразу после закрывающего тега form:

    Ничего особенного, это просто div . Стили уже заданы для него и изображений в нём, так что больше здесь ничего делать не надо. Теперь изменим функцию handleFiles на следующую:

    Function handleFiles(files) {
    files = [...files]
    files.forEach(uploadFile)
    files.forEach(previewFile)
    }

    Есть несколько способов сделать это, например композиция или простой колбэк forEach , в котором запускается uploadFile и previewFile , и это тоже сработает. Таким образом, когда вы перетащите или выбираете несколько изображений, они будут показаны почти мгновенно ниже формы. Интересная мысль по этому поводу: в некоторых приложениях вы можете не захотеть действительно загружать изображения на сервер, а вместо этого хранить ссылки на них в localStorage или в каком-нибудь другом кеше на стороне пользователя, чтобы приложение имело к ним доступ позже. Я лично не могу придумать хорошие сценарии использования этого, но я готов поспорить, что такие есть.

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

    Отслеживание прогресса

    Если что-нибудь занимает некоторое время, индикатор прогресса помогает пользователю понять, что процесс идёт, и показывает, как долго это что-нибудь будет выполняться. Добавить индикатора прогресса довольно легко благодаря HTML5 тегу progress . Давайте начнём с добавления его в HTML-код.

    Вы можете вставить его сразу после элемента label или между элементами form и div для предпросмотра изображений, как больше нравится. Вообще, вы можете добавить его куда захотите в пределах тега body . Стили для этого примера не добавлены, так что будет отрисован браузерный элемент по умолчанию, который вполне пригоден. Теперь давайте добавим JavaScript. Сначала рассмотрим реализацию с использованием fetch , а затем покажем версию для XMLHttpRequest . Для начала нам понадобится пара новых переменных в верхней части скрипта:

    Let filesDone = 0
    let filesToDo = 0
    let progressBar = document.getElementById("progress-bar")

    При использовании fetch мы только можем определить, когда загрузка завершена, так что единственная информация, которую мы отслеживаем: сколько файлов выбрано для загрузки (переменная filesToDo) и количество уже загруженных файлов (переменная filesDone). Также мы храним ссылку на элемент #progress-bar , чтобы мы могли быстро обновлять его. Теперь давайте создадим пару функций для управления прогрессом:

    Function initializeProgress(numfiles) {
    progressBar.value = 0
    filesDone = 0
    filesToDo = numfiles
    } function progressDone() {
    filesDone++
    progressBar.value = filesDone / filesToDo * 100
    }

    Когда мы только начинаем загрузку, вызовем функцию initializeProgress для сброса состояния индикатора. Затем, с каждой выполненой загрузкой, мы вызываем функцию progressDone для увеличения числа загруженых файлов на единицу и обновления индикатора для демонстрации прогресса. Итак, добавим вызовы этих функций, обновив пару старых:

    Function handleFiles(files) {
    files = [...files]
    initializeProgress(files.length) // 0; i--) {
    uploadProgress.push(0)
    }
    } function updateProgress(fileNumber, percent) {
    uploadProgress = percent
    let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length
    progressBar.value = total
    }

    Теперь initializeProgress инициализирует массив с длиной, равной numFiles , который заполнен нулями, означающими, что каждый файл загружен на 0%. В updateProgress мы видим какое из изображений обновляет свой прогресс и изменяем значение элемента с нужным индексом на предоставленный percent . Затем мы вычисляем общий процент выполнения как среднее среди всех процентов и обновляем индикатор прогресса, чтобы отобразить вычисленное значение. Мы по-прежнему вызываем initializeProgress в handleFiles также, как делали это в примере с fetch . Таким образом, всё что нам нужно, это обновить uploadFile , добавив вызов updateProgress .

    Function uploadFile(file, i) { //

    Рекомендуем почитать

    Наверх