Skip to content

Swiper с превью

ts
if (document.querySelector<HTMLElement>('.part__slider')) {
    const thumbsSlider = new Swiper('.part__navigation-thumbs', {
        modules: [],
        observer: true,
        observeParents: true,
        slidesPerView: 'auto',
        spaceBetween: 10,
        autoHeight: false,
        speed: 800,
        watchSlidesProgress: true,
        allowTouchMove: false,
    })

    new Swiper('.part__slider', {
        modules: [Thumbs, Navigation],
        observer: true,
        observeParents: true,
        slidesPerView: 1,
        spaceBetween: 20,
        autoHeight: false,
        speed: 800,
        watchSlidesProgress: true,
        thumbs: {
            swiper: thumbsSlider,
        },
        navigation: {
            prevEl: '.part__button--prev',
            nextEl: '.part__button--next',
        },
        on: {
            slideChange: function (this: Swiper) {
                let activeIndex = this.activeIndex + 1
                let nextSlide = document.querySelector(
                    `.thumbs-description__slider .swiper-slide:nth-child(${activeIndex + 1})`
                )
                let prevSlide = document.querySelector(
                    `.thumbs-description__slider .swiper-slide:nth-child(${activeIndex - 1})`
                )

                if (nextSlide && !nextSlide.classList.contains('swiper-slide-visible')) {
                    this.thumbs.swiper.slideNext()
                } else if (prevSlide && !prevSlide.classList.contains('swiper-slide-visible')) {
                    this.thumbs.swiper.slidePrev()
                }
            },
        },
    })
}

Кастомная пагинация в Swiper

js
pagination: {
  el: '.chooser__fraction',
  type: 'custom',
  renderCustom: function (swiper, current, total) {
    return current + '/' + '<span>' + total + '</span>'
  },
},

Swiper c таймером

js
if (document.querySelector('.hero__slider')) {
    new Swiper('.hero__slider', {
        modules: [Pagination, Autoplay],
        observer: true,
        observeParents: true,
        slidesPerView: 1,
        spaceBetween: 0,
        speed: 800,
        autoplay: {
            duration: 5000,
        },
        loop: true,

        pagination: {
            el: '.hero__pagination',
            clickable: true,
        },
        on: {
            slideChange: resetAnimation,
        },
    })

    function resetAnimation() {
        const elements = document.querySelectorAll('.hero__pagination .swiper-pagination-bullet')
        const activeElement = document.querySelector('.hero__pagination .swiper-pagination-bullet-active')

        elements.forEach((element) => {
            element.classList.remove('animated')
            requestAnimationFrame(() => {
                element.style.animationName = 'none'
                setTimeout(() => {
                    element.style.animationName = ''
                }, 0)
            })
        })

        if (activeElement.nextElementSibling) {
            activeElement.nextElementSibling?.classList.add('animated')
        } else {
            elements[0].classList.add('animated')
        }
    }

    resetAnimation()
}

Мегаменю

HTML

html
<div class="megamenu">
    <nav data-js-megamenu-body class="megamenu__body">
        <ul data-js-megamenu-list class="megamenu__list">
            <li data-js-megamenu-item class="megamenu__item">
                <div data-js-megamenu-link class="megamenu__link">
                    <div class="megamenu__label">
                        <span>Каталог</span>
                    </div>
                    <svg class="megamenu__arrow" width="16" height="16">
                        <use href="images/icons.svg#catalog-arrow"></use>
                    </svg>
                </div>
                <div data-js-megamenu-wrapper class="megamenu__wrapper">
                    <button data-js-megamenu-back type="button" class="megamenu__back">
                        <svg width="16" height="16" fill="none">
                            <use href="images/icons.svg#catalog-back"></use>
                        </svg>
                        <span>Меню</span>
                    </button>
                    <ul class="megamenu__sublist">
                        <li data-js-megamenu-subitem class="megamenu__subitem">
                            <button data-js-megamenu-sublink type="button" class="megamenu__sublink">
                                <span class="megamenu__label">
                                    <svg width="24" height="24">
                                        <use href="images/icons.svg#catalog-1"></use>
                                    </svg>
                                    <span>Кондиционеры</span>
                                </span>
                                <svg class="megamenu__arrow" width="16" height="16">
                                    <use href="images/icons.svg#catalog-arrow"></use>
                                </svg>
                            </button>
                            <div data-js-megamenu-subwrapper class="megamenu__subwrapper">
                                <button data-js-megamenu-back type="button" class="megamenu__back">
                                    <svg width="16" height="16" fill="none">
                                        <use href="images/icons.svg#catalog-back"></use>
                                    </svg>
                                    <span>Кондиционеры</span>
                                </button>
                                <div class="megamenu__content megamenu-content">
                                    <ul class="megamenu-content__list">
                                        <li class="megamenu-content__item"><a href="#">Настенные кондиционеры</a></li>
                                        <li class="megamenu-content__item"><a href="#">Мульти-сплит системы</a></li>
                                        <li class="megamenu-content__item"><a href="#">Кассетные кондиционеры</a></li>
                                        <li class="megamenu-content__item"><a href="#">Канальные кондиционеры</a></li>
                                    </ul>
                                </div>
                            </div>
                        </li>
                    </ul>
                </div>
            </li>
        </ul>
    </nav>
