HTML5 и drag & drop нескольких объектов

  • Tutorial
Перетаскиванием объектов на HTML5 никого уже не удивишь, но все же попробую рассказать кое о чем интересном, а именно, как сделать красивое перетаскивание нескольких объектов, используя только HTML5.

На странице есть набор элементов типа А, которые можно по одному или группой перетащить в элемент типа Б. Пользователь должен видеть, перетаскивает он один элемент или несколько. Перетаскивать один элемент просто, достаточно присвоить свойству draggable значение true, наверняка, все это делали.

У меня уже готова страница с перетаскиванием «по-одному». Элементы с классом item можно выделить и перенести в элемент с классом dropzone (выделяем, кликнув по квадратику).

Демо некрасивого перетаскивания

Для выделения нужно кликнуть по элементу, перетаскивать можно только выделенные элементы, можно перетаскивать несолько элементов за один раз.



Структура страницы:

<div class="items-container">
    <div class="items">
        <div class="item"><span>a</span></div>
        <div class="item"><span>b</span></div>
        <div class="item"><span>c</span></div>
        <div class="item"><span>d</span></div>
        <div class="item"><span>e</span></div>
        <div class="item"><span>f</span></div>
        <div class="item"><span>g</span></div>
    </div>
</div>

<div class="dropzone-container">
    <div class="dropzone"></div>
</div>

Код:

// jQuery убирает у объектов событий "лишние" свойства, поэтому, если мы хотим использовать HTML5
// примочки вместе с jQuery, нужно включить для событий свойство dataTransfer.
jQuery.event.props.push('dataTransfer');

// И еще парочку.
jQuery.event.props.push('pageX');
jQuery.event.props.push('pageY');

// Элементы для перетаскивания.
$('.item')

    // По клику устанавливаем/снимаем выделение, переключаем свойство draggable.
    .on('click', function(e) {
        e.preventDefault();
        $(this).toggleClass('selected');
        this.draggable = $(this).hasClass('selected');
    })

    // Перед тем как начать перетаскивать элементы,
    .on('dragstart', function(e) {
        var html = '',
            // находим все выделенные элементы,
            $selectedItems = $('.items .selected');

        // собираем HTML выделенных элементов.
        $selectedItems.each(function() {
            html += this.outerHTML;
        });

        // Устанавливаем собранный HTML в качестве данных для перетаскивания.
        // Это никак не влияет на визуальную часть.
        e.dataTransfer.setData('text/html', html);

        return true;
    })

    .on('dragend', function(e) {
        resetUI();
    });

// Дропзона
$('.dropzone')

    // При наведении добавляем класс dragover
    .on('dragenter', function(e) {
        $(this).addClass('dragover');
    })

    // Убираем класс dragover
    .on('dragleave', function(e) {
        $(this).removeClass('dragover');
    })
    .on('dragover', function(e) {
        // Чтобы до элемента дошло событие drop, нужно запретить передачу по цепочке события dragover
        if (e.preventDefault) e.preventDefault();
        return false;
    })

    // Обрабатываем drop
    .on('drop', function(e) {
        // Достаем HTML из события
        var html = e.dataTransfer.getData('text/html');

        // Добавляем HTML к дропзоне
        $(this).append(html);

        resetUI();
        return true;
    });

function resetUI() {
    $('.selected').removeClass('selected').attr('draggable', false);
    $('.dragover').removeClass('dragover');
}

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

По старинке это делается просто — подписываемся на события мыши и таскаем группу элементов за мышью. С приходом HTML5 от части этой рутины можно избавиться, переложив заботу об анимации на плечи браузера. Работать будет так же как и в случае с одним элементом.

Чтобы при перетаскивании нескольких элементов получить «правильную картинку» воспользуемся методом setDragImage объекта e.dataTransfer.

function setDragImage(image, x, y)
image — элемент, изображение которого будет использовано при перетаскивании.
x и y — смещение.

