Адаптивная галерея изображений для сайта на JavaScript.
Вступление.
Галерея изображений становится неотъемлемой частью любого сайта и, тем более, продающего лендинга. В этой статье я расскажу вам, как создать адаптивную галерею начиная с HTML-вёрстки и заканчивая JavaScript, подключить к ней анимационные эффекты и различные способы управления прокруткой, в том числе с использованием swipe.
Для начала составим техническое задание на создание адаптивной галереи изображений:
- Галерея должна быть адаптивной и должна корректно работать на любом устройстве с разрешением от 320px и выше.
-
Для управления галерей изображений используем максимально возможные варианты:
— кнопками навигации «prev» и «next»;
— пагинация;
— клавишами клавиатуры со стрелочками «вправо» и «влево»;
— вращением колёсика мыши;
— перетаскивание с помощью курсора мыши;
— на мобильных устройствах пролистывание пальцем (swipe);
— автоматическое прокручивание галереи. - Перечисленные способы навигации можно комбинировать, подключать или отключать при необходимости.
- Переход к следующему изображению при перетаскивании осуществиться, если изображение было перемещено на расстояние не менее 50% от его ширины. В противном случае, слайдер плавно вернётся к текущему изображению.
- К галерее изображений возможно подключение модуля анимации для создания анимационных эффектов при смене изображения.
HTML-вёрстка адаптивной галереи изображений.
Сначала я вам представлю HTML-вёрстку галереи изображений, а потом её прокомментирую.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
<div class="wrap"> <h1>Галерея 1 - слайдер</h1> <div class="gallery" data-setting="setup1"> <div class="slider"> <div class="stage"> <div> <img src="images/img_01.jpg" alt=""> </div> <div> <img src="images/img_02.jpg" alt=""> </div> <div> <img src="images/img_03.jpg" alt=""> </div> <div> <img src="images/img_04.jpg" alt=""> </div> <div> <img src="images/img_05.jpg" alt=""> </div> <div> <img src="images/img_06.jpg" alt=""> </div> <div> <img src="images/img_07.jpg" alt=""> </div> <div> <img src="images/img_08.jpg" alt=""> </div> <div> <img src="images/img_09.jpg" alt=""> </div> <div> <img src="images/img_10.jpg" alt=""> </div> <div> <img src="images/img_11.jpg" alt=""> </div> <div> <img src="images/img_12.jpg" alt=""> </div> <div> <img src="images/img_13.jpg" alt=""> </div> <div> <img src="images/img_14.jpg" alt=""> </div> <div> <img src="images/img_15.jpg" alt=""> </div> <div> <img src="images/img_16.jpg" alt=""> </div> </div> </div> <div class="control"> <div class="nav-ctrl" data-hidden="true"> <span class="prev" data-shift="prev">prev</span> <span class="next" data-shift="next">next</span> </div> <ul class="dots-ctrl" data-hidden="true"></ul> </div> </div> </div> |
Рассмотрим подробнее назначение контейнеров и блоков из которых состоит галерея:
- div.gallery
- Родительский контейнер галереи. В атрибуте
data-setting
содержит имя объекта с настройками галереи. - div.slider
- Определяет размер видимой части нашей адаптивной галереи. Количество одновременно показываемых изображений в данном контейнере определяется в настойках.
- div.stage
- В этом контейнере размещены все элементы галереи изображений. Изменяя его позиционирование относительно родительского элемента, мы получим прокрутку галереи.
- div.control
-
Содержит управление пошаговой и постраничной навигации.
- div.nav-ctrl
- Элементы управления, расположенные в контейнере, отвечают за прокручивание галереи на один шаг (одно изображение). Можно из этих элементов убрать текст, разместить их справа и слева от галереи и отобразить в них стрелочки «вправо» и «влево», используя фоновые изображения. Никакое изменение вёрстки при этом не требуется, достаточно внести изменения в таблицу стилей.
- div.dots-ctrl
- Немаркированный список, в котором располагаются элементы постраничной навигации. Они генерируются скриптом в зависимости от настроек галереи изображений.
В вёрстке допущены пару упрощений, которые не влияют на работоспособность галереи, но подробно останавливаться на них в рамках данной статьи нет смысла — это отдельная тема.
- При клике на изображение не будет открываться его полноразмерный вариант.
- В HTML-вёрстке сразу присутствует код элементов как пошаговой, так и постраничной навигации. Достаточно было бы вставить в вёрстку только их родительский элемент —
<div class="control">
, а саму навигацию создавать «на лету» с помощью JavaScript, в зависимости от настроек галереи изображений.
Таблица стилей для адаптивной галереи изображений.
Представлены только стили, относящиеся непосредственно к самой галерее.
Для уменьшения размера таблицы стилей, я не буду приводить свойства с вендорными префиксами, обеспечивающими кроссбраузерность. Не забывайте прописывать их при реализации своих проектов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
[data-hidden = true] { display: none; } [data-hidden = false] { display: block; } .nav-ctrl span, .dots-ctrl li { transition: all 0.3s; } .gallery { width: 100%; visibility: hidden; margin: 0 auto 30px; } .slider { width: 100%; max-width: 1024px; position: relative; overflow: hidden; margin: 0 auto; } .stage { overflow: hidden; } .stage > div { float: left; position: relative; } .stage > div img { width: 100%; height: auto; } .control { width: 100%; position: relative; } .nav-ctrl { text-align: center; margin-top: 15px; } .nav-ctrl span { width: 100px; height: 30px; display: inline-block; font-weight: 500; font-size: 12px; line-height: 30px; color: #eee; font-family: 'Roboto', sans-serif; text-transform: uppercase; text-align: center; margin: 0 5px; border: solid 1px #357ebd; border-radius: 3px; outline: none; user-select: none; background-color: #428bca; cursor: pointer; transition: all 0.3s; } .nav-ctrl span:hover { border-color: #285e8e; background-color: #3276b1; } .nav-ctrl .disable, .nav-ctrl .disable:hover { border: solid 1px #357ebd; background-color: #428bca; opacity: 0.4; cursor: default; } .dots-ctrl { text-align: center; margin-top: 15px; } .dots-ctrl li { width: 10px; height: 10px; display: inline-block; border-radius: 50%; background-color: #09f; opacity: 0.4; cursor: pointer; } .dots-ctrl li + li { margin-left: 10px; } .dots-ctrl .active, .dots-ctrl li:hover { opacity: 1; } |
Инициализация адаптивной галереи изображений.
Первое, что мы сделаем, это создадим анонимную самозапускающуюся функцию, внутри которой и будет расположен наш код.
Нужно взять за правило ограничивать область видимости скрипта, чтобы исключить конфликты с другими JS-скриптами подключенными к странице.
1 2 3 4 5 6 |
;(function() { 'use strict'; })(); |
При написании JS-скрипта мы будем использовать конструкцию Class. Это позволит создать несколько экземпляров галереи изображений на одной странице.
Создание экземпляров адаптивной галереи изображений.
В-первую очередь, создадим коллекцию галерей, которые расположены на странице. Далее, с помощью метода for...of
переберём полученную коллекцию, при этом мы выполним следующие действия:
— определим вариант настроект для данной галереи;
— создадим экземпляр текущей галереи, используя конструктор класса Gallery
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
;(function() { 'use strict'; class Gallery { constructor(gallery, setup) { } } // выбираем все галереи на странице const galleries = document.querySelectorAll('.gallery'); // перебираем полученную коллекцию элементов for (let gallery of galleries) { // вариант настроек для данной галереи const setup = gallery.dataset.setting; // создаём экземпляр галереи const slider = new Gallery(gallery, setting[setup]); } })(); |
Весь дальнейший JS-код мы будем писать внутри конструкции class Gallery { ... }
.
Прежде всего рассмотрим конструктор класса Gallery
. Конструктор инициализирует ряд объектов и переменных, содержащих информацию об экземпляре галереи. В качестве аргумента конструктор принимает объект адаптивной галереи, экземпляр которого создаётся в данный момент, а также объект с индивидуальными настройками галереи.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Gallery { constructor(gallery, setup) { this.gallery = gallery; this.setup = setup; // контейнер в котором отображаются элементы галереи this.slider = this.gallery.querySelector('.slider'); // контейнер, непосредственно в котором расположены элементы слайдера this.stage = this.gallery.querySelector('.stage'); // элементы слайдера this.items = this.gallery.querySelectorAll('.stage > div'); // количество элементов в слайдере this.count = this.items.length; this.current = 0; // index координаты текущего элемента this.next = 0; // index координаты следующего элемента this.pressed = false; // указывает, что совершилось событие 'mousedown' this.start = 0; // координата, с которой начато перетаскивание this.shift = 0; // на сколько был перемещён курсор относительно start // построение галереи исходя из полученных настроек this.init(); } } |
Статичные свойства и методы адаптивной галереи изображений.
Кроме объектов и переменных, инициализированных в конструкторе, класс Gallery
содержит ряд статических свойств, констант и методов.
К статичным свойствам относится объект defaults
с настройками по-умолчанию. Константы содержат код клавиш «стрелочка влево» и «стрелочка вправо».
Статические методы относятся к самому классу, а не его экземплярам и объектам. Мы будем использовать два статических метода:
- extend()
- объединяет и перезаписывает значения двух объектов и выдаёт общий результат
- xpos()
- возвращает координату Х текущего положения курсора или пальца
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class Gallery { // настройки по-умолчанию static defaults = { margin: 10, // расстояние между элементами [px] visibleItems: 1, // сколько элементов показывать одновременно border: 0, // толщина рамки изображения прописанная в CSS [px] responsive: true, // адаптивная галерея autoScroll: false, // автоматическое прокручивание interval: 3000, // задержка при автоматическом прокручивании [ms] nav: true, // показать/скрыть кнопки next/prev dots: false, // показать/скрыть постраничную навигацию keyControl: false, // управление клавишами вправо / влево animated: false, // включение анимации baseTransition: 0.4, // скорость анимации, при изменении CSS свойств delayTimer: 250, // время задержки при resize страницы [ms] limit: 30 // ограничиваем перемещение крайних элементов [px] }; static LEFT = 37; // код клавиши 'стрелочка влево' static RIGHT = 39; // код клавиши 'стрелочка вправо' constructor(gallery, setup) { ... } // объединяет и перезаписывает значения двух объектов // и выдаёт общий результат static extend(out) { out = out || {}; for (let i = 1; i < arguments.length; i++) { if (!arguments[i]) continue; for (let key in arguments[i]) { if (arguments[i].hasOwnProperty(key)) out[key] = arguments[i][key]; } } return out; }; // возвращает координату Х текущего положения // курсора или пальца static xpos(e) { // touch event // проверяем, сформирован ли список точек на текущем элементе // (список пальцев, которые вступили в контакт) if (e.targetTouches && (e.targetTouches.length >= 1)) { // положение первой точки прикосновения, относительно левого края браузера return e.targetTouches[0].clientX; } // mouse event return e.clientX; } } |
Запуск инициализации адаптивной галереи изображений.
Инициализация галереи начинается вызовом функции init
из конструктора класса Gallery
. В свою очередь, она вызывает ряд функций, которые:
- Объединяют дефолтные настройки адаптивной галереи изображений
defaults
с пользовательскими настройкамиsetup
, полученными в качестве второго аргумента. Результат помещается в объектoptions
— это основной объект, с которым будет работать JS-скрипт галереи. - Рассчитывают и формируют каркас галереи с учётом данных записанных в объект настроек
options
. - Получают координату
X
каждого элемента галереи и помещает её в массив. - Формируют навигацию по галерее.
- Устанавливают обработчики событий.
Код функции init
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
init() { // объединяем дефолтные настройки с настройками галереи this.options = Gallery.extend({}, Gallery.defaults, this.setup); // формируем каркас галереи this.setSizeCarousel(); // заполняем массив с координатами X каждого элемента слайдера this.setCoordinates(); // формируем управление слайдером в зависимости от настроек this.initControl(); // устанавливаем обработчики событий, если ещё не устанавливались if (this.events) return; this.registerEventsHandler(); } |
Рассмотрим каждую функцию, вызываемую из функции init
подробнее.
Объединяем дефолтные и пользовательские настройки адаптивной галереи изображений.
Статическая Функция extend
заменяет значения свойств объекта defaults
на значения свойств объекта setup
, в котором находятся пользовательские настройки. Полученный результат записывается в объект options
, принадлежащий текущему экземпляру галереи. Если бы на странице было несколько галерей, то каждому экземпляру галереи соответствовал свой объект options
, содержащий индивидуальные настройки данной галереи.
JS-код функции был представлен ранее.
Формируем каркас адаптивной галереи изображений.
Теперь, с учётом полученных настроек, прописанных в объекте options
, и текущей ширины страницы, необходимо сформировать каркас галереи, вычислив размер окна слайдера и размеры элементов галереи.
Всеми вычислениями занимается функция setSizeCarousel
. Алгоритм работы функции:
-
1
Получает ширину контейнера с классом
slider
. Этот контейнер является вьюпортом галереи — именно в нём происходит перемещение его элементов. Так как галерея должна быть адаптивной, то ширина данного контейнера зависит от размеров окна браузера.123this.widthSlider = this.slider.offsetWidth; -
2
Вычисляется максимальный индекс, который может быть у текущего элемента, чтобы на последней странице галереи при использовании пагинатора наблюдалось кол-во элементов равное
visibleItems
.123this.max = this.count - this.options.visibleItems;Давайте внимательно посмотрим на рисунок.
На рисунке показаны две галереи прокрученные до конца. Для верхней галереи свойство
max
не применялось, индекс последнего элемента равен 11 (отсчёт индекса начинается с 0), а последним элементом, до которого возможна прокрутка, является «Element 12».
В нижней галерее использован максимальный индекс (this.max = 12 - 3
), поэтому слайдер прокрутился только до «Element 10». В результате, всё окно слайдера заполнено изображениями, и в таком варианте галерея выглядит красивее. -
3
Вычисляет ширину элемента слайдера. Элемент слайдера — это контейнер
<DIV>
внутри которого находится изображение. Ширина элемента зависит от количества одновременно видимых элементов — свойствоvisibleItems
, размера отступа между элементами — свойствоmargin
и общей ширины слайдера —widthSlider
.123const width = (this.widthSlider - this.options.margin * (this.options.visibleItems - 1)) / this.options.visibleItems;От ширины слайдера вычитаем сумму отступов уменьшенную на 1, т. к. отступ последнего видимого элемента не попадает в окно слайдера и полученную разность делим на количество видимых элементов.
-
4
Вычисляет расстояние между элементами. Это значение понадобится в дальнейшем для получения координаты
X
каждого элемента галереи.123this.width = width + this.options.margin; -
5
Вычисляет ширину контейнера
<div class="stage">
, являющегося родителем для элементов для слайдера.123this.widths = this.width * this.count; -
6
Используя полученные значения, задаёт стили контейнеру
stage
и каждому элементу слайдера.1234567this.stage.style.width = this.widths + 'px';for (let item of this.items) {item.style.cssText = `width:${width}px; margin-right:${this.options.margin}px;`;}
Полный код функции setSizeCarousel
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
setSizeCarousel() { // получаем ширину слайдера - вьюпорт, в котором прокручиваются элементы галереи this.widthSlider = this.slider.offsetWidth; // если разрешена адаптация галереи, то необходимо, используя свойство // 'options.adaptive', получить значения разрешений (контрольные точки), // при которых будут меняться количество видимых элементов галереи и // и другие её настройки if (this.options.responsive) this.setAdaptiveOptions(); // максимальный индекс, который может быть у текущего элемента // чтобы на последней странице галереи при использовании пагинатора // наблюдалось кол-во элементов равное options.visibleItems this.max = this.count - this.options.visibleItems; // получаем ширину элемента слайдера, которая зависит от колличества // одновременно видимых элементов, размера отступа между элементами и // общей ширины слайдера // от ширины слайдера вычитаем сумму отступов // уменьшенную на 1, т.к. отступ последнего видимого элемента не попадает // в окно слайдера (контейнер '.stage') const width = (this.widthSlider - this.options.margin * (this.options.visibleItems - 1)) / this.options.visibleItems; // значение, по которому отсчитываются координаты // состоит из ширины элемента слайдера и его margin-right // другими словами - растоянием между левыми границами элементов слайдера this.width = width + this.options.margin; // ширина контейнера '.stage', непосредственно в котором // расположены элементы слайдера this.widths = this.width * this.count; // задаётся стиль ширины контейнера '.stage' this.stage.style.width = this.widths + 'px'; // перебираем коллекцию элементов слайдера и // прописываем ширину и правый отступ для каждого элемента for (let item of this.items) { item.style.cssText = `width:${width}px; margin-right:${this.options.margin}px;`; } // после того, как каркас галереи построен, все размеры элементов // вычислены и прописаны в стилях, делаем карусель видимой через // свойство стиля 'visibility' setTimeout(() => { this.gallery.style.visibility = 'visible' }, 350); } |
Получаем координату X каждого элемента адаптивной галереи изображений.
Теперь необходимо получить координату X
каждого элемента слайдера, которые будут использоваться при листании галереи. За заполнение массива с координатами X отвечает функция setCoordinates
, вызываемая из функции init
. Функция простая и будет достаточно комментариев в самом коде.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
setCoordinates() { // координата первого элемента, от неё и будет идти отсчёт let point = 0; // добавляем новое свойство в объект 'options' // пока это пустой массив this.coordinates = []; // заполняем в цикле массив пока количество его элементов // не станет равно количеству элементов слайдера, // т.е. будет записана координата X каждого элемента while (this.coordinates.length < this.count) { // добавляем в конец массива текущее значение переменной 'point' // которое равно координате X текущего элемента слайдера this.coordinates.push(point); // вычитаем из текущей координаты ширину блока, равную // сумме ширины элемента слайдера и отступа или другими // словами - расстояние между левыми границами элементов point -= this.width; } } |
Формирование навигации по адаптивной галерее изображений.
Теперь мы рассмотрим формирование визуальных элементов навигации по галерее:
1. кнопки «prev» и «next», сдвигающие слайдер на один элемент;
2. постраничная навигация в виде точек, прокручивающая слайдер на количество элементов, указанных в свойстве visibleItems
.
Запускается формирование навигации по галерее из функции init
, вызовом функции initControl
.
Подключение элементов навигации осуществляется путём присвоения атрибутам data-hidden
значения false
. В рассматриваемой нами галерее, навигация кнопками разрешена по умолчанию, а разрешение постраничной навигации прописано в пользовательских настройках setup
.
Нам необходимо будет получить ряд DOM-объектов, поэтому давайте ещё раз посмотрим на HTML-вёрстку блока навигации:
1 2 3 4 5 6 7 8 9 |
<div class="control"> <div class="nav-ctrl" data-hidden="true"> <span class="prev" data-shift="prev">prev</span> <span class="next" data-shift="next">next</span> </div> <ul class="dots-ctrl" data-hidden="true"></ul> </div> |
Рассмотрим, какие задачи выполняет функция initControl
:
-
Получает объект контейнера с классом
nav-ctrl
и объект немаркированного списка с классомdots-ctrl
. Эти объекты необходимы для того, чтобы показывать элементы навигации, устанавливая значение атрибутаdata-hidden
вfalse
или скрывать их, меняя значениеdata-hidden
наtrue
, в зависимости от значений свойствnav
иdots
в настройках адаптивной галереи.
Кроме этого, на объект списка будет повешен обработчик события для управления постраничной навигацией. -
Получает объекты кнопок управления «prev» и «next». На них будут повешены обработчики событий для прокручивания галереи вправо / влево на один элемент.
При помощи функцииsetNavStyle
кнопка «prev» визуально делается неактивной, т. к. при инициализации галереи текущим устанавливается первый элемент и, соответственно, нет никаких предыдущих элементов. -
Вызывает функцию
creatDotsCtrl
, которая исходя из общего количества элементов галереи и значения свойстваvisibleItems
заполняет список<ul class="dots-ctrl"></ul>
элементами постраничной навигации. Первый элемент делает визуально активным.
Записывает в массив координатуX
первого элемента каждой страницы.
Код самой функции initControl
с подробными комментариями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
initControl() { // объект с кнопками навигации 'prev / next' this.navCtrl = this.gallery.querySelector('.nav-ctrl'); // объект перелистывания галереи с помощью пагинации this.dotsCtrl = this.gallery.querySelector('.dots-ctrl'); if (this.options.nav === true) { // кнопка 'prev' this.btnPrev = this.navCtrl.querySelector('[data-shift=prev]'); // кнопка 'next' this.btnNext = this.navCtrl.querySelector('[data-shift=next]'); // устанавливаем стили для кнопок 'prev' и 'next' this.setNavStyle(); // делаем навигацию видимой this.navCtrl.dataset.hidden = false; } else { // делаем навигацию невидимой this.navCtrl.dataset.hidden = true; } if (this.options.dots === true) { // формируем постраничную навигацию this.creatDotsCtrl(); // делаем постраничную навигацию видимой this.dotsCtrl.dataset.hidden = false; } else { // делаем постраничную навигацию невидимой this.dotsCtrl.dataset.hidden = true; } } |
Функция setNavStyle
управляет внешним видом кнопок «prev» и «next», делая неактивной кнопку «prev», если слайдер находится в самом начале. Если слайдер прокручен до самого конца, то неактивной будет кнопка «next». Во всех остальных случая обе кнопки выглядят активными.
Код функции очень простой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
setNavStyle() { // убираем у всех кнопок класс 'disable', теперь // обе кнопки выглядят активными this.btnPrev.classList.remove('disable'); this.btnNext.classList.remove('disable'); if (this.current == 0) { // если первый элемент является текущим, то блокируем попытку просмотра // предыдущего элемента, т.к. его не существует и делаем кнопку // 'prev' неактивной this.btnPrev.classList.add('disable'); } else if (this.current >= this.count - this.options.visibleItems) { // если последний элемент появился на экране, при этом не важен // его индекс, блокируем и делаем неактивной кнопку просмотра след. // элемента на экране будет наблюдаться столько элементов, // сколько указано в visibleItems this.btnNext.classList.add('disable'); } } |
Теперь рассмотрим, как правильно рассчитать и сгенерировать элементы постраничной навигации — ими являются элементы списка LI
. Именно LI
будет определять внешний вид пагинатора и управление галереей — клик по этому элементу вызовет прокрутку слайдера.
За формирование пагинации отвечает функция creatDotsCtrl
, вызываемая из функции initControl
. Функция, используя метод document.createElement(tag)
, создаёт новый элемент LI
. Далее, запускается цикл while
с шагом visibleItems
. При каждой итерации цикла выполняются следующие операции:
-
Создаётся клон полученного элемента списка
LI
. Если это первый элемент в списке, то клону добавляется классactive
. -
С помощью метода
parentElem.appendChild(elem)
клон добавляется в конец объектаdotsCtrl
и в массивspots
. Этот массив в дальнейшем понадобится для получения индекса элемента в списке. -
Рассчитывается следующую координату Х, к которой необходимо прокрутить слайдер при постраничной навигации. При расчёте используется полученный ранее максимальный индекс
max
.
Код функции creatDotsCtrl
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
creatDotsCtrl() { // массив с элементы управления постраничной навигацией this.spots = []; // при ресайзе страницы удаляем элементы постраничной навигации, т.к // она будет перестроена исходя из новых настроек, актуальных для текущего // разрешения (ширины экрана) this.dotsCtrl.innerHTML = ''; // создаём элемент списка, внутри которых будут находится // элементы, управляющие постраничной навигацией const li = document.createElement('li'); // создаём элемент span, который будет отображать точку // const span = document.createElement('span'); let i = 0, point = 0, clone; // добавляем созданный элемент 'span' в элемент списка 'li' // li.appendChild(span); while (i < this.count) { // создаём клон полученного элемента списка clone = li.cloneNode(true); // добавляем клон (элемент 'li') в конец объекта 'dotsCtrl' this.dotsCtrl.appendChild(clone); // и в массив this.spots.push(clone); // увеличиваем i на количество видимых элементов галереи i += this.options.visibleItems; // рассчитываем следующую координату Х, к которой необходимо прокрутить // слайдер при постраничной навигации point = (i <= this.max) ? point - this.width * this.options.visibleItems : -this.width * this.max; } this.setDotsStyle(); } |
Функция setDotsStyle
управляет внешним видом элементов постраничной навигации, выделяя элемент, соответствующий группе изображений, до которых была прокручена галерея. Код функции несложный, поэтому можно обойтись комментариями в самом коде:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
setDotsStyle() { // перебираем массив и делаем все элементы массива неактивными for (let spot of this.spots) { spot.classList.remove('active'); } // находим индекс элемента, который необходимо сделать активным // метод Math.trunc() возвращает целую часть числа путём удаления всех дробных знаков. const i = (this.next < this.max) ? Math.trunc(this.next / this.options.visibleItems) : this.spots.length - 1; // добавляем класс элементу с найденным индексом this.spots[i].classList.add('active'); } |
Итак, у нас есть экземпляр адаптивной галереи, сформированной в соответствии с дефолтными и пользовательскими настройками, сформирована и настроена навигация именно для этого экземпляра галереи изображений. Теперь необходимо осуществить регистрацию обработчиков событий, относящиеся именно к этой же галерее.
Регистрация обработчиков событий адаптивной галереи изображений
Регистрация обработчиков событий адаптивной галереи изображений осуществляется в функции registerEventsHandler
, вызываемой из функции init
. Обработчик назначается вызовом метода addEventListener
, который привязывается к элементу, на котором событие регистрируется.
При использовании метода
addEventListener
теряется контекст вызова — this
, который ссылается на текущий объект. Используя встроенный в JavaScript метод bind
, можно напрямую передать контекст вызова в функцию обработчика события.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
registerEventsHandler(e) { // автоматическое прокручивание, запускается установкой в настройках // значения свойства 'autoScroll' в true // т.к. мы делаем слайдер, а не карусель использовать автоскролл лучше // при выводе только одного изображения if (this.options.autoScroll) { setInterval(() => this.autoScroll(), this.options.interval); } // управление кликом по кнопкам 'prev / next' объекта 'navCtrl' this.navCtrl.addEventListener('click', this.navControl.bind(this)); // управление постраничной навигацией точками this.dotsCtrl.addEventListener('click', this.dotsControl.bind(this)); // управление клавишами вправо / влево // будет корректно работать, если на странице только одна галерея, // по умолчанию управление отключено if (this.options.keyControl) { window.addEventListener('keydown', this.keyControl.bind(this)); } // просмотр полноразмерной фотографии this.gallery.querySelector('.slider').addEventListener('click', this.showPhoto.bind(this)); // mouse events // управление колёсиком мыши, управление работает, если указатель // мыши находится над DIV'ом с классом 'slider' this.gallery.querySelector('.slider').addEventListener('wheel', this.wheelControl.bind(this)); // нажатие кнопки мыши на слайдер this.stage.addEventListener('mousedown', this.tap.bind(this)); // прокрутка слайдера перемещением мыши this.stage.addEventListener('mousemove', this.drag.bind(this)); // отпускание кнопки мыши this.stage.addEventListener('mouseup', this.release.bind(this)); // курсор мыши выходит за пределы DIV'а с классом 'slider' this.stage.addEventListener('mouseout', this.release.bind(this)); // touch events // касание экрана пальцем this.stage.addEventListener('touchstart', this.tap.bind(this)); // перемещение пальца по экрану (swipe) this.stage.addEventListener('touchmove', this.drag.bind(this)); // палец отрывается от экрана this.stage.addEventListener('touchend', this.release.bind(this)); // флаг, информирующий о том, что обработчики событий установлены this.events = true; } |
Итак, инициализация и построение адаптивной галереи изображений закончены. Прежде чем рассматривать функционал её скроллинга, давайте подведём итог и кратко изложим, что было сделано:
- Объединены дефолтные и пользовательские настройки, на основании которых создаётся и функционирует галерея.
- Исходя из текущего размера окна браузера и значения свойства
visibleItems
получены размеры отдельных элементов галереи, слайдера и самой галереи в целом. - Получены координаты X каждого элемента галереи.
- В соответствии с настройками, создано пошаговое и постраничное управление галереей.
- Установлены обработчики событий, позволяющие прокручивать галерею.
Прокрутка адаптивной галереи изображений.
В первую очередь, мы рассмотрим функции, которые непосредственно реализуют механизм прокрутки адаптивной фотогалереи, а уже потом варианты их вызова, в зависимости от способа управления скроллом.
Принцип прокручивания адаптивной галереи изображений.
У нас есть контейнер stage
, в котором в одну строку размещены изображения, и являющийся дочерним элементом контейнера slider
.
Контейнер slider
имеет стиль overflow: hidden
, в результате чего отображается только та часть stage
, которая находится внутри границ slider
, остальная часть будет скрыта. Ширина slider
и количество отображаемых элементов зависит от значения свойства visibleItems
.
Меняя позиционирование контейнера stage
по горизонтали, можно отображать в области контейнера slider
изображения, скрытые в данный момент. Для смещения stage
будем использовать свойство transform:translateX()
, а за точку отсчёта смещения возьмём левую границу контейнера slider
.
Таким образом, чтобы прокрутить галерею к нужному изображению, необходимо подставить соответствующее значение в свойство transform:translateX()
.
Значения координат всех изображений были получены и записаны в массив coordinates
при вызове функции setCoordinates
.
Данный функционал реализован в двух основных функциях:
getNextCoordinates
— возвращает координату Х элемента, до которого должен переместиться контейнерstage
;scroll
— непосредственно реализует механизм прокручивания, а также изменяет визуальное отображение элементов пошаговой и постраничной навигации по мере прокручивания галереи.
Рассмотрим работу этих функций подробнее.
Получение координаты, до которой должна проскроллиться адаптивная галерея изображений.
Функция принимает один аргумент — direction
, имеющий два значения, определяющие направление перемещения:
-1 — переход к предыдущему элементу;
1 — переход к следующему элементу.
Кроме этого, функция использует свойства конструктора:
- current — индекс координаты текущего элемента;
- count — количество элементов в галерее;
- visibleItems — количество элементов отображаемых в слайдере;
- max — максимальный индекс, который может быть у текущего элемента.
Используя полученный аргумент и свойства конструктора, функция может накладывать ограничения на прокручивание галереи:
— попытка перейти к предыдущему элементу, когда текущим является первый элемент;
— попытка перейти к следующему элементу, когда текущий элемент имеет индекс равный max
.
Ограничения на прокручивание галереи действуют при любом способе управления. В дальнейшем акцентировать внимание на них не будем, подразумевая, что они работают по-умолчанию.
Код функции getNextCoordinates
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
getNextCoordinates(direction) { if (typeof(direction) !== 'number') return this.coordinates[this.current]; // direction - направление перемещения: -1 - left, 1 - right if (this.options.autoScroll && this.current >= this.count - this.options.visibleItems) { this.next = 0; } else { // попытка прокрутить к предыдущему элементу, когда текущим является первый элемент if (this.current == 0 && direction == -1 || // попытка просмотреть следующую группу элементов при постраничной навигации, но // все элементы после текущего выведены во вьюпорт слайдера (this.current >= this.max) && direction == 1) return; // получаем индекс следующего элемента this.next += direction; } // возвращаем координату след. элемента - координату, до которой // необходимо продвинуть галерею return this.coordinates[this.next]; } |
Реализация прокрутки адаптивной галереи изображений.
Как я писал выше, за непосредственную реализацию прокрутки отвечает функция scroll
. Функция принимает два аргумента:
1. координата X, полученная функцией getNextCoordinates
;
2. transition — время анимации прокрутки.
Может возникнуть резонный вопрос: зачем передавать время анимации отдельным параметром, когда оно прописано в настройках галереи и его значение есть в объекте options
?
Всё упирается в постраничную навигацию. Если прокрутка будет на соседнюю страницу, то базового значения baseTransition
будет достаточно. А если необходимо перейти сразу на несколько страниц или к последней странице галереи? В этом случае, при использовании значения baseTransition
, изображения просто промелькнут по экрану без заметной плавности, что не очень красиво. Поэтому, при постраничной навигации значение transition
рассчитывается в зависимости от того, на сколько страниц нужно проскроллить галерею. Подробнее это будет описано в разделе постраничной навигации.
При рассмотрении функции scroll
необходимо учесть ещё один очень важный момент — это переключение, по мере прокрутки, состояния элементов навигации из неактивного состояния в активное и наоборот. Для этого в код добавлен вызов двух функций (setNavStyle
и setDotsStyle
), которые будут менять стили отображения кнопок управления и элементов постраничной навигации в зависимости от текущего индекса.
Подробно описывать работу функции scroll
нет необходимости — она достаточная простая, вполне хватит комментариев в JS-коде функции:
Код функции scroll
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
scroll(x, transition) { // если аргумент х не является числом, прекращаем работу функции if (typeof(x) !== 'number') return; // прописываем новые стили для смещения (прокручивания) галереи // к следующему элементу this.stage.style.cssText = `width:${this.widths}px; height:${this.items[0].offsetHeight}px; transform:translateX(${x}px); transition:${transition}s`; // после прокручивания, индекс след. элемента становится текущим this.current = (this.next < this.max) ? this.next : this.max; // меняем стили отображения кнопок управления в зависимости от // текущего индекса if (this.options.nav) this.setNavStyle(); // меняем стили элементов постраничной навигации if (this.options.dots) this.setDotsStyle(); } |
Разобравшись с тем, как работает механизм прокрутки галереи, можно перейти к рассмотрению различных вариантов управления этой прокруткой.
Управление прокруткой адаптивной галереи изображений.
Напомню, что все обработчики событий, вызывающие функции управления прокруткой галереи, находятся в функции registerEventsHandler
, рассмотренной ранее.
Автоматическая прокрутка адаптивной галереи изображений.
Для включения автоматической прокрутки, необходимо присвоить свойству autoScroll
значение «true». Делается это в пользовательских настройках — объект setting
. Если автоскролл включен, то с помощью метода setInterval
с периодичностью, заданной в свойстве interval
статического свойства defaults
, вызывается функция autoScroll
.
1 2 3 4 5 6 7 8 9 10 |
autoScroll(e) { // получаем координату Х элемента, до которого должен переместиться слайдер // галерея всегда прокручивается вправо, поэтому аргумент, через который // передаётся direction, всегда равен 1 const x = this.getNextCoordinates(1); // запускаем прокручивание галереи this.scroll(x, this.options.baseTransition); } |
Управление прокруткой адаптивной галереи изображений с помощью кнопок «prev» и «next».
При нажатии на кнопку «prev» или «next» вызывается функция navControl
. Направление скролла галереи определяется по значению data-атрибута data-shift
. В зависимости от его значения, контейнер с изображениями stage
смещается или к предыдущему, или к следующему изображению.
Код функции navControl
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
navControl(e) { // если клик был сделан не по элементу 'span' объекта // navCtrl, прекращаем работу функции if (e.target.tagName != 'SPAN') return; // определяем направление прокручивания галереи // зависит от кнопки, по которой был сделан клик // -1 - prev, 1 - next const d = (e.target.dataset.shift === 'next') ? 1 : -1; // получаем координату Х элемента, до которого должен переместиться слайдер const x = this.getNextCoordinates(d); // запускаем прокручивание галереи this.scroll(x, this.options.baseTransition); } |
Постраничная прокрутка адаптивной галереи изображений с помощью пагинатора.
Управление постраничной прокруткой галереи реализовано в функции dotsControl
, которая решает две задачи:
- получения координаты X первого изображения на странице, до которой будет прокручиваться галерея;
- расчёт времени, за которое галерея прокрутиться к выбранной странице, с учётом количества изображений, на которые нужно проскроллить галерею.
Для получения координаты X, функция dotsControl
не обращается к функции getNextCoordinates
, а использует свой алгоритм:
-
1
Получает объект элемента навигации, по которому был сделан клик.
-
2
Находит аналогичный элемент и его индекс в массиве
spots
— этот массив был создан при формировании постраничной навигации и содержит все элементы пагинатора.
Для поиска индекса элемента в массивеspots
используем встроенный методindexOf
, который возвращает индекс элемента в массиве или -1, если такого индекса нет. -
3
Получает индекс изображения, до которого нужно прокрутить галерею, путём умножения индекса элемента пагинатора на значение свойства
visibleItems
.
Для расчёта времени анимации, используя индексы текущей и полученной координаты X, вычисляем, на сколько элементов (изображений) будет прокручена галерея. Это значение умножаем на коэффициент и прибавляем к базовому значению baseTransition
.
Таким образом, время анимации будет напрямую зависеть от того, сколько страниц (количество изображений на этих страницах) необходимо прокрутить.
Код функции dotsControl
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
dotsControl(e) { // если клик был сделан не по элементу 'span' объекта dotsCtrl или // по активному элементу, соответствующему текущей странице, // прекращаем работу функции if (e.target.tagName != 'LI' || e.target.classList.contains('active')) return; // находим индекс элемента 'span' в массиве 'spots' // этот индекс понадобится для поиска координаты // в массиве 'coordinates' const i = this.spots.indexOf(e.target); // если элемент в массиве 'spots' не найден, прекращаем работу функции if (i == -1) return; // получаем индекс координаты, до которой будет прокручиваться галерея this.next = i * this.options.visibleItems; // ограничиваем индекс координаты, чтобы при переходе на последнюю страницу, // она была полностью заполнена, т.е. на последней странице должно быть // всегда visibleItems элементов this.next = (this.next <= this.max) ? this.next : this.max; // координата, до которой будет происходить scroll const x = this.coordinates[this.next]; // вычисляем, на сколько элементов будет прокручена галерея const n = Math.abs(this.current - this.next); // увеличиваем время анимации скролла в зависимости от количества // прокручиваемых элементов const t = this.options.baseTransition + n * 0.07; // запускаем прокручивание галереи this.scroll(x, t); } |
Управление прокруткой адаптивной галереи изображений с помощью клавиатуры.
При таком способе управления используются клавиши «←» (влево) и «→» (вправо), имеющие виртуальные десятизначные коды 37 и 39 соответственно.
Для включения управления с помощью клавиатуры, необходимо в пользовательских настройках setting
, присвоить свойству keyControl
значение «true». По-умолчанию установлено значение «false», т. к. при размещении нескольких галерей на одной странице, управление с клавиатуры будет всеми галереями одновременно.
Реализовано такое управление в функции keyControl
. Подробно расписывать работу функции не буду — код очень простой, достаточно комментариев в самом коде.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
keyControl(e) { // проверяем код нажатой клавиши и исходя из полученного // кода определяем направление прокручивания галереи // если код не соотвествует клавишам 'влево' или 'вправо', // прекращаем работу функции if (e.which !== Gallery.RIGHT && e.which !== Gallery.LEFT) return; const d = (e.which === Gallery.RIGHT) ? 1 : -1; // получаем координату Х элемента, до которого должна переместиться галерея const x = this.getNextCoordinates(d); // запускаем прокручивание галереи this.scroll(x, this.options.baseTransition); } |
Управление прокруткой адаптивной галереи изображений с помощью колёсика мыши.
Управление колёсиком мышки будет работать только в том случае, если курсор находится над галерей. Это обуславливается способом регистрации обработчика события.
1 2 3 |
this.gallery.querySelector('.slider').addEventListener('wheel', this.wheelControl.bind(this)); |
При такой регистрации обработчика события, функция wheelControl
будет вызвана, только если указатель мыши находится над DIV’ом с классом ‘slider’ конкретного экземпляра галереи — this.gallery
. Такой подход обеспечивает корректное управление при любом количестве галерей на одной странице.
Для определения направления прокрутки галереи используем свойство deltaY
, показывающее количество прокрученных пикселей по вертикали со знаком, соответствующим направлению прокрутки.
Код функции wheelControl
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
wheelControl(e) { // отключаем поведение по умолчанию - скролл страницы e.preventDefault(); // определяем направление перемещения в зависимости от направления // вращения колёсика мыши const d = (e.deltaY > 0) ? 1 : -1; // получаем координату Х элемента, до которого должен переместиться слайдер const x = this.getNextCoordinates(d); // запускаем прокручивание галереи this.scroll(x, this.options.baseTransition); } |
Управление прокруткой адаптивной галереи изображений перетаскиванием мышью и свайпом.
Рассмотрим ещё два очень похожих способа прокручивания галереи: перетаскивание с помощью мыши и листание галереи пальцем (swipe) на мобильных устройствах. Несмотря на то, что первый способ основан на событиях мыши, а второй на touch событиях, они очень похожи и используют одни и те же функции.
Ещё раз обратимся к функции registerEventsHandler
, к той части, где регистрируются mouse events и touch event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
registerEventsHandler(e) { ..... // mouse events // управление колёсиком мыши, управление работает, если указатель // мыши находится над DIV'ом с классом 'slider' this.gallery.querySelector('.slider').addEventListener('wheel', this.wheelControl.bind(this)); // нажатие кнопки мыши на слайдер this.stage.addEventListener('mousedown', this.tap.bind(this)); // прокрутка слайдера перемещением мыши this.stage.addEventListener('mousemove', this.drag.bind(this)); // отпускание кнопки мыши this.stage.addEventListener('mouseup', this.release.bind(this)); // курсор мыши выходит за пределы DIV'а с классом 'slider' this.stage.addEventListener('mouseout', this.release.bind(this)); // touch events // касание экрана пальцем this.stage.addEventListener('touchstart', this.tap.bind(this)); // перемещение пальца по экрану (swipe) this.stage.addEventListener('touchmove', this.drag.bind(this)); // палец отрывается от экрана this.stage.addEventListener('touchend', this.release.bind(this)); ..... } |
Как видео из представленного кода, независимо от источника, события обрабатываются одними и теми же функциями:
- tap
- нажатие левой кнопки мыши или касание экрана пальцем;
- drag
- перемещением мыши или пальца по экрану;
- release
- отпускание кнопки мыши или палец отрывается от экрана, а также выход за пределы контейнера «slider» курсора мыши.
При работе этих функций будет требоваться информация о текущем положении курсора или пальца. Для этой цели у нас есть статическая функция xpos
, которая возвращает координату X текущего положения курсора мыши или пальца на экране мобильного гаджета без учета прокрутки.
Напомню JS-код функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static xpos(e) { // touch event // проверяем, сформирован ли список точек на текущем элементе // (список пальцев, которые вступили в контакт) if (e.targetTouches && (e.targetTouches.length >= 1)) { // положение первой точки прикосновения, относительно левого края браузера return e.targetTouches[0].clientX; } // mouse event return e.clientX; } |
Работа функции «tap».
Функция tap
, используя вызов функции xpos
, получает координату X, с которой начато перетаскивание и устанавливает флаг нажатия.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
tap(e) { // отключаем действия по умолчанию e.preventDefault(); e.stopPropagation(); // если нажата не левая кнопка мыши, прекращаем работу функции if (event.which && event.which != 1) return; // расстояние от левой границы экрана до курсора без учета прокрутки, // т. е. начальная координата Х, с которой начато перетаскивание this.start = Gallery.xpos(e); // устанавливаем флаг нажатия this.pressed = true; } |
Работа функции «drag».
Функция drag
отслеживает перемещение курсора мыши или пальца по экрану и, при выполнении определённых условий, передаёт в функцию scroll
разницу между текущей координатой X и смещением курсора от точки, с которой начато перетаскивание.
Остановимся подробнее на условиях, влияющих на работу функции:
-
1
Проверяется, выставлен ли флаг нажатия
pressed
. Если этот флаг проигнорировать, то функция начнёт работать и пытаться проскроллить галерею, даже когда курсор просто перемещается над ней. Результат будет непредсказуемым.123if (this.pressed === false) return; -
2
Получив смещение курсора мыши или пальца от начальной позиции
shift
, проверяем его размер и, если он меньше трёх пикселей, функция прекращает свою работу. Таким образом исключается случайное смещение (дрожание) курсора мыши или пальца.123if (Math.abs(this.shift) < 3) return; -
3
Накладывается ограничение на прокрутку галереи в её крайних положениях.
Допустим, галерея находится в исходном состоянии и первый её элемент является активным. При попытке перетащить её вправо, как для просмотра предыдущего изображения, галерея смещается максимум на величину указанную в свойствеoptions.limit
, а после этого плавно возвращается в исходное состояние.Аналогичное поведение будет у галереи, если она прокручена до последнего элемента, но её тянут влево.
Для реализации такого алгоритма, предварительно необходимо вычислить:
-
Общую ширину всех невидимых в данный момент элементов слайдера:
123const remaining = this.widths - this.width * this.options.visibleItems; -
Разницу между текущей координатой и смещением курсора от точки старта, с которой начато перетаскивание:
123const delta = this.coordinates[this.current] - this.shift;
Полный код поведения галереи изображений в крайних положениях:
123456789// общая ширина всех невидимых в данный момент элементов слайдераconst remaining = this.widths - this.width * this.options.visibleItems;// разница между текущей координатой и смещением курсора от// точки старта, с которой начато перетаскиваниеconst delta = this.coordinates[this.current] - this.shift;// останавливаем прокручивание галереи при достижении первого или последнего элементаif (delta > this.options.limit || Math.abs(delta) - remaining > this.options.limit) return; -
Полный код функции drag
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
drag(e) { // отключаем действия по умолчанию e.preventDefault(); e.stopPropagation(); // если не нажата левая кнопка мыши, прекращаем работу функции if (this.pressed === false) return; // смещение курсора мыши или пальца от начальной позиции this.shift = this.start - Gallery.xpos(e); // исключаем дрожание курсора или пальца if (Math.abs(this.shift) < 3) return; // общая ширина всех невидимых в данный момент элементов слайдера const remaining = this.widths - this.width * this.options.visibleItems; // разница между текущей координатой и смещением курсора от // точки старта, с которой начато перетаскивание const delta = this.coordinates[this.current] - this.shift; // останавливаем прокручивание галереи при достижении первого или последнего элемента if (delta > this.options.limit || Math.abs(delta) - remaining > this.options.limit) return; // перемещаем слайдер на величину смещения курсора относительно // точки старта (начальной координаты Х) this.scroll(delta, 0); } |
Работа функции «release».
Функция release
обрабатывает окончание пролистывания галереи изображений и начинает работу в момент, когда отпускается левая кнопка мыши или палец отрывается от экрана, а так же, когда курсор мыши или палец выходит за пределы видимой области галереи.
Пролистывание галереи пользователем может прерваться в любой момент, и при этом не обязательно, что сдвиг был сделан на расстояние кратное ширине изображения. В такой ситуации, необходимо определить в какую сторону необходимо докрутить галерею к ближайшему элементу.
На представленном рисунке показано перемещение галереи влево, при этом элемент «Element 4», являющийся текущим, сдвинут на расстояние меньше половины свой ширины. В такой ситуации, галерея вернётся в исходное положение и «Element 4» останется текущим.
Если «Element 4» сдвинуть на расстояние большее половины его ширины, то галерея прокрутиться дальше и текущим станет «Element 5».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
release(e) { // отключаем действия по умолчанию e.preventDefault(); e.stopPropagation(); // если не было нажатия на кнопку мыши или тапа пальцем, // прекращаем работу функции if (this.pressed === false) return; // рассчитываем направление прокрутки галереи const d = (Math.abs(this.shift) > this.width / 2) ? Math.round(this.shift / this.width) : ''; // определяем координату X ближайшего элемента const x = this.getNextCoordinates(d); // запускаем доводку прокручивания галереи к ближайшему элементу this.scroll(x, this.options.baseTransition); // сбрасываем флаг this.pressed = false; } |
Адаптация галереи изображений к изменению разрешения.
Адаптивность необходима для того, чтобы галерея изображений во всём диапазоне разрешений выглядела корректно, сохраняла свою функциональность и, при этом, отсутствовала горизонтальная прокрутка.
В функции setSizeCarousel
реализована основа адаптивности:
- Ширина видимой области слайдера (viewport), в которой прокручиваются изображения, зависит от ширины страницы.
- Ширина элементов (изображений) рассчитывается в зависимости от размера вьюпорта и количества одновременно выводимых изображений.
- Ширина контейнера с классом
stage
зависит от рассчитанной ширины изображений.
Но для полноценной адаптивности этого недостаточно. Нужно заставить меняться настройки галереи в зависимости от разрешения экрана. Для этой цели добавим в пользовательские настройки свойство adaptive
. В качестве его значения используем ассоциативный массив, ключами которого будут разрешения экрана, при которых необходимо изменить настройки галереи изображений, а значения будут представлять объекты, содержащие эти настройки, записанные в литеральной форме.
Код объекта adaptive
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
adaptive: { // настройка работает в диапазоне разрешений 320-560px 320: { // одновременно выводится 1 элемент visibleItems: 1, // расстояние между изображениями 5px margin: 5, // запрещаем постраничную навигацию dots: false }, // настройка работает в диапазоне разрешений 560-768px 560: { // одновременно выводится 1 элемент visibleItems: 2, // расстояние между изображениями 5px margin: 5, // запрещаем постраничную навигацию dots: false }, // настройка работает в диапазоне разрешений 768-1024px 768: { // одновременно выводятся 2 элемента visibleItems: 3, }, // настройка работает в диапазоне разрешений 1024 и выше 1024: { // одновременно выводятся 3 элемента visibleItems: 4 } } |
Для обработки ассоциативного массива, являющегося значением свойства adaptive
и получения настроек, соответствующих текущему разрешению экрана, создадим функцию setAdaptiveOptions
. Алгоритм работы функции:
-
Создаёт массив из ключей, являющимися контрольными точками (break points).
-
Сравнивая ширину страницы (документа) со значениями break point из массива, определяет ближайшую контрольную точку «снизу». Эта точка будет служить ключом к объекту с настройками для данного диапазона разрешений.
Например, разрешение экрана 640px. Ближайшие к нему контрольные точки — 560 и 768. Выбрана будет 560. -
Считывает все свойства в объекте с полученным ключом и записывает их значения в объект
options
поверх существующих.
Код функции setAdaptiveOptions
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
setAdaptiveOptions() { // размер видимой части окна браузера const width = document.documentElement.clientWidth; // массив с контрольными точками const points = []; // текущая контрольная точка let point; // получаем массив из контрольных точек (break point) for (let key in this.options.adaptive) { points.push(key); } // сравнивая ширину страницы (документа) со значениями break point из массива, // определяем ближайшую контрольную точку 'снизу'. Эта точка будет служить // ключом к объекту с настройками для данного диапазона ширины страницы for (let i = 0, j = points.length; i < j; i++) { let a = points[i], b = (points[i + 1] !== undefined) ? points[i + 1] : points[i]; if (width <= points[0]) { point = points[0]; } else if (width >= a && width < b) { point = a; } else if (width >= points[points.length - 1]) { point = points[points.length - 1]; } } // записываем полученные из object[point] настройки в options // данные настройки будут записаны поверх существующих const setting = this.options.adaptive[point]; for (let key in setting) { this.options[key] = setting[key]; } } |
В результате работы функции setAdaptiveOptions
, мы получили настройки галереи изображений, оптимальные для текущего разрешения. Вызов данной функции происходит из функции setSizeCarousel
, формирующей каркас галереи:
1 2 3 4 5 6 7 8 9 10 11 |
setSizeCarousel() { ... // если разрешена адаптация галереи, то необходимо, используя свойство // 'options.adaptive', получить значения разрешений (контрольные точки), // при которых будут меняться количество видимых элементов галереи и // и другие её настройки if (this.options.responsive) this.setAdaptiveOptions(); ... } |
Итак, что мы имеем на данный момент. При запуске адаптивной галереи изображений, происходит её инициализация и подключаются настройки соответствующие текущему разрешению экрана монитора или мобильного гаджета. В результате, не зависимо от разрешения, галерея будет выглядеть оптимально для данного экрана.
Кажется, на этом написание скрипта можно закончить. К сожалению, есть одно но. Например, пользователь просматривал галерею на планшете или смартфоне в ландшафтном режиме, а потом повернул в портретный режим. Или другой пример, пользователь самостоятельно изменил размеры окна браузера. Что произойдёт в этом случае? Ничего. Настройки галереи останутся без изменения и, скорее всего, её просматривать станет неудобно.
Что объединяет эти два примера? И в одном, и в другом случае меняется разрешение экрана по горизонтали. Другими словами возникает событие resize
. Следовательно, чтобы оптимизировать галерею под новое разрешение, необходимо назначить обработчик этому событию, который повторно запустит инициализацию галереи через вызов функции init
.
Инициализация адаптивной галереи изображений при изменении разрешения экрана.
Первое, что нужно сделать — это назначить обработчик для события resize
. Для этого, в самое начало функции registerEventsHandler
, регистрирующей обработчики событий, добавим следующий JS-код:
1 2 3 4 |
// регистрируем обработчик изменения размеров окна браузера window.addEventListener('resize', this.resize.bind(this)); |
Как видно из кода, при изменении разрешения, будет вызываться функция resize
. Может возникнуть вопрос, а почему сразу не вызвать функцию init
и не перестроить галерею под новое разрешение? Дело в том, что необходимо запомнить, а после инициализации галереи вывести активным именно то изображение, которое было активным до изменения разрешения.
Давайте разберём алгоритм работы функции resize
:
-
Вызывает функцию
init
, запуская инициализацию и построение каркаса галереи изображений с учётом нового разрешения. -
Получает новый индекс и координату X текущего элемента. Это очень важно, когда пользователь просматривает последние изображения галереи. Давайте посмотрим на рисунок и всё станет понятно без лишних объяснений.
-
Вызывает функцию
scroll
для докрутки галереи (при необходимости) к текущему элементу.
Для уменьшения нагрузки на браузер, обработку изменившихся параметров функция resize
производит через заданный период времени, используя таймер-планировщик setTimeout
.
Полный код функции resize
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
resize() { // обнуляем таймер clearTimeout(this.resizeTimer); // чтобы уменьшить нагрузку, обработку изменившихся параметров производим // через заданный период времени, используя таймер-планировщик this.resizeTimer = setTimeout(() => { // инициализируем галерею с учётом нового разрешения экрана this.init(); // получаем новый индекс текущего элемента this.current = (this.current <= this.max) ? this.current : this.max; // после изменения каркаса слайдера под новое разрешение, // находим новую координату текущего элемента let x = this.coordinates[this.current]; // прокручиваем слайдер до элемента, который до начала ресайзинга // был текущим this.scroll(x, this.options.baseTransition); }, this.options.delayTimer); } |
На этом создание адаптивной галереи изображений закончено. Посмотреть пример галереи и скачать HTML-вёрстку и полный JS-код, Вы можете по ссылкам, указанным в начале страницы.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
-
Спасибо за лестный отзыв )))
Стараюсь все статьи писать в таком стиле.-
А самое прекрасное в этой галерее, помимо адаптивности, которую днем с огнем не найдешь во встроенных бутстрапах или slick слайдерах.
Это то, что добавив в массив defaults новый параметр, например wheel
Ты можешь простым условием включать и отключать слушатель событий и использовать его только в случае определенного разрешения — в этом случае галерею можно разложить на слайды, и сделать на мобилке пролистывание вниз обычных слайдов, а на адаптиве том же разрешении 560 сделать галерею с одним или двумя элементами — это безумно удобно и круто.
Это прекрасный инструмент и я всем его советую!
-
-
-
Кое-что нашел, в функциях
if (this.mouseDrag !== false) …
и
if (typeof window.ontouchstart !== ‘undefined’ && this.touchDrag !== false)…должно быть
if (this.options.mouseDrag !== false)
и
if (typeof window.ontouchstart !== ‘undefined’ && this.options.touchDrag !== false) (не хватает options)И еще заметил, что этой функцией нельзя управлять в адаптиве, это происходит из-за того, что при переопределении функции, есть условие
if(!this.events)
{
this.registerEvents();
}тогда не срабатывает замена свойства.
Был бы признателен за подсказку, как можно переопределять данное свойство при адаптиве, чтобы не делать костыльных условий
-
Несколько изменил скрипт, вообще отказавшись от управления разрешением на использование ‘mouse events’ и ‘touch events’. Перетаскивание мышью и свайп между собой не конфликтуют, за исключением одного момента: отрыв пальца от экрана, кроме события ‘touchend’, может вызвать событие ‘click’. Чтобы исключить это, в функциях ‘tap’, ‘drag’ и ‘release’ после строки:
e.preventDefault();
необходимо добавить:
e.stopPropagation();
Не знаю, может это не лучшее решение, но, к сожалению, на данный момент другого я не вижу. Может кто-то из посетителей предложит свой, более правильный вариант.
-
Добрый день Вальдемар! Подскажите пожалуйста как можно прикрепить к адаптивной галереи изображений (сейчас у меня на локальном сайте сделан fullscreen slider на весь экран — 5 слайдов) css3 свойства — blur, scale, opacity, transform scale? Дело в том, что первый слайд, загружается с означенными эффектами, а вот последующие слайды, проходят без эффектов, только скролом -вот пример эффекта, к-рый пытаюсь установить на слайды:
/* slide opacity images — плавное появление изображения */
.slide-opacity {
opacity: 1;
transition: opacity 5ms ease-in-out;
}.slide-opacity {
opacity: 0;
}/* slide-blur images — blur, scale, opacity размытые изображения */
.slide-blur {
filter: blur(.5vw);
transform: scale(1.075);
}blur-in {
will-change: transform, opacity;
animation: blur-in 1s ease-in-out .5s;
}@keyframes blur-in {
0% { transform: scale(1.075); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}И второй вопрос, наверное дополняет первый. На полноэкранном слайдере, хотелось прикреплять рекламные блоки capture используя CSS3 2D Transform (https://html5book.ru/css3-transform/), допустим некий Привет! Я твой рекламный блок! )) 250х100 px, на котором размещена информация. Ваша адаптивная галерея легковесна, лаконична, просто супер! Грузить и устанавливать ради эффектов монструозные слайдеры по 1000 строк, которых с избытком в интернете, совсем уж не хочется. Буду признателен за подсказки!
-
Попробуйте изображение обернуть ещё в один DIV и уже к нему применять ваши классы.
И ещё, плавное появление изображения сделайте через анимацию с использованием @keyframes.
-
-
почему файл в браузере открывается, а на локальном сервере видно только «Aдаптивная галерея изображений» и «prev next» …не запускается js-код или не подключены стили? как исправить?
-
Скорее всего у вас проблема с путями вызова css и js файлов
-
-
Здравствуйте. Сделайте, пожалуйста, урок с подгружающимися изображениями. Где при открытии страницы существуют только два слайда. Первый слайд на виду, а второй скрыт. При прокрутке на второй слайд, подгружается третий слайд, если информация о нем есть в базе. И так далее, пока изображения не закончатся.
-
Круто. Мне понравилось
-
Спасибо. Попробовал добавить команду открытия модального окна внизу галереи с увеличенной фотографией при двойном нажатии (dblclick) левой кнопки мышки, безуспешно. Не получается перехватить dblclick. Буду признателен за подсказки!
-
Проблема в том, что клики перехватывает обработчик события «mousedown», вызывающий функцию tap. Эта функция, используя e.preventDefault(), отключает действия по умолчанию.
Чтобы решить эту проблему, вам придётся отказаться от листания галереи мышью, а в случае использования галереи на мобильных устройствах — от листания свапом.
Как вариант, чтобы не отказываться от существующего функционала, можно над картинкой разместить иконку (например, лупа) с абсолютным позиционированием. Открывать полноразмерную картинку можно кликая по иконке.
-
-
Крутой урок. А возможно задать этому слайдеру бесконечность?
-
Это не предусмотрено.
-
-
спасибо!
-
Привет. win xp не работает вообще никак.Достаточно было в конце js ставить alert(), чтобы убедиться в кривом коде. Удачи в разработке
-
У меня в каждом слайде есть ссылка. На мобильной версии эта ссылка не кликается и не тапается.
Как сделать чтобы ссылка кликалась?
Я не знаю кто это писал, но у меня создалось впечатление, что это просто умнейший человек.
Тут все настолько понятно описано, надо просто расставить около каждой переменной console.log, чтобы посмотреть что в нее передается и прочитать работу не понятных тебе функций.
Но эти многабукв того определенно стоят.
Спасибо большое!