Главная » Frontend » Drag-and-drop перемещение элементов по категориям на jQuery
2016/02/04Метки: , , ,

Drag-and-drop перемещение элементов по категориям на jQuery

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

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

Пример работы скрипта перетаскивания элементов

Пример работы скрипта перетаскивания элементов

Важно! Сразу следует отметить, что будем рассматривать не drag events, а стандартные mouse events для десктопных устройств и touch events для тач-устройств. Т.е. по сути будем эмулировать drag-and-drop события.

Макет

Интерфейс состоит из двух частей: списка папок (левая колонка) и списка элементов (правая колонка). Все тривиально и стандартно.

Для минимизации трудозатрат макет в примере сверстан на основе весьма популярного фреймворка bootstrap, однако стили можно легко написать самому, не прибегая к сторонним css-фреймворкам. В представленном примере были использованы плагины:

  1. underscore.js – для шаблонизации данных, приходящих от сервера;
  2. nanoScroller.js – для стилизации скроллбара для случая, когда список папок занимает по высоте больше места, чем высота окна.

Алгоритм сортировки фотографий по альбомам

Не углубляясь в подробности алгоритм будет следующим:

  1. Вешаем обработчик событий mousedown/touchstart на перетаскиваемые элементы.
  2. При возникновении события mousedown/touchstart вешаем:
    • Событие mousemove/touchmove.
    • Событие mouseup/touchend.
  3. При возникновении события mousemove/touchmove:
    • Определяем dom-элемент, над которым находится курсор мыши.
    • Если активный элемент drop-областью, то подсвечиваем его.
  4. При возникновении события mouseup/touchend:
    • Проверяем было ли событие типа drag-and-drop и является ли элемент, над которым закончилось действие пользователя, drop-областью.
    • В случае успешного прохождения проверок инициируем ajax-запрос на перемещение элемента в папку. Для этого потребуется передать id элемента и id папки.
    • После того, как запрос на перемещение элемента исполнен, в ответ будет получен список папок с обновленным количеством элементов. Рендерим полученные данные.

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

Список drop-областей

При верстке данного блока следует обратить внимание, что каждую папку, в которую у пользователя будет возможность переносить фотографии, необходимо обернуть в div с вспомогательным классом b-folders__item и указанием id в data-атрибуте. С помощью этого класса мы будем узнавать, является ли данная область целевой, т.е. отлавливать «бросок» фотографии на эту область. И при успешном выполнении также следует отправлять ajax-запрос, для которого потребуется указать id папки, в которую перемещается элемент.

Функционал находится в файле 30-category.js. Непосредственно к теме статьи этот код не относится. Он является вспомогательным и обеспечивает «плавание» меню с папками вслед за скроллом страницы, а также применяет кастомный скроллбар для случая, когда все элементы не влезают в видимую область.

Список drag-элементов

Функционал находится в файле 40-folders.js. Нет смысла подробно описывать последовательность действий – весь код хорошо прокомментирован. Приведем основной код, которому посвящена статья, и остановимся на некоторых моментах.

