793 lines
34 KiB
TypeScript
793 lines
34 KiB
TypeScript
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<HTMLElement>('[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<HTMLBodyElement>('body')!
|
||
if (bodyLockStatus) {
|
||
const lockPadding = document.querySelectorAll<HTMLElement>('[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<HTMLBodyElement>('body')
|
||
if (bodyLockStatus) {
|
||
const lockPadding = document.querySelectorAll<HTMLElement>('[data-lp]')
|
||
for (let index = 0; index < lockPadding.length; index++) {
|
||
const el = lockPadding[index]
|
||
el.style.paddingRight = `${window.innerWidth - document.querySelector<HTMLElement>('.wrapper')!.offsetWidth}px`
|
||
}
|
||
body!.style.paddingRight = `${window.innerWidth - document.querySelector<HTMLElement>('.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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[data-tabs-titles]')
|
||
const tabsTitleItemsNodeList = tabsMediaItem.querySelectorAll<HTMLElement>('[data-tabs-title]')
|
||
const tabsContent = tabsMediaItem.querySelector<HTMLElement>('[data-tabs-body]')
|
||
const tabsContentItemsNodeList = tabsMediaItem.querySelectorAll<HTMLElement>('[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<HTMLElement>('[data-tabs-titles]>*')
|
||
const tabsContentNodeList = tabsBlock.querySelectorAll<HTMLElement>('[data-tabs-body]>*')
|
||
const tabsBlockIndex = tabsBlock.dataset.tabsIndex
|
||
const tabsActiveHashBlock = tabsActiveHash[0] === tabsBlockIndex
|
||
|
||
if (tabsActiveHashBlock) {
|
||
const tabsActiveTitle = tabsBlock.querySelector<HTMLElement>('[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<HTMLElement>('[data-tabs-title]'))
|
||
const tabsContent = Array.from(tabsBlock.querySelectorAll<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>, 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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[data-showmore-content]')
|
||
const showMoreButton = showMoreBlock.querySelector<HTMLElement>('[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<HTMLElement>('[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<T>(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
|
||
}
|