Пишем генератор галереи изображений со встроенным слайдером


Доброго времени суток, друзья!

Вместо введения (постановка задачи)

Все началось с изучения чужих слайдеров (готовых решений в сети, типа bxslider, owlcarousel и slick). Когда-нибудь я напишу подробные руководства по работе с этими инструментами (sweet dreams). Появилось желание написать свой слайдер. Однако вскоре (в том числе, после прочтения нескольких статей на Хабре) пришло осознание, что просто слайдер — это для слабаков. Нужно что-то более радикальное.

В итоге придумал себе такую задачу: написать генератор адаптивной галереи со встроенным слайдером.

Условия:

  • Возможность загружать любое количество изображений (из любого места на жестком диске).
  • Галерея состоит из загруженных изображений, разметка формируется «на лету» с соблюдением семантики HTML5.
  • Галерея одинаково хорошо смотрится на экранах с различным разрешением.
  • При клике на любом изображении генерируется слайдер.
  • При генерации слайдера затемняется фон.
  • Изображение, по которому кликнули — первый слайд.
  • Переключение слайдов реализовано через DOM.
  • Слайды переключаются плавно.
  • Возможность управлять переключением слайдов с помощью кнопок и клавиатуры.
  • Возможность вернуться к галерее при клике на текущем слайде и кнопке, а также с помощью клавиатуры.
  • Чистый JavaScript (вся разметка через JS).
  • Минимум кода.

Итак, поехали (как сказал Гагарин, отправляясь в космос).

Разметка выглядит так:

<div class="wrap">
    <input type="file" multiple accept="image/*">
    <button>generate gallery</button>
</div>

Из интересного здесь разве что атрибуты multiple и accept тега input. Первый атрибут позволяет загружать несколько файлов, второй — устанавливает фильтр на типы файлов, которые можно загрузить. В данном случае accept имеет значение «image/*», означающее, что можно загружать только изображения (любые).

Сразу наведем красоту (добавим стили):

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: radial-gradient(circle, skyblue, steelblue);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

button {
    padding: 0.25em;
    font-family: monospace;
    text-transform: uppercase;
    cursor: pointer;
}

.darken {
    position: absolute;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.4);
    z-index: -1;
}

.slider {
    width: 100%;
    display: inherit;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}

figure {
    margin: 0.5em;
    width: 300px;
    display: inherit;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    transition: 0.2s;
}

figcaption {
    font-size: 1.2em;
    text-transform: capitalize;
    text-align: center;
    margin-bottom: 0.25em;
    color: #ddd;
    text-shadow: 1px 1px rgba(0, 0, 0, 0.4);
}

img {
    max-width: 80%;
    max-height: 80vh;
    cursor: pointer;
}

.button {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 30px;
    background: none;
    border: none;
    outline: none;
    filter: invert();
}

.left {
    left: 2em;
}

.right {
    right: 2em;
}

.close {
    top: 2em;
    right: 1em;
}

Тут даже говорить не о чем (.darken — затемнение).

Двигаемся дальше… к JS.

Находим кнопку и вешаем на нее слушатель:

let button = document.querySelector("button");
button.addEventListener("click", generateGallery);

Весь дальнейший код будет находиться в функции generateGallery дабы избежать «not defined» без return:

function generateGallery() {
    // код галереи и слайдера
}

Находим input, проверяем, что он не пустой, получаем коллекцию загруженных файлов, удаляем .wrap и создаем контейнер для галереи:

let input = document.querySelector("input");
// проверяем, что input не пустой
if(input.files.length == 0) return;
let files = input.files;
// просто счетчик
let i;

// удаляем .wrap, он нам больше не нужен
let wrap = document.querySelector(".wrap");
document.body.removeChild(wrap);

// создаем контейнер для галереи, возможно, его следовало назвать gallery
let slider = document.createElement("div");
slider.className = "slider";
document.body.appendChild(slider);

Перебираем коллекцию файлов, получаем имя и адрес каждого файла, создаем разметку, формируем подписи к изображениям и сами изображения:

for (i = 0; i < files.length; i++) {
    let file = files[i];
    // URL.createObjectURL позволяет загружать файлы из любого места на жестком диске, но при этом возникают проблемы с получением готового кода
    // ссылки, сформированные этим методом, сохраняют работоспособность только во время сессии браузера
    // попытки декодировать строку с адресом при получении готового кода не привели к успеху, поэтому я решил воспользоваться другим способом
    /*let src = URL.createObjectURL(file);*/

    // получаем имя файла
    let name = file.name;

    // этот способ предполагает, что изображения находятся в папке img на одном уровне со скриптом
    let src = `img/${name}`;

    // создаем разметку: figure, figcaption, img
    let figure = document.createElement("figure");
    slider.appendChild(figure);

    let figcaption = document.createElement("figcaption");

    // для того, чтобы избавиться от расширения файла при выводе его имени в подпись к изображению, используем регулярное выражение
    // (?=.) - опережающая проверка: найти один или более символов, за которыми следует точка
    // в данном случае мы не используем w, потому что имя файла может быть не на латинице
    let regexp = /.+(?=.)/;
    name = name.match(regexp);
    // получаем массив ["имя", index: 0, input: "имя.jpg", groups: undefined]
    // нас интересует первый элемент
    figcaption.innerText = name[0];
    figure.appendChild(figcaption);

    // создаем изображение
    let img = document.createElement("img");
    img.src = src;
    figure.appendChild(img);
}