</div>

TS

ts
const megamenuInit = (): void => {
    const SELECTORS = {
        megaList: '[data-js-megamenu-list]',
        megaBody: '[data-js-megamenu-body]',
        megaLink: '[data-js-megamenu-link]',
        megaItem: '[data-js-megamenu-item]',
        megaWrapper: '[data-js-megamenu-wrapper]',
        megaBack: '[data-js-megamenu-back]',
        subLink: '[data-js-megamenu-sublink]',
        subItem: '[data-js-megamenu-subitem]',
        subWrapper: '[data-js-megamenu-subwrapper]',
    }

    const mediaQuery: MediaQueryList = window.matchMedia('(max-width: 991px)')
    const isActiveClass: string = 'is-active'
    const isHidenClass: string = 'is-hidden'
    let initialized: boolean = false
    let megaMenuBody: HTMLElement | null = null

    // Обработчики событий
    function handleBackClick(event: MouseEvent): void {
        const target = event.target as HTMLElement
        if (!target.closest(SELECTORS.megaBack)) {
            return
        }

        // Определяем текущий уровень вложенности меню
        const subWrapper = target.closest<HTMLElement>(SELECTORS.subWrapper)
        const wrapper = target.closest<HTMLElement>(SELECTORS.megaWrapper)

        // Обработка клика для подменю
        if (subWrapper) {
            subWrapper.classList.remove(isActiveClass)
            document
                .querySelectorAll<HTMLElement>(SELECTORS.subItem)
                .forEach((subItem) => subItem.classList.remove(isHidenClass))
            return
        }

        // Обработка клика для основного меню
        if (wrapper) {
            wrapper.classList.remove(isActiveClass)
            megaMenuBody!.classList.remove(isHidenClass)
        }
    }

    function handleMegaLinkClick(event: MouseEvent): void {
        const target = (event.target as HTMLElement).closest<HTMLElement>(SELECTORS.megaLink)
        if (!target) {
            return
        }

        if (target.nextElementSibling) {
            event.preventDefault()
        }

        const menuItem = target.closest<HTMLElement>(SELECTORS.megaItem)
        if (!menuItem) {
            return
        }

        const wrapper = menuItem.querySelector<HTMLElement>(SELECTORS.megaWrapper)
        if (wrapper) {
            wrapper.classList.add(isActiveClass)
            megaMenuBody!.classList.add(isHidenClass)
        }
    }

    function handleSubLinkClick(event: MouseEvent): void {
        const target = (event.target as HTMLElement).closest<HTMLElement>(SELECTORS.subLink)
        if (!target) {
            return
        }

        if (target.nextElementSibling) {
            event.preventDefault()
        }

        const subItem = target.closest<HTMLElement>(SELECTORS.subItem)
        if (!subItem) {
            return
        }

        const subWrapper = subItem.querySelector<HTMLElement>(SELECTORS.subWrapper)
        if (subWrapper) {
            subWrapper.classList.add(isActiveClass)
            document
                .querySelectorAll<HTMLElement>(SELECTORS.subItem)
                .forEach((item) => item.classList.add(isHidenClass))
        }
    }

    function init(): void {
        if (!mediaQuery.matches) {
            cleanup()
            return
        }

        if (initialized) {
            return
        }

        const megaList = document.querySelector<HTMLElement>(SELECTORS.megaList)
        megaMenuBody = document.querySelector<HTMLElement>(SELECTORS.megaBody)

        if (!megaList || !megaMenuBody) {
            return
        }

        megaList.addEventListener('click', handleMegaLinkClick)
        megaList.addEventListener('click', handleSubLinkClick)
        megaList.addEventListener('click', handleBackClick)

        initialized = true
    }

    function cleanup(): void {
        if (!initialized) {
            return
        }

        const megaList = document.querySelector<HTMLElement>(SELECTORS.megaList)

        if (megaList) {
            megaList.removeEventListener('click', handleMegaLinkClick)
            megaList.removeEventListener('click', handleSubLinkClick)
            megaList.removeEventListener('click', handleBackClick)
        }

        document.querySelectorAll<HTMLElement>('.is-active, .is-hidden').forEach((element) => {
            element.classList.remove(isActiveClass, isHidenClass)
        })

        initialized = false
    }

    // Первичная инициализация
    init()

    // Обработка изменения размера экрана
    mediaQuery.addEventListener('change', init)

    // Очистка при выгрузке страницы
    window.addEventListener('pagehide', cleanup)
}

