1587 lines
50 KiB
JavaScript
1587 lines
50 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
||
|
||
// Брейкпоинты
|
||
const sizeXL = window.matchMedia('(min-width: 1280px)');
|
||
const sizeLG = window.matchMedia('(min-width: 1024px)');
|
||
const sizeMD = window.matchMedia('(min-width: 768px)');
|
||
const sizeSM = window.matchMedia('(min-width: 576px)');
|
||
const breakpoints = [sizeSM, sizeMD, sizeLG, sizeXL];
|
||
|
||
|
||
// Выпадающее мобильное меню
|
||
const menuBtn = document.querySelector('.header-buttons__menu-btn');
|
||
const headerDropdown = document.querySelector('.header-dropdown');
|
||
if (menuBtn && headerDropdown) {
|
||
menuBtn.addEventListener('click', function () {
|
||
this.classList.toggle('is-active');
|
||
if (this.classList.contains('is-active')) {
|
||
headerDropdown.classList.add('is-active');
|
||
ScrollLock.lock({
|
||
scrollable: ['.header-dropdown'],
|
||
fixed: ['.header-main']
|
||
});
|
||
} else {
|
||
headerDropdown.classList.remove('is-active');
|
||
setTimeout(() => {
|
||
ScrollLock.unlock();
|
||
}, 400);
|
||
}
|
||
});
|
||
sizeLG.addEventListener('change', (e) => {
|
||
if (e.matches && menuBtn?.classList.contains('is-active')) {
|
||
menuBtn.classList.remove('is-active');
|
||
headerDropdown?.classList.remove('is-active');
|
||
ScrollLock.unlock();
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// Открытие подменю в шапке на tablet
|
||
const headerMain = document.querySelector('.header-main');
|
||
if (headerMain) {
|
||
const parentLinks = headerMain.querySelectorAll('.header-menu__list > li.has-children > a');
|
||
parentLinks.forEach((link) => {
|
||
link.addEventListener('click', (event) => {
|
||
const item = link.closest('.header-menu__item.has-children');
|
||
const submenu = item?.querySelector(':scope > .header-menu__submenu');
|
||
if (!item || !submenu) return;
|
||
const isSubmenuVisible = window.getComputedStyle(submenu).visibility === 'visible';
|
||
if (isSubmenuVisible) return;
|
||
event.preventDefault();
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// Подменю-аккордеон для mobile
|
||
function initMenuAccordion(prefix) {
|
||
const toggleSelector = `.${prefix}-menu__submenu-toggle`;
|
||
const itemSelector = `.${prefix}-menu__item.has-children`;
|
||
const submenuSelector = `.${prefix}-menu__submenu`;
|
||
document.querySelectorAll(toggleSelector).forEach(toggle => {
|
||
toggle.addEventListener('click', () => {
|
||
const item = toggle.closest(itemSelector);
|
||
if (!item || item.classList.contains('is-animating')) return;
|
||
const submenu = item.querySelector(`:scope > ${submenuSelector}`);
|
||
if (!submenu) return;
|
||
const isOpen = item.classList.contains('is-open');
|
||
isOpen
|
||
? closeSubmenu(submenu, item, toggle)
|
||
: openSubmenu(submenu, item, toggle);
|
||
});
|
||
});
|
||
}
|
||
function openSubmenu(submenu, item, toggle) {
|
||
item.classList.add('is-animating');
|
||
submenu.style.height = '0px';
|
||
item.classList.add('is-open');
|
||
toggle.setAttribute('aria-expanded', 'true');
|
||
submenu.offsetHeight;
|
||
submenu.style.height = `${submenu.scrollHeight}px`;
|
||
submenu.addEventListener('transitionend', function onTransitionEnd(event) {
|
||
if (event.propertyName !== 'height') return;
|
||
submenu.style.height = 'auto';
|
||
item.classList.remove('is-animating');
|
||
submenu.removeEventListener('transitionend', onTransitionEnd);
|
||
});
|
||
}
|
||
function closeSubmenu(submenu, item, toggle) {
|
||
item.classList.add('is-animating');
|
||
submenu.style.height = `${submenu.scrollHeight}px`;
|
||
submenu.offsetHeight;
|
||
item.classList.remove('is-open');
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
submenu.style.height = '0px';
|
||
submenu.addEventListener('transitionend', function onTransitionEnd(event) {
|
||
if (event.propertyName !== 'height') return;
|
||
submenu.style.removeProperty('height');
|
||
item.classList.remove('is-animating');
|
||
submenu.removeEventListener('transitionend', onTransitionEnd);
|
||
});
|
||
}
|
||
initMenuAccordion('header');
|
||
initMenuAccordion('footer');
|
||
|
||
|
||
// Бегущая строка с текстом
|
||
function initScroller(scroller) {
|
||
const track = scroller.querySelector('.hero-scroller__track');
|
||
const originalGroup = scroller.querySelector('.hero-scroller__group');
|
||
if (!track || !originalGroup) return;
|
||
const speed = Number(scroller.dataset.speed) || 80;
|
||
let offset = 0;
|
||
let lastTime = 0;
|
||
let rafId = null;
|
||
function clearClones() {
|
||
track.querySelectorAll('[data-scroller-clone]').forEach((clone) => {
|
||
clone.remove();
|
||
});
|
||
}
|
||
function fillTrack() {
|
||
clearClones();
|
||
const scrollerWidth = scroller.offsetWidth;
|
||
const groupWidth = originalGroup.offsetWidth;
|
||
if (!scrollerWidth || !groupWidth) return;
|
||
let trackWidth = groupWidth;
|
||
while (trackWidth < scrollerWidth + groupWidth * 2) {
|
||
const clone = originalGroup.cloneNode(true);
|
||
clone.setAttribute('data-scroller-clone', '');
|
||
clone.setAttribute('aria-hidden', 'true');
|
||
track.appendChild(clone);
|
||
trackWidth += groupWidth;
|
||
}
|
||
}
|
||
function animate(time) {
|
||
if (!lastTime) lastTime = time;
|
||
const delta = (time - lastTime) / 1000;
|
||
lastTime = time;
|
||
offset -= speed * delta;
|
||
let firstGroup = track.firstElementChild;
|
||
while (firstGroup && Math.abs(offset) >= firstGroup.offsetWidth) {
|
||
offset += firstGroup.offsetWidth;
|
||
track.appendChild(firstGroup);
|
||
firstGroup = track.firstElementChild;
|
||
}
|
||
track.style.transform = `translate3d(${offset}px, 0, 0)`;
|
||
rafId = requestAnimationFrame(animate);
|
||
}
|
||
function restart() {
|
||
cancelAnimationFrame(rafId);
|
||
offset = 0;
|
||
lastTime = 0;
|
||
track.style.transform = 'translate3d(0, 0, 0)';
|
||
fillTrack();
|
||
rafId = requestAnimationFrame(animate);
|
||
}
|
||
const resizeObserver = new ResizeObserver(restart);
|
||
resizeObserver.observe(scroller);
|
||
if (document.fonts) {
|
||
document.fonts.ready.then(restart);
|
||
} else {
|
||
restart();
|
||
}
|
||
}
|
||
document.querySelectorAll('[data-scroller]').forEach(initScroller);
|
||
|
||
|
||
// Универсальный helper для Swiper
|
||
class ResponsiveSwiper {
|
||
constructor(target, options = {}, breakpoint = null) {
|
||
this.el = typeof target === 'string' ? document.querySelector(target) : target;
|
||
if (!this.el) return;
|
||
this.options = options;
|
||
this.swiper = null;
|
||
this.breakpoint = breakpoint;
|
||
this.wrapper = this.el.querySelector('.swiper-wrapper');
|
||
if (breakpoint) {
|
||
this.handle = this.handle.bind(this);
|
||
this.breakpoint.addEventListener('change', this.handle);
|
||
this.handle(this.breakpoint);
|
||
} else {
|
||
this.init();
|
||
}
|
||
}
|
||
handle(e) {
|
||
e.matches ? this.destroy() : this.init();
|
||
}
|
||
init() {
|
||
if (!this.swiper) {
|
||
this.wrapper?.classList.remove('is-grid');
|
||
this.swiper = new Swiper(this.el, this.options);
|
||
}
|
||
}
|
||
destroy() {
|
||
if (this.swiper) {
|
||
this.swiper.destroy(true, true);
|
||
this.swiper = null;
|
||
this.wrapper?.classList.add('is-grid');
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Остановка autoplay Swiper когда он не виден
|
||
function pauseSwiperAutoplayOnVisibility(swiper, options = {}) {
|
||
const {
|
||
threshold = 0.5,
|
||
root = null,
|
||
rootMargin = '0px'
|
||
} = options;
|
||
if (!swiper || !swiper.el || !swiper.autoplay || !swiper.params.autoplay) return;
|
||
if (!('IntersectionObserver' in window)) return;
|
||
const disableOnInteraction = swiper.params.autoplay.disableOnInteraction === true;
|
||
let stoppedByVisibility = false;
|
||
let stoppedByInteraction = false;
|
||
let isVisibleEnough = true;
|
||
function pauseByVisibility() {
|
||
if (stoppedByInteraction) return;
|
||
if (stoppedByVisibility) return;
|
||
if (!swiper.autoplay.running) return;
|
||
stoppedByVisibility = true;
|
||
swiper.autoplay.stop();
|
||
}
|
||
function resumeByVisibility() {
|
||
if (stoppedByInteraction) return;
|
||
if (!stoppedByVisibility) return;
|
||
if (document.hidden) return;
|
||
swiper.autoplay.start();
|
||
stoppedByVisibility = false;
|
||
}
|
||
function onAutoplayStop() {
|
||
if (stoppedByVisibility) return;
|
||
if (disableOnInteraction) {
|
||
stoppedByInteraction = true;
|
||
}
|
||
}
|
||
function onVisibilityChange() {
|
||
if (document.hidden) {
|
||
pauseByVisibility();
|
||
} else if (isVisibleEnough) {
|
||
resumeByVisibility();
|
||
}
|
||
}
|
||
swiper.on('autoplayStop', onAutoplayStop);
|
||
const observer = new IntersectionObserver((entries) => {
|
||
const entry = entries[0];
|
||
isVisibleEnough = entry.isIntersecting && entry.intersectionRatio >= threshold;
|
||
if (isVisibleEnough) {
|
||
resumeByVisibility();
|
||
} else {
|
||
pauseByVisibility();
|
||
}
|
||
}, {
|
||
root,
|
||
rootMargin,
|
||
threshold: [0, threshold, 1]
|
||
});
|
||
observer.observe(swiper.el);
|
||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||
swiper.on('destroy', () => {
|
||
observer.disconnect();
|
||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||
swiper.off('autoplayStop', onAutoplayStop);
|
||
});
|
||
}
|
||
|
||
|
||
// Слайдер преимуществ
|
||
new ResponsiveSwiper('.advantages-slider__container', {
|
||
loop: false,
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 15,
|
||
breakpoints: {
|
||
576: {
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 20
|
||
},
|
||
768: {
|
||
slidesPerView: 2.25,
|
||
spaceBetween: 20
|
||
}
|
||
}
|
||
}, sizeLG);
|
||
|
||
|
||
// Range-слайдеры в калькуляторе
|
||
document.querySelectorAll('.js-range-field').forEach((field) => {
|
||
const range = field.querySelector('.js-range');
|
||
const numberEl = field.querySelector('.js-range-number');
|
||
if (!range || !numberEl) return;
|
||
const updateRange = () => {
|
||
const min = range.min === '' ? 0 : Number(range.min);
|
||
const max = range.max === '' ? 100 : Number(range.max);
|
||
const value = Number(range.value);
|
||
const progress = max === min
|
||
? 0
|
||
: ((value - min) / (max - min)) * 100;
|
||
range.style.setProperty('--range-progress', `${progress}%`);
|
||
numberEl.textContent = value;
|
||
};
|
||
updateRange();
|
||
range.addEventListener('input', updateRange);
|
||
});
|
||
|
||
|
||
// Калькулятор заработка
|
||
const calc = document.querySelector('.income-calc');
|
||
if (calc) {
|
||
const formatMoney = (value) => {
|
||
return new Intl.NumberFormat('ru-RU').format(value) + ' ₽';
|
||
};
|
||
const rates = window.themeIncomeCalc?.rates || {};
|
||
const updateCalc = () => {
|
||
const formData = new FormData(calc);
|
||
const type = formData.get('work_type');
|
||
const city = formData.get('city');
|
||
const hours = Number(formData.get('hours_per_day'));
|
||
const days = Number(formData.get('days_per_week'));
|
||
const hourIncome = Number(rates?.[type]?.[city]) || 0;
|
||
const dayIncome = hourIncome * hours;
|
||
const weekIncome = dayIncome * days;
|
||
const monthIncome = weekIncome * 4;
|
||
calc.querySelector('.js-income-hour').textContent = formatMoney(hourIncome);
|
||
calc.querySelector('.js-income-day').textContent = formatMoney(dayIncome);
|
||
calc.querySelector('.js-income-week').textContent = formatMoney(weekIncome);
|
||
calc.querySelector('.js-income-month').textContent = formatMoney(monthIncome);
|
||
};
|
||
calc.addEventListener('input', updateCalc);
|
||
updateCalc();
|
||
const footnoteTexts = {
|
||
taxi: 'класса авто, района, активности\u00a0водителя',
|
||
cargo: 'класса авто, района, активности\u00a0водителя',
|
||
courier: 'района, активности\u00a0курьера'
|
||
};
|
||
document.querySelectorAll('.js-calc-type').forEach((radio) => {
|
||
radio.addEventListener('change', function () {
|
||
document.querySelector('.js-footnote-text').textContent = footnoteTexts[this.value];
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// Настройки Fancybox
|
||
const defaults = Fancybox.getDefaults();
|
||
const carouselDefaults = defaults.Carousel || {};
|
||
Object.assign(defaults, {
|
||
idle: false,
|
||
Hash: false,
|
||
placeFocusBack: false,
|
||
zoomEffect: false,
|
||
Carousel: {
|
||
...carouselDefaults,
|
||
Thumbs: {
|
||
...(carouselDefaults.Thumbs || {}),
|
||
type: 'classic',
|
||
showOnStart: false
|
||
}
|
||
},
|
||
l10n: {
|
||
...(Fancybox.l10n?.en_EN || {}),
|
||
CLOSE: 'Закрыть',
|
||
NEXT: 'Вперед',
|
||
PREV: 'Назад',
|
||
MODAL: 'Вы можете закрыть окно нажатием ESC',
|
||
ERROR: 'Ошибка, попробуйте повторить попытку',
|
||
IMAGE_ERROR: 'Изображение не найдено',
|
||
ELEMENT_NOT_FOUND: 'HTML элемент не найден',
|
||
AJAX_NOT_FOUND: 'Ошибка загрузки AJAX: Не найдено',
|
||
AJAX_FORBIDDEN: 'Ошибка загрузки AJAX: Запрещено',
|
||
IFRAME_ERROR: 'Ошибка при загрузке фрейма',
|
||
ITERATEZOOM: 'Увеличить',
|
||
TOGGLE_ZOOM: 'Увеличить',
|
||
TOGGLE_THUMBS: 'Предпросмотр',
|
||
TOGGLE_SLIDESHOW: 'Слайдшоу',
|
||
TOGGLE_FULLSCREEN: 'Полный экран',
|
||
DOWNLOAD: 'Загрузить'
|
||
},
|
||
on: {
|
||
destroy: () => {
|
||
const interceptFocus = (e) => {
|
||
e.target.blur();
|
||
document.removeEventListener('focusin', interceptFocus);
|
||
};
|
||
document.addEventListener('focusin', interceptFocus);
|
||
setTimeout(() => {
|
||
document.removeEventListener('focusin', interceptFocus);
|
||
}, 50);
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
// Сертификаты на странице О компании
|
||
Fancybox.bind('[data-fancybox="certs"]');
|
||
|
||
|
||
// Слайдер отзывов
|
||
new ResponsiveSwiper('.reviews-slider__container', {
|
||
loop: false,
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 15,
|
||
breakpoints: {
|
||
576: {
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 20
|
||
},
|
||
768: {
|
||
slidesPerView: 2.25,
|
||
spaceBetween: 20
|
||
},
|
||
1024: {
|
||
slidesPerView: 3,
|
||
spaceBetween: 20
|
||
},
|
||
1280: {
|
||
slidesPerView: 'auto',
|
||
spaceBetween: 20
|
||
}
|
||
},
|
||
pagination: {
|
||
el: '.reviews-slider__pagination',
|
||
bulletClass: 'reviews-slider__pagination-item',
|
||
bulletActiveClass: 'is-active'
|
||
}
|
||
});
|
||
|
||
|
||
// Аккордеон
|
||
function initAccordion(options) {
|
||
const {
|
||
rootSelector = document,
|
||
triggerSelector,
|
||
itemSelector,
|
||
panelSelector,
|
||
activeClass = 'is-active',
|
||
animatingClass = 'is-animating',
|
||
closeSiblings = false
|
||
} = options;
|
||
const root = typeof rootSelector === 'string'
|
||
? document.querySelector(rootSelector)
|
||
: rootSelector;
|
||
if (!root) return;
|
||
function clearTransition(panel) {
|
||
if (panel._accordionTransitionHandler) {
|
||
panel.removeEventListener('transitionend', panel._accordionTransitionHandler);
|
||
panel._accordionTransitionHandler = null;
|
||
}
|
||
}
|
||
function openPanel(panel, item, trigger) {
|
||
clearTransition(panel);
|
||
item.classList.add(animatingClass);
|
||
panel.style.height = '0px';
|
||
item.classList.add(activeClass);
|
||
trigger?.setAttribute('aria-expanded', 'true');
|
||
panel.offsetHeight;
|
||
panel.style.height = `${panel.scrollHeight}px`;
|
||
panel._accordionTransitionHandler = function (event) {
|
||
if (event.propertyName !== 'height') return;
|
||
clearTransition(panel);
|
||
if (item.classList.contains(activeClass)) {
|
||
panel.style.height = 'auto';
|
||
}
|
||
item.classList.remove(animatingClass);
|
||
};
|
||
panel.addEventListener('transitionend', panel._accordionTransitionHandler);
|
||
}
|
||
function closePanel(panel, item, trigger) {
|
||
clearTransition(panel);
|
||
item.classList.add(animatingClass);
|
||
panel.style.height = `${panel.scrollHeight}px`;
|
||
panel.offsetHeight;
|
||
item.classList.remove(activeClass);
|
||
trigger?.setAttribute('aria-expanded', 'false');
|
||
panel.style.height = '0px';
|
||
panel._accordionTransitionHandler = function (event) {
|
||
if (event.propertyName !== 'height') return;
|
||
clearTransition(panel);
|
||
if (!item.classList.contains(activeClass)) {
|
||
panel.style.removeProperty('height');
|
||
}
|
||
item.classList.remove(animatingClass);
|
||
};
|
||
panel.addEventListener('transitionend', panel._accordionTransitionHandler);
|
||
}
|
||
function closeSiblingItems(currentItem) {
|
||
const parent = currentItem.parentElement;
|
||
if (!parent) return;
|
||
const siblings = parent.querySelectorAll(itemSelector);
|
||
siblings.forEach((item) => {
|
||
if (item === currentItem) return;
|
||
if (!item.classList.contains(activeClass)) return;
|
||
if (item.classList.contains(animatingClass)) return;
|
||
const panel = item.querySelector(`:scope > ${panelSelector}`);
|
||
const trigger = item.querySelector(`:scope ${triggerSelector}`);
|
||
if (!panel) return;
|
||
closePanel(panel, item, trigger);
|
||
});
|
||
}
|
||
root.addEventListener('click', (event) => {
|
||
const trigger = event.target.closest(triggerSelector);
|
||
if (!trigger) return;
|
||
if (root !== document && !root.contains(trigger)) return;
|
||
const item = trigger.closest(itemSelector);
|
||
if (!item || item.classList.contains(animatingClass)) return;
|
||
const panel = item.querySelector(`:scope > ${panelSelector}`);
|
||
if (!panel) return;
|
||
const isOpen = item.classList.contains(activeClass);
|
||
if (isOpen) {
|
||
closePanel(panel, item, trigger);
|
||
} else {
|
||
if (closeSiblings) {
|
||
closeSiblingItems(item);
|
||
}
|
||
openPanel(panel, item, trigger);
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// FAQ
|
||
initAccordion({
|
||
rootSelector: '.faq-list',
|
||
itemSelector: '.faq-item',
|
||
triggerSelector: '.faq-item__question',
|
||
panelSelector: '.faq-item__answer'
|
||
});
|
||
|
||
|
||
// Слайдер статей
|
||
new ResponsiveSwiper('.articles-slider__container', {
|
||
loop: false,
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 15,
|
||
breakpoints: {
|
||
576: {
|
||
slidesPerView: 1.25,
|
||
spaceBetween: 20
|
||
},
|
||
768: {
|
||
slidesPerView: 2.25,
|
||
spaceBetween: 20
|
||
}
|
||
}
|
||
}, sizeLG);
|
||
|
||
|
||
// Слайдер блога
|
||
const blogSlider = new ResponsiveSwiper('.blog-slider__container', {
|
||
loop: true,
|
||
autoHeight: true,
|
||
autoplay: {
|
||
pauseOnMouseEnter: true,
|
||
disableOnInteraction: true
|
||
},
|
||
effect: 'fade',
|
||
fadeEffect: {
|
||
crossFade: true,
|
||
},
|
||
pagination: {
|
||
el: '.blog-slider__pagination',
|
||
type: 'bullets',
|
||
bulletElement: 'button',
|
||
horizontalClass: 'swiper-pagination',
|
||
bulletClass: 'swiper-pagination-item',
|
||
bulletActiveClass: 'is-active',
|
||
clickable: true
|
||
}
|
||
});
|
||
pauseSwiperAutoplayOnVisibility(blogSlider.swiper, { threshold: 0.5 });
|
||
|
||
|
||
// Плавная прокрутка якорей
|
||
document.addEventListener('click', (e) => {
|
||
const link = e.target.closest('.article-toc__link, .hero-buttons__info-btn, .vacancies-item__respond-btn');
|
||
if (!link) return;
|
||
const href = link.getAttribute('href');
|
||
if (!href) return;
|
||
let url;
|
||
try {
|
||
url = new URL(href, window.location.href);
|
||
} catch {
|
||
return;
|
||
}
|
||
if (
|
||
url.origin !== window.location.origin ||
|
||
url.pathname !== window.location.pathname ||
|
||
!url.hash
|
||
) {
|
||
return;
|
||
}
|
||
const targetId = decodeURIComponent(url.hash.slice(1));
|
||
const target = document.getElementById(targetId);
|
||
if (!target) return;
|
||
e.preventDefault();
|
||
const header = document.querySelector('.header-main');
|
||
const headerRect = header ? header.getBoundingClientRect() : null;
|
||
const headerBottom = headerRect ? headerRect.bottom : 0;
|
||
const targetTop = target.getBoundingClientRect().top + window.scrollY - headerBottom - 20;
|
||
window.scrollTo({ top: targetTop, behavior: 'smooth' });
|
||
});
|
||
|
||
|
||
// AJAX-фильтр статей
|
||
function initArticlesFilter() {
|
||
const filter = document.querySelector('.articles-filter');
|
||
const grid = document.querySelector('.articles-grid');
|
||
const showMoreContainer = document.querySelector('.articles-showmore');
|
||
const showMoreBtn = showMoreContainer?.querySelector('.articles-showmore__btn');
|
||
if (!filter || !grid || !window.themeAjax?.nonces?.articles) return;
|
||
let currentRequest = null;
|
||
const getCategoryFromUrl = (url) => {
|
||
const parsedUrl = new URL(url, window.location.origin);
|
||
return parsedUrl.searchParams.get('category') || '';
|
||
};
|
||
const toggleShowMoreButton = () => {
|
||
if (!showMoreContainer) return;
|
||
const currentPage = parseInt(grid.dataset.page || 1, 10);
|
||
const maxPages = parseInt(grid.dataset.maxPages || 1, 10);
|
||
if (currentPage >= maxPages || maxPages === 0) {
|
||
showMoreContainer.classList.add('is-hidden');
|
||
} else {
|
||
showMoreContainer.classList.remove('is-hidden');
|
||
}
|
||
};
|
||
const setLoading = (isLoading, isAppend) => {
|
||
if (!isAppend) {
|
||
grid.classList.toggle('is-loading', isLoading);
|
||
filter.classList.toggle('is-loading', isLoading);
|
||
if (showMoreContainer) {
|
||
showMoreContainer.classList.toggle('is-loading', isLoading);
|
||
}
|
||
}
|
||
if (showMoreBtn) {
|
||
showMoreBtn.disabled = isLoading;
|
||
if (isAppend && isLoading) {
|
||
showMoreBtn.textContent = 'Загрузка...';
|
||
} else {
|
||
showMoreBtn.textContent = 'Показать еще';
|
||
}
|
||
}
|
||
};
|
||
const setActiveByCategory = (category) => {
|
||
const items = filter.querySelectorAll('.articles-filter__item');
|
||
const links = filter.querySelectorAll('.articles-filter__link');
|
||
items.forEach((item) => item.classList.remove('is-active'));
|
||
links.forEach((link) => {
|
||
const linkCategory = getCategoryFromUrl(link.href);
|
||
if (linkCategory === category) {
|
||
link.closest('.articles-filter__item')?.classList.add('is-active');
|
||
}
|
||
});
|
||
};
|
||
const loadArticles = async (category = '', page = 1, append = false) => {
|
||
if (currentRequest) {
|
||
currentRequest.abort();
|
||
}
|
||
currentRequest = new AbortController();
|
||
const formData = new FormData();
|
||
formData.append('action', 'theme_filter_articles');
|
||
formData.append('section', grid.dataset.section || '');
|
||
formData.append('nonce', window.themeAjax.nonces.articles);
|
||
formData.append('category', category);
|
||
formData.append('page', page);
|
||
setLoading(true, append);
|
||
try {
|
||
const response = await fetch(window.themeAjax.ajaxUrl, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'same-origin',
|
||
signal: currentRequest.signal,
|
||
});
|
||
if (!response.ok) throw new Error('Network response was not ok');
|
||
const result = await response.json();
|
||
if (!result.success) throw new Error('Ajax request failed');
|
||
if (append) {
|
||
grid.insertAdjacentHTML('beforeend', result.data.html);
|
||
} else {
|
||
grid.innerHTML = result.data.html;
|
||
}
|
||
grid.dataset.page = String(result.data.page);
|
||
grid.dataset.maxPages = String(result.data.max_pages);
|
||
grid.dataset.category = result.data.category;
|
||
toggleShowMoreButton();
|
||
return result.data;
|
||
} catch (error) {
|
||
if (error.name !== 'AbortError') {
|
||
console.error(error);
|
||
}
|
||
return null;
|
||
} finally {
|
||
setLoading(false, append);
|
||
currentRequest = null;
|
||
}
|
||
};
|
||
filter.addEventListener('click', async (event) => {
|
||
const link = event.target.closest('.articles-filter__link');
|
||
if (!link) return;
|
||
event.preventDefault();
|
||
const nextUrl = new URL(link.href, window.location.origin);
|
||
const category = getCategoryFromUrl(nextUrl.href);
|
||
setActiveByCategory(category);
|
||
grid.dataset.page = '1';
|
||
const result = await loadArticles(category, 1, false);
|
||
if (!result) return;
|
||
if (window.location.href !== nextUrl.href) {
|
||
window.history.pushState({ articleCategory: category }, '', nextUrl.href);
|
||
}
|
||
});
|
||
if (showMoreBtn) {
|
||
showMoreBtn.addEventListener('click', async () => {
|
||
const nextPage = parseInt(grid.dataset.page || 1, 10) + 1;
|
||
grid.dataset.page = String(nextPage);
|
||
const currentCategory = grid.dataset.category || '';
|
||
|
||
await loadArticles(currentCategory, nextPage, true);
|
||
});
|
||
}
|
||
window.addEventListener('popstate', async () => {
|
||
const category = getCategoryFromUrl(window.location.href);
|
||
setActiveByCategory(category);
|
||
grid.dataset.page = '1';
|
||
await loadArticles(category, 1, false);
|
||
});
|
||
toggleShowMoreButton();
|
||
}
|
||
initArticlesFilter();
|
||
|
||
|
||
// Анимация статистики
|
||
const statsBlock = document.querySelector('.about-stats');
|
||
const statItems = document.querySelectorAll('.about-stats__item-value');
|
||
const easeOutExpo = (x) => {
|
||
return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
|
||
};
|
||
const animateValue = (el) => {
|
||
const originalHTML = el.innerHTML.trim();
|
||
const match = originalHTML.match(/^([\d\s,\xA0]+)(.*)$/i);
|
||
if (!match) return;
|
||
let numberStr = match[1];
|
||
let suffix = match[2];
|
||
const trailingSpaceMatch = numberStr.match(/([\s\xA0]+)$/);
|
||
if (trailingSpaceMatch) {
|
||
suffix = trailingSpaceMatch[1] + suffix;
|
||
numberStr = numberStr.replace(/([\s\xA0]+)$/, '');
|
||
}
|
||
const isFloat = numberStr.includes(',');
|
||
const cleanNumber = parseFloat(numberStr.replace(/[\s\xA0]/g, '').replace(',', '.'));
|
||
if (isNaN(cleanNumber)) return;
|
||
const duration = 2000;
|
||
let startTimestamp = null;
|
||
const step = (timestamp) => {
|
||
if (!startTimestamp) startTimestamp = timestamp;
|
||
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
||
const easeProgress = easeOutExpo(progress);
|
||
let currentValue = cleanNumber * easeProgress;
|
||
let displayValue = '';
|
||
if (isFloat) {
|
||
displayValue = currentValue.toFixed(1).replace('.', ',');
|
||
} else {
|
||
displayValue = Math.round(currentValue).toLocaleString('ru-RU');
|
||
}
|
||
el.innerHTML = displayValue + suffix;
|
||
if (progress < 1) {
|
||
window.requestAnimationFrame(step);
|
||
} else {
|
||
el.innerHTML = originalHTML;
|
||
}
|
||
};
|
||
window.requestAnimationFrame(step);
|
||
};
|
||
const observer = new IntersectionObserver((entries, obs) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
statItems.forEach(el => animateValue(el));
|
||
obs.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, {
|
||
threshold: 0.3
|
||
});
|
||
if (statsBlock) {
|
||
observer.observe(statsBlock);
|
||
}
|
||
|
||
|
||
// Слайдер функций
|
||
new ResponsiveSwiper('.features-slider__container', {
|
||
loop: false,
|
||
slidesPerView: 1.5,
|
||
spaceBetween: 15,
|
||
breakpoints: {
|
||
576: {
|
||
slidesPerView: 1.5,
|
||
spaceBetween: 20
|
||
},
|
||
768: {
|
||
slidesPerView: 2.5,
|
||
spaceBetween: 20
|
||
}
|
||
}
|
||
}, sizeLG);
|
||
|
||
|
||
// Вакансии
|
||
initAccordion({
|
||
rootSelector: '.vacancies-list',
|
||
itemSelector: '.vacancies-item',
|
||
triggerSelector: '.vacancies-item__header',
|
||
panelSelector: '.vacancies-item__body',
|
||
// closeSiblings: true
|
||
});
|
||
|
||
|
||
// AJAX-фильтр вакансий
|
||
function initVacanciesFilter() {
|
||
const filter = document.querySelector('.vacancies-filter');
|
||
const trigger = filter?.querySelector('.vacancies-filter__trigger');
|
||
const valueEl = filter?.querySelector('.vacancies-filter__value');
|
||
const optionsList = filter?.querySelector('.vacancies-filter__options');
|
||
const links = optionsList?.querySelectorAll('.vacancies-filter__link');
|
||
const items = optionsList?.querySelectorAll('.vacancies-filter__option');
|
||
const list = document.querySelector('.vacancies-list');
|
||
const count = document.querySelector('.vacancies-count');
|
||
const showMoreContainer = document.querySelector('.vacancies-showmore');
|
||
const showMoreBtn = showMoreContainer?.querySelector('.vacancies-showmore__btn');
|
||
if (!filter || !trigger || !optionsList || !list || !window.themeAjax?.nonces?.vacancies) return;
|
||
let currentRequest = null;
|
||
let activeIndex = Array.from(items).findIndex(i => i.classList.contains('is-selected'));
|
||
const open = () => {
|
||
filter.classList.add('is-open');
|
||
trigger.setAttribute('aria-expanded', 'true');
|
||
const targetLink = links[activeIndex >= 0 ? activeIndex : 0];
|
||
if (targetLink) targetLink.focus();
|
||
};
|
||
const close = (returnFocus = false) => {
|
||
filter.classList.remove('is-open');
|
||
trigger.setAttribute('aria-expanded', 'false');
|
||
if (returnFocus) trigger.focus();
|
||
};
|
||
const getCategoryFromUrl = (url) => {
|
||
return new URL(url, window.location.origin).searchParams.get('category') || '';
|
||
};
|
||
const toggleShowMoreButton = () => {
|
||
if (!showMoreContainer) return;
|
||
const currentPage = parseInt(list.dataset.page || 1, 10);
|
||
const maxPages = parseInt(list.dataset.maxPages || 1, 10);
|
||
if (currentPage >= maxPages || maxPages === 0) {
|
||
showMoreContainer.classList.add('is-hidden');
|
||
} else {
|
||
showMoreContainer.classList.remove('is-hidden');
|
||
}
|
||
};
|
||
const setLoading = (isLoading, isAppend) => {
|
||
if (!isAppend) {
|
||
list.classList.toggle('is-loading', isLoading);
|
||
filter.classList.toggle('is-loading', isLoading);
|
||
if (showMoreContainer) {
|
||
showMoreContainer.classList.toggle('is-loading', isLoading);
|
||
}
|
||
}
|
||
if (showMoreBtn) {
|
||
showMoreBtn.disabled = isLoading;
|
||
if (isAppend && isLoading) {
|
||
showMoreBtn.textContent = 'Загрузка...';
|
||
} else {
|
||
showMoreBtn.textContent = 'Показать еще';
|
||
}
|
||
}
|
||
};
|
||
const loadVacancies = async (category = '', append = false) => {
|
||
if (currentRequest) currentRequest.abort();
|
||
currentRequest = new AbortController();
|
||
const formData = new FormData();
|
||
formData.append('action', 'theme_filter_vacancies');
|
||
formData.append('nonce', window.themeAjax.nonces.vacancies);
|
||
formData.append('category', category);
|
||
formData.append('page', list.dataset.page || 1);
|
||
setLoading(true, append);
|
||
try {
|
||
const response = await fetch(window.themeAjax.ajaxUrl, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'same-origin',
|
||
signal: currentRequest.signal,
|
||
});
|
||
if (!response.ok) throw new Error('Network response was not ok');
|
||
const result = await response.json();
|
||
if (!result.success) throw new Error('Ajax request failed');
|
||
if (append) {
|
||
list.insertAdjacentHTML('beforeend', result.data.html);
|
||
} else {
|
||
list.innerHTML = result.data.html;
|
||
}
|
||
list.dataset.page = String(result.data.page);
|
||
list.dataset.maxPages = String(result.data.max_pages);
|
||
list.dataset.category = result.data.category;
|
||
if (count) count.textContent = String(result.data.count);
|
||
toggleShowMoreButton();
|
||
return result.data;
|
||
} catch (error) {
|
||
if (error.name !== 'AbortError') console.error(error);
|
||
return null;
|
||
} finally {
|
||
setLoading(false, append);
|
||
currentRequest = null;
|
||
}
|
||
};
|
||
const selectOption = async (link) => {
|
||
const li = link.parentElement;
|
||
const category = getCategoryFromUrl(link.href);
|
||
const nextUrl = link.href;
|
||
items.forEach(i => i.classList.remove('is-selected'));
|
||
li.classList.add('is-selected');
|
||
valueEl.textContent = link.textContent;
|
||
activeIndex = Array.from(items).indexOf(li);
|
||
close(true);
|
||
list.dataset.page = '1';
|
||
const result = await loadVacancies(category, false);
|
||
if (!result) return;
|
||
if (window.location.href !== nextUrl) {
|
||
window.history.pushState({ vacancyCategory: category }, '', nextUrl);
|
||
}
|
||
};
|
||
trigger.addEventListener('click', () => {
|
||
filter.classList.contains('is-open') ? close(false) : open();
|
||
});
|
||
trigger.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
filter.classList.contains('is-open') ? close(true) : open();
|
||
} else if (e.key === 'Escape') {
|
||
close(true);
|
||
} else if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
open();
|
||
}
|
||
});
|
||
links.forEach((link, idx) => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
selectOption(link);
|
||
});
|
||
link.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
selectOption(link);
|
||
} else if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
links[(idx + 1) % links.length].focus();
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
links[(idx - 1 + links.length) % links.length].focus();
|
||
} else if (e.key === 'Escape') {
|
||
close(true);
|
||
}
|
||
});
|
||
});
|
||
document.addEventListener('click', e => {
|
||
if (filter.classList.contains('is-open') && !filter.contains(e.target)) {
|
||
close(false);
|
||
}
|
||
});
|
||
if (showMoreBtn) {
|
||
showMoreBtn.addEventListener('click', async () => {
|
||
const nextPage = parseInt(list.dataset.page || 1, 10) + 1;
|
||
list.dataset.page = String(nextPage);
|
||
const currentCategory = list.dataset.category || '';
|
||
await loadVacancies(currentCategory, true);
|
||
});
|
||
}
|
||
window.addEventListener('popstate', async () => {
|
||
const category = getCategoryFromUrl(window.location.href);
|
||
const matchedLink = Array.from(links).find(l => getCategoryFromUrl(l.href) === category);
|
||
if (matchedLink) {
|
||
const matchedLi = matchedLink.parentElement;
|
||
items.forEach(i => i.classList.remove('is-selected'));
|
||
matchedLi.classList.add('is-selected');
|
||
valueEl.textContent = matchedLink.textContent;
|
||
activeIndex = Array.from(items).indexOf(matchedLi);
|
||
}
|
||
list.dataset.page = '1';
|
||
await loadVacancies(category, false);
|
||
});
|
||
toggleShowMoreButton();
|
||
}
|
||
initVacanciesFilter();
|
||
|
||
|
||
// Кастомный file input
|
||
function updateFileField(input) {
|
||
const field = input.closest('.file-field');
|
||
const text = field?.querySelector('.file-field__text');
|
||
if (!field || !text) return;
|
||
const placeholder = text.dataset.placeholder || 'Прикрепить документ';
|
||
const files = Array.from(input.files || []);
|
||
if (!files.length) {
|
||
text.textContent = placeholder;
|
||
field.classList.remove('is-filled');
|
||
return;
|
||
}
|
||
field.classList.add('is-filled');
|
||
if (files.length === 1) {
|
||
text.textContent = files[0].name;
|
||
} else {
|
||
text.textContent = `Выбрано файлов: ${files.length}`;
|
||
}
|
||
}
|
||
function resetFileFields(form) {
|
||
form.querySelectorAll('.file-field__input').forEach((input) => {
|
||
input.value = '';
|
||
updateFileField(input);
|
||
});
|
||
}
|
||
function initFileFields() {
|
||
document.querySelectorAll('.file-field__input').forEach((input) => {
|
||
updateFileField(input);
|
||
|
||
input.addEventListener('change', () => {
|
||
updateFileField(input);
|
||
});
|
||
});
|
||
}
|
||
initFileFields();
|
||
|
||
|
||
// Маска для поля "email или телефон"
|
||
function initContactInputs() {
|
||
const inputs = document.querySelectorAll('[data-masked-input]');
|
||
if (!inputs.length) return;
|
||
function isValidEmail(value) {
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
}
|
||
function getDigits(value) {
|
||
return value.replace(/\D/g, '');
|
||
}
|
||
function hasEmailChars(value) {
|
||
return /[a-zа-яё@]/i.test(value);
|
||
}
|
||
function hasOnlyPhoneChars(value) {
|
||
return /^[\d\s()+.-]+$/.test(value);
|
||
}
|
||
function isDeleteInputType(inputType) {
|
||
return inputType && inputType.startsWith('delete');
|
||
}
|
||
function getInputType(input, event) {
|
||
return event?.inputType || input._contactBefore?.inputType || '';
|
||
}
|
||
function isDeleteEvent(input, event) {
|
||
return isDeleteInputType(getInputType(input, event));
|
||
}
|
||
function normalizeRuPhoneDigits(value) {
|
||
const raw = value.trim();
|
||
let digits = getDigits(value);
|
||
if (!digits) {
|
||
return raw.startsWith('+') ? '7' : '';
|
||
}
|
||
if (digits[0] === '8') {
|
||
digits = `7${digits.slice(1)}`;
|
||
} else if (digits[0] === '9') {
|
||
digits = `7${digits}`;
|
||
} else if (digits[0] !== '7') {
|
||
digits = `7${digits}`;
|
||
}
|
||
return digits.slice(0, 11);
|
||
}
|
||
function formatRuPhone(value) {
|
||
const digits = normalizeRuPhoneDigits(value);
|
||
const rest = digits.slice(1);
|
||
if (!digits) return '';
|
||
let result = '+7';
|
||
if (rest.length > 0) result += ` (${rest.slice(0, 3)}`;
|
||
if (rest.length >= 3) result += ')';
|
||
if (rest.length > 3) result += ` ${rest.slice(3, 6)}`;
|
||
if (rest.length > 6) result += `-${rest.slice(6, 8)}`;
|
||
if (rest.length > 8) result += `-${rest.slice(8, 10)}`;
|
||
return result;
|
||
}
|
||
function isPhoneOnlyInput(input) {
|
||
return input.type === 'tel' || input.dataset.contactMode === 'phone';
|
||
}
|
||
function shouldUsePhoneMode(input, value) {
|
||
const raw = value.trim();
|
||
if (isPhoneOnlyInput(input)) return true;
|
||
if (!raw) return false;
|
||
if (hasEmailChars(raw)) return false;
|
||
if (!hasOnlyPhoneChars(raw)) return false;
|
||
if (raw.startsWith('+')) return true;
|
||
const digits = getDigits(raw);
|
||
const threshold = Number(input.dataset.phoneThreshold || 4);
|
||
return digits.length >= threshold;
|
||
}
|
||
function shouldClearPhoneOnDelete(input, value, event) {
|
||
if (!isDeleteEvent(input, event)) return false;
|
||
const raw = value.trim();
|
||
const digits = getDigits(raw);
|
||
if (!raw) return false;
|
||
if (!hasOnlyPhoneChars(raw)) return false;
|
||
return digits.length <= 1;
|
||
}
|
||
function removeDigitNearCaret(value, caretPosition, inputType) {
|
||
if (inputType === 'deleteContentForward') {
|
||
for (let i = caretPosition; i < value.length; i++) {
|
||
if (/\d/.test(value[i])) {
|
||
return value.slice(0, i) + value.slice(i + 1);
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
for (let i = caretPosition - 1; i >= 0; i--) {
|
||
if (/\d/.test(value[i])) {
|
||
return value.slice(0, i) + value.slice(i + 1);
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
function fixPhoneDeleteFormatting(input, value, event) {
|
||
const before = input._contactBefore;
|
||
const inputType = getInputType(input, event);
|
||
if (!before) return value;
|
||
if (!isDeleteInputType(inputType)) return value;
|
||
if (before.start !== before.end) return value;
|
||
const wasPhoneMode = shouldUsePhoneMode(input, before.value);
|
||
const isPhoneMode = shouldUsePhoneMode(input, value);
|
||
if (!wasPhoneMode && !isPhoneMode) return value;
|
||
const formattedCurrentValue = formatRuPhone(value);
|
||
if (formattedCurrentValue !== before.value) return value;
|
||
const beforeDigitsCount = getDigits(before.value).length;
|
||
const currentDigitsCount = getDigits(value).length;
|
||
if (currentDigitsCount < beforeDigitsCount) return value;
|
||
return removeDigitNearCaret(before.value, before.start, inputType);
|
||
}
|
||
function validateContactInput(input) {
|
||
const value = input.value.trim();
|
||
if (!value) {
|
||
input.setCustomValidity('');
|
||
return;
|
||
}
|
||
if (shouldUsePhoneMode(input, value)) {
|
||
const digits = normalizeRuPhoneDigits(value);
|
||
if (digits.length !== 11) {
|
||
input.setCustomValidity('Введите телефон полностью');
|
||
return;
|
||
}
|
||
input.setCustomValidity('');
|
||
return;
|
||
}
|
||
if (!isPhoneOnlyInput(input) && !isValidEmail(value)) {
|
||
input.setCustomValidity('Введите корректный email или телефон');
|
||
return;
|
||
}
|
||
input.setCustomValidity('');
|
||
}
|
||
function handleInput(input, event) {
|
||
let value = input.value;
|
||
if (!value.trim()) {
|
||
if (!isPhoneOnlyInput(input)) {
|
||
input.inputMode = 'email';
|
||
}
|
||
input.setCustomValidity('');
|
||
return;
|
||
}
|
||
const wasPhoneMode = input._contactBefore && shouldUsePhoneMode(input, input._contactBefore.value);
|
||
const isPhoneMode = shouldUsePhoneMode(input, value);
|
||
if (isPhoneMode || wasPhoneMode) {
|
||
value = fixPhoneDeleteFormatting(input, value, event);
|
||
if (shouldClearPhoneOnDelete(input, value, event)) {
|
||
input.value = '';
|
||
if (!isPhoneOnlyInput(input)) {
|
||
input.inputMode = 'email';
|
||
}
|
||
input.setCustomValidity('');
|
||
return;
|
||
}
|
||
if (shouldUsePhoneMode(input, value) || wasPhoneMode || isPhoneOnlyInput(input)) {
|
||
input.inputMode = 'tel';
|
||
input.value = formatRuPhone(value);
|
||
input.setCustomValidity('');
|
||
return;
|
||
}
|
||
}
|
||
if (!isPhoneOnlyInput(input)) {
|
||
input.inputMode = 'email';
|
||
}
|
||
input.setCustomValidity('');
|
||
}
|
||
inputs.forEach(input => {
|
||
function rememberBeforeState(inputType = '') {
|
||
input._contactBefore = {
|
||
value: input.value,
|
||
start: input.selectionStart,
|
||
end: input.selectionEnd,
|
||
inputType
|
||
};
|
||
}
|
||
input.addEventListener('keydown', event => {
|
||
if (event.key === 'Backspace') rememberBeforeState('deleteContentBackward');
|
||
if (event.key === 'Delete') rememberBeforeState('deleteContentForward');
|
||
});
|
||
input.addEventListener('beforeinput', event => {
|
||
rememberBeforeState(event.inputType);
|
||
});
|
||
input.addEventListener('input', event => {
|
||
handleInput(input, event);
|
||
});
|
||
input.addEventListener('blur', () => {
|
||
validateContactInput(input);
|
||
});
|
||
});
|
||
const forms = new Set(Array.from(inputs).map(inp => inp.form).filter(Boolean));
|
||
forms.forEach(form => {
|
||
if (form._contactValidationBound) return;
|
||
form.addEventListener('submit', event => {
|
||
let isFormValid = true;
|
||
let firstInvalidInput = null;
|
||
inputs.forEach(input => {
|
||
if (input.form === form) {
|
||
validateContactInput(input);
|
||
if (!input.validity.valid) {
|
||
isFormValid = false;
|
||
if (!firstInvalidInput) firstInvalidInput = input;
|
||
}
|
||
}
|
||
});
|
||
if (!isFormValid) {
|
||
event.preventDefault();
|
||
if (firstInvalidInput) {
|
||
firstInvalidInput.reportValidity();
|
||
}
|
||
}
|
||
});
|
||
form._contactValidationBound = true;
|
||
});
|
||
}
|
||
initContactInputs();
|
||
|
||
|
||
// Возврат в категорию блога
|
||
function initArticleBackUrl() {
|
||
const storageKey = 'articleBackUrl';
|
||
const blogPath = '/blog';
|
||
const normalizePath = (path) => {
|
||
return path.replace(/\/$/, '');
|
||
};
|
||
const isBlogArchivePath = (path) => {
|
||
return normalizePath(path) === blogPath;
|
||
};
|
||
const isBlogSinglePath = (path) => {
|
||
const normalizedPath = normalizePath(path);
|
||
return /^\/blog\/[^/]+$/.test(normalizedPath);
|
||
};
|
||
const saveBackUrlFromBlog = () => {
|
||
const articlesGrid = document.querySelector('.articles-grid');
|
||
const articlesFilter = document.querySelector('.articles-filter');
|
||
if (!articlesGrid && !articlesFilter) return;
|
||
document.addEventListener('click', (event) => {
|
||
const link = event.target.closest('a');
|
||
if (!link) return;
|
||
if (!link.closest('.articles-item')) return;
|
||
if (link.closest('.articles-filter')) return;
|
||
const currentUrl = new URL(window.location.href);
|
||
if (!isBlogArchivePath(currentUrl.pathname)) return;
|
||
sessionStorage.setItem(
|
||
storageKey,
|
||
JSON.stringify({
|
||
url: currentUrl.href,
|
||
time: Date.now(),
|
||
})
|
||
);
|
||
});
|
||
};
|
||
const applyBackUrlToButton = () => {
|
||
const currentUrl = new URL(window.location.href);
|
||
if (!isBlogSinglePath(currentUrl.pathname)) {
|
||
sessionStorage.removeItem(storageKey);
|
||
return;
|
||
}
|
||
const backButton = document.querySelector('.article-goback__btn');
|
||
if (!backButton) return;
|
||
const savedData = sessionStorage.getItem(storageKey);
|
||
if (!savedData) return;
|
||
try {
|
||
const data = JSON.parse(savedData);
|
||
const savedUrl = new URL(data.url, window.location.origin);
|
||
if (savedUrl.origin !== window.location.origin) return;
|
||
if (!isBlogArchivePath(savedUrl.pathname)) return;
|
||
const oneDay = 24 * 60 * 60 * 1000;
|
||
if (Date.now() - data.time > oneDay) {
|
||
sessionStorage.removeItem(storageKey);
|
||
return;
|
||
}
|
||
backButton.href = savedUrl.href;
|
||
} catch (error) {
|
||
sessionStorage.removeItem(storageKey);
|
||
}
|
||
};
|
||
saveBackUrlFromBlog();
|
||
applyBackUrlToButton();
|
||
}
|
||
initArticleBackUrl();
|
||
|
||
|
||
// AJAX-загрузка постов
|
||
function initLoadMore(gridSelector) {
|
||
const grid = document.querySelector(gridSelector);
|
||
if (!grid) return;
|
||
const type = grid.dataset.type;
|
||
const nonce = window.themeAjax?.nonces?.[type];
|
||
if (!nonce) return;
|
||
const showMoreContainer = grid.closest('[class*="-grid"]')
|
||
?.parentElement
|
||
?.querySelector('[class*="-showmore"]')
|
||
?? grid.parentElement?.querySelector('[class*="-showmore"]');
|
||
const showMoreBtn = showMoreContainer?.querySelector('[class*="-showmore__btn"]');
|
||
if (!showMoreBtn) return;
|
||
let currentRequest = null;
|
||
const toggleButton = () => {
|
||
const current = parseInt(grid.dataset.page || 1, 10);
|
||
const max = parseInt(grid.dataset.maxPages || 1, 10);
|
||
showMoreContainer.classList.toggle('is-hidden', current >= max || max === 0);
|
||
};
|
||
const setLoading = (isLoading) => {
|
||
grid.classList.toggle('is-loading', isLoading);
|
||
showMoreBtn.disabled = isLoading;
|
||
showMoreBtn.textContent = isLoading ? 'Загрузка...' : 'Показать еще';
|
||
};
|
||
const loadMore = async (page) => {
|
||
currentRequest?.abort();
|
||
currentRequest = new AbortController();
|
||
const formData = new FormData();
|
||
formData.append('action', 'theme_load_more');
|
||
formData.append('nonce', nonce);
|
||
formData.append('page', page);
|
||
formData.append('type', type);
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch(window.themeAjax.ajaxUrl, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'same-origin',
|
||
signal: currentRequest.signal,
|
||
});
|
||
if (!response.ok) throw new Error('Network response was not ok');
|
||
const result = await response.json();
|
||
if (!result.success) throw new Error('Ajax request failed');
|
||
grid.insertAdjacentHTML('beforeend', result.data.html);
|
||
grid.dataset.page = String(result.data.page);
|
||
grid.dataset.maxPages = String(result.data.max_pages);
|
||
toggleButton();
|
||
} catch (error) {
|
||
if (error.name !== 'AbortError') {
|
||
console.error(`Load more error [${type}]:`, error);
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
currentRequest = null;
|
||
}
|
||
};
|
||
showMoreBtn.addEventListener('click', () => {
|
||
const nextPage = parseInt(grid.dataset.page || 1, 10) + 1;
|
||
loadMore(nextPage);
|
||
});
|
||
toggleButton();
|
||
}
|
||
initLoadMore('.news-grid');
|
||
initLoadMore('.reviews-grid');
|
||
|
||
|
||
// Отправка форм на AJAX
|
||
function initAjaxForms() {
|
||
const forms = document.querySelectorAll('[data-ajax-form]');
|
||
if (!forms.length || typeof Fancybox === 'undefined') return;
|
||
const createThanksContent = () => {
|
||
const thanks = document.querySelector('#thanks');
|
||
if (!thanks) {
|
||
console.error('Element "#thanks" not found');
|
||
return null;
|
||
}
|
||
const clone = thanks.cloneNode(true);
|
||
clone.classList.add('is-cloned');
|
||
clone.removeAttribute('id');
|
||
clone.style.display = '';
|
||
return clone;
|
||
};
|
||
const replaceCurrentFancyboxContent = (form) => {
|
||
const thanksContent = createThanksContent();
|
||
if (!thanksContent) return false;
|
||
const slide = form.closest('.fancybox__slide');
|
||
if (!slide) return false;
|
||
const content = form.closest('.f-html') || slide.querySelector('.f-html');
|
||
if (!content) return false;
|
||
content.classList.remove('is-visible', 'is-hidden', 'is-replaced');
|
||
content.classList.add('is-replacing');
|
||
void content.offsetWidth;
|
||
content.replaceChildren(thanksContent);
|
||
content.classList.remove('is-replacing');
|
||
content.classList.add('is-replaced');
|
||
void content.offsetWidth;
|
||
requestAnimationFrame(() => {
|
||
content.classList.add('is-visible');
|
||
});
|
||
return true;
|
||
};
|
||
const showThanks = (form) => {
|
||
const isInsideFancybox = Boolean(form.closest('.fancybox__slide'));
|
||
if (isInsideFancybox) {
|
||
const isReplaced = replaceCurrentFancyboxContent(form);
|
||
if (isReplaced) return;
|
||
}
|
||
Fancybox.show([{
|
||
src: '#thanks',
|
||
type: 'clone',
|
||
}], {
|
||
mainClass: 'form-fancybox',
|
||
closeButton: false,
|
||
dragToClose: false,
|
||
showClass: 'is-visible',
|
||
hideClass: 'is-hidden'
|
||
});
|
||
};
|
||
const setLoading = (form, isLoading) => {
|
||
form.classList.toggle('is-loading', isLoading);
|
||
form.querySelectorAll('[type="submit"]').forEach((button) => {
|
||
button.disabled = isLoading;
|
||
});
|
||
};
|
||
forms.forEach((form) => {
|
||
form.addEventListener('submit', async (event) => {
|
||
event.preventDefault();
|
||
const senderUrl = form.dataset.ajaxUrl || form.action || window.sender;
|
||
if (!senderUrl) {
|
||
console.error('Form sender URL not found');
|
||
return;
|
||
}
|
||
setLoading(form, true);
|
||
try {
|
||
const formData = new FormData(form);
|
||
const response = await fetch(senderUrl, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'same-origin',
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error('Form request failed');
|
||
}
|
||
showThanks(form);
|
||
setTimeout(() => {
|
||
form.reset();
|
||
resetFileFields(form);
|
||
}, 500);
|
||
} catch (error) {
|
||
console.error('Form submit error:', error);
|
||
} finally {
|
||
setLoading(form, false);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
initAjaxForms();
|
||
|
||
|
||
// Табы блока «О продукте»
|
||
function initTabs(tabsBlock) {
|
||
const tablist = tabsBlock.querySelector('[role="tablist"]');
|
||
const tabs = Array.from(tabsBlock.querySelectorAll('[role="tab"]'));
|
||
const panels = Array.from(tabsBlock.querySelectorAll('[role="tabpanel"]'));
|
||
if (!tablist || !tabs.length || !panels.length) return;
|
||
const scrollTabIntoView = (tab, options = {}) => {
|
||
const { behavior = 'smooth', align = 'nearest' } = options;
|
||
if (tablist.scrollWidth <= tablist.clientWidth) return;
|
||
const styles = window.getComputedStyle(tablist);
|
||
const paddingStart = parseFloat(styles.paddingInlineStart) || 0;
|
||
const paddingEnd = parseFloat(styles.paddingInlineEnd) || 0;
|
||
const tabStart = tab.offsetLeft;
|
||
const tabEnd = tabStart + tab.offsetWidth;
|
||
const visibleStart = tablist.scrollLeft + paddingStart;
|
||
const visibleEnd = tablist.scrollLeft + tablist.clientWidth - paddingEnd;
|
||
let left = tablist.scrollLeft;
|
||
if (align === 'start' || tabStart < visibleStart) {
|
||
left = tabStart - paddingStart;
|
||
} else if (tabEnd > visibleEnd) {
|
||
left = tabEnd - tablist.clientWidth + paddingEnd;
|
||
}
|
||
tablist.scrollTo({
|
||
left: Math.max(0, left),
|
||
behavior,
|
||
});
|
||
};
|
||
const activateTab = (activeTab, options = {}) => {
|
||
const { focus = false, scroll = true } = options;
|
||
const target = activeTab.dataset.tab;
|
||
if (!target) return;
|
||
tabs.forEach((tab) => {
|
||
const isActive = tab === activeTab;
|
||
tab.classList.toggle('is-active', isActive);
|
||
tab.setAttribute('aria-selected', String(isActive));
|
||
tab.tabIndex = isActive ? 0 : -1;
|
||
});
|
||
panels.forEach((panel) => {
|
||
const isActive = panel.dataset.tabPanel === target;
|
||
panel.classList.toggle('is-active', isActive);
|
||
panel.hidden = !isActive;
|
||
});
|
||
if (focus) activeTab.focus();
|
||
if (scroll) scrollTabIntoView(activeTab);
|
||
};
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
activateTab(tab);
|
||
});
|
||
tab.addEventListener('keydown', (event) => {
|
||
const currentIndex = tabs.indexOf(tab);
|
||
let nextIndex = null;
|
||
switch (event.key) {
|
||
case 'ArrowRight':
|
||
case 'ArrowDown':
|
||
nextIndex = (currentIndex + 1) % tabs.length;
|
||
break;
|
||
case 'ArrowLeft':
|
||
case 'ArrowUp':
|
||
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||
break;
|
||
case 'Home':
|
||
nextIndex = 0;
|
||
break;
|
||
case 'End':
|
||
nextIndex = tabs.length - 1;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
activateTab(tabs[nextIndex], { focus: true });
|
||
});
|
||
});
|
||
const initialTab = tabs.find((tab) => tab.getAttribute('aria-selected') === 'true') || tabs[0];
|
||
activateTab(initialTab, { scroll: false });
|
||
requestAnimationFrame(() => {
|
||
scrollTabIntoView(initialTab, { behavior: 'auto', align: 'start' });
|
||
});
|
||
}
|
||
document.querySelectorAll('[data-tabs]').forEach(initTabs);
|
||
|
||
|
||
// Согласие на использование cookie
|
||
function initCookieConsent() {
|
||
const cookieConsent = document.querySelector('.cookie-consent');
|
||
if (!cookieConsent) return;
|
||
const cookieButtons = cookieConsent.querySelectorAll('.cookie-consent__buttons .btn');
|
||
const cookieName = 'cookie_consent_accepted';
|
||
const showDelay = 2000;
|
||
const getCookie = (name) => {
|
||
return document.cookie.split('; ').find(row => row.startsWith(`${name}=`))?.split('=')[1];
|
||
};
|
||
const setCookie = (name, value, days) => {
|
||
const maxAge = days * 24 * 60 * 60;
|
||
document.cookie = `${name}=${value}; max-age=${maxAge}; path=/; SameSite=Lax`;
|
||
};
|
||
if (getCookie(cookieName) === 'true') return;
|
||
window.addEventListener('load', () => {
|
||
setTimeout(() => {
|
||
cookieConsent.classList.add('is-active');
|
||
}, showDelay);
|
||
});
|
||
cookieButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
cookieConsent.classList.remove('is-active');
|
||
setCookie(cookieName, 'true', 30);
|
||
});
|
||
});
|
||
}
|
||
initCookieConsent();
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
}); |