// обработчик на нажатие мыши
body.on('mousedown touchstart', '.b-photos__item .b-photos__link', function(e) {
    var self = $(this),
        offerId = self.data('id'),
        pos = {},
        drop,
        touchElem,
        currentTarget,
        timeMouseDown = new Date().getTime(),
        moveInitFlag = false,
        moveInit = function() {

            // курсор
            body.addClass('drag');

            // создаем фейк элемент
            body.append('<div id="drag-drop" class="b-photos__touch">' + self.find('.b-photos__media').html() + '</div>');
            touchElem = $('#drag-drop');
            touchElem.css({
                left: pos.x + 2,
                top: pos.y + w.scrollTop() + 2
            });

            // отменяем клик
            self.one('mouseup touchend click tap', function(e) {
                e.preventDefault();
            });

            // помечаем, что пользователь тащит
            moveInitFlag = true;
        };

    // выбираем все дроп-зоны
    dropFolders = parent.find('.' + baseClass + '__item');

    // отмена перетаскивания элемента
    e.preventDefault();

    // сохранение позиции курсора
    pos.x = e.clientX;
    pos.y = e.clientY;

    // вешаем на элемент обработчик мув
    body.on('mousemove touchmove', function(e) {
        var isDropZone,
            currentPos = {},
            touches = e.originalEvent.touches;

        currentTarget = $(e.target);

        // bugfix Chrome https://code.google.com/p/chromium/issues/detail?id=161464
        // на событие mousedown хром генерирует событие touchmove
        if (new Date().getTime() < timeMouseDown + 10) {
            return;
        }

        // сохраняем позицию курсора
        currentPos.x = e.clientX;
        currentPos.y = e.clientY;

        // инициализация перетаскивания
        if (!moveInitFlag) moveInit();

        // обновляем позицию курсора и target, если тач устройство
        if (touches !== undefined) {
            currentPos.x = touches[0].clientX;
            currentPos.y = touches[0].clientY;
            currentTarget = $(document.elementFromPoint(currentPos.x, currentPos.y));
        }

        // обновление позиции превьюшки
        touchElem.css({
            left: currentPos.x + 2,
            top: currentPos.y + w.scrollTop() + 2
        });

        // находится ли курсор над drop-зоной ?
        isDropZone = person.testDropZone(currentTarget);

        // если target является папкой
        if (isDropZone) {
            dropFolders.removeClass(baseClass + '__item--drop');
            isDropZone.addClass(baseClass + '__item--drop');
        } else {
            dropFolders.removeClass(baseClass + '__item--drop');
        }
    });

    // отжимаем кнопку мыши
    body.one('mouseup touchend', function(e) {
        var elem = $(e.target),
            isDropZone,
            folderId = 0,
            folderName = '',
            folderCurrent = null,
            touches = e.originalEvent.changedTouches;

        // отвязываем события перемещения
        body.off('mousemove touchmove');

        // проверям какое событие произошло
        if (!moveInitFlag) return;

        // отменяем действие по умолчанию
        e.preventDefault();
        e.stopPropagation();

        // сбрасываем курсор
        body.removeClass('drag');

        // удаляем превьюшку 
        if (touchElem) touchElem.remove();

        // обновляем target, если тач устройство
        if (touches !== undefined) {
            // не работает в Safari
            // elem = $(document.elementFromPoint(touches[0].clientX, touches[0].clientY));

            // поэтому берем последнее значение из события touchmove
            elem = currentTarget;
        }

        // находится ли курсор над drop-зоной ?
        isDropZone = person.testDropZone(elem);

        // если target является папкой
        if (isDropZone) {

            folderId = isDropZone.data('id');
            folderName = isDropZone.find('.b-category-list__val').text();

            if (folderId === 0) folderId = '';

            // id текущей папки
            folderCurrent = parent.data('folderCurrent');

            if (folderCurrent === folderId) {
                // показываем сообщение
                DND.alert.show({
                    text: DND.CONST.MESSAGES.FOLDERS.INFOLDER,
                    time: 2000
                });

            } else {

                // запрос на перемещение папки
                $.ajax({
                    type: 'POST',
                    url: parent.data('folderMove'),
                    data: {
                        folder_id: folderId,
                        offer_id: offerId
                    },
                    dataType: 'json',
                    beforeSend: function() {
                        // блокируем блок 
                        self.parent().append('<div class="b-photos__load"></div>');
                    }
                }).done(function(data) {
                    var currentElem,
                        allCount,
                        alert;

                    // получен список папок

                    // прогоняем данные по шаблону и обновляем код блока
                    parent.find('.' + baseClass + '__list').html(DND.tmpl.other['tmpl__folders-list']({
                        folders: data,
                        current: folderCurrent
                    }));

                    // меняем счетчик количества объектов в папке Мое портфолио
                    (data[0].cnt_offers > 0) 
                        ? allCount = '(' + data[0].cnt_offers + ')'
                        : allCount = '';
                    parent.find('.' + baseClass + '__parent .b-category-list__count').text(allCount);

                    // удаляем итем со страницы
                    currentElem = self.closest('.b-photos__item');
                    currentElem.hide(500, function() {
                        currentElem.remove();

                        // выраниванием список
                        // DND.visual.valignListPortfolio();

                        // инициируем событие перемещения презентации в другу папку
                        body.trigger('folder.move');
                    });

                    // показываем сообщение
                    alert = DND.alert.show({
                        text: DND.CONST.MESSAGES.FOLDERS.AFTER_MOVE + ' «' + folderName + '».',
                        time: 2000
                    });

                }).fail(function() {
                    self.parent().find('.b-photos__load').fadeOut(300);

                    DND.alert.show({
                        text: DND.CONST.MESSAGES.ERROR_DEFAULT,
                        time: 2000
                    });
                });
            }
        }

        // убираем стили перетаскивания с папок
        dropFolders.removeClass(baseClass + '__item--drop');

        return false;
    });
});

Как получать координаты для touch-устройств?

Для десктопных устройств в объекте события event есть свойства clientX и clientY, однако в touch-устройствах они отсутствуют. В то же время в объекте события есть свойство touches. Следует всего лишь взять первый элемент массива и так же получить значения свойств clientX и clientY.

[event.touches[0].clientX, event.touches[0].clientY]

Как получить dom-элемент по координатам мыши?

Чтобы получить элемент, над которым находится курсор необходимо использовать метод elementFromPoint:

document.elementFromPoint(event.clientX, event.clientY)

Как определить, что dom-элемент является drop-областью?

Всё просто, используем метод closest, который ищем ближайшего предка с указанным классом. Если такого не нашлось, значит элемент не является drop-областью.

Баг с определением элемента по координатам в safari на touch-устройствах

На момент написания статьи в Safari был замечен баг, заключающийся в том, что метод elementFromPoint отрабатывал неверно, и возвращался указатель на родителя на несколько уровней выше. Этот момент обозначен в коде, соответствующая строка закомментирована. Поэтому в качестве альтернативы было решено брать последнее запомненное значение из события touchmove.

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

Метки: Метки: , , ,
Понравилась статья? — Ставь лайк!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

×
Новости и обзор новинок рынка строительной техники.
Подпишитесь на обновления нашей группы!