Drag-n-drop в HTML5

Введение

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

Drag-n-drop теперь есть в HTML5. Спецификация определяет событийный механизм, JavaScript API и дополнительные элементы разметки, позволяя таскать по странице любой ее элемент. Я не думаю, что кто-то будет против поддержки drag-n-drop на уровне браузера. Поддержка браузерами drag-n-drop приведет к созданию более быстрых, "отзывчивых" web-приложений.


Есть ли поддержка в браузере?

Многим приложениям внезапно отключившийся drag-n-drop может очень повредить. Например, представьте что рисунки фигурок в шахматах не двигаются. Упс! Очень важно выяснить поддерживает ли браузер пользователя drag-n-drop (да, вообщем-то, и любой другой используемый функционал). Если drag-n-drop недоступен, следует вернуться к использованию какой-либо JS библиотеки.

Если вы используете API, проверяйте поддержку функционала с помощью библиотек, а не по User-Aget`у. Одна из лучших библиотек для этого называется Modernizr. Modernizr имеет логическое свойство на каждый проверяемый функционал. Таким образом, проверка на поддержку drag-n-drop выглядит следующим образом:

  1. if (Modernizr.draganddrop) {
  2. // браузер поддерживает drag-n-drop HTML5
  3. } else {
  4. // не поддерживает, нужно использовать JS библиотеку, как раньше
  5. }

Создание перетаскиваемых элементов

Сделать элемент перетаскиваемым очень просто. Достаточно добавить атрибут draggable=true. Перетаскивать можно что угодно - рисунки, ссылки, вообщем, любые узлы DOM.

Давайте, в качестве примера, сделаем перетаскиваемые колонки. Разметка:

  1. <div id="columns">
  2. <div class="column" draggable="true"><header>A</header></div>
  3. <div class="column" draggable="true"><header>B</header></div>
  4. <div class="column" draggable="true"><header>C</header></div>
  5. </div>

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

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

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

которое можно бросить на адресную строку, в тэг input или, даже, на рабочий стол. Используя drag-n-drop HTML5, можно добавить этот функционал любому элементу.

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

  1. <style>
  2. /* не разрешаем выделять перетаскиваемые элементы */
  3. [draggable] {
  4. -moz-user-select: none;
  5. -khtml-user-select: none;
  6. -webkit-user-select: none;
  7. user-select: none;
  8. }
  9. .column {
  10. height: 150px;
  11. width: 150px;
  12. float: left;
  13. border: 2px solid #666666;
  14. background-color: #ccc;
  15. margin-right: 5px;
  16. -webkit-border-radius: 10px;
  17. -moz-border-radius: 10px;
  18. border-radius: 10px;
  19. -webkit-box-shadow: inset 0 0 3px #000;
  20. box-shadow: inset 0 0 3px #000;
  21. text-align: center;
  22. cursor: move;
  23. }
  24. .column header {
  25. color: #fff;
  26. text-shadow: #000 0 1px;
  27. box-shadow: 5px;
  28. padding: 5px;
  29. background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  30. background: -webkit-gradient(linear, left top, right top,
  31. color-stop(0, rgb(0,0,0)),
  32. color-stop(0.50, rgb(79,79,79)),
  33. color-stop(1, rgb(21,21,21)));
  34. border-bottom: 1px solid #ddd;
  35. -webkit-border-top-left-radius: 10px;
  36. -moz-border-radius-topleft: 10px;
  37. border-top-left-radius: 10px;
  38. -webkit-border-top-right-radius: 10px;
  39. -moz-border-radius-topright: 10px;
  40. border-top-right-radius: 10px;
  41. }
  42. </style>

Результат (только таскаются, ничего больше):

A

B

C

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

Ловим событие перетаскивания

Вот список событий, которые позволяют отслеживать процесс драг-н-дропа:

  • dragstart
  • drag
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend

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

1. Начинаем перетаскивать

После того как мы добавили атрибут draggable="true", назначим обработчик события dragstart каждому элементу.

Этот код будет делать перетаскиваемый элемент на 40% прозрачнее:

  1. function handleDragStart(e) {
  2. this.style.opacity = '0.4'; // this/e.target перетаскиваемый элемент
  3. }
  4. var cols = document.querySelectorAll('#columns .column');
  5. [].forEach.call(cols, function(col) {
  6. col.addEventListener('dragstart', handleDragStart, false);
  7. });

Результат:

A

B

C

После события dragstart, его источник станет на 40% прозрачнее, что очень хорошо для создания обратной связи, т.о. пользователь видит, какой элемент он двигает. Теперь надо сделать его не прозрачным, после того, как он будет брошен. Вполне логично, что это делается в обработчике события dragend. Но об этом чуть позже.

2. dragenter, dragover и dragleave

События dragenter, dragover и dragleave можно использовать для того, чтобы сделать процесс переноса более наглядным и красивым. Например, можно добавить рамку тому элементу, над которым происходит перетаскивание. Это позволит пользователям увидеть, где можно бросить элемент.

  1. <style>
  2. .column.over {
  3. border: 2px dashed #000;
  4. }
  5. </style>
  1. function handleDragStart(e) {
  2. this.style.opacity = '0.4'; // this/e.target перетаскиваемый элемент
  3. }
  4. function handleDragOver(e) {
  5. if (e.preventDefault) {
  6. e.preventDefault(); // Обязательно. Позволяет "бросать" элемент.
  7. }
  8. e.dataTransfer.dropEffect = 'move'; // смотри часть про DataTransfer.
  9. this.addClassName('over');
  10. return false;
  11. }
  12. function handleDragEnter(e) {
  13. // this/e.target элемент над которым перетаскивают что-то
  14. this.addClassName('over');
  15. }
  16. function handleDragLeave(e) {
  17. this.removeClassName('over'); // this/e.target элемент над которым закончили перетаскивать
  18. }
  19. var cols = document.querySelectorAll('#columns .column');
  20. [].forEach.call(cols, function(col) {
  21. col.addEventListener('dragstart', handleDragStart, false);
  22. col.addEventListener('dragenter', handleDragEnter, false);
  23. col.addEventListener('dragover', handleDragOver, false);
  24. col.addEventListener('dragleave', handleDragLeave, false);
  25. });

Три места в коде, на которые стоит обратить внимание:

  • this/e.target различаются, в зависимости от события.
  • В случае перетаскивания чего-нибудь вроде ссылки, надо запретить браузеру переходить по ней. Для этого надо вызвать e.preventDefault() в обработчике события. Либо вернуть false. Разные браузеры по разному реализуют это, но если использовать и то и другое - вреда не будет.
  • Для переключения класса "over" используется событие dragenter, а не dragover. Если использовать dragover, обработчик будет вызываться очень много раз. Что очень не хорошо.

Функции для добавления, удаления, переключения CSS классов:

  1. Element.prototype.hasClassName = function(name) {
  2. return new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)").test(this.className);
  3. };
  4. Element.prototype.addClassName = function(name) {
  5. if (!this.hasClassName(name)) {
  6. this.className = this.className ? [this.className, name].join(' ') : name;
  7. }
  8. };
  9. Element.prototype.removeClassName = function(name) {
  10. if (this.hasClassName(name)) {
  11. var c = this.className;
  12. this.className = c.replace(new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)", "g"), "");
  13. }
  14. };

Подсказка: вместо определения таких вот дополнительных функций или использования таких библиотек, как jQuery, вы можете воспользоваться Element.classList API, который уже поддерживается последними сборками FF, WebKit и Chromium.

3. Бросаем

Для обработки процесса бросания элемента, надо назначить обработчик событий drop и dragend. В нем надо отменить поведение браузера по умолчанию, т.к. это может быть переход на другую страницу. Вы можете остановить дальнейшую обработку события вызвав e.stopPropagation().

Обработчики этих событий довольно малы, в drop больше ничего не происходит, а в dragend удаляется класс 'over' из всех колонок:

  1. ...
  2. function handleDrop(e) {
  3. // this / e.target это элемент куда "бросают".
  4. if (e.stopPropagation) {
  5. e.stopPropagation(); // чтобы браузер не делал редирект.
  6. }
  7. // смотри часть про DataTransfer.
  8. return false;
  9. }
  10. function handleDragEnd(e) {
  11. // this/e.target это источник события.
  12. [].forEach.call(cols, function (col) {
  13. col.removeClassName('over');
  14. });
  15. }
  16. var cols = document.querySelectorAll('#columns .column');
  17. [].forEach.call(cols, function(col) {
  18. col.addEventListener('dragstart', handleDragStart, false);
  19. col.addEventListener('dragenter', handleDragEnter, false)
  20. col.addEventListener('dragover', handleDragOver, false);
  21. col.addEventListener('dragleave', handleDragLeave, false);
  22. col.addEventListener('drop', handleDrop, false);
  23. col.addEventListener('dragend', handleDragEnd, false);
  24. });

Результат:

A

B

C

Как вы можете заметить наш пример все еще не обрабатывает бросание элемента. Добавим объект DataTransfer.

Объект DataTransfer

В свойстве dataTransfer вся магия drag-n-drop. В нем храниться данные отсылаемые в процессе перетаскивания. dataTransfer задается в событии dragset и используется в событии drop. Вызов e.dataTransfer.setData(format, data) устанавливает mimetype и данные нужные для перетаскивания.

В нашем примере, мы сохраняем исходный код перетаскиваемого элемента:

  1. var dragSrcEl = null;
  2. function handleDragStart(e) {
  3. // this это перетаскиваемый элемент
  4. this.style.opacity = '0.4';
  5. dragSrcEl = this;
  6. e.dataTransfer.effectAllowed = 'move';
  7. e.dataTransfer.setData('text/html', this.innerHTML);
  8. }

Свойство dataTransfer имеет функцию getData(format) для получения сохраненных данных. Изменим процесс бросания элемента:

  1. function handleDrop(e) {
  2. // this/e.target - перетаскиваемый элемент
  3. if (e.stopPropagation) {
  4. e.stopPropagation(); // отменяет переход по ссылке
  5. }
  6. // Если ничего не перетащили - ничего не делаем.
  7. if (dragSrcEl != this) {
  8. // изменяем исходный код элементов
  9. dragSrcEl.innerHTML = this.innerHTML;
  10. this.innerHTML = e.dataTransfer.getData('text/html');
  11. }
  12. return false;
  13. }

В глобальной переменной dragSrcEl храниться перетаскиваемый элемент. В handleDragStart() сохраняется значение этой переменной, а в handleDrop() ее содержимое меняется HTML с колонкой в которую бросают перетаскиваемый элемент.

Результат:

A

B

C

Параметры перетаскивания

Свойство dataTransfer предоставляет возможность тонкой настройки отображения процесса переноса.

dataTransfer.effectAllowed

Эффект, поддерживаемый целевым элементом перетаскивания. Как правило, это значение задается обработчиком события dragstart. Может принимать следующие значения: none, copy, copyLink, copyMove, link, linkMove, move, all и uninitialized.

dataTransfer.dropEffect

Эффект, выбранный пользователем или целевым элементом. Может принимать следующие значение: none, copy, link, move.

e.dataTransfer.setDragImage(img element, x, y)

Вместо того чтобы использовать при перетаскивании полупрозрачную картинку по умолчанию, отобразим свою иконку:

var dragIcon = document.createElement('img');

dragIcon.src = 'logo.png';

dragIcon.width = 100;

e.dataTransfer.setDragImage(dragIcon, -10, -10);

Результат (с логотипом гугла, когда перетаскиваете):

A

B

C

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

С помощью drag-n-drop можно перетаскивать файлы с рабочего стола в браузер. По крайней мере Google Chrome поддерживает этот функционал.

Перетаскивание файлов с рабочего стола в браузер

Перетаскивание файлов происходит почти так же, как и обычный drag-n-drop. Отличие только в обработке бросания файлов. Вместо использования dataTransfer.getData(), данные о файлах хранятся в dataTransfer.files:

  1. function handleDrop(e) {
  2. e.stopPropagation(); // останавливаем дальнейшую передачу события
  3. e.preventDefault();
  4. var files = e.dataTransfer.files;
  5. for (var i = 0, f; f = files[i]; i++) {
  6. // читаем объекты File из FileList
  7. }
  8. }

О том как использовать drag-n-drop для перетаскивания файлов в браузер, подробно написано в статье "Reading local files in JavaScript".

Перетаскивание из браузера на рабочий стол

Для того разобраться, как перетаскивать файлы на рабочий стол, смотрите "Drag out files like Gmail" от CSS Ninja.

Окончательный пример

Конечный вариант, немного измененный и со счетчиком перемещений:

A

B

C

D

Самое интересное, что все колонки являются одновременно и перетаскиваемыми элементами и местами, куда можно перетащить. Обычно делается несколько по другому. Примеры можно посмотреть на html5demos.com/drag.

Заключение

Бесспорно, модель drag-n-drop в HTML5 является более сложной, по сравнению с тем же jQuery UI. Однако, если вы можете использовать встроенный в браузер функционал - используйте его! В конце концов, это же и есть цель HTML5, стандартизировать возможности для разработки сложных приложений в браузере. Думаю, популярные библиотеки со временем включат в свой состав поддержку встроенной реализации браузером drag-n-drop.

Ссылки



6 Responses to Drag-n-drop в HTML5

  1. gravatar

    HTML5, ня! Интересно, когда он всё-таки станет стандартом?

  2. gravatar

    Очень подробно и доступно, спасибо за хорошее изложение. Могу посоветовать добавить в ссылки видео с CAMP 5, в одном из последних докладов был неплохой материал

  3. gravatar

    эхх ослик 6 не понимает

  4. gravatar

    Opera 11 тоже не понимает, появляется только значок перетаскивания

  5. gravatar

    А вы просто делайте, и предлагайте делать всем веб-мастерам сайты с пользованием HTML5, и тогда идиотам (по статистике IQ юзеров IE6 ниже юзеров всех остальных браузеров) придётся перейти на нормальные современные браузеры.

  6. gravatar

    Good post about Drag & Drop http://plutov.by/post/html5_drag_and_drop

Leave a Reply