Адаптивная галерея изображений для сайта на 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, чтобы посмотреть что в нее передается и прочитать работу не понятных тебе функций.
Но эти многабукв того определенно стоят.
Спасибо большое!