Drag-and-drop перемещение элементов по категориям на jQuery
Пожалуй, самым простым интерфейсным решением сортировки элементов по категориям является механизм перетаскивания: перетащить – бросить – сохранить. Такое решение является стандартным и в операционных системах, когда вы перекладываете папки или файлы из одного места в другое. Поэтому и на сайте грех не использовать уже известную пользователю логику. К слову, в gmail также письма можно перетаскивать на ярлыки.
Итак, рассмотрим, как решить задачу drag-and-drop сортировки с использованием javascript. Предположим, что у пользователя есть список из нескольких фотографий, и ему необходимо разложить их по соответствующим папкам/альбомам. Приведенный ниже пример может быть легко адаптирован под ваши нужды.
Важно! Сразу следует отметить, что будем рассматривать не drag events, а стандартные mouse events для десктопных устройств и touch events для тач-устройств. Т.е. по сути будем эмулировать drag-and-drop события.
Макет
Интерфейс состоит из двух частей: списка папок (левая колонка) и списка элементов (правая колонка). Все тривиально и стандартно.
Для минимизации трудозатрат макет в примере сверстан на основе весьма популярного фреймворка bootstrap, однако стили можно легко написать самому, не прибегая к сторонним css-фреймворкам. В представленном примере были использованы плагины:
- underscore.js – для шаблонизации данных, приходящих от сервера;
- nanoScroller.js – для стилизации скроллбара для случая, когда список папок занимает по высоте больше места, чем высота окна.
Алгоритм сортировки фотографий по альбомам
Не углубляясь в подробности алгоритм будет следующим:
- Вешаем обработчик событий mousedown/touchstart на перетаскиваемые элементы.
- При возникновении события mousedown/touchstart вешаем:
- Событие mousemove/touchmove.
- Событие mouseup/touchend.
- При возникновении события mousemove/touchmove:
- Определяем dom-элемент, над которым находится курсор мыши.
- Если активный элемент drop-областью, то подсвечиваем его.
- При возникновении события 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 событий.
Добавить комментарий