Встроенные функции перетаскивания в HTML5

HTML5 Rocks

Введение

Уже много лет мы используем для упрощения сложных элементов интерфейса, таких как анимации, скругленные углы и перетаскивание, библиотеки JQuery и Dojo. Несомненно, внешняя привлекательность важна для создания качественных веб-сайтов, но зачем нужна библиотека для общих задач, которые используют все разработчики?

Перетаскивание (drag and drop, DnD) – одна из важнейших функций в HTML5. Эта спецификация определяет событийный механизм, API JavaScript и дополнительную метку, показывающую, что все элементы на странице могут перетаскиваться. Вряд ли кто-то будет возражать против встраивания в браузер дополнительных функций. Реализованная в браузере функция перетаскивания означает более быстрые и управляемые веб-приложения.

Обнаружение функций

Многие приложения, в которых используется перетаскивание, не могут без него обходиться. Представьте себе, что вам пришлось бы продумывать шахматную партию, не имея возможности перемещать фигуры. Несмотря на полную поддержку браузера, определять, реализовано ли в нем перетаскивание (или иная функция HTML5, выполняющая те же задачи), нужно для того, чтобы обеспечить решение, способное качественно уменьшать мощность. Если функция перетаскивания недоступна, работу приложения можно реализовать, подключив библиотеку.

Если вам необходимы возможности API, вместо определения параметра User-Agent браузера используйте обнаружение функций. Одна из лучших библиотек для обнаружения функций называется Modernizr. Для каждой проверяемой функции Modernizr задает логическое значение. В результате получается очень короткий код проверки на наличие функции перетаскивания:

if (Modernizr.draganddrop) {
  // Browser supports HTML5 DnD.
} else {
  // Fallback to a library solution.
}

Создание перетаскиваемого содержания

Сделать объект перетаскиваемым очень легко: для этого используется атрибут draggable=true. Перетаскивание можно включать практически для любых элементов, в т. ч. изображений, ссылок, файлов и даже узлов DOM.

В качестве примера начнем с создания перегруппируемых столбцов. Базовая метка может выглядеть примерно так:

<div id="columns">
  <div class="column" draggable="true"><header>A</header></div>
  <div class="column" draggable="true"><header>B</header></div>
  <div class="column" draggable="true"><header>C</header></div>
</div>

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

Перетаскивание изображения в браузере
Большинство браузеров поддерживают перетаскивание изображений по умолчанию.

Его можно перенести в адресную строку, элемент <input type="file" /> и даже на рабочий стол. Чтобы сделать перетаскиваемыми другие типы содержания, необходимо использовать API перетаскивания HTML5.

С помощью CSS3 можно представить разметку в виде столбцов. С добавлением атрибута cursor: move пользователи получают визуальный индикатор подвижности элемента:

<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
.column {
  height: 150px;
  width: 150px;
  float: left;
  border: 2px solid #666666;
  background-color: #ccc;
  margin-right: 5px;
  -webkit-border-radius: 10px;
  -ms-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 3px #000;
  -ms-box-shadow: inset 0 0 3px #000;
  box-shadow: inset 0 0 3px #000;
  text-align: center;
  cursor: move;
}
.column header {
  color: #fff;
  text-shadow: #000 0 1px;
  box-shadow: 5px;
  padding: 5px;
  background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -webkit-gradient(linear, left top, right top,
                               color-stop(0, rgb(0,0,0)),
                               color-stop(0.50, rgb(79,79,79)),
                               color-stop(1, rgb(21,21,21)));
  background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  border-bottom: 1px solid #ddd;
  -webkit-border-top-left-radius: 10px;
  -moz-border-radius-topleft: 10px;
  -ms-border-radius-topleft: 10px;
  border-top-left-radius: 10px;
  -webkit-border-top-right-radius: 10px;
  -ms-border-top-right-radius: 10px;
  -moz-border-radius-topright: 10px;
  border-top-right-radius: 10px;
}
</style>

Результат (элементы могут перетаскиваться, но ничего не делают):

А
Б
В

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

Прослушивание событий перетаскивания

Для контроля над процессом перетаскивания можно использовать целый ряд событий:

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

Для обработки алгоритма перетаскивания необходимо понимать, где находятся исходный элемент (откуда начинается перетаскивание), полезные данные (что мы пытаемся перетащить) и цель (область, в которой должен оказаться объект). Исходным элементом может быть изображение, список, ссылка, файловый объект, блок HTML-кода, что угодно. Цель – это зона (или несколько зон) перетаскивания, принимающая данные, которые пользователь пытается перетащить. Помните о том, что не все элементы могут быть целью (например, изображения).

1. Начало перетаскивания

Определив в содержании атрибуты draggable="true", добавьте обработчики событий dragstart, которые будут запускать последовательность перетаскивания для каждого столбца.

Как только пользователь начнет перетаскивать столбец, этот код уменьшит его непрозрачность до 40%:

function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
});

Результат:

А
Б
В

Поскольку целью события dragstart является наш исходный элемент, установка для атрибута this.style.opacity значения 40% позволит пользователю видеть перемещение выбранного элемента. Нам осталось только вернуть 100-процентную непрозрачность столбцов после перетаскивания. Для решения этой задачи используется событие dragend. Подробнее мы рассмотрим его попозже.

2. dragenter, dragover и dragleave

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

