очередной слайдер, которого никто не просил
Published: 2022-03-26 06:17:52

натыкаясь на вакансии от разных контор, я периодически берусь за тестовые задания, чтобы поразвлечься. в этот раз выбор пал на, кто бы сомневался, очередной слайдер. ну и пусть.

собираем проект, пара библиотек нам пригодится:

```
npm init
yarn add path express pug path
yarn add --dev nodemon
```

сразу добавим `dev` скрипт в `package.json`, который будет стучаться к нашему сервер-скрипту:

```
"scripts": {
    "dev": "nodemon src/server.js"
}
```

создадим папку `src` и файл `server.js`, в котором опишем наш сервер и движок для pug шаблонов:

```
const express = require('express')
const path = require('path')
const fs = require('fs')

const app = express()
const port = '8000'

app.listen(port, () => {
    console.log(`Listening on http://localhost:${port}`)
})

app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug')
app.use(express.static(path.join(__dirname, 'public')))

app.get('/', (_, res) => {
    try {
        let paths = fs.readdirSync(path.join(__dirname + '/public/images/')) // там будут храниться наши картинки для слайд-шоу
        let combinedPaths = paths.map(p => `images/${p}`)

        res.render('index', {
            imagesPaths: combinedPaths
        })
    } catch (err) {
        throw err
    }
})
```

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

```
doctype html
html(lang='en')
  head
    meta(charset='utf-8')
    link(rel='stylesheet' href='styles/index.css')
    meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no')
  body
    main.wrapper
        section.slider
            each src in imagesPaths
                article.slider__item.slider__item--appear.slide
                    figure
                        img(src=src alt='pic')
            section.slider__controls
                button
                button
    script(type='module' src='scripts/index.js')
```

переходим к самому вкусному - к интерактивности, которую возьмет на себя скрипт `scripts/index.js`. сперва обезопасим себя и дождемся загрузки DOM дерева:

```
document.addEventListener('DOMContentLoaded', () => {
  …
}, false)
```

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

```
const slidesList = document.querySelectorAll('.slider__item')
const controlsList = document.querySelector('.slider__controls').children
const leftControl = controlsList[0]
const rightControl = controlsList[1] // больше двух детей не предполагается

let interval = null
let currentIndex = 1
```

в конце скрипта инициализируем события и рендеринг самого слайдера:

```
document.addEventListener('keydown', e => {
    if (e.key === 'ArrowLeft') moveSlider('left') // переключение левой стрелкой
    if (e.key === 'ArrowRight') moveSlider('right') // переключение правой стрелкой
    if (e.key === ' ' && e.target.nodeName !== 'BUTTON') {
        if (interval) {
            stopSliding() // остановка на текущем слайде по нажатию на пробел
        } else {
            proceedSliding() // продолжение слайд-шоу, если была остановка
        }
    }
})
leftControl.addEventListener('click', () => moveSlider('left'), false)
rightControl.addEventListener('click', () => moveSlider('right'), false)

initSlider(slidesList)
```

столько функций еще не написано! надо исправлять:

```
const initSlider = slides => {
    if (currentIndex > slides.length) currentIndex = 1
    if (currentIndex < 1) currentIndex = slides.length

    updateSlider(slidesList)
}

const moveSlider = to => {
    if (to === 'right') updateCurrentIndex('right')
    else updateCurrentIndex('left')

    initSlider(slidesList)
}

…

const stopSliding = () => {
    clearInterval(interval)
    interval = null // очищаем интервал, чтобы остановить слайд-шоу

    slidesList[currentIndex - 1].classList.add('slide--stopped')
}

const proceedSliding = () => {
    interval = setInterval(() => {
        updateCurrentIndex('right')
        initSlider(slidesList)
    }, 3000)
    slidesList[currentIndex - 1].classList.remove('slide--stopped')
}
```

многие участки дублируются, потому выносим их в отдельные хендлеры:

```
const updateSlider = slides => {
    clearInterval(interval)

    for (let i = 0; i < slides.length; i++) {
        slides[i].classList.add('slider__item--inactive') // стили класса будут удалять слайд из разметки
        slides[i].classList.remove('slide--stopped')
    }

    slides[currentIndex - 1].classList.remove('slider__item--inactive')

    interval = setInterval(() => { // переопределяем интервал, ведь слайд-шоу не просто так шоу
        updateCurrentIndex('right')
        initSlider(slidesList)
    }, 3000)
}

…

const updateCurrentIndex = to => {
    if (to === 'right') currentIndex++
    else currentIndex--
}
```

ой, почти все! добавим стили и анимации для появления слайдов:

```
* {
    box-sizing: border-box;
}

body, section, figure, button {
    padding: 0;
    margin: 0;
}

.wrapper {
    width: 100vw;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(0, 0, 0, 1);
}

.slider {
    position: relative;
    width: 100%;
    height: 540px;
}

.slider__views-counter {
    position: absolute;
    top: -28px;
    left: 0;
    margin: 0;
    margin-left: 14px;
    font-family: 'Helvetica';
    font-size: 20px;
    line-height: 24px;
    color: white;
}

.slider__item {
    display: flex;
    height: 100%;
}

.slider__item--inactive {
    display: none;
}

.slider__item--appear {
    animation-name: slide-appearing;
    animation-duration: 1.5s;
}

.slide {
    align-items: center;
}

.slide--stopped {
    outline: 5px auto Highlight;
}

.slide figure {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.slide figure img {
    max-width: 100%;
    width: auto;
    max-height: 100%;
    height: auto;
}

.slider__controls {
    position: absolute;
    z-index: 2;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    margin: auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
}

.slider__controls button {
    position: relative;
    width: 14%;
    max-width: 80px;
    height: 40%;
    border: none;
    background: transparent;
    cursor: pointer;
    border-radius: 8px;
    background-color: rgba(255, 255, 255, .14);
}

.slider__controls button:hover {
    background-color: rgba(255, 255, 255, .18);
}

.slider__controls button:active {
    box-shadow: 0px 0px 22px rgba(0, 0, 0, 0.2);
}

.slider__controls button::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    margin: auto;
    width: 60%;
    height: 60%;
    background-image: url(/icons/arrow.svg);
    background-size: 100% 100%;
    background-repeat: no-repeat;
    filter: invert();
}

.slider__controls button:nth-child(2)::after {
    transform: rotate(180deg);
}
  
@keyframes slide-appearing {
    from {
        opacity: .2;
    }
    to {
        opacity: 1;
    }
}

@media screen and (min-width: 1020px) {
    .slider {
      width: 80%;
      max-width: 1020px;
    }

    .slider__views-counter {
        margin-left: 0;
    }

    .slider__controls button {
        max-width: 40px;
    }
  }

```

пора бы глянуть, чего уже там нарисовалось то. запустим `yarn dev` и перейдем по `http://localhost:8000`:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/516kolihee5dharkglw6.gif)

а что, дизайн вышел покурить? ну и пусть, ведь важнее, что внутри.