Files
hantico/wp-content/themes/hantico/assets/js/scripts.js
T

1587 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});