Доброго времени суток, друзья!
Вместо введения (постановка задачи)
Все началось с изучения чужих слайдеров (готовых решений в сети, типа 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.
Благодарю за внимание.