562 lines
17 KiB
JavaScript
562 lines
17 KiB
JavaScript
(function ($) {
|
||
'use strict';
|
||
|
||
$(function () {
|
||
const $expertise = $('.expertise');
|
||
|
||
if (!$expertise.length || typeof Swiper !== 'function') {
|
||
return;
|
||
}
|
||
|
||
const $slider = $expertise.find('.expertise__cases');
|
||
const $wrapper = $expertise.find('.expertise__track');
|
||
const $tabs = $expertise.find('.expertise__tab');
|
||
const sourceSlides = $wrapper.find('.expertise-card').map(function () {
|
||
const $slide = $(this);
|
||
|
||
return {
|
||
group: $slide.data('expertise-group'),
|
||
html: this.outerHTML,
|
||
};
|
||
}).get();
|
||
|
||
let expertiseSwiper = null;
|
||
let activeFilter = null;
|
||
|
||
const getSlidesByFilter = function (filter) {
|
||
return sourceSlides.filter(function (slide) {
|
||
return slide.group === filter;
|
||
});
|
||
};
|
||
|
||
const updateTabs = function (filter) {
|
||
$tabs.each(function () {
|
||
const $tab = $(this);
|
||
const isActive = $tab.data('expertise-filter') === filter;
|
||
|
||
$tab
|
||
.toggleClass('is-active', isActive)
|
||
.attr({
|
||
'aria-selected': String(isActive),
|
||
'aria-pressed': String(isActive),
|
||
});
|
||
});
|
||
};
|
||
|
||
const destroySlider = function () {
|
||
if (!expertiseSwiper) {
|
||
return;
|
||
}
|
||
|
||
expertiseSwiper.destroy(true, true);
|
||
expertiseSwiper = null;
|
||
};
|
||
|
||
const getLoopSlidesHtml = function (slides) {
|
||
// Для loop в Swiper важно, чтобы реальных слайдов было заметно больше,
|
||
// чем помещается в видимой области. Иначе Swiper отключает loop на широких экранах.
|
||
const minSlidesForLoop = 24;
|
||
const repeatCount = Math.max(1, Math.ceil(minSlidesForLoop / slides.length));
|
||
let html = '';
|
||
|
||
for (let i = 0; i < repeatCount; i += 1) {
|
||
slides.forEach(function (slide) {
|
||
html += slide.html;
|
||
});
|
||
}
|
||
|
||
return html;
|
||
};
|
||
|
||
const initSlider = function () {
|
||
const slidesCount = $wrapper.children('.swiper-slide').length;
|
||
const loopEnabled = slidesCount > 1;
|
||
|
||
expertiseSwiper = new Swiper($slider[0], {
|
||
slidesPerView: 'auto',
|
||
spaceBetween: 16,
|
||
speed: 650,
|
||
grabCursor: true,
|
||
allowTouchMove: true,
|
||
watchOverflow: false,
|
||
loop: loopEnabled,
|
||
loopedSlides: Math.min(12, slidesCount),
|
||
loopAdditionalSlides: Math.min(12, slidesCount),
|
||
loopPreventsSliding: false,
|
||
normalizeSlideIndex: true,
|
||
observer: true,
|
||
observeParents: true,
|
||
breakpoints: {
|
||
768: {
|
||
spaceBetween: 24,
|
||
},
|
||
1200: {
|
||
spaceBetween: 32,
|
||
},
|
||
},
|
||
});
|
||
};
|
||
|
||
const renderSlides = function (filter) {
|
||
const slides = getSlidesByFilter(filter);
|
||
|
||
if (!slides.length) {
|
||
return;
|
||
}
|
||
|
||
destroySlider();
|
||
$wrapper.html(getLoopSlidesHtml(slides));
|
||
initSlider();
|
||
};
|
||
|
||
const setActiveFilter = function (filter) {
|
||
if (activeFilter === filter || !getSlidesByFilter(filter).length) {
|
||
return;
|
||
}
|
||
|
||
activeFilter = filter;
|
||
updateTabs(filter);
|
||
renderSlides(filter);
|
||
};
|
||
|
||
const initialFilter = $tabs.filter('.is-active').first().data('expertise-filter')
|
||
|| $tabs.first().data('expertise-filter');
|
||
|
||
setActiveFilter(initialFilter);
|
||
|
||
$tabs.on('click', function () {
|
||
setActiveFilter($(this).data('expertise-filter'));
|
||
});
|
||
});
|
||
})(jQuery);
|
||
|
||
|
||
(function ($) {
|
||
'use strict';
|
||
|
||
$(function () {
|
||
const $results = $('.results');
|
||
|
||
if (!$results.length) {
|
||
return;
|
||
}
|
||
|
||
$results.addClass('is-animation-ready');
|
||
|
||
const showResults = function (target) {
|
||
$(target).addClass('is-in-view');
|
||
};
|
||
|
||
if (!('IntersectionObserver' in window)) {
|
||
$results.each(function () {
|
||
showResults(this);
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
const observer = new IntersectionObserver(function (entries, currentObserver) {
|
||
entries.forEach(function (entry) {
|
||
if (!entry.isIntersecting) {
|
||
return;
|
||
}
|
||
|
||
showResults(entry.target);
|
||
currentObserver.unobserve(entry.target);
|
||
});
|
||
}, {
|
||
threshold: 0.24,
|
||
rootMargin: '0px 0px -12% 0px',
|
||
});
|
||
|
||
$results.each(function () {
|
||
observer.observe(this);
|
||
});
|
||
});
|
||
})(jQuery);
|
||
|
||
(function ($) {
|
||
'use strict';
|
||
|
||
$(function () {
|
||
const $faq = $('.faq');
|
||
|
||
if (!$faq.length) {
|
||
return;
|
||
}
|
||
|
||
$faq.on('click', '.faq-item__button', function () {
|
||
const $button = $(this);
|
||
const $item = $button.closest('.faq-item');
|
||
const $panel = $item.find('.faq-item__panel').first();
|
||
const isOpen = $button.attr('aria-expanded') === 'true';
|
||
|
||
$button.attr('aria-expanded', String(!isOpen));
|
||
|
||
if (isOpen) {
|
||
$panel.stop(true, true).slideUp(220, function () {
|
||
$panel.attr('hidden', true).removeAttr('style');
|
||
});
|
||
} else {
|
||
$panel.removeAttr('hidden').hide().stop(true, true).slideDown(220, function () {
|
||
$panel.removeAttr('style');
|
||
});
|
||
}
|
||
});
|
||
});
|
||
})(jQuery);
|
||
|
||
|
||
(function ($) {
|
||
'use strict';
|
||
|
||
$(function () {
|
||
const $forms = $('.contact-sentence');
|
||
|
||
if (!$forms.length) {
|
||
return;
|
||
}
|
||
|
||
const options = {
|
||
task: ['лендинг', 'интернет-магазин', 'интеграцию', 'дизайн', 'доработку сайта'],
|
||
method: ['Telegram', 'WhatsApp', 'звонок', 'email'],
|
||
time: ['09:00–12:00', '12:00–15:00', '15:00–18:00', 'любое время'],
|
||
};
|
||
|
||
let $dateInput = $('.contact-date-input');
|
||
|
||
if (!$dateInput.length) {
|
||
$dateInput = $('<input class="contact-date-input" type="date" aria-hidden="true">').appendTo('body');
|
||
}
|
||
|
||
const cleanValue = function ($field) {
|
||
const text = $.trim($field.text()).replace(/\s+/g, ' ');
|
||
const placeholder = $.trim($field.data('placeholder') || '');
|
||
const normalized = text.replace(/[()]/g, '').replace(/▾/g, '').trim();
|
||
|
||
if ($field.hasClass('is-placeholder') || normalized === placeholder) {
|
||
return '';
|
||
}
|
||
|
||
return normalized;
|
||
};
|
||
|
||
const getPlaceholderText = function ($field) {
|
||
const placeholder = $.trim($field.data('placeholder') || '');
|
||
const withArrow = $field.hasClass('contact-field--selector');
|
||
|
||
return '(' + placeholder + (withArrow ? ' ▾' : '') + ')';
|
||
};
|
||
|
||
const setFieldValue = function ($field, value, isPlaceholder) {
|
||
const text = isPlaceholder ? getPlaceholderText($field) : value;
|
||
|
||
$field
|
||
.text(text)
|
||
.toggleClass('is-placeholder', !!isPlaceholder)
|
||
.toggleClass('is-filled', !isPlaceholder && !!value)
|
||
.removeClass('is-error');
|
||
|
||
updateHidden($field.closest('form'));
|
||
};
|
||
|
||
const updateHidden = function ($form) {
|
||
$form.find('.contact-field').each(function () {
|
||
const $field = $(this);
|
||
const name = $field.data('field');
|
||
|
||
if (!name) {
|
||
return;
|
||
}
|
||
|
||
$form.find('input[name="' + name + '"]').val(cleanValue($field));
|
||
});
|
||
};
|
||
|
||
const selectAll = function (el) {
|
||
const range = document.createRange();
|
||
const selection = window.getSelection();
|
||
|
||
range.selectNodeContents(el);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
};
|
||
|
||
const setCaretEnd = function (el) {
|
||
const range = document.createRange();
|
||
const selection = window.getSelection();
|
||
|
||
range.selectNodeContents(el);
|
||
range.collapse(false);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
};
|
||
|
||
const formatPhone = function (value) {
|
||
const raw = String(value || '').replace(/\s+/g, '');
|
||
const hasPlus = raw.indexOf('+') === 0;
|
||
const digits = raw.replace(/\D/g, '').slice(0, 11);
|
||
|
||
if (!digits) {
|
||
return hasPlus ? '+' : '';
|
||
}
|
||
|
||
let formatted = hasPlus || digits.charAt(0) === '7' ? '+' : '';
|
||
|
||
formatted += digits.slice(0, 1);
|
||
|
||
if (digits.length > 1) {
|
||
formatted += ' ' + digits.slice(1, 4);
|
||
}
|
||
|
||
if (digits.length > 4) {
|
||
formatted += ' ' + digits.slice(4, 7);
|
||
}
|
||
|
||
if (digits.length > 7) {
|
||
formatted += ' ' + digits.slice(7, 9);
|
||
}
|
||
|
||
if (digits.length > 9) {
|
||
formatted += ' ' + digits.slice(9, 11);
|
||
}
|
||
|
||
return formatted;
|
||
};
|
||
|
||
const openDropdown = function ($field) {
|
||
const type = $field.data('type');
|
||
const values = options[type] || [];
|
||
|
||
if (!values.length) {
|
||
return;
|
||
}
|
||
|
||
$('.contact-dropdown').remove();
|
||
|
||
const rect = $field[0].getBoundingClientRect();
|
||
const $dropdown = $('<ul class="contact-dropdown" role="listbox"></ul>');
|
||
|
||
values.forEach(function (value) {
|
||
$('<li role="option"></li>').text(value).appendTo($dropdown);
|
||
});
|
||
|
||
$('body').append($dropdown);
|
||
|
||
const left = Math.min(rect.left + window.pageXOffset - 10, window.pageXOffset + window.innerWidth - $dropdown.outerWidth() - 12);
|
||
|
||
$dropdown.css({
|
||
top: rect.bottom + window.pageYOffset + 8,
|
||
left: Math.max(12, left),
|
||
});
|
||
|
||
$dropdown.on('click', 'li', function () {
|
||
setFieldValue($field, $(this).text(), false);
|
||
$dropdown.remove();
|
||
});
|
||
};
|
||
|
||
const openDatePicker = function ($field) {
|
||
const rect = $field[0].getBoundingClientRect();
|
||
const picker = $dateInput[0];
|
||
|
||
$dateInput
|
||
.val('')
|
||
.css({
|
||
left: rect.left + 'px',
|
||
top: rect.top + 'px',
|
||
width: Math.max(rect.width, 24) + 'px',
|
||
height: Math.max(rect.height, 24) + 'px',
|
||
})
|
||
.off('change.contact')
|
||
.one('change.contact', function () {
|
||
if (this.value) {
|
||
setFieldValue($field, this.value, false);
|
||
}
|
||
});
|
||
|
||
requestAnimationFrame(function () {
|
||
try {
|
||
if (typeof picker.showPicker === 'function') {
|
||
picker.showPicker();
|
||
} else {
|
||
picker.focus();
|
||
picker.click();
|
||
}
|
||
} catch (error) {
|
||
picker.focus();
|
||
picker.click();
|
||
}
|
||
});
|
||
};
|
||
|
||
const isEmail = function (value) {
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
};
|
||
|
||
const validateForm = function ($form) {
|
||
let valid = true;
|
||
let $firstError = null;
|
||
|
||
const markError = function ($field) {
|
||
valid = false;
|
||
$field.addClass('is-error');
|
||
|
||
if (!$firstError) {
|
||
$firstError = $field;
|
||
}
|
||
};
|
||
|
||
$form.find('.contact-field').removeClass('is-error');
|
||
|
||
const required = ['name', 'task', 'phone', 'email', 'method', 'date', 'time'];
|
||
|
||
required.forEach(function (name) {
|
||
const $field = $form.find('.contact-field[data-field="' + name + '"]');
|
||
const value = cleanValue($field);
|
||
|
||
if (!value) {
|
||
markError($field);
|
||
}
|
||
});
|
||
|
||
const $name = $form.find('.contact-field[data-field="name"]');
|
||
const $phone = $form.find('.contact-field[data-field="phone"]');
|
||
const $email = $form.find('.contact-field[data-field="email"]');
|
||
const name = cleanValue($name);
|
||
const phone = cleanValue($phone);
|
||
const email = cleanValue($email);
|
||
|
||
if (name && name.length < 2) {
|
||
markError($name);
|
||
}
|
||
|
||
if (phone && phone.replace(/\D/g, '').length < 10) {
|
||
markError($phone);
|
||
}
|
||
|
||
if (email && !isEmail(email)) {
|
||
markError($email);
|
||
}
|
||
|
||
if (!valid && $firstError) {
|
||
$form.find('.contact-sentence__status')
|
||
.removeClass('is-success')
|
||
.text('Заполните подсвеченные поля.');
|
||
$('html, body').animate({ scrollTop: $firstError.offset().top - 120 }, 260);
|
||
}
|
||
|
||
return valid;
|
||
};
|
||
|
||
$forms.each(function () {
|
||
updateHidden($(this));
|
||
});
|
||
|
||
$(document).on('click keydown', '.contact-field--editable', function (event) {
|
||
const $field = $(this);
|
||
|
||
if ($field.attr('contenteditable')) {
|
||
return;
|
||
}
|
||
|
||
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
|
||
const value = cleanValue($field);
|
||
|
||
$field.attr('contenteditable', 'true');
|
||
$field.text(value);
|
||
$field.removeClass('is-placeholder is-error');
|
||
$field.focus();
|
||
selectAll($field[0]);
|
||
});
|
||
|
||
$(document).on('input', '.contact-field--editable', function () {
|
||
const $field = $(this);
|
||
|
||
if ($field.data('type') === 'phone') {
|
||
$field.text(formatPhone($field.text()));
|
||
setCaretEnd(this);
|
||
}
|
||
|
||
updateHidden($field.closest('form'));
|
||
});
|
||
|
||
$(document).on('paste', '.contact-field--editable[data-type="phone"]', function (event) {
|
||
event.preventDefault();
|
||
|
||
const clipboard = event.originalEvent.clipboardData || window.clipboardData;
|
||
const text = clipboard ? clipboard.getData('text') : '';
|
||
|
||
$(this).text(formatPhone(text));
|
||
setCaretEnd(this);
|
||
updateHidden($(this).closest('form'));
|
||
});
|
||
|
||
$(document).on('blur keydown', '.contact-field--editable[contenteditable="true"]', function (event) {
|
||
if (event.type === 'keydown' && event.key !== 'Enter') {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
|
||
const $field = $(this);
|
||
const value = $.trim($field.text()).replace(/\s+/g, ' ');
|
||
|
||
$field.removeAttr('contenteditable');
|
||
|
||
if (!value) {
|
||
setFieldValue($field, '', true);
|
||
} else {
|
||
setFieldValue($field, value, false);
|
||
}
|
||
});
|
||
|
||
$(document).on('click keydown', '.contact-field--selector', function (event) {
|
||
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
const $field = $(this);
|
||
|
||
if ($field.data('type') === 'date') {
|
||
openDatePicker($field);
|
||
} else {
|
||
openDropdown($field);
|
||
}
|
||
});
|
||
|
||
$(document).on('click', function () {
|
||
$('.contact-dropdown').remove();
|
||
});
|
||
|
||
$forms.on('submit', function (event) {
|
||
event.preventDefault();
|
||
|
||
const $form = $(this);
|
||
const $status = $form.find('.contact-sentence__status');
|
||
|
||
updateHidden($form);
|
||
|
||
if (!validateForm($form)) {
|
||
return;
|
||
}
|
||
|
||
const payload = Object.fromEntries(new FormData($form[0]).entries());
|
||
|
||
console.log('Contact form payload:', payload);
|
||
|
||
$status
|
||
.addClass('is-success')
|
||
.text('Заявка подготовлена. Подключите обработчик отправки формы.');
|
||
});
|
||
});
|
||
})(jQuery);
|