megamenuInit()

CSS

scss
.megamenu {
    &__link {
        position: relative;
        display: flex;
        gap: rem(10);
        align-items: center;
        justify-content: space-between;
        width: 100%;
        padding-block: rem(10);
        padding-right: rem(24);
    }

    &__label {
        display: flex;
        gap: rem(10);
        align-items: center;
        font-size: 15px;
        font-weight: 700;
        line-height: 107%;
    }

    &__arrow {
        transition: transform 0.3s ease-in-out;

        path {
            transition: fill 0.3s ease-in-out;
        }
    }

    &__wrapper {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        display: flex;
        visibility: hidden;
        flex-direction: column;
        width: 100%;
        height: 100%;
        padding-top: rem(16);
        margin: 0;
        color: rgb(24 27 32 / 80%);
        pointer-events: auto;
        background-color: $whiteColor;
        opacity: 1;
        transform: translateX(120%);
        transition: all 0.3s ease-in-out;

        @include adaptive-value('padding-inline', 40, 16);

        &.is-active {
            z-index: 10;
            visibility: visible;
            opacity: 1;
            transform: translateX(0%);
        }
    }

    &__back {
        position: relative;
        display: flex;
        gap: rem(10);
        align-items: center;
        width: 100%;
        padding-block: rem(10);
        margin-bottom: rem(18);
        font-size: rem(15);
        font-weight: 700;
        line-height: 107%;
        color: $greenColor;
        background-color: $lightColor;

        &::before {
            position: absolute;
            top: 0;
            left: -50px;
            width: 50px;
            height: 100%;
            content: '';
            background-color: $lightColor;
        }

        &::after {
            position: absolute;
            top: 0;
            right: -50px;
            width: 50px;
            height: 100%;
            content: '';
            background-color: $lightColor;
        }
    }

    &__sublist {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        gap: rem(70);
        max-width: rem(1280);
        margin-inline: auto;

        @media (max-width: em(1139)) {
            display: block;
            margin: 0;
            overflow: hidden auto;
        }
    }

    &__sublink {
        display: flex;
        align-items: center;
        justify-content: space-between;
        width: 100%;
        padding-block: rem(10);
    }

    &__subwrapper {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        display: flex;
        visibility: hidden;
        flex-direction: column;
        width: 100%;
        height: 100%;
        padding-top: rem(16);
        margin: 0;
        color: rgb(24 27 32 / 80%);
        pointer-events: auto;
        background-color: $whiteColor;
        opacity: 1;
        transform: translateX(120%);
        transition: all 0.3s ease-in-out;

        @include adaptive-value('padding-inline', 40, 16);

        &.is-active {
            z-index: 10;
            visibility: visible;
            opacity: 1;
            transform: translateX(0%);
        }
    }
}

Генерация ссылок из заголовков блоков

ts
function pageNavigation() {
    const content = document.querySelector<HTMLElement>('[data-js-navigation-content]')
    const navigationList = document.querySelector<HTMLElement>('[data-js-navigation-list]')

    console.log(navigationList)

    if (!content || !navigationList) {
        return
    }

    const blockTitles = content.querySelectorAll<HTMLTitleElement>('h2')

    blockTitles.forEach((title, index) => {
        title.setAttribute('id', `target-${index}`)
        navigationList.insertAdjacentHTML(
            'beforeend',
            `<li data-goto="#target-${index}" data-js-navigation-item data-goto-top="140" class="page-navigation__item">${title.innerHTML}</li>`
        )
    })

    // Выделяем пункт меню про скроле
    const sidebarLinks = navigationList.querySelectorAll<HTMLLIElement>('[data-js-navigation-item]')

    if (!sidebarLinks.length) {
        return
    }

    window.addEventListener('scroll', function () {
        let scrollPos = window.scrollY
        sidebarLinks.forEach((link) => {
            const currentrentLink = link
            const refElements = document.querySelectorAll<HTMLElement>(currentrentLink.getAttribute('data-goto') || '')

            refElements.forEach((element) => {
                if (element.offsetTop - 30 <= scrollPos) {
                    sidebarLinks.forEach((link) => {
                        link.classList.remove('active')
                    })
                    currentrentLink.classList.add('active')
                }
            })
        })
    })
}

