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