Свайп плашек
Свайп плашек как в почтовых клиентах, когда появляются кнопки "в архив" и "удалить" под самой плашкой, с помощью javascript.
Общий принцип заключается в том, что имеется два html-дива, расположенные друг над другом. В нижнем по углам стоят кнопки, а в верхнем плашка, которую мы будем сдвигать, обнажая нижележащие кнопки. Сдвигать будем с помощью css-стиля translateX.
Свайп базируется на трех javascript-событиях touchstart, touchmove, touchend. Это момент прикосновения, передвижение и отпускание. В момент прикосновения однократно возникает событие touchstart. По мере перетаскивания браузер постоянно генерит событие touchmove и сообщает текущие координаты пальца. В момент отпускания однократно возникает событие touchend и в нем уже нет информации о текущем положении пальца, поэтому последнее положение пальца нужно сохранять у себя в переменных заранее.
Кроме того список плашек может быть прокручиваемым по вертикали, и тут появляется событие scroll. Итого у нас есть четыре программных точки, которые нужно продумать.
В момент прикосновения нужно инициализировать переменные, которые будут использоваться в моменты перетаскивания и отпускания. Это позиция прикосновения и текущая позиция вертикального скролла. Ну и сбрасываем флаг скролла, о чем ниже написано подробнее.
Кроме того есть два нюанса, которые нужно учесть.
Первый заключается в том, что микродвижения пальца сразу видимы по движению плашки и выглядит и ощущается это не красиво. Для этого нужно выполнять две компенсации. В браузере chrom было бы достаточно одной, поскольку они видимо уже продумали и побороли вертикальные микродвижения. Но вот браузер firefox честно дергает скроллируемый блок, пока палец ложится на экран и увеличивает площадь соприкосновения и это будет мешать дальнейшему свайпу. Вернемся к двум компенсациям.
1. На событии touchmove не сдвигать блок по горизонтали пока палец не сдвинется на какое-то минимальное число пикселей. Экспериментальным путем выбрано 16 пикселей.
2. На событии прокрутки откатывать прокрутку пока палец не сдвинется по вертикали на минимальное число пикселей. Экспериментальным путем выбрано 5 пикселей. Тут важно не перепутать сдвиг именно пальца, а не саму прокрутку. Иначе получится эффект что если медленно вести пальцем долго, то можно его продвинуть далеко, а скролл так и не заработает.
Это обеспечивает некоторую мертвую зону и дает возможность двигать пальцем не строго по вертикали или строго по горизонтали, а с некоторой ошибкой, которая присуща человеку.
Второй нюанс, который нужно учесть, заключается в том, что нельзя одновременно и скролить и двигать плашку по горизонтали. Поэтому если мы определили что начался горизонтальный свайп, то нужно заблокировать вертикальный скролл. А если начался вертикальный скролл, то нужно заблокировать горизонтальный свайп. Для этого нужна пара переменных, которые мы будем устанавливать в true.
В результате получаем следующее описание кода.
Событие scroll:
1. Проверяем флаг, обозначающий что начался свайп и если он установлен, откатываем скоролл и завершаем код.
2. Вычисляем сдвиг пальца по вертикали и если он был менее 5 пикселей, то откатываем скролл в старую позицию и завершаем код.
3. Если же сдвиг оказался больше 5 пикселей, то скролл выставляем в старый скролл плюс сдвиг пальца и устанавливаем переменную блокирующую горизонтальный свайп в true.
Событие touchstart:
1. Запоминаем текущее состояние вертикальной прокрутки.
2. Запоминаем координаты прикосновения X и Y - для дальнейших вычислений.
3. Сбрасываем флаг скролла. Потому что скроллирование его устанавливает, но не сбрасывает.
Событие touchmove:
1. Вычисляем и запоминаем смещение пальца по вертикали. Это нужно для вертикального скролла.
2. Проверяем переменную что начался вертикальный скролл и если да, то завершаем код.
3. Вычисляем сдвиг пальца по оси X, и если меньше 16 и не начался свайп завершаем код. Если свайп начался, то продолжаем код, иначе будет заклинивание если приблизились к начальной точке. Сдвиг запоминаем в переменной в плашке - она пригодится при событии отпускания.
4. Устанавливаем флаг свайпа.
5. Пробегаемся по всем плашкам и сбрасываем сдвинутые.
6. Берем старую позицию плашки по оси X и добавляем к ней смещение пальца. Старая позиция будет либо в нуле, либо нет если плашка сдвинута для показа кнопки под ней.
Событие touchend:
1. Проверяем переменную что начался вертикальный скролл и если да, то завершаем код.
2. Узнаем величину левой и правой стоп-области, там где плашка фиксируется в сдвинутом состоянии.
3. Если плашка заехала в позицию меньше минимальной, то ставим минимальную.
4. Если плашка заехала в позицию больше максимальной, то ставим максимальную.
5. Иначе сбрасываем ее в 0.
6. Запоминаем позицию в переменной. Это нужно для свайпа на место. И сдвигаем плашку с помощью css.
7. Сбрасываем переменную обозначающую начавшийся горизонтальный свайп в false.
8. Сбрасываем переменную обозначающую начавшийся скролл в false.
Теперь нужно определиться с именами переменных и где их хранить. Ведь на странице может быть несколько элементов с вертикальной прокруткой и плашками которые нужно свайпать. В мобильнике с маленьким экраном такое навряд ли возможно, но когда у нас будут экраны-столы и свайпать будут несколько человек, то этот алгоритм и там может пригодиться. Поэтому переменные будем хранить внутри объекта прокручиваемого вертикально и внутри плашки, в виде их свойств.
Имя текущей плашки которую свайпают: plashka
Имя объекта с вертикальной прокруткой: scroller
Прокрутка на момент прикосновения: old_scroll_top
Флаг начала свайпа: swipe_flag
Флаг начала скроллинга: scroll_flag
Координата прикосновения X: touch_start_x
Координата прикосновения Y: touch_start_y
Сдвиг плашки: swipe_pos
Сдвиг пальца по горизонтали: swipe_dx (в плашке храним)
Сдвиг пальца по вертикали: swipe_dy (храним в скроллере)
document.querySelectorAll( "body.logged .pages-grid" ).forEach( function( scroller ) {
scroller.ticking = false;
scroller.addEventListener( "scroll", function( e ) {
if ( ! scroller.ticking ) {
scroller.ticking = true;
window.requestAnimationFrame( function() {
scroller.ticking = false;
} );
}
if ( scroller.swipe_flag ) {
scroller.scrollTop = scroller.old_scroll_top;
return;
}
if ( Math.abs( scroller.swipe_dy ) < 6 ) {
scroller.scrollTop = scroller.old_scroll_top;
return;
} else {
scroller.scrollTop = scroller.old_scroll_top - scroller.swipe_dy;
}
scroller.scroll_flag = true;
} );
} );
let plashka = product.querySelector( ".bp" );
plashka.addEventListener( "touchstart", function( e ) {
let scroller = product.closest( ".pages-grid" );
this.scroller = scroller;
scroller.old_scroll_top = scroller.scrollTop;
scroller.touch_start_x = e.touches[0].clientX;
scroller.touch_start_y = e.touches[0].clientY;
this.scroller.scroll_flag = false;
} );
plashka.addEventListener( "touchmove", function( e ) {
this.scroller.swipe_dy = e.touches[0].clientY - this.scroller.touch_start_y;
if ( this.scroller.scroll_flag ) {
return;
}
this.swipe_dx = e.touches[0].clientX - this.scroller.touch_start_x;
if ( Math.abs( this.swipe_dx ) < 16 && ! this.scroller.swipe_flag ) {
return;
}
this.scroller.swipe_flag = true;
this.scroller.querySelectorAll( ".bp" ).forEach( function( other ) {
if ( plashka != other ) {
other.style.transform = "";
other.swipe_pos = 0;
}
} );
let translate_x = +this.swipe_pos + this.swipe_dx;
this.style.transform = `translateX(${translate_x}px)`;
this.style.transition = "0s"; // откл анимацию сдвига
} );
plashka.addEventListener( "touchend", function( e ) {
if ( this.scroller.scroll_flag ) {
return;
}
let left = -this.scroller.getAttribute( "data-min-swipe-left" );
let right = +this.scroller.getAttribute( "data-min-swipe-right" );
if ( this.swipe_pos + this.swipe_dx < left ) {
this.swipe_pos = left;
} else if ( this.swipe_pos + this.swipe_dx > right ) {
this.swipe_pos = right;
} else {
this.swipe_pos = 0;
}
this.style.transform = `translateX(${this.swipe_pos}px)`;
this.style.transition = ""; // вкл анимацию сдвига
this.scroller.scroll_flag = false;
this.scroller.swipe_flag = false;
} );
PS: Коррекция по скроллу
В текущей реализации выходит что идет постоянная коррекция скролла математическими вычислениями от старой позиции. Наверно это чуть лишнее и правильнее было бы добавить еще один флаг, который бы обозначал заморозку скролла и как только процесс скролла пошел, то сбрасывать этот флаг и уже не менять скролл математически, а предоставить браузеру свободу. Но и так работает хорошо. Чтобы не усложнять, оставим как есть.