Итак, перед началом перетаскивания, после того как мы установили данные для перетаскивания e.dataTransfer.setData('text/html', html), нам нужно собрать правильный элемент-картинку для передачи в метод setDragImage. Сперва определим переменные, которые понадобятся для этого:

// Элемент, за который тащим.
var $draggedItem = $(e.currentTarget),
    draggedItemOffset = $draggedItem.offset(),

    // Прямоугольник, в который вписываются выделенные элементы.
    frame = getFrame($selectedItems),

    // Координаты точки, за которую будем тащить.
    dx = e.pageX - draggedItemOffset.left + (draggedItemOffset.left - frame.lx),
    dy = e.pageY - draggedItemOffset.top + (draggedItemOffset.top - frame.ly),

    // Элемент, который будем передавать как image в setDragImage.
    $image = $(document.createElement('div'));

Используя frame, установим нужный размер и координаты элементу-картинке:
$image.css({
    position: 'absolute',
    // Спрячем его подниз, чтобы не обрывал событие dragstart.
    zIndex: -1,
    left: frame.lx,
    top: frame.ly,
    width: Math.abs(frame.lx - frame.rx),
    height: Math.abs(frame.ly - frame.ry)
});

Добавим копии выделенных элементов к $image:
$selectedItems.each(function(i, item) {
    var $item = $(item),
        $clone = $item.clone(),
        itemOffset = $item.offset();

    // Позиционируем клоны внутри $image.
    $clone.css({
        position: 'absolute',
        left: itemOffset.left - frame.lx,
        top: itemOffset.top - frame.ly
    });

    $image.append($clone);
});

Финальный аккорд:
// Добавляем $image на страницу.
$('body').append($image);

// Устанавливаем $image в качестве картинки для перетаскивания.
e.dataTransfer.setDragImage($image.get(0), dx, dy);

// Удаляем $image через 1 милисекунду. Если удалить сразу,
// то вызов setDragImage произойдет до того как отрендерится $image.
window.setTimeout(function() {
    $image.remove();
}, 1);

Метод getFrame используем для нахождения прямоугольника, в который вписываются выделенные элементы:
function getFrame($items) {
    var offset = $items.first().offset(),
        frame = { lx: offset.left, ly: offset.top, rx: offset.left, ry: offset.top };

    $items.each(function() {
        var $this = $(this),
        offset = $this.offset(),
        width = $this.width(),
        height = $this.height();

        if (offset.left < frame.lx) frame.lx = offset.left;
        if (offset.top  < frame.ly) frame.ly = offset.top;
        if (offset.left + width > frame.rx) frame.rx = offset.left + width;
        if (offset.top + height > frame.ry) frame.ry = offset.top + height;

    });
    return frame;
}

Демо красивого перетаскивания
Код примеров на GitHub
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    +3
    Кто уже пользуется html5 drag&drop в продакшене?

    Мне очень нравится, что если открыть два этих примера в разных окнах, то можно перетаскивать между окнами. Это можно использовать в многооконном браузерном приложении, коих, пока, почти, не существует.
      +4
      Собственно, автор пользуется.
      0
      Вёрстка у меня немного едет из-за прижатия к нижней границе экрана, лучше сделать margin-top у dropzone-container и отказаться от абсолютного позиционирования не вижу смысла тут в нём у этого и блока items.
        +2
        Это ахрененно!!! Работает даже меж расово между FF и Chrome
          0
          при перетаскивании из хрома в FF в FF появляются иероглифы вместо квадратов
            0
            А если из FF в chrome, то все ок)
              +1
              С iPhone на Android не пробовали потаскать? Может здесь новый способ обмена данными вырисовался)
                +1
                Хотел бы я на это посмотреть :D

                PS: Не сдержался, попробовал перетащить с локального компьютера, ну удаленный рабочий стол сервера от hetzner :D
                Хотя и так можно было догадаться что не получится)))
                  +5
                  Таможня не пустила.
                    0
                    Кибер Почта России в действии!
          0
          Было бы круто еще фолбэк под тач добавить
            0
            Думаю, добавлю в скором времени.

          Only users with full accounts can post comments. Log in, please.