Мы хотим генерировать слайдер при клике по изображению. Для этого, мы находим все figure и вешаем на каждый слушатель:

let figures = document.querySelectorAll("figure");
for (i = 0; i < figures.length; i++) {
    let figure = figures[i];
    figure.addEventListener("click", () => {
        // обратите внимание, что в качестве параметра мы передаем figure, по которому кликнули
        generateSlider(figure);
    });
}

Далее работаем внутри функции generateSlider:

function generateSlider(figure) {
    // код слайдера
}

Затемняем фон:

darkenBack();
function darkenBack() {
    // проверяем, имеется ли затемнение
    // если отсутствует, добавляем, в противном случае, удаляем
    if (document.querySelector(".darken") == null) {
        let div = document.createElement("div");
        div.className = "darken";
        document.body.appendChild(div);
    } else {
        let div = document.querySelector(".darken");
        document.body.removeChild(div);
    }
}

Мы будет выводить на экран по одному слайду. Не забываем, что переключение слайдов должно быть плавным. Этого легко добиться с помощью прозрачности и небольшого перехода (transition). Поэтому накладываем изображения друг на друга, размещаем их по центру, и делаем все изображения, кроме «кликнутого», прозрачными:

for (i = 0; i < figures.length; i++) {
    if (figures[i].hasAttribute("style")) {
        figures[i].removeAttribute("style");
    } else {
        figures[i].setAttribute("style", "margin: 0; width: auto; position: absolute; opacity: 0;");
    }
}

// кнопки генерируются каждый раз при открытии/закрытии слайдера
if (figure.hasAttribute("style")) {
    figure.style.opacity = 1;
    generateButtons();
} else generateButtons();

Далее создаем кнопки переключения слайдов и закрытия галереи. Код получился длинным и скучным (возможно, генерировать кнопки каждый раз при запуске слайдера, было не лучшей идеей):

Код создания кнопок:

function generateButtons() {
    if (document.querySelector(".buttons") == null) {
        // создаем контейнер для кнопок
        let buttons = document.createElement("div");
        buttons.className = "buttons";
        slider.appendChild(buttons);

        // создаем левую кнопку
        let leftButton = document.createElement("button");
        leftButton.className = "button left";
        let leftImg = document.createElement("img");
        leftImg.src = "https://thebestcode.ru/media/sliderGenerator/buttons/left.png";
        leftButton.appendChild(leftImg);
        buttons.appendChild(leftButton);
        leftButton.addEventListener("click", () => changeSlide("-"));

        // создаем правую кнопку
        let rightButton = document.createElement("button");
        rightButton.className = "button right";
        let rightImg = document.createElement("img");
        rightImg.src = "https://thebestcode.ru/media/sliderGenerator/buttons/right.png";
        rightButton.appendChild(rightImg);
        buttons.appendChild(rightButton);
        rightButton.addEventListener("click", () => changeSlide("+"));

        // создаем кнопку закрытия слайдера
        let closeButton = document.createElement("button");
        closeButton.className = "button close";
        let closeImg = document.createElement("img");
        closeImg.src = "https://thebestcode.ru/media/sliderGenerator/buttons/close.png";
        closeButton.appendChild(closeImg);
        buttons.appendChild(closeButton);
        closeButton.addEventListener("click", () => generateSlider(figure));
    } else {
        // если кнопки созданы, удаляем их
        let buttons = document.querySelector(".buttons");
        slider.removeChild(buttons);
    }
}

Переключение слайдов реализуется с помощью функции changeSlide, которой в качестве параметра передается, соответственно, «+» или «-«:

function changeSlide(e) {
    // делаем все слайды прозрачными
    for (i = 0; i < figures.length; i++) {
        figures[i].style.opacity = 0;
    }
    if (e == "-") {
        // если текущий слайд является первым изображением, переключаем на последнее изображение
        if (figure == figures[0]) {
            figure = figures[figures.length - 1];
        } else {
            figure = figure.previousElementSibling;
        }
    } else if (e == "+") {
        // если текущий слайд является последним изображением, переключаемся на первое изображение
        if (figure == figures[figures.length - 1]) {
            figure = figures[0];
        } else {
            figure = figure.nextElementSibling;
        }
    }
    // текущий слайд делаем непрозрачным
    figure.style.opacity = 1;
}

Добавляем возможность переключения слайдов и закрытия галереи с помощью клавиатуры:

document.addEventListener("keydown", e => {
    // стрелка влево
    if (e.keyCode == 37 || e.keyCode == 189) {
        changeSlide("-");
    // стрелка вправо
    } else if (e.keyCode == 39 || e.keyCode == 187) {
        changeSlide("+");
    // esc
    } else if(e.keyCode == 27) {
        generateSlider(figure);
    }
});

Вот и все, генератор адаптивной галереи со встроенным слайдером готов. Задача выполнена. Условия соблюдены. Ближе к концу понял, что «минимум кода» и «вся разметка формируется на лету с помощью JS» противоречат друг другу, но было уже поздно (it’s too late to apologize или как там у One Republic?).

Результат можно посмотреть здесь.

Обратите внимание, что на Codepen мы используем URL.createObjectURL для формирования ссылок на изображения, потому что Codepen не видит папку img.

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр