// управляющий класс App с методом init(), в котором собраны все используемые методы с комментариями о том, что конкретно делает каждый метод class App { constructor() { this.patternPhone = /^(\+7|7|8)?[\s\-]?\(?[489][0-9]{2}\)?[\s\-]?[0-9]{3}[\s\-]?[0-9]{2}[\s\-]?[0-9]{2}$/; // рег. выражение для поля 'телефон'; this.patternEmail = /^[a-zA-Z0-9._%+-\.]+@[a-z0-9.-]+\.[a-z]{2,}$/i; // рег. выражение для поля 'электронная почта'; } init() { console.log('init'); this.stickyHeader(); // липкий хедер; this.controlBurgerMenu(); // бургер-меню; this.smoothScroll(); // плавный скролл к якорю (smooth scroll); this.scrollUp(); // кнопка наверх; this.addToFavorites(); // добавить в избранное (звёздочка); this.initTypicalSlider(); // типовые слайдеры; this.initPartnerslSlider(); // слайдер с партнёрами; this.controlFilters(); // фильтры на главном экране; this.controlPopups(); // открытие/закрытие поп-апов; this.controlContactUsPopup(); // открытие/закрытие поп-апа 'обратный звонок'; this.sendForm('.js_popup_feedback_form', '[data-popup="success"]'); // отправка формы в поп-апе обратной связи; this.sendForm('.js_popup_viewing_form', '[data-popup="success"]'); // отправка формы в поп-апе 'записаться на просмотр'; this.sendForm('.js_footer_feedback_form', '[data-popup="success"]'); // отправка формы в футере; this.sendForm('.js_contacts_form', '.js_contacts_success'); // отправка формы на странице контакты; this.sendForm('.js_popup_sending_form_', '[data-popup="success"]'); //this.sendOffer(); //отправка предложения по e-mail; //this.setGeneralMap(); // карта на странице карт; this.setComplexMap('complex-map', [55.726591050908745, 37.57244549999999], 'ЖК Садовые кварталы'); // карта на странице 'ЖК'; this.setComplexMap('offer-map', [55.70851106903402, 37.65864349999999], 'Аренда торгового помещения 321,6 м2'); // карта на странице 'Предложение'; this.setCatalogSorts(); // сортировка на странице 'каталог'; this.initIntroSlider(); // слайдер на странице жк и на странице предложения; this.setTabs('.js_offer_side_tab', '.js_offer_side_item'); // табы с планами объекат и этажа на странице предложения; this.setTabs('.js_offer_side_popup_tab', '.js_offer_side_popup_item'); // табы с планами объекат и этажа в поп-апе на странице предложения; this.sontrolOfferSidePopup(); // логика открытия нужного таба при открытии поп-апа с планами объекат и этажа на странице предложения; this.setCustomGallery(); // галлерея; this.setCookies() // куки; this.setFooterSpoilers() // аккордеон в футере; } // фиксация
fixBodyPosition() { setTimeout(function () { // ставим необходимую задержку, чтобы не было «конфликта» в случае, если функция фиксации вызывается сразу после расфиксации (расфиксация отменяет действия расфиксации из-за одновременного действия) if (!document.body.hasAttribute('data-body-scroll-fix')) { // получаем позицию прокрутки let scrollPosition = window.pageYOffset || document.documentElement.scrollTop; // ставим нужные стили document.body.setAttribute('data-body-scroll-fix', scrollPosition); // Cтавим атрибут со значением прокрутки document.body.style.overflow = 'hidden'; document.body.style.position = 'fixed'; document.body.style.top = '-' + scrollPosition + 'px'; document.body.style.left = '0'; document.body.style.width = '100%'; if (window.innerWidth >= 1200) { document.body.style.paddingRight = '8px'; } } }, 15); // можно задержку ещё меньше, но работает хорошо именно с этим значением на всех устройствах и браузерах } // расфиксация unfixBodyPosition() { if (document.body.hasAttribute('data-body-scroll-fix')) { // получаем позицию прокрутки из атрибута let scrollPosition = document.body.getAttribute('data-body-scroll-fix'); // удаляем атрибут document.body.removeAttribute('data-body-scroll-fix'); // удаляем ненужные стили document.body.style.overflow = ''; document.body.style.position = ''; document.body.style.top = ''; document.body.style.left = ''; document.body.style.width = ''; document.body.style.paddingRight = ''; // прокручиваем страницу на полученное из атрибута значение window.scroll(0, scrollPosition); } } // бургер-меню controlBurgerMenu() { const headerBurger = document.querySelector('.js_header_burger'); if (headerBurger) { const menu = document.querySelector('.js_menu'); const menuClose = menu.querySelector('.js_menu_close'); headerBurger.addEventListener('click', () => { menu.classList.add('active'); this.fixBodyPosition(); }); menu.addEventListener('click', (e) => { if (e.target == menu) { menu.classList.remove('active'); this.unfixBodyPosition(); } }); menuClose.addEventListener('click', () => { menu.classList.remove('active'); this.unfixBodyPosition(); }); } } // липкий хедер stickyHeader() { const header = document.querySelector('.js_header'); if (header) { window.addEventListener('scroll', () => { if (window.scrollY > 200) { header.classList.add('fixed'); } else { header.classList.remove('fixed'); } }); }; } // плавный скролл к якорю (smooth scroll) smoothScroll() { const smoothLinks = document.querySelectorAll('.js_smooth_link'); if (smoothLinks.length) { smoothLinks.forEach(link => { link.addEventListener('click', function (e) { e.preventDefault(); let href = this.getAttribute('href').substring(1); const scrollTarget = document.getElementById(href); // const topOffset = document.querySelector('.header').offsetHeight; const topOffset = 0; // если не нужен отступ сверху const elementPosition = scrollTarget.getBoundingClientRect().top; const offsetPosition = elementPosition - topOffset; window.scrollBy({ top: offsetPosition, behavior: 'smooth' }); }); }); } } // кнопка наверх scrollUp() { const toTopBtn = document.querySelector('.js_btn_up'); if (toTopBtn) { toTopBtn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } } // добавить в избранное (звёздочка) addToFavorites() { const cardFavorites = document.querySelectorAll('.js_card_favorites'); if (cardFavorites.length) { cardFavorites.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); item.classList.toggle('active'); }); }); } } // типовые слайдеры initTypicalSlider() { const slidersWraps = document.querySelectorAll('.slider__wrap'); if (slidersWraps.length) { slidersWraps.forEach(wrap => { const slider = wrap.querySelector('.swiper'); const prev = wrap.querySelector('.swiper-button-prev'); const next = wrap.querySelector('.swiper-button-next'); const pagination = wrap.querySelector('.swiper-pagination'); let swiper1 = new Swiper(slider, { navigation: { nextEl: next, prevEl: prev, }, pagination: { el: pagination, clickable: true, }, slidesPerView: 1, spaceBetween: 20, observer: true, observeParents: true, observeSlideChildren: true, breakpoints: { 480: { slidesPerView: 1.5, }, 640: { slidesPerView: 2, }, 780: { slidesPerView: 2.5, }, 920: { slidesPerView: 3, }, 1024: { slidesPerView: 3.4 }, 1200: { slidesPerView: 4, } } }); }); } } // метод, делающий число удобночитаемым (добавляет пробел справа через каждые 3 цифры) prettify(num) { const withoutSpace = num.replace(/[^\d]/g, ''); //убирает все символы; return withoutSpace.replace(/(?!^)(?=(?:\d{3})+(?:\.|$))/gm, ' '); //ставит пробелы; } // фильтры на главном экране controlFilters() { const heroFilters = document.querySelectorAll('.js_hero_filter'); const heroSearchBtns = document.querySelectorAll('.js_hero_search_btn'); if (heroFilters.length) { heroFilters.forEach(filter => { const heroFilterInput = filter.querySelector('.js_hero_filter_input'); const heroFilterCurrent = filter.querySelector('.js_hero_filter_current'); const heroFilterItems = filter.querySelectorAll('.hero-filter__item'); const heroFilterFields = filter.querySelectorAll('.js_hero_filter_field'); const heroFilterFrom = filter.querySelector('.js_hero_filter_from'); const heroFilterTo = filter.querySelector('.js_hero_filter_to'); const heroFilterReset = filter.querySelector('.js_hero_filter_reset'); heroFilterCurrent.addEventListener('click', () => { if (filter.classList.contains('active')) { filter.classList.remove('active'); heroSearchBtns.forEach(btn => { btn.disabled = false; }); } else { heroFilters.forEach(filter => { filter.classList.remove('active'); }); filter.classList.add('active'); heroSearchBtns.forEach(btn => { btn.disabled = true; }); } }); if (heroFilterItems.length) { heroFilterItems.forEach(item => { item.addEventListener('click', () => { heroFilterCurrent.textContent = item.textContent; heroFilterInput.value = item.dataset.val; filter.classList.remove('active'); heroSearchBtns.forEach(btn => { btn.disabled = false; }); }); }); } if (heroFilterFields.length) { const heroFilterMin = heroFilterFrom.dataset.min; const heroFilterMax = heroFilterTo.dataset.max; let heroFilterFromVal; let heroFilterToVal; heroFilterFields.forEach(field => { field.addEventListener('input', () => { field.value = this.prettify(field.value); heroFilterReset.classList.remove('active'); heroFilterFields.forEach(field => { if (field.value != "") { heroFilterReset.classList.add('active'); } }); }); }); heroFilterFrom.addEventListener('change', () => { heroFilterFromVal = +heroFilterFrom.value.replace(/\s/g, ''); heroFilterToVal = +heroFilterTo.value.replace(/\s/g, ''); if (heroFilterToVal != '' && heroFilterFromVal > heroFilterToVal) { heroFilterFrom.value = heroFilterTo.value; } else if (heroFilterFromVal < +heroFilterMin) { heroFilterFrom.value = this.prettify(heroFilterMin); } else if (heroFilterFromVal > +heroFilterMax) { heroFilterFrom.value = this.prettify(heroFilterMax); } }); heroFilterTo.addEventListener('change', () => { heroFilterFromVal = +heroFilterFrom.value.replace(/\s/g, ''); heroFilterToVal = +heroFilterTo.value.replace(/\s/g, ''); if (heroFilterFromVal != '' && heroFilterToVal < heroFilterFromVal) { heroFilterTo.value = heroFilterFrom.value; } else if (heroFilterToVal < +heroFilterMin) { heroFilterTo.value = this.prettify(heroFilterMax); } else if (heroFilterToVal > +heroFilterMax) { heroFilterTo.value = this.prettify(heroFilterMax); } }); heroFilterReset.addEventListener('click', () => { heroFilterFields.forEach(field => { field.value = ''; }); heroFilterReset.classList.remove('active'); }); } }); document.addEventListener('click', (e) => { if (!e.target.closest('.js_hero_filter_dropdown') && !e.target.closest('.js_hero_filter_current')) { heroFilters.forEach(filter => { filter.classList.remove('active'); }); heroSearchBtns.forEach(btn => { btn.disabled = false; }); } }); } } // открытие/закрытие типовых поп-апов controlPopups() { const popupShowBtns = document.querySelectorAll('[data-btn]'); const popups = document.querySelectorAll('[data-popup]'); if (popupShowBtns.length) { popupShowBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); popups.forEach(popup => { popup.classList.remove('active'); // если какойто поп-ап открыт, то закрываем его; this.unfixBodyPosition(); if (btn.dataset.btn == popup.dataset.popup) { popup.classList.add('active'); this.fixBodyPosition(); } }); }); }); popups.forEach(popup => { const popupCloseBtns = popup.querySelectorAll('.js_popup_close'); popupCloseBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); popup.classList.remove('active'); this.unfixBodyPosition(); }); }); popup.addEventListener('click', (e) => { if (e.target == popup) { popup.classList.remove('active'); this.unfixBodyPosition(); } }); }); } } // открытие/закрытие поп-апа 'обратный звонок' controlContactUsPopup() { const contactUsBtn = document.querySelector('.js_btn_contact_us'); const contactUsPopup = document.querySelector('.js_contact_us'); if (contactUsPopup) { const contactUsPopupCloseBtns = contactUsPopup.querySelectorAll('.js_contact_us_close'); contactUsBtn.addEventListener('click', (e) => { e.preventDefault(); if (contactUsPopup.classList.contains('active')) { contactUsPopup.classList.remove('active'); } else { contactUsPopup.classList.add('active'); } }); contactUsPopupCloseBtns.forEach(btn => { btn.addEventListener('click', () => { contactUsPopup.classList.remove('active'); }); }); document.addEventListener('click', (e) => { if (!e.target.closest('.js_contact_us') && !e.target.closest('.js_btn_contact_us')) { contactUsPopup.classList.remove('active'); } }); } } // валидатор форм validateForm(input) { // функция добавления ошибки const createError = (text) => { input.classList.add('error'); input.classList.remove('no-error'); if (input.closest('label').querySelector('span.error')) { input.closest('label').querySelector('span.error').remove(); input.closest('label').insertAdjacentHTML('beforeend', `${text}`); } else { input.closest('label').insertAdjacentHTML('beforeend', `${text}`); } } // функция удаления ошибки const removeError = () => { input.classList.remove('error'); input.classList.add('no-error'); if (input.closest('label').querySelector('span.error')) { input.closest('label').querySelector('span.error').remove(); } } // проверяем на правильность заполнения поля 'Телефон' if (input.classList.contains('js_input_phone') && input.value == "") { createError('Заполните, пожалуйста, поле'); } else if (input.classList.contains('js_input_phone') && input.value.search(this.patternPhone) == 0) { removeError(); } else if (input.classList.contains('js_input_phone')) { createError('Укажите, пожалуйста, корректный телефон'); } // проверяем правильность заполнения поля 'Электронная почта' if (input.classList.contains('js_input_email') && input.value == "") { createError('Заполните, пожалуйста, поле'); } else if (input.classList.contains('js_input_email') && input.value.search(this.patternEmail) == 0) { removeError(); } else if (input.classList.contains('js_input_email')) { createError('Укажите, пожалуйста, корректный e-mail'); } } // отправка форм sendForm(formEl, success) { const form = document.querySelector(formEl); if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); const formInputs = form.querySelectorAll('input'); const formBtn = form.querySelector('.js_form_btn'); formInputs.forEach(input => { // перебираем все инпуты в форме; this.validateForm(input); input.addEventListener('input', () => { this.validateForm(input); }); }); if (!form.querySelector('.error')) { //проверяем, чтоб все инпуты прошли валидацию (чтоб не было в форме ни одного элемента с класссом error); // сюда пишем команды, которые должны сработать после успешной валидации; console.log('validate'); formBtn.classList.add('btn-animate'); formBtn.disabled = true; const formData = new FormData(form); console.log(...formData); const response = await fetch(e.target.action, { method: e.target.method, body: formData }); if (response.ok) { setTimeout(() => { // имитация отправки, когда отправка будет настроена, нужно достать всё из setTimeout() и удалить его; console.log('Отправлено'); formBtn.classList.remove('btn-animate'); formBtn.disabled = false; if (document.querySelector('[data-popup="feedback"]')) { document.querySelector('[data-popup="feedback"]').classList.remove('active'); } if (document.querySelector('[data-popup="viewing"]')) { document.querySelector('[data-popup="viewing"]').classList.remove('active'); } document.querySelector(success).classList.add('active'); this.fixBodyPosition(); form.reset(); formInputs.forEach(input => { input.classList.remove('no-error'); }); }, 2000) } else { formBtn.classList.remove('btn-animate'); formBtn.disabled = false; alert('Ошибка'); } } else { console.log('no-validate'); form.querySelector('.error').focus(); //фокус к полю с ошибкой; } }); } } //отправка предложения по e-mail sendOffer() { const form = document.querySelector('.js_popup_sending_form'); if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); const formInputs = form.querySelectorAll('input'); const formBtn = form.querySelector('.js_form_btn'); formInputs.forEach(input => { // перебираем все инпуты в форме; this.validateForm(input); input.addEventListener('input', () => { this.validateForm(input); }); }); if (!form.querySelector('.error')) { //проверяем, чтоб все инпуты прошли валидацию (чтоб не было в форме ни одного элемента с класссом error); // сюда пишем команды, которые должны сработать после успешной валидации; console.log('validate'); formBtn.classList.add('btn-animate'); formBtn.disabled = true; const formData = new FormData(form); console.log(...formData); const response = await fetch(e.target.action, { method: e.target.method, body: formData }); if (response.ok) { setTimeout(() => { // имитация отправки, когда отправка будет настроена, нужно достать всё из setTimeout() и удалить его; console.log('Отправлено'); formBtn.classList.remove('btn-animate'); formBtn.disabled = false; if (document.querySelector('[data-popup="sending"]')) { document.querySelector('[data-popup="sending"]').classList.remove('active'); } this.fixBodyPosition(); form.reset(); formInputs.forEach(input => { input.classList.remove('no-error'); }); }, 2000) } else { formBtn.classList.remove('btn-animate'); formBtn.disabled = false; alert('Ошибка'); } } else { console.log('no-validate'); form.querySelector('.error').focus(); //фокус к полю с ошибкой; } }); } } // карта на странице 'ЖК' setComplexMap(id, coords, caption) { if (document.querySelector('#' + id)) { // Дождёмся загрузки API и готовности DOM. ymaps.ready(init); function init() { const map = new ymaps.Map(id, { // При инициализации карты обязательно нужно указать её центр и коэффициент масштабирования. center: coords, zoom: 16, controls: [] }); // Создаём макет содержимого. const MyIconContentLayout = ymaps.templateLayoutFactory.createClass( '