<style>
.column.over {
  border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }

  e.dataTransfer.dropEffect = 'move';  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnter(e) {
  // this / e.target is the current hover target.
  this.classList.add('over');
}

function handleDragLeave(e) {
  this.classList.remove('over');  // this / e.target is previous target element.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false);
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
});

В этом коде стоит отметить несколько моментов.

  • Атрибут this/e.target отличается для каждого типа событий и зависит от места в модели событий перетаскивания.
  • При перетаскивании такого объекта, как ссылка, необходимо переопределить поведение браузера по умолчанию, т. е. переход по ней. Для этого нужно вызвать функцию e.preventDefault() в событии dragover. Другой вариант – использовать в том же обработчике значение false. Потребность в таких мерах зависит от конкретного браузера, но они определенно не помешают.
  • Для переключения класса over используется событие dragenter, а не dragover Если использовать dragover, наш класс CSS будет переключаться слишком часто, поскольку событие dragover срабатывает при каждом наведении на столбец. В результате обработчику браузера придется выполнять слишком много лишней работы. Чем меньше перерисовок, тем лучше.

3. Завершение перетаскивания

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

Наш пример со столбцами не обойдется без события drop, но прежде необходимо кое-что доработать, удалив класс over из каждого столбца с помощью события dragend:

...

function handleDrop(e) {
  // this / e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // stops the browser from redirecting.
  }

  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnd(e) {
  // this/e.target is the source node.

  [].forEach.call(cols, function (col) {
    col.classList.remove('over');
  });
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false)
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
  col.addEventListener('drop', handleDrop, false);
  col.addEventListener('dragend', handleDragEnd, false);
});

Результат:

А
Б
В

Если вы тщательно следили за происходящим, то заметили, что перетаскивание столбцов в нашем примере по-прежнему не работает так, как нужно. Введем объект DataTransfer.

Объект DataTransfer

Свойство DataTransfer – это то самое место, где реализуется перетаскивание. Оно содержит часть данных, отправляемых при выполнении этого действия. Объект dataTransfer устанавливается в событии dragstart, а считывается и обрабатывается в событии drop. При вызове функции e.dataTransfer.setData(format, data) содержанию объекта назначается MIME-тип, а полезные данные передаются в качестве аргументов.

В нашем примере в качестве полезных данных выступает фактический HTML-код исходного столбца:

var dragSrcEl = null;

function handleDragStart(e) {
  // Target (this) element is the source node.
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

Удобно, что у объекта dataTransfer есть также функция getData(format), позволяющая доставлять перетаскиваемые данные по MIME-типу. Вот как выглядит измененный процесс перетаскивания столбца:

function handleDrop(e) {
  // this/e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // Stops some browsers from redirecting.
  }

  // Don't do anything if dropping the same column we're dragging.
  if (dragSrcEl != this) {
    // Set the source column's HTML to the HTML of the columnwe dropped on.
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

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

Результат:

А
Б
В

Свойства перетаскивания

Объект dataTransfer обладает свойствами, которые создают визуальную подсказку для пользователей в процессе перетаскивания. Их также можно использовать для управления реакцией каждой цели перетаскивания на определенный тип данных.

dataTransfer.effectAllowed
Ограничивает "тип перетаскивания", которое пользователь может выполнять с элементом. Это свойство используется в модели обработки перетаскивания для инициализации объекта dropEffect во время событий dragenter и dragover. Это свойства может принимать следующие значения: none, copy, copyLink, copyMove, link, linkMove, move, all и uninitialized.
dataTransfer.dropEffect
Управляет реакцией, которую пользователь получает во время событий dragenter и dragover. Когда перетаскиваемый объект наводится на целевой элемент, указатель браузера принимает вид, соответствующий типу предполагаемой операции (например, копирование, перенос и т. д.). Свойство может принимать следующие значения: 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);

Результат (при перетаскивании этих столбцов должен отображаться логотип Google):

А
Б
В

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

С помощью API перетаскивания можно перетаскивать файлы с рабочего стола в веб-приложение, открытое в окне браузера. Кроме того, Google Chrome позволяет перетаскивать файловые объекты из окна браузера на рабочий стол.

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

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

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; f = files[i]; i++) {
    // Read the File objects in this FileList.
  }
}

Полное руководство по перетаскиванию файлов с рабочего стола в браузер приведено в разделе Перетаскивание как способ выбора статьи Чтение локальных файлов в JavaScript.

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

Полное руководство по перетаскиванию файлов из браузера на рабочий стол приведено в статье Перетаскивание файлов как из Gmail на веб-сайте CSS Ninja.

Примеры

Вот конечный вариант с улучшенным внешним видом и счетчиком перемещений:

А
Б
В
Г

В этом примере со столбцами интересно то, что каждый из них является одновременно исходным объектом и целью перетаскивания. Чаще всего исходный и целевой элементы различаются. Посмотрите пример: html5demos.com/drag.

Заключение

Модель перетаскивания в HTML5 сложнее, чем в других решениях, таких как интерфейс JQuery. Однако если у вас есть возможность задействовать встроенный API браузера, ее нужно использовать. В конце концов, в этом и заключается суть HTML5: стандартизировать и сделать доступным весь набор собственных возможностей браузера. Надеемся, что со временем в популярные библиотеки, в которых используется функция перетаскивания, будет включена поддержка HTML5 по умолчанию и возможность по-разному настраивать JS-решение.

Ссылки

Comments

0