(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 = $('').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 = $(''); values.forEach(function (value) { $('
  • ').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);