pageNavigation()

Изображение вместо Youtube плеера

js
const videos = document.querySelectorAll('.video')

if (videos) {
    videos.forEach((video) => {
        const data = video.querySelector('button')
        const url = 'https://i.ytimg.com/vi/' + data.dataset.popupYoutube + '/maxresdefault.jpg'
        video.querySelector('img').src = url
        if (video.querySelector('source')) {
            video.querySelector('source').srcset = url
        }
    })
}

Файл

js
const fileInput = document.querySelector('.file input[type=file]')

if (fileInput) {
    fileInput.addEventListener('change', () => {
        fileInput.closest('.file').querySelector('.file__text').textContent = fileInput.files[0].name
    })
}

Показать еще (кастом)

js
import { _slideUp, _slideDown } from './functions'

function hideBlock(block, row, moreButton, lessButton, qty) {
    const blocks = document.querySelectorAll(block)

    let rowTarget = row
    let moreButtonTarget = moreButton
    let lessButtonTarget = lessButton

    if (blocks.length) {
        blocks.forEach((block) => {
            const rows = block.querySelectorAll(rowTarget)
            const moreButton = block.querySelector(moreButtonTarget)
            const lessButton = block.querySelector(lessButtonTarget)
            let myArray = Array.from(rows)
            let hiddenRows = myArray.slice(qty)

            if (hiddenRows <= 5) {
                moreButton.style.display = 'none'
                lessButton.style.display = 'none'
                block.classList.add('showmore-active')
            }

            hiddenRows.forEach((hiddenRow) => {
                hiddenRow.classList.add('_hidden-item')
                hiddenRow.classList.add('_hidden')

                lessButton.style.display = 'none'

                moreButton.addEventListener('click', () => {
                    hiddenRow.classList.remove('_hidden')
                    _slideDown(hiddenRow)

                    lessButton.style.display = 'flex'
                    moreButton.style.display = 'none'

                    block.classList.add('showmore-active')
                })

                lessButton.addEventListener('click', () => {
                    _slideUp(hiddenRow)
                    lessButton.style.display = 'none'
                    moreButton.style.display = 'flex'

                    block.classList.remove('showmore-active')

                    setTimeout(() => {
                        hiddenRow.classList.add('_hidden')
                    }, 500)
                })
            })
        })
    }
}

hideBlock('.showmore', '.showmore__item', '.showmore__button--more', '.showmore__button--less', 3)

Range слайдер

HTML

html
<div id="range" data-from="1" data-to="30" data-start="10" class="range"></div>

JS

js
import * as noUiSlider from 'nouislider'

export function rangeInit() {
    const priceSlider = document.querySelector('#range')
    if (priceSlider) {
        let from = priceSlider.getAttribute('data-from')
        let to = priceSlider.getAttribute('data-to')
        let start = priceSlider.getAttribute('data-start')

        noUiSlider.create(priceSlider, {
            start: [Number(start)],
            connect: false,
            range: {
                min: Number(from),
                max: Number(to),
            },
        })

        const input = document.querySelector('#term')
        let sliderValueNumber

        sliderValueNumber = priceSlider.noUiSlider.get(true)
        input.value = Math.round(sliderValueNumber) + ' лет'

        function setValue() {
            sliderValueNumber = priceSlider.noUiSlider.get(true)
            input.value = Math.round(sliderValueNumber) + ' лет'
        }

        priceSlider.noUiSlider.on('slide', setValue)
    }
}
rangeInit()

Прелоадер

HTML

html
<div class="preloader">
    <div class="preloader__row">
        <div class="preloader__item"></div>
        <div class="preloader__item"></div>
    </div>
</div>

JS

js
window.addEventListener('load', function (e) {
    document.body.classList.add('loaded_hiding')
    window.setTimeout(function () {
        document.body.classList.add('loaded')
        document.body.classList.remove('loaded_hiding')
    }, 500)
})

CSS

scss
.preloader {
    position: fixed;
    inset: 0;
    z-index: 1001;
    background: $whiteColor;

    &__row {
        position: relative;
        top: 50%;
        left: 50%;
        width: rem(70);
        height: rem(70);
        margin-top: rem(-35);
        margin-left: rem(-35);
        text-align: center;
        animation: preloader-rotate 2s infinite linear;
    }

    &__item {
        position: absolute;
        top: 0;
        display: inline-block;
        width: rem(35);
        height: rem(35);
        background-color: $accentColor;
        border-radius: 100%;
        animation: preloader-bounce 2s infinite ease-in-out;

        &:last-child {
            top: auto;
            bottom: 0;
            animation-delay: -1s;
        }
    }
}

