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.