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">
      <img src="images/diff/button.svg" alt="" loading="lazy" />
    </button>
  </div>
</div>

TS

js
function initDiffSlider() {
  const sliders = document.querySelectorAll<HTMLElement>('[data-js-diff-slider]')

  if (!sliders.length) {
    return
  }

  sliders.forEach((slider) => {
    const input = slider.querySelector('input[type="range"]')

    if (!input) {
      return
    }

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

    const stopPropagation = (e: Event) => {
      e.stopPropagation()
    }

    input.addEventListener('touchstart', stopPropagation, { passive: true })
    input.addEventListener('touchmove', stopPropagation, { passive: true })
    input.addEventListener('touchend', stopPropagation, { passive: true })

    input.addEventListener('pointerdown', stopPropagation)
    input.addEventListener('pointermove', stopPropagation)
    input.addEventListener('pointerup', stopPropagation)
  })
}

initDiffSlider()

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;
    top: 50%;
    width: 100%;
    height: rem(80);
    cursor: pointer;
    opacity: 0;
    transform: translateY(-50%);
  }

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

  // .diff-slider__button
  &__button {
    position: absolute;
    top: 50%;
    left: var(--position);
    display: flex;
    align-items: center;
    justify-content: center;
    width: rem(80);
    height: rem(80);
    pointer-events: none;
    background-color: var(--color-white);
    border-radius: 50%;
    transform: translate(-50%, -50%);
  }
}

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

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