PageSourceSearch

https://savras.ru/local/templates/savras/assets/js/main.js

js savras.ru collected 2026-09-26 18:15:18 UTC 18,154 bytes, 713 lines download raw bytes

1"use strict";
2
3document.addEventListener("DOMContentLoaded", function () {
4  if (typeof Fancybox !== "undefined" && typeof Fancybox.bind === "function") {
5      Fancybox.bind("[data-fancybox]", {
6      });
7  }
8});
9
10Notiflix.Loading.init({
11  svgColor: "#294634",
12  fontFamily: "CoFo",
13  backgroundColor: 'rgba(0,0,0,0.3)',
14  messageColor: "#000",
15});
16
17Notiflix.Report.init({
18  fontFamily: 'CoFo',
19  backOverlayClickToClose: true,
20  titleMaxLength: 200,
21  messageMaxLength: 200,
22  buttonMaxLength: 200,
23  borderRadius: 0,
24  success: {
25    svgColor: '#294634',
26    buttonBackground: '#294634',
27    backOverlayColor: 'rgba(0, 0, 0, 0.3)',
28  },
29  failure: {
30    backOverlayColor: 'rgba(0, 0, 0, 0.3)',
31  }
32});
33
34const maskElements = document.querySelectorAll("[data-phone-mask]");
35if (maskElements.length > 0) {
36  maskElements.forEach((item) => {
37    let mask = IMask(item, {
38      mask: "+{7} (000) 000-00-00",
39      lazy: true,
40    });
41    item.addEventListener("focus", function () {
42      mask.updateOptions({ lazy: false });
43      mask.alignCursor();
44    });
45    item.addEventListener("blur", function () {
46      mask.updateOptions({ lazy: true });
47    });
48  });
49}
50
51
52let buttons = document.querySelectorAll('.menu__link');
53
54buttons.forEach((elem)=>{
55  elem.addEventListener('click',()=>{
56    menuBody.classList.remove('_active');
57    burgerMenu.classList.remove('_active');
58  })
59})
60
61const menuLinks = document.querySelectorAll('.menu__link[data-goto]');
62
63if (menuLinks.length > 0) {
64  menuLinks.forEach(menuLink => {
65    menuLink.addEventListener("click", onMenuLinkClick);
66  });
67  function onMenuLinkClick(e) {
68    const menuLink = e.target;
69    if(menuLink.dataset.goto && document.querySelector(menuLink.dataset.goto)) {
70        const gotoBlock = document.querySelector(menuLink.dataset.goto);
71        const gotoBlockValue = gotoBlock.getBoundingClientRect().top + scrollY - document.querySelector('header').offsetHeight;
72
73              window.scrollTo({
74        top:gotoBlockValue,
75        behavior: "smooth"
76      });
77      e.preventDefault();
78    }
79  }
80}
81
82window.addEventListener('scroll', () => {
83  if(pageYOffset > 50) {
84    document.querySelector('.header').classList.add('header__bg');
85  } else {
86    document.querySelector('.header').classList.remove('header__bg');
87  }
88});
89
90const input = document.getElementById('formNumPopup');
91const btnPlus = document.querySelector('.form__plus');
92const btnMinus = document.querySelector('.form__minus');
93
94if (input && btnPlus && btnMinus) {
95
96  let value = 1; 
97  input.value = value;
98
99  btnPlus.addEventListener('click', () => {
100    if (value < 100) {
101      value++;
102      input.value = value;
103    }
104  });
105
106  btnMinus.addEventListener('click', () => {
107    if (value > 1) {
108      value--;
109      input.value = value;
110    }
111  });
112
113  input.addEventListener('input', () => {
114    let num = parseInt(input.value);
115
116    if (isNaN(num) || num < 1) num = 1;
117    if (num > 100) num = 100;
118
119    value = num;
120    input.value = value;
121  });
122}
123
124document.addEventListener('DOMContentLoaded', () => {
125
126  document.querySelectorAll('form').forEach((form) => {
127
128    form.addEventListener("submit", e => {
129      e.preventDefault();
130      form.dispatchEvent(new CustomEvent("smartCaptchaSuccess"));
131    });
132
133    form.addEventListener('smartCaptchaSuccess', async (e) => {
134      e.preventDefault();
135
136      const errorCount = validateForm(form);
137      if (errorCount !== 0) {
138          Notiflix.Loading.remove();
139          return;
140      }
141
142      Notiflix.Loading.hourglass("Отправляем данные...");
143
144      form.classList.add('_sending');
145
146      const formData = new FormData(form);
147
148      const dateInput = form.querySelector('#formDatePopup');
149      if (dateInput) {
150        const raw = formData.get(dateInput.name);
151        if (raw) {
152          const [y,m,d] = String(raw).split('-');
153          formData.set(dateInput.name, `${d}.${m}.${y}`);
154        }
155      }
156
157      try {
158        const response = await fetch(form.action, {
159          method: form.method || 'POST',
160          body: formData,
161          credentials: 'same-origin', // чтобы сессия/куки были ок
162          headers: { 'X-Requested-With': 'XMLHttpRequest' },
163        });
164
165        if (!response.ok) {
166          Notiflix.Loading.remove();
167          Notiflix.Report.failure("Ошибка отправки", "", "Закрыть");
168        }
169
170        const html = await response.text();
171
172        const doc = new DOMParser().parseFromString(html, 'text/html');
173
174        const success = doc.querySelector('.form__success, .success-message, .form-note, .form__note');
175
176        if (success) {
177          form.reset();
178          YandexSmartCaptcha.reset();
179          Notiflix.Loading.remove();
180          let message = form.dataset.success ? form.dataset.success : "Ваше сообщение отправлено";
181          Notiflix.Report.success(message, "", "Закрыть");
182          return;
183        }
184
185          // иначе — ошибка: достанем текст ошибок из ответа
186          const errorBlock = doc.querySelector('.form__error-global, .errortext, .form-error');
187          if (errorBlock) {
188            Notiflix.Loading.remove();
189            Notiflix.Report.failure("Ошибка отправки", errorBlock.textContent.trim(), "Закрыть");
190            YandexSmartCaptcha.reset();
191          return;
192        }
193
194        alert('Не удалось отправить форму: неизвестный ответ сервера');
195      } catch (err) {
196        console.error(err);
197        Notiflix.Loading.remove();
198        Notiflix.Report.failure("Ошибка при отправке формы", "", "Закрыть");
199        YandexSmartCaptcha.reset();
200      } finally {
201        form.classList.remove('_sending');
202      }
203    });
204  });
205
206  function validateForm(form) {
207    let error = 0;
208
209    form.querySelectorAll('._req').forEach((input) => {
210      removeError(input);
211
212      const val = input.value.trim();
213
214      if (input.classList.contains('_email') && !emailTest(val)) {
215        addError(input);
216        error++;
217      } else if (input.classList.contains('_number') && !phoneTest(val)) {
218        addError(input);
219        error++;
220      } else if (input.type === 'checkbox' && !input.checked) {
221        addError(input);
222        error++;
223      } else if (!val) {
224        addError(input);
225        error++;
226      }
227    });
228
229    return error;
230  }
231
232  function addError(input) {
233    let parent = input.closest('.form__item');
234    parent = parent ? parent : input.closest('.checkbox');
235    parent.classList.add('_error');
236    input.classList.add('_error');
237
238    const errMsg = parent.querySelector('.form__error');
239    if (errMsg) {
240      errMsg.classList.add('view');
241    }
242  }
243
244  function removeError(input) {
245    let parent = input.closest('.form__item');
246    parent = parent ? parent : input.closest('.checkbox');
247    parent.classList.remove('_error');
248    input.classList.remove('_error');
249
250    const errMsg = parent.querySelector('.form__error');
251    if (errMsg) errMsg.classList.remove('view');
252  }
253
254  const emailTest = (val) =>
255    /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(val);
256
257  const phoneTest = (val) =>
258    /^\+7\s\(\d{3}\)\s\d{3}-\d{2}-\d{2}$/.test(val);
259
260});
261
262document.addEventListener('DOMContentLoaded', function() {
263    if (!document.getElementById('map') || typeof ymaps === 'undefined') return;
264
265        ymaps.ready(function() {
266        var map = new ymaps.Map('map', {
267            center: [55.761457, 37.614159],
268            zoom: 16,
269            behaviors: ['drag', 'multiTouch']
270        });
271
272        const balloonData = window.balloonData;
273
274        var balloonContent = `
275            <div class="contacts__balloon">
276                <img class="contacts__balloon_img"
277                     src="${balloonData.logo}"
278                     alt="Savras"
279                     width="200"
280                     height="145">
281
282                <div class="contacts__balloon_address">
283                    ${balloonData.address}
284                </div>
285
286                <a class="contacts__balloon_phone"
287                   href="tel:${balloonData.phone_raw}">
288                    ${balloonData.phone_pretty}
289                </a>
290            </div>
291        `;
292
293                var placemark = new ymaps.Placemark(
294            [55.761457, 37.614159],
295            {
296                hintContent: 'Наш офис в Москве',
297                balloonContent: balloonContent
298            },
299            {
300                iconLayout: 'default#image',
301                iconImageHref: 'images/icons/map-icon.svg',
302                iconImageSize: [45, 50],
303                iconImageOffset: [-25, -50],
304                balloonCloseButton: true,
305                balloonPanelMaxMapArea: 0,
306                balloonOffset: [50, -20],
307            }
308        );
309
310                map.geoObjects.add(placemark);
311
312        setTimeout(function() {
313            placemark.balloon.open();
314        }, 1000);
315    });
316});
317
318const burgerMenu = document.querySelector('.burger'); 
319const menuBody = document.querySelector('.menu');
320const menuArrow = document.querySelector('.menu__top_arrow');
321
322if (burgerMenu) {
323  burgerMenu.addEventListener("click", function () {
324
325    const isOpen = burgerMenu.classList.toggle('_active');
326    menuBody.classList.toggle('_active');
327
328    document.body.classList.toggle('body-lock', isOpen); 
329  });
330
331  if (menuArrow) {
332    menuArrow.addEventListener("click", function () {
333      burgerMenu.classList.remove('_active');
334      menuBody.classList.remove('_active');
335      document.body.classList.remove('body-lock'); 
336    });
337  }
338}
339
340const popupLinks = document.querySelectorAll('.popup-link');
341const body = document.body;
342const lockPadding = document.querySelectorAll(".lock-padding");
343
344let unlock = true;
345
346
347function lockBody() {
348  const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
349
350  body.style.paddingRight = scrollBarWidth + "px";
351
352  lockPadding.forEach(el => {
353    el.style.paddingRight = scrollBarWidth + "px";
354  });
355
356  body.classList.add('body-lock');
357}
358
359function unlockBody() {
360  body.classList.remove('body-lock');
361
362  body.style.paddingRight = "0px";
363
364  lockPadding.forEach(el => {
365    el.style.paddingRight = "0px";
366  });
367}
368
369
370function popupOpen(currentPopup) {
371  if (currentPopup && unlock) {
372    const popupActive = document.querySelector('.popup.open');
373
374    if (popupActive) {
375      popupClose(popupActive, false);
376    }
377
378    currentPopup.removeAttribute('inert');
379    currentPopup.classList.add('open');
380
381    lockBody();
382
383    currentPopup.addEventListener("click", e => {
384      if (!e.target.closest('.popup__content')) {
385        popupClose(currentPopup);
386      }
387    });
388  }
389}
390
391
392function popupClose(popupActive, doUnlock = true) {
393  if (popupActive && unlock) {
394    popupActive.classList.remove('open');
395    popupActive.setAttribute('inert', '');
396
397    unlockBody();
398  }
399}
400
401
402popupLinks.forEach(link => {
403  link.addEventListener("click", e => {
404    e.preventDefault();
405    const popupName = link.getAttribute('href').replace('#', '');
406    const currentPopup = document.getElementById(popupName);
407    popupOpen(currentPopup);
408  });
409});
410
411document.querySelectorAll('.close-popup').forEach(btn => {
412  btn.addEventListener("click", e => {
413    e.preventDefault();
414    popupClose(btn.closest('.popup'));
415  });
416});
417
418document.addEventListener("keydown", e => {
419  if (e.key === "Escape") {
420    const popupActive = document.querySelector('.popup.open');
421    if (popupActive) popupClose(popupActive);
422  }
423});
424
425
426
427document.addEventListener('DOMContentLoaded', () => {
428  const dropdowns = document.querySelectorAll('.dropdown');
429
430  dropdowns.forEach(dropdown => {
431    const btn = dropdown.querySelector('.dropdown__button');
432    const list = dropdown.querySelector('.dropdown__list');
433    const items = dropdown.querySelectorAll('.dropdown__item');
434    const input = dropdown.querySelector('.dropdown__input-hidden');
435
436    const closeDropdown = () => {
437      list.classList.remove('dropdown__list--active');
438      btn.setAttribute('aria-expanded', 'false');
439    };
440
441    const openDropdown = () => {
442      document.querySelectorAll('.dropdown__list--active').forEach(l => l.classList.remove('dropdown__list--active'));
443      document.querySelectorAll('.dropdown__button--active').forEach(b => b.classList.remove('dropdown__button--active'));
444
445      list.classList.add('dropdown__list--active');
446      btn.setAttribute('aria-expanded', 'true');
447      btn.classList.add('dropdown__button--active');
448    };
449
450    btn.addEventListener('click', (e) => {
451      e.stopPropagation();
452      list.classList.contains('dropdown__list--active') ? closeDropdown() : openDropdown();
453    });
454
455    items.forEach(item => {
456      item.addEventListener('click', (e) => {
457        e.stopPropagation();
458        btn.textContent = item.textContent;
459        input.value = item.dataset.value;
460        closeDropdown();
461        btn.focus();
462      });
463    });
464
465    document.addEventListener('click', (e) => {
466      if (!dropdown.contains(e.target)) {
467        closeDropdown();
468      }
469    });
470
471    document.addEventListener('keydown', (e) => {
472      if (e.key === 'Escape') closeDropdown();
473    });
474  });
475});
476
477
478
479const sliders = document.querySelectorAll('.slider');
480
481if (sliders.length > 0) {
482  sliders.forEach(slider => {
483    new Swiper(slider, {
484      loop: true,
485      slidesPerView: 1,
486      speed: 1000,
487      autoHeight: false,
488      navigation: {
489        nextEl: slider.querySelector('.swiper-button-next'),
490        prevEl: slider.querySelector('.swiper-button-prev'),
491      },
492      pagination: {
493        el: slider.querySelector('.swiper-pagination'),
494        clickable: true,
495      },
496    });
497  });
498}
499
500
501let slidersRestaurant = [];
502
503function initRestaurantSliders() {
504    const sliderElems = document.querySelectorAll('.restaurant__slider');
505
506    sliderElems.forEach((sliderElem, index) => {
507        if (window.innerWidth <= 768 && !slidersRestaurant[index]) {
508            slidersRestaurant[index] = new Swiper(sliderElem, {
509                slidesPerView: 1.2,
510                spaceBetween: 10,
511                scrollbar: {
512                    el: sliderElem.querySelector('.swiper-scrollbar'), 
513                    draggable: true,
514                    hide: false,
515                },
516            });
517        }
518
519        if (window.innerWidth > 768 && slidersRestaurant[index]) {
520            slidersRestaurant[index].destroy(true, true);
521            slidersRestaurant[index] = null;
522        }
523    });
524}
525
526initRestaurantSliders();
527
528window.addEventListener('resize', initRestaurantSliders);
529
530const eventSlider = document.querySelectorAll('.event__slider');
531if (eventSlider.length > 0) {
532  eventSlider.forEach(slider => {
533  new Swiper(slider, {
534    loop: true,
535    direction: 'horizontal',
536    loop: true,
537    spaceBetween: 20,
538    speed: 1000,
539    autoHeight: false,
540    scrollbar: {
541        el: slider.querySelector('.swiper-scrollbar'), 
542        draggable: true,
543        hide: false,
544    },
545    breakpoints: {
546      320: {
547        slidesPerView: 1.2,
548      },
549      640: {
550        slidesPerView: 3,
551      },
552      1440: {
553        slidesPerView: 5,
554      }
555    }
556  });
557  });
558}
559
560/* =========================================================
561   YANDEX METRIKA GOALS
562   Counter: 108598200
563   ========================================================= */
564
565(function () {
566
567  const METRIKA_COUNTER_ID = 108598200;
568
569  function sendMetrikaGoal(goalName) {
570    if (typeof window.ym !== 'function') {
571      console.warn('[Metrika] ym is not available:', goalName);
572      return;
573    }
574
575    window.ym(METRIKA_COUNTER_ID, 'reachGoal', goalName);
576
577    if (
578      window.location.search.includes('_ym_debug=1') ||
579      window.location.search.includes('_ym_debug=2')
580    ) {
581      console.log('[Metrika goal]', goalName);
582    }
583  }
584
585
586  function isSocialLink(link) {
587    if (!link || !link.href) {
588      return false;
589    }
590
591    let url;
592
593    try {
594      url = new URL(link.href, window.location.href);
595    } catch (e) {
596      return false;
597    }
598
599    const host = url.hostname
600      .replace(/^www\./, '')
601      .toLowerCase();
602
603    const socialHosts = [
604      't.me',
605      'telegram.me',
606      'telegram.org',
607
608      'wa.me',
609      'whatsapp.com',
610      'api.whatsapp.com',
611
612      'vk.com',
613      'vk.ru',
614
615      'instagram.com',
616      'facebook.com',
617
618      'max.ru',
619      'web.max.ru',
620
621      'youtube.com',
622      'youtu.be'
623    ];
624
625    return socialHosts.some(function (socialHost) {
626      return (
627        host === socialHost ||
628        host.endsWith('.' + socialHost)
629      );
630    });
631  }
632
633
634  document.addEventListener('click', function (event) {
635
636    const target = event.target;
637
638    /*
639     * reserve-send
640     * Клик по кнопке "Забронировать"
641     * внутри формы ReMarked
642     */
643    const reserveSendButton = target.closest(
644      '.remarked-primary-widget__submit'
645    );
646
647    if (reserveSendButton) {
648      sendMetrikaGoal('reserve-send');
649      return;
650    }
651
652
653    /*
654     * phone-click
655     * Клик по любому номеру телефона
656     */
657    const phoneLink = target.closest(
658      'a[href^="tel:"]'
659    );
660
661    if (phoneLink) {
662      sendMetrikaGoal('phone-click');
663      return;
664    }
665
666
667    /*
668     * reserve-click
669     * Клик по любой кнопке,
670     * открывающей бронирование ReMarked
671     *
672     * Учитываем и новую,
673     * и старую схему кнопок
674     */
675    const reserveButton = target.closest(
676      'a[href="#openReMarkedWidget"], .open__primary__widget'
677    );
678
679    if (reserveButton) {
680      sendMetrikaGoal('reserve-click');
681      return;
682    }
683
684
685    /*
686     * menu-click
687     * Клик по карточкам ресторанного меню
688     */
689    const link = target.closest('a');
690
691    if (
692      link &&
693      link.closest('.gallery--menu')
694    ) {
695      sendMetrikaGoal('menu-click');
696      return;
697    }
698
699
700    /*
701     * social-click
702     * Соцсети и мессенджеры
703     */
704    if (
705      link &&
706      isSocialLink(link)
707    ) {
708      sendMetrikaGoal('social-click');
709    }
710
711  }, true);
712
713})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.