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

jgdy16mz4exmbv5ftn47b-ozoxs.png

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

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


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

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

Условия:

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


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


Из интересного здесь разве что атрибуты 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;
}

p {
    margin: 0.5em;
    padding: 0.5em;
    background: #ddd;
    color: #222;
    text-align: justify;
}


Тут даже говорить не о чем (.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);
    });
}


Разметка готова, можно ее получить. Для этого надо создать соответствующую кнопку:

let getCodeButton = document.createElement('button')
getCodeButton.innerText = 'get code'
document.body.appendChild(getCodeButton)

// вешаем слушатель
getCodeButton.addEventListener('click', () => {
    // проверяем, выводился ли код
    // если не выводился, выводим
    // если выводился, ничего не делаем
    if (document.querySelector('p') == null) {
        // получаем внешний HTML слайдера
        let code = slider.outerHTML
        // создаем контейнер для кода
        let p = document.createElement('p')
        // помещаем код в контейнер
        p.innerText = code
        document.body.appendChild(p)

        // выделяем все содержимое p
        document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
    } else return
})


Далее работаем внутри функции 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);
    }
}


Скрываем/показываем кнопку для получения кода:

getCodeButton.style.display == 'none' ? getCodeButton.style.display = 'block' : getCodeButton.style.display = 'none'
// вопрос на засыпку: почему код getCodeButton.style.display == 'block' ? getCodeButton.style.display = 'none' : getCodeButton.style.display = 'block' не работает?


Скрываем/показываем готовый код, если он сгенерирован:

// пытаемся инициализировать p
let p = document.querySelector('p')
// если получилось
if(p != null) {
    p.style.display == 'none' ? p.style.display = 'block' : p.style.display = 'none'
}


Мы будет выводить на экран по одному слайду. Не забываем, что переключение слайдов должно быть плавным. Этого легко добиться с помощью прозрачности и небольшого перехода (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. При этом, получить работоспособный код не выйдет.

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

Отредактированный вариант (для тех, кто не справился, и тех, кому лень):
let slider = document.querySelector('.slider')

let figures = document.querySelectorAll("figure");
for (i = 0; i < figures.length; i++) {
  let figure = figures[i];
  figure.addEventListener("click", () => {
    generateSlider(figure);
  });
}

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);
    }
  }

  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);
    }
  }

  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("+");
    } else if (e.keyCode == 27) {
      generateSlider(figure);
    }
  });
}


Благодарю за внимание. Всех благ. Любые предложения по улучшению и конструктивная критика приветствуются.

© Habrahabr.ru