@keyframes preloader-rotate {
    100% {
        transform: rotate(360deg);
    }
}

@keyframes preloader-bounce {
    0%,
    100% {
        transform: scale(0);
    }

    50% {
        transform: scale(1);
    }
}

.loaded_hiding .preloader {
    opacity: 0;
    transition: 1s opacity;
}

.loaded .preloader {
    display: none;
}

Слайдер «До/после»

HTML

html
<div data-js-diff-slider class="diff-slider">
    <div class="diff-slider__wrapper">
        <div class="diff-slider__images">
            <img src="images/diff/before.jpg" alt="" class="diff-slider__image diff-slider__image--before" />
            <img src="images/diff/after.jpg" alt="" class="diff-slider__image diff-slider__image--after" />
        </div>
        <input
            type="range"
            min="0"
            max="100"
            value="50"
            aria-label="Percentage of before photo shown"
            class="diff-slider__input" />
        <div class="diff-slider__line" aria-hidden="true"></div>
        <button type="button" class="diff-slider__button" aria-hidden="true">
            <button type="button" class="diff-slider__button" aria-hidden="true">
                <img src="images/diff/button.svg" alt="" loading="lazy" />
            </button>
        </button>
    </div>
</div>

JS

js
function diffSlider() {
    const sliders = document.querySelectorAll(['data-js-diff-slider'])

    sliders.forEach((slider) => {
        slider.addEventListener('input', (e) => {
            slider.style.setProperty('--position', `${e.target.value}%`)
        })
    })
}

diffSlider()

CSS

scss
.diff-slider {
    --position: 50%;

    &__wrapper {
        position: relative;
    }

    // .diff-slider__images
    &__images {
        position: relative;
        aspect-ratio: 800/480;
        overflow: hidden;

        @include adaptive-value('border-radius', 20, 10);
    }

    // .diff-slider__image
    &__image {
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: left;

        // .diff-slider__image--before
        &--before {
            position: absolute;
            inset: 0;
            width: var(--position);
        }
    }

    // .diff-slider__input
    &__input {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        cursor: pointer;
        opacity: 0;
    }

    // .diff-slider__line
    &__line {
        position: absolute;
        inset: 0;
        left: var(--position);
        width: 0.2rem;
        height: 100%;
        pointer-events: none;
        background-color: $whiteColor;
        transform: translateX(-50%);
    }

    // .diff-slider__button
    &__button {
        position: absolute;
        top: 50%;
        left: var(--position);
        pointer-events: none;
        filter: drop-shadow(0 4px 4px rgb(0 0 0 / 12%));
        transform: translate(-50%, -50%);

        img {
            height: auto;

            @include adaptive-value('max-width', 80, 54);
        }
    }
}

Скролл к центру элемента

JS

js
function scrollIntoView() {
    const elements = document.querySelectorAll(['data-js-scroll-center'])

    elements.forEach((element) => {
        element.addEventListener('click', () => {
            element.scrollIntoView({
                behavior: 'smooth',
                block: 'center',
                inline: 'center',
            })
        })
    })
}

scrollIntoView()

Обрезка текста

CSS

scss
.text {
    display: -webkit-box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}

Инициализация Swiper на определенной ширине

TS

ts
const directionsSliderInit = () => {
    if (document.querySelector<HTMLElement>('.categories')) {
        return new Swiper('.categories', {
            modules: [],
            observer: true,
            observeParents: true,
            slidesPerView: 1.8,
            spaceBetween: 10,
            autoHeight: false,
            speed: 800,
            watchSlidesProgress: true,
            on: {},
        })
    }
}

breakpointSliderEnabler(767, directionsSliderInit)

function breakpointSliderEnabler(width: number, callback: () => Swiper | undefined) {
    const breakpoint = window.matchMedia(`(max-width:${width}px)`)
    let slider: Swiper | undefined

    const init = function () {
        if (breakpoint.matches) {
            slider = callback()
        } else if (!breakpoint.matches && slider) {
            slider.destroy(true, true)
        }
    }

    breakpoint.addEventListener('change', init)

    init()
}

Активация карты по клику

JS

js
function activateMapOnClick() {
    const mapWrapperElement = document.querySelector('#map')

    if (mapWrapperElement) {
        document.addEventListener('click', (event) => {
            mapWrapperElement.classList.toggle('is-active', event.target === mapWrapperElement)
        })
    }
}

activateMapOnClick()

CSS

scss
#map {
    &:not(.is-active) * {
        pointer-events: none;
    }
}