import type { IBreakpoint, IMediaQueryItem, IMediaQueryResult, IMobile } from '../types' /* Проверка мобильного браузера */ export const isMobile: IMobile = { Android(): RegExpMatchArray | null { return navigator.userAgent.match(/Android/i) }, BlackBerry(): RegExpMatchArray | null { return navigator.userAgent.match(/BlackBerry/i) }, iOS(): RegExpMatchArray | null { return navigator.userAgent.match(/iPhone|iPad|iPod/i) }, Opera(): RegExpMatchArray | null { return navigator.userAgent.match(/Opera Mini/i) }, Windows(): RegExpMatchArray | null { return navigator.userAgent.match(/IEMobile/i) }, any(): boolean { return Boolean( isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows() ) }, } /* Добавление класса touch для HTML если браузер мобильный */ export function addTouchClass(): void { if (isMobile.any()) { document.documentElement.classList.add('touch') } } // Добавление loaded для HTML после полной загрузки страницы export function addLoadedClass(): void { if (!document.documentElement.classList.contains('loading')) { window.addEventListener('load', () => { setTimeout(() => { document.documentElement.classList.add('loaded') }, 0) }) } } // Получение хеша в адресе сайта export function getHash(): string | undefined { if (location.hash) { return location.hash.replace('#', '') } return undefined } // Указание хеша в адресе сайта export function setHash(hash: string): void { const newHash = hash ? `#${hash}` : window.location.href.split('#')[0] history.pushState('', '', newHash) } // Учет плавающей панели на мобильных устройствах при 100vh export function fullVHfix(): void { const fullScreens = document.querySelectorAll('[data-fullscreen]') if (fullScreens.length && isMobile.any()) { const fixHeight = (): void => { const vh = window.innerHeight * 0.01 document.documentElement.style.setProperty('--vh', `${vh}px`) } fixHeight() window.addEventListener('resize', fixHeight) } } // Вспомогательные модули плавного раскрытия и закрытия объекта ====================================================================================================================================================================== export function _slideUp(target: HTMLElement, duration: number = 500, showmore: number = 0): void { if (!target.classList.contains('_slide')) { target.classList.add('_slide') target.style.transitionProperty = 'height, margin, padding' target.style.transitionDuration = `${duration}ms` target.style.height = `${target.offsetHeight}px` void target.offsetHeight target.style.overflow = 'hidden' target.style.height = showmore ? `${showmore}px` : '0' target.style.paddingTop = '0' target.style.paddingBottom = '0' target.style.marginTop = '0' target.style.marginBottom = '0' window.setTimeout(() => { target.hidden = !showmore if (!showmore) { target.style.removeProperty('height') target.style.removeProperty('overflow') } target.style.removeProperty('padding-top') target.style.removeProperty('padding-bottom') target.style.removeProperty('margin-top') target.style.removeProperty('margin-bottom') target.style.removeProperty('transition-duration') target.style.removeProperty('transition-property') target.classList.remove('_slide') // Создаем событие document.dispatchEvent( new CustomEvent('slideUpDone', { detail: { target, }, }) ) }, duration) } } export function _slideDown(target: HTMLElement, duration: number = 500, showmore: number = 0): void { if (!target.classList.contains('_slide')) { target.classList.add('_slide') target.hidden = false if (showmore) { target.style.removeProperty('height') } const height = target.offsetHeight target.style.overflow = 'hidden' target.style.height = showmore ? `${showmore}px` : '0' target.style.paddingTop = '0' target.style.paddingBottom = '0' target.style.marginTop = '0' target.style.marginBottom = '0' void target.offsetHeight target.style.transitionProperty = 'height, margin, padding' target.style.transitionDuration = `${duration}ms` target.style.height = `${height}px` target.style.removeProperty('padding-top') target.style.removeProperty('padding-bottom') target.style.removeProperty('margin-top') target.style.removeProperty('margin-bottom') window.setTimeout(() => { target.style.removeProperty('height') target.style.removeProperty('overflow') target.style.removeProperty('transition-duration') target.style.removeProperty('transition-property') target.classList.remove('_slide') // Создаем событие document.dispatchEvent( new CustomEvent('slideDownDone', { detail: { target, }, }) ) }, duration) } } export function _slideToggle(target: HTMLElement, duration: number = 500): void { if (target.hidden) { return _slideDown(target, duration) } else { return _slideUp(target, duration) } } // Вспомогательные модули блокировки прокрутки и скачка ==================================================================================================================================================================================================================================================================================== // eslint-disable-next-line import/no-mutable-exports export let bodyLockStatus = true export function bodyUnlock(delay: number = 500) { const body = document.querySelector('body')! if (bodyLockStatus) { const lockPadding = document.querySelectorAll('[data-lp]') setTimeout(() => { for (let index = 0; index < lockPadding.length; index++) { const el = lockPadding[index] el.style.paddingRight = '0' } body.style.paddingRight = '0' document.documentElement.classList.remove('lock') }, delay) bodyLockStatus = false setTimeout(() => { bodyLockStatus = true }, delay) } } export function bodyLock(delay: number = 500) { const body = document.querySelector('body') if (bodyLockStatus) { const lockPadding = document.querySelectorAll('[data-lp]') for (let index = 0; index < lockPadding.length; index++) { const el = lockPadding[index] el.style.paddingRight = `${window.innerWidth - document.querySelector('.wrapper')!.offsetWidth}px` } body!.style.paddingRight = `${window.innerWidth - document.querySelector('.wrapper')!.offsetWidth}px` document.documentElement.classList.add('lock') bodyLockStatus = false setTimeout(() => { bodyLockStatus = true }, delay) } } export function bodyLockToggle(delay: number = 500) { if (document.documentElement.classList.contains('lock')) { bodyUnlock(delay) } else { bodyLock(delay) } } // Модуль работы со спойлерами ======================================================================================================================================================================================================================= export function spoilers(): void { const spoilersArray = document.querySelectorAll('[data-spoilers]') if (spoilersArray.length > 0) { // Получение обычных спойлеров const spoilersRegular = Array.from(spoilersArray).filter((item) => { return !item.dataset.spoilers?.split(',')[0] }) const setSpoilerAction = (e: Event): void => { const el = e.target as HTMLElement const spoilerTitle = el.closest('[data-spoiler]') as HTMLElement | null if (spoilerTitle) { const spoilersBlock = spoilerTitle.closest('[data-spoilers]') as HTMLElement const oneSpoiler = spoilersBlock.hasAttribute('data-one-spoiler') const spoilerSpeed = spoilersBlock.dataset.spoilersSpeed ? Number.parseInt(spoilersBlock.dataset.spoilersSpeed) : 500 if (!spoilersBlock.querySelectorAll('._slide').length) { if (oneSpoiler && !spoilerTitle.classList.contains('_spoiler-active')) { hideSpoilersBody(spoilersBlock) } spoilerTitle.classList.toggle('_spoiler-active') const nextElement = spoilerTitle.nextElementSibling as HTMLElement if (nextElement) { _slideToggle(nextElement, spoilerSpeed) } } } } // Инициализация const initSpoilers = (spoilersArray: HTMLElement[], matchMedia: MediaQueryList | false = false): void => { spoilersArray.forEach((spoilersBlock) => { if (matchMedia && 'item' in spoilersBlock) { spoilersBlock = (spoilersBlock as unknown as IBreakpoint).item } if ((matchMedia as MediaQueryList)?.matches || !matchMedia) { spoilersBlock.classList.add('_spoiler-init') initSpoilerBody(spoilersBlock) spoilersBlock.addEventListener('click', setSpoilerAction) } else { spoilersBlock.classList.remove('_spoiler-init') initSpoilerBody(spoilersBlock, false) spoilersBlock.removeEventListener('click', setSpoilerAction) } }) } // Работа с контентом const initSpoilerBody = (spoilersBlock: HTMLElement, hideSpoilerBody = true): void => { const spoilerTitlesNodeList = spoilersBlock.querySelectorAll('[data-spoiler]') const spoilerTitles = Array.from(spoilerTitlesNodeList).filter((item) => { return item.closest('[data-spoilers]') === spoilersBlock }) if (spoilerTitles.length) { spoilerTitles.forEach((spoilerTitle) => { if (hideSpoilerBody) { spoilerTitle.removeAttribute('tabindex') if (!spoilerTitle.classList.contains('_spoiler-active')) { const nextElement = spoilerTitle.nextElementSibling as HTMLElement if (nextElement) { nextElement.hidden = true } } } else { spoilerTitle.setAttribute('tabindex', '-1') const nextElement = spoilerTitle.nextElementSibling as HTMLElement if (nextElement) { nextElement.hidden = false } } }) } } // Инициализация обычных спойлеров if (spoilersRegular.length) { initSpoilers(spoilersRegular, false) } // Получение спойлеров с медиа запросами const mdQueriesArray = dataMediaQueries(spoilersArray, 'spoilers') if (mdQueriesArray && mdQueriesArray.length) { mdQueriesArray.forEach((mdQueriesItem) => { mdQueriesItem.matchMedia.addEventListener('change', () => { initSpoilers(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) initSpoilers(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) } const hideSpoilersBody = (spoilersBlock: HTMLElement): void => { const spoilerActiveTitle = spoilersBlock.querySelector('[data-spoiler]._spoiler-active') const spoilerSpeed = spoilersBlock.dataset.spoilersSpeed ? Number.parseInt(spoilersBlock.dataset.spoilersSpeed) : 500 if (spoilerActiveTitle && !spoilersBlock.querySelectorAll('._slide').length) { spoilerActiveTitle.classList.remove('_spoiler-active') const nextElement = spoilerActiveTitle.nextElementSibling as HTMLElement if (nextElement) { _slideUp(nextElement, spoilerSpeed) } } } // Закрытие при клике вне спойлера const spoilersClose = document.querySelectorAll('[data-spoiler-close]') if (spoilersClose.length) { document.addEventListener('click', (e: MouseEvent) => { const el = e.target as HTMLElement if (!el.closest('[data-spoilers]')) { spoilersClose.forEach((spoilerClose) => { const spoilersBlock = spoilerClose.closest('[data-spoilers]') as HTMLElement | null if (spoilersBlock?.classList.contains('_spoiler-init')) { const spoilerSpeed = spoilersBlock.dataset.spoilersSpeed ? Number.parseInt(spoilersBlock.dataset.spoilersSpeed) : 500 spoilerClose.classList.remove('_spoiler-active') const nextElement = spoilerClose.nextElementSibling as HTMLElement if (nextElement) { _slideUp(nextElement, spoilerSpeed) } } }) } }) } } } // Модуль работы с табами ======================================================================================================================================================================================================================= export function tabs(): void { const tabs = document.querySelectorAll('[data-tabs]') let tabsActiveHash: string[] = [] if (tabs.length > 0) { const hash = getHash() if (hash && hash.startsWith('tab-')) { tabsActiveHash = hash.replace('tab-', '').split('-') } Array.from(tabs).forEach((tabsBlock, index) => { if (tabsBlock.getAttribute('data-tabs') === 'hover') { tabsBlock.addEventListener('mouseover', setTabsAction) } else { tabsBlock.addEventListener('click', setTabsAction) } tabsBlock.classList.add('_tab-init') tabsBlock.setAttribute('data-tabs-index', String(index)) initTabs(tabsBlock) }) // Получение спойлеров с медиа запросами const mdQueriesArray = dataMediaQueries(tabs, 'tabs') if (mdQueriesArray && mdQueriesArray.length) { mdQueriesArray.forEach((mdQueriesItem) => { // Событие mdQueriesItem.matchMedia.addEventListener('change', () => { setTitlePosition(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) setTitlePosition(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) } } function setTitlePosition(tabsMediaArray: HTMLElement[], matchMedia: MediaQueryList): void { tabsMediaArray.forEach((tabsMediaItem) => { const tabsTitles = tabsMediaItem.querySelector('[data-tabs-titles]') const tabsTitleItemsNodeList = tabsMediaItem.querySelectorAll('[data-tabs-title]') const tabsContent = tabsMediaItem.querySelector('[data-tabs-body]') const tabsContentItemsNodeList = tabsMediaItem.querySelectorAll('[data-tabs-item]') if (!tabsTitles || !tabsContent) { return } const tabsTitleItems = Array.from(tabsTitleItemsNodeList).filter((item) => { return item.closest('[data-tabs]') === tabsMediaItem }) const tabsContentItems = Array.from(tabsContentItemsNodeList).filter((item) => { return item.closest('[data-tabs]') === tabsMediaItem }) tabsContentItems.forEach((tabsContentItem, index) => { if (matchMedia.matches) { tabsContent.append(tabsTitleItems[index]) tabsContent.append(tabsContentItem) tabsMediaItem.classList.add('_tab-spoiler') } else { tabsTitles.append(tabsTitleItems[index]) tabsMediaItem.classList.remove('_tab-spoiler') } }) }) } // Работа с контентом function initTabs(tabsBlock: HTMLElement): void { const tabsTitlesNodeList = tabsBlock.querySelectorAll('[data-tabs-titles]>*') const tabsContentNodeList = tabsBlock.querySelectorAll('[data-tabs-body]>*') const tabsBlockIndex = tabsBlock.dataset.tabsIndex const tabsActiveHashBlock = tabsActiveHash[0] === tabsBlockIndex if (tabsActiveHashBlock) { const tabsActiveTitle = tabsBlock.querySelector('[data-tabs-titles]>._tab-active') if (tabsActiveTitle) { tabsActiveTitle.classList.remove('_tab-active') } } if (tabsContentNodeList.length) { const tabsContent = Array.from(tabsContentNodeList).filter((item) => { return item.closest('[data-tabs]') === tabsBlock }) const tabsTitles = Array.from(tabsTitlesNodeList).filter((item) => { return item.closest('[data-tabs]') === tabsBlock }) tabsContent.forEach((tabsContentItem, index) => { if (tabsTitles[index]) { tabsTitles[index].setAttribute('data-tabs-title', '') tabsContentItem.setAttribute('data-tabs-item', '') if (tabsActiveHashBlock && index === Number.parseInt(tabsActiveHash[1])) { tabsTitles[index].classList.add('_tab-active') } tabsContentItem.hidden = !tabsTitles[index].classList.contains('_tab-active') } }) } } function setTabsStatus(tabsBlock: HTMLElement): void { const tabsTitles = Array.from(tabsBlock.querySelectorAll('[data-tabs-title]')) const tabsContent = Array.from(tabsBlock.querySelectorAll('[data-tabs-item]')) const tabsBlockIndex = tabsBlock.dataset.tabsIndex function isTabsAnimate(tabsBlock: HTMLElement): number | false { if (tabsBlock.hasAttribute('data-tabs-animate')) { return tabsBlock.dataset.tabsAnimate ? Number.parseInt(tabsBlock.dataset.tabsAnimate) : 500 } return false } const tabsBlockAnimate = isTabsAnimate(tabsBlock) if (tabsContent.length > 0) { const isHash = tabsBlock.hasAttribute('data-tabs-hash') tabsContent.forEach((tabsContentItem, index) => { const tabTitle = tabsTitles[index] if (!tabTitle) { return } if (tabTitle.classList.contains('_tab-active')) { if (tabsBlockAnimate) { _slideDown(tabsContentItem, tabsBlockAnimate) } else { tabsContentItem.hidden = false } if (isHash && !tabsContentItem.closest('.popup')) { setHash(`tab-${tabsBlockIndex}-${index}`) } } else { if (tabsBlockAnimate) { _slideUp(tabsContentItem, tabsBlockAnimate) } else { tabsContentItem.hidden = true } } }) } } function setTabsAction(e: Event): void { const el = e.target as HTMLElement const tabTitle = el.closest('[data-tabs-title]') as HTMLElement | null if (tabTitle) { const tabsBlock = tabTitle.closest('[data-tabs]') as HTMLElement | null if (tabsBlock && !tabTitle.classList.contains('_tab-active') && !tabsBlock.querySelector('._slide')) { const tabActiveTitle = tabsBlock.querySelector('[data-tabs-title]._tab-active') if (tabActiveTitle) { tabActiveTitle.classList.remove('_tab-active') } tabTitle.classList.add('_tab-active') setTabsStatus(tabsBlock) } e.preventDefault() } } } // Обработка медиа запросов из атрибутов export function dataMediaQueries(array: NodeListOf, dataSetValue: string): IMediaQueryResult[] { // Получение объектов с медиа запросами const media = Array.from(array).filter((item) => { const dataset = item.dataset[dataSetValue] return dataset ? dataset.split(',')[0] : false }) // Инициализация объектов с медиа запросами if (media.length) { const breakpointsArray: IBreakpoint[] = [] media.forEach((item) => { const params = item.dataset[dataSetValue] if (!params) { return } const paramsArray = params.split(',') breakpointsArray.push({ value: paramsArray[0], type: paramsArray[1] ? paramsArray[1].trim() : 'max', item, }) }) // Получаем уникальные брейкпоинты const mdQueries = uniqArray( breakpointsArray.map((item) => { return `(${item.type}-width: ${item.value}px),${item.value},${item.type}` }) ) return mdQueries.map((breakpoint): IMediaQueryResult => { const [query, value, type] = breakpoint.split(',') const matchMedia = window.matchMedia(query) const itemsArray = breakpointsArray .filter((item) => { return item.value === value && item.type === type }) .map((item) => { return item.item }) return { itemsArray, matchMedia, } }) } return [] } // Модуль работы с меню (бургер) ======================================================================================================================================================================================================================= export function menuInit(): void { if (document.querySelector('[data-menu-button]')) { document.addEventListener('click', (e: MouseEvent) => { const target = e.target as HTMLElement if (bodyLockStatus && target.closest('[data-menu-button]')) { bodyLockToggle() document.documentElement.classList.toggle('menu-open') } }) } } export function menuOpen() { bodyLock() document.documentElement.classList.add('menu-open') } export function menuClose() { bodyUnlock() document.documentElement.classList.remove('menu-open') } // Модуль "показать еще" ======================================================================================================================================================================================================================= export function showMore(): void { window.addEventListener('load', () => { const showMoreBlocks = document.querySelectorAll('[data-showmore]') let showMoreBlocksRegular: HTMLElement[] = [] let mdQueriesArray: IMediaQueryItem[] | undefined if (showMoreBlocks.length) { // Получение обычных объектов showMoreBlocksRegular = Array.from(showMoreBlocks).filter((item) => { return !item.dataset.showmoreMedia }) // Инициализация обычных объектов if (showMoreBlocksRegular.length) { initItems(showMoreBlocksRegular, false) } document.addEventListener('click', showMoreActions) window.addEventListener('resize', showMoreActions) // Получение объектов с медиа запросами mdQueriesArray = dataMediaQueries(showMoreBlocks, 'showmoreMedia') if (mdQueriesArray && mdQueriesArray.length) { mdQueriesArray.forEach((mdQueriesItem) => { // Событие mdQueriesItem.matchMedia.addEventListener('change', () => { initItems(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) }) initItemsMedia(mdQueriesArray) } } function initItemsMedia(mdQueriesArray: IMediaQueryItem[]): void { mdQueriesArray.forEach((mdQueriesItem) => { initItems(mdQueriesItem.itemsArray, mdQueriesItem.matchMedia) }) } function initItems(showMoreBlocks: HTMLElement[], matchMedia: MediaQueryList | false): void { showMoreBlocks.forEach((showMoreBlock) => { initItem(showMoreBlock, matchMedia) }) } function initItem(showMoreBlock: HTMLElement, matchMedia: MediaQueryList | false = false): void { showMoreBlock = matchMedia && 'item' in showMoreBlock ? (showMoreBlock as unknown as IBreakpoint).item : showMoreBlock if ((matchMedia as MediaQueryList)?.matches || !matchMedia) { const showMoreContent = showMoreBlock.querySelector('[data-showmore-content]') const showMoreButton = showMoreBlock.querySelector('[data-showmore-button]') if (!showMoreContent || !showMoreButton) { return } const hiddenHeight = getHeight(showMoreBlock, showMoreContent) if (hiddenHeight < getOriginalHeight(showMoreContent)) { _slideUp(showMoreContent, 0, hiddenHeight) showMoreButton.hidden = false } else { _slideDown(showMoreContent, 0, hiddenHeight) showMoreButton.hidden = true } } } function getHeight(showMoreBlock: HTMLElement, showMoreContent: HTMLElement) { let hiddenHeight = 0 const showMoreType = showMoreBlock.dataset.showmore ? showMoreBlock.dataset.showmore : 'size' if (showMoreType === 'items') { const showMoreTypeValue = showMoreContent.dataset.showmoreContent ? showMoreContent.dataset.showmoreContent : 3 const showMoreItems = showMoreContent.children for ( let index = 0; index < Number.parseInt(showMoreTypeValue as string) && index < showMoreItems.length; index++ ) { const showMoreItem = showMoreItems[index] hiddenHeight += (showMoreItem as HTMLElement).offsetHeight } } else { const showMoreTypeValue = showMoreContent.dataset.showmoreContent ? showMoreContent.dataset.showmoreContent : 150 hiddenHeight = Number.parseInt(showMoreTypeValue as string) } return hiddenHeight } function getOriginalHeight(showMoreContent: HTMLElement) { let parentHidden: HTMLElement | null = null const hiddenHeight = showMoreContent.offsetHeight showMoreContent.style.removeProperty('height') if (showMoreContent.closest(`[hidden]`)) { parentHidden = showMoreContent.closest(`[hidden]`) as HTMLElement parentHidden.hidden = false } const originalHeight = showMoreContent.offsetHeight if (parentHidden) { parentHidden.hidden = true } showMoreContent.style.height = `${hiddenHeight}px` return originalHeight } function showMoreActions(e: Event): void { const targetEvent = e.target as HTMLElement const targetType = e.type if (targetType === 'click') { if (targetEvent.closest('[data-showmore-button]')) { const showMoreButton = targetEvent.closest('[data-showmore-button]') as HTMLElement const showMoreBlock = showMoreButton.closest('[data-showmore]') as HTMLElement const showMoreContent = showMoreBlock.querySelector('[data-showmore-content]') if (!showMoreContent) { return } const showMoreSpeed = showMoreBlock.dataset.showmoreButton ? Number.parseInt(showMoreBlock.dataset.showmoreButton as string) : 500 const hiddenHeight = getHeight(showMoreBlock, showMoreContent) if (!showMoreContent.classList.contains('_slide')) { showMoreBlock.classList.contains('_showmore-active') ? _slideUp(showMoreContent, showMoreSpeed, hiddenHeight) : _slideDown(showMoreContent, showMoreSpeed, hiddenHeight) showMoreBlock.classList.toggle('_showmore-active') } } } else if (targetType === 'resize') { if (showMoreBlocksRegular.length) { initItems(showMoreBlocksRegular, false) } if (mdQueriesArray && mdQueriesArray.length) { initItemsMedia(mdQueriesArray) } } } }) } // ================================================================================================================================================================================================================================================================================================================ // Прочие полезные функции ================================================================================================================================================================================================================================================================================================================ // ================================================================================================================================================================================================================================================================================================================ // FLS (Full Logging System) export function FLS(message: string): void { setTimeout(() => { if (window.FLS) { // eslint-disable-next-line no-console console.log(message) } }, 0) } // Получить цифры из строки export function getDigFromString(item: string): number { return Number.parseInt(item.replace(/\D/g, '')) } // Форматирование цифр типа 100 000 000 export function getDigFormat(item: number | string): string { return item.toString().replace(/(\d)(?=(?:\d\d\d)+(?:\D|$))/g, '$1 ') } // Убрать класс из всех элементов массива export function removeClasses(array: HTMLElement[], className: string): void { for (let i = 0; i < array.length; i++) { array[i].classList.remove(className) } } // Уникализация массива export function uniqArray(array: T[]): T[] { return array.filter((item, index, self) => { return self.indexOf(item) === index }) } // Функция получения индекса внутри родителя export function indexInParent(parent: Element, element: Element): number { const array = Array.prototype.slice.call(parent.children) return Array.prototype.indexOf.call(array, element) } // Функция проверяет, скрыт ли объект export function isHidden(el: HTMLElement): boolean { return el.offsetParent === null }