This commit is contained in:
561
assets/js/main.js
Normal file
561
assets/js/main.js
Normal file
@@ -0,0 +1,561 @@
|
||||
(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);
|
||||
Reference in New Issue
Block a user