PageSourceSearch

https://vestovoi.ru/js/contacts-page.js?v=20260812

js vestovoi.ru collected 2026-10-07 19:43:03 UTC 12,208 bytes, 321 lines download raw bytes

1(() => {
2  const root = document.getElementById('contacts-page');
3  if (!root) return;
4
5  const menuButton = root.querySelector('.menu-toggle');
6  const menu = root.querySelector('.main-nav');
7  const form = root.querySelector('form[form-marker="contacts"]');
8  const toast = root.querySelector('.toast');
9  let toastTimer;
10
11  const showToast = (message) => {
12    if (!toast) return;
13    window.clearTimeout(toastTimer);
14    toast.textContent = message;
15    toast.classList.add('is-visible');
16    toastTimer = window.setTimeout(() => toast.classList.remove('is-visible'), 5000);
17  };
18
19  const initBrandGlobe = () => {
20    const canvas = root.querySelector('#brand-globe');
21    const shell = canvas?.closest('.brand-globe-shell');
22    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
23
24    if (!canvas || !shell || !window.planetaryjs || !window.d3 || !window.topojson) return;
25
26    const planet = window.planetaryjs.planet();
27    const autorotate = (degreesPerSecond) => (currentPlanet) => {
28      let lastTick = null;
29      let paused = false;
30
31      currentPlanet.plugins.autorotate = {
32        pause: () => { paused = true; },
33        resume: () => { paused = false; }
34      };
35
36      currentPlanet.onDraw(() => {
37        const now = Date.now();
38        if (paused || !lastTick || !degreesPerSecond) {
39          lastTick = now;
40          return;
41        }
42        const rotation = currentPlanet.projection.rotate();
43        rotation[0] += degreesPerSecond * (now - lastTick) / 1000;
44        if (rotation[0] >= 180) rotation[0] -= 360;
45        currentPlanet.projection.rotate(rotation);
46        lastTick = now;
47      });
48    };
49
50    planet.loadPlugin(autorotate(reducedMotion ? 0 : 7));
51    planet.loadPlugin(window.planetaryjs.plugins.earth({
52      topojson: { file: '/home/world-110m-withlakes.json' },
53      oceans: { fill: '#00a3d3' },
54      land: { fill: '#b9e3ec' },
55      borders: { stroke: 'rgba(14,122,165,.68)', lineWidth: .35 }
56    }));
57    planet.loadPlugin((currentPlanet) => {
58      currentPlanet.onInit(() => shell.classList.add('is-ready'));
59    });
60
61    if (window.matchMedia('(pointer: fine)').matches) {
62      planet.loadPlugin(window.planetaryjs.plugins.drag({
63        onDragStart() { this.plugins.autorotate.pause(); },
64        onDragEnd() { this.plugins.autorotate.resume(); }
65      }));
66    }
67
68    let lastSize = 0;
69    const resizeBrandGlobe = () => {
70      const size = Math.max(18, Math.round(shell.clientWidth));
71      if (size === lastSize) return;
72      lastSize = size;
73
74      const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
75      canvas.style.width = size + 'px';
76      canvas.style.height = size + 'px';
77      canvas.width = Math.round(size * pixelRatio);
78      canvas.height = Math.round(size * pixelRatio);
79      canvas.getContext('2d').setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
80      planet.projection.scale(size * .49).translate([size / 2, size / 2]);
81    };
82
83    resizeBrandGlobe();
84    planet.projection.rotate([-103, -32, 0]);
85
86    try {
87      planet.draw(canvas);
88    } catch (error) {
89      shell.classList.remove('is-ready');
90      return;
91    }
92
93    if ('ResizeObserver' in window) {
94      new ResizeObserver(resizeBrandGlobe).observe(shell);
95    } else {
96      window.addEventListener('resize', resizeBrandGlobe, { passive: true });
97    }
98  };
99
100  const initMenu = () => {
101    if (!menuButton || !menu) return;
102
103    const closeMenu = () => {
104      menu.classList.remove('is-open');
105      menuButton.setAttribute('aria-expanded', 'false');
106      menuButton.setAttribute('aria-label', 'Открыть меню');
107      document.body.classList.remove('nav-open');
108    };
109
110    menuButton.addEventListener('click', () => {
111      const willOpen = menuButton.getAttribute('aria-expanded') !== 'true';
112      menu.classList.toggle('is-open', willOpen);
113      menuButton.setAttribute('aria-expanded', String(willOpen));
114      menuButton.setAttribute('aria-label', willOpen ? 'Закрыть меню' : 'Открыть меню');
115      document.body.classList.toggle('nav-open', willOpen);
116    });
117
118    menu.querySelectorAll('a').forEach((link) => link.addEventListener('click', closeMenu));
119  };
120
121  const initCaptcha = () => {
122    if (!form) return;
123
124    const captchaRoot = form.querySelector('[data-contact-captcha]');
125    const captchaRefresh = captchaRoot?.querySelector('[data-captcha-refresh]');
126    const captchaStatus = captchaRoot?.querySelector('[data-captcha-status]');
127    if (!captchaRoot || !captchaRefresh) return;
128
129    const refreshCaptcha = async () => {
130      const currentCaptcha = captchaRoot.querySelector('.captcha-cont');
131      const currentKey = currentCaptcha?.querySelector('input[name="flex_captchakey"]')?.value || '';
132      const length = captchaRoot.dataset.captchaLength || '5';
133      const query = new URLSearchParams({
134        width: '200',
135        height: '60',
136        length,
137        key: currentKey,
138        _: String(Date.now())
139      });
140
141      captchaRefresh.disabled = true;
142      captchaRefresh.classList.add('is-loading');
143      captchaRefresh.setAttribute('aria-busy', 'true');
144      if (captchaStatus) captchaStatus.textContent = 'Обновляем…';
145
146      try {
147        const response = await fetch('/ajax/captcha_get.php?' + query.toString(), {
148          method: 'GET',
149          credentials: 'same-origin',
150          cache: 'no-store',
151          headers: { 'X-Requested-With': 'XMLHttpRequest' }
152        });
153        if (!response.ok) throw new Error('Captcha request failed');
154
155        const template = document.createElement('template');
156        template.innerHTML = (await response.text()).trim();
157        const nextCaptcha = template.content.querySelector('.captcha-cont');
158        if (!nextCaptcha || !currentCaptcha) throw new Error('Captcha response is invalid');
159
160        currentCaptcha.replaceWith(nextCaptcha);
161        captchaRoot.classList.remove('has-error');
162        if (captchaStatus) captchaStatus.textContent = 'Картинка обновлена';
163
164        const nextInput = nextCaptcha.querySelector('input[name="flex_captcha"]');
165        if (nextInput) nextInput.focus();
166      } catch (error) {
167        if (captchaStatus) captchaStatus.textContent = 'Не удалось обновить';
168      } finally {
169        captchaRefresh.disabled = false;
170        captchaRefresh.classList.remove('is-loading');
171        captchaRefresh.removeAttribute('aria-busy');
172      }
173    };
174
175    captchaRefresh.addEventListener('click', refreshCaptcha);
176  };
177
178  const initForm = () => {
179    if (!form) return;
180
181    const field = (marker, control) => form.querySelector('[data-contact-field="' + marker + '"] ' + control);
182    const nameInput = field('name', 'input');
183    const companyInput = field('company', 'input');
184    const phoneInput = field('phone', 'input');
185    const emailInput = field('email', 'input');
186    const topicSelect = field('topic', 'select');
187    const messageInput = field('message', 'textarea');
188
189    if (nameInput) {
190      nameInput.placeholder = 'Алексей';
191      nameInput.autocomplete = 'name';
192    }
193    if (companyInput) {
194      companyInput.placeholder = 'Название компании';
195      companyInput.autocomplete = 'organization';
196    }
197    if (phoneInput) {
198      phoneInput.type = 'tel';
199      phoneInput.placeholder = '+7 999 000-00-00';
200      phoneInput.autocomplete = 'tel';
201      phoneInput.inputMode = 'tel';
202      phoneInput.minLength = 5;
203    }
204    if (emailInput) {
205      emailInput.placeholder = '[email protected]';
206      emailInput.autocomplete = 'email';
207    }
208    if (messageInput) {
209      messageInput.placeholder = 'Маршрут, объёмы, сроки или номер действующей доставки';
210    }
211    if (topicSelect && !topicSelect.querySelector('option[value=""]')) {
212      const placeholder = document.createElement('option');
213      placeholder.value = '';
214      placeholder.textContent = 'Выберите направление';
215      placeholder.selected = true;
216      topicSelect.insertBefore(placeholder, topicSelect.firstChild);
217    }
218    if (topicSelect) topicSelect.required = true;
219
220    root.querySelectorAll('[data-topic]').forEach((button) => {
221      button.addEventListener('click', () => {
222        if (topicSelect) topicSelect.value = button.dataset.topic;
223        root.querySelector('#request')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
224        window.setTimeout(() => topicSelect?.focus({ preventScroll: true }), 650);
225      });
226    });
227
228    if (!window.jQuery) return;
229    const $ = window.jQuery;
230
231    $(form).off('submit').on('submit', (event) => {
232      event.preventDefault();
233      if (!form.reportValidity()) return;
234
235      const $form = $(form);
236      const $submit = $form.find('button[type="submit"]');
237      const $status = $form.find('.form-status');
238      const originalSubmitHtml = $submit.html();
239
240      $form.find('.errors-cont, .error').remove();
241      $form.find('[name^="user_form["]').css('border-color', '');
242      $form.find('[data-contact-captcha]').removeClass('has-error');
243      $submit.prop('disabled', true).text('Отправляем…');
244      $status.text('Отправляем…');
245
246      $.ajax({
247        enctype: 'multipart/form-data',
248        type: 'post',
249        processData: false,
250        contentType: false,
251        cache: false,
252        data: new FormData(form),
253        dataType: 'json'
254      }).done((response) => {
255        if (response.state) {
256          form.reset();
257          showToast('Спасибо! Запрос передан логистической команде.');
258          $form.slideUp('slow', () => $form.replaceWith(response.tpl));
259          return;
260        }
261
262        const $errors = $('<div class="errors-cont" role="alert"></div>').prependTo($form);
263        const errors = response.errors || ['Не удалось отправить форму. Попробуйте ещё раз.'];
264
265        $.each(errors, (index, value) => {
266          if (typeof value === 'object') {
267            $.each(value, (fieldIndex, fieldError) => {
268              const fieldId = fieldError[0];
269              const $field = $form.find(
270                '[name="user_form[' + fieldId + ']"], [name="user_form[' + fieldId + '][]"]'
271              );
272              const $fieldContainer = $field.closest('.field, .contact-consent');
273              const $error = $('<span class="error" field="' + fieldId + '"></span>')
274                .text(fieldError[1]);
275
276              $field.css('border-color', '#e95d66');
277              if ($fieldContainer.length) $error.appendTo($fieldContainer);
278              else $error.insertAfter($field.last());
279            });
280          } else {
281            if (/робот|капч|изображ/i.test(String(value))) {
282              $form.find('[data-contact-captcha]').addClass('has-error');
283            }
284            $('<span></span>').text(value).appendTo($errors);
285          }
286        });
287
288        $status.text('Проверьте заполнение');
289      }).fail(() => {
290        $('<div class="errors-cont" role="alert"></div>')
291          .text('Не удалось отправить форму. Попробуйте ещё раз или позвоните: +7 (495) 822-02-04.')
292          .prependTo($form);
293        $status.text('Не удалось отправить');
294      }).always(() => {
295        $submit.prop('disabled', false).html(originalSubmitHtml);
296      });
297    });
298  };
299
300  const initReveal = () => {
301    const revealItems = root.querySelectorAll('.reveal');
302    if ('IntersectionObserver' in window && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
303      const observer = new IntersectionObserver((entries) => {
304        entries.forEach((entry) => {
305          if (!entry.isIntersecting) return;
306          entry.target.classList.add('is-visible');
307          observer.unobserve(entry.target);
308        });
309      }, { threshold: .1, rootMargin: '0px 0px -40px' });
310      revealItems.forEach((element) => observer.observe(element));
311      return;
312    }
313    revealItems.forEach((element) => element.classList.add('is-visible'));
314  };
315
316  initMenu();
317  initForm();
318  initCaptcha();
319  initReveal();
320  initBrandGlobe();
321})();

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.