1document.addEventListener('DOMContentLoaded', () => { 2 initHeroVideo(); 3 tabsInit(); 4 initPhoneInput(); 5 initializeSliders(); 6 initHeaderNav(); 7 initFileUpload(); 8 faqInit(); 9 newShowcaseAccordionInit(); 10 initVideoCards(); 11 hydrateMobileCountries(); 12 hydrateMirrors(); 13 countriesMapInit(); 14 modalInit(); 15 initObservers(); 16 quizInit(); 17 initVideoJS(); 18 initGallerySlider(); 19 initLazyVideo(); 20 initProject360Modal(); 21 initFormField(); 22 initToggleActive('.js-more', '.js-card'); 23 initSmoothAnchorScroll(); 24 initPortfolioAboutSync(); 25 initPortfolioBreadcrumbsOffset(); 26 initBlogPostOffset(); 27 initBlogPostNav(); 28 initPortfolioHeroMobileHeight(); 29 initPortfolioGallerySlider(); 30 initPortfolioReasonsSlider(); 31 initNewExperienceTextToggle(); 32 initNewExperienceCountriesToggle(); 33}); 34 35function initProject360Modal() { 36 const modal = document.getElementById('modal-project-360'); 37 if (!modal) return; 38 if (initProject360Modal._inited) return; // guard against double-binding after AJAX modals load 39 initProject360Modal._inited = true; 40 41 const iframe = modal.querySelector('.modal-project-360__iframe'); 42 const wrapper = iframe.parentElement; // конÑÐµÐ¹Ð½ÐµÑ Ñ iframe и кнопкой fullscreen 43 const fullscreenBtn = modal.querySelector('.iframe__fullscreen-btn'); 44 const loader = modal.querySelector('.iframe__loader'); 45 const heading = modal.querySelector('.modal-project-360__heading'); 46 const headingTitle = heading ? heading.querySelector('.modal-project-360__heading-title') : null; 47 const headingAreaWrap = heading 48 ? heading.querySelector('.modal-project-360__heading-area-wrap') 49 : null; 50 const headingArea = heading ? heading.querySelector('.modal-project-360__heading-area') : null; 51 52 const fillHeading = (el) => { 53 if (!heading) return; 54 const title = ((el && el.dataset.title) || '').trim(); 55 const area = ((el && el.dataset.area) || '').trim(); 56 if (headingTitle) headingTitle.textContent = title; 57 if (headingArea) headingArea.textContent = area ? area + ' м²' : ''; 58 if (headingAreaWrap) headingAreaWrap.hidden = !area; 59 heading.hidden = !title && !area; 60 }; 61 62 const pageTrigger = document.querySelector('[data-modal="modal-project-360"]'); 63 if (pageTrigger) fillHeading(pageTrigger); 64 65 let currentSrc = ''; 66 let originalParent = wrapper.parentElement; 67 let placeholder = document.createElement('div'); 68 placeholder.style.display = 'none'; 69 originalParent.insertBefore(placeholder, wrapper); 70 71 // оÑкÑÑÑÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ Ð¸ подÑÑавиÑÑ iframe 72 document.addEventListener('click', (e) => { 73 const btn = e.target.closest('[data-modal="modal-project-360"]'); 74 if (!btn) return; 75 76 fillHeading(btn); 77 78 const src = btn.dataset.iframe; 79 if (!src) return; 80 if (currentSrc === src && iframe.src) return; 81 82 currentSrc = src; 83 loader.classList.remove('loaded'); 84 loader.hidden = false; 85 iframe.src = src; 86 87 setTimeout(() => loader.classList.add('loaded'), 2500); 88 }); 89 90 // кнопка fullscreen 91 fullscreenBtn.addEventListener('click', () => { 92 if (!wrapper.classList.contains('fullscreen-fixed')) { 93 // вклÑÑаем fullscreen 94 document.body.appendChild(wrapper); // пеÑеноÑим в body 95 wrapper.classList.add('fullscreen-fixed'); 96 fullscreenBtn.textContent = 'â'; 97 document.body.style.overflow = 'hidden'; 98 } else { 99 // вÑклÑÑаем fullscreen 100 wrapper.classList.remove('fullscreen-fixed'); 101 fullscreenBtn.textContent = 'â¶'; 102 placeholder.parentNode.insertBefore(wrapper, placeholder); // возвÑаÑаем 103 document.body.style.overflow = ''; 104 } 105 }); 106 107 // закÑÑÑие модалки 108 document.addEventListener('click', (e) => { 109 if (!e.target.closest('#modal-project-360 [data-close-modal]')) return; 110 111 iframe.src = ''; 112 currentSrc = ''; 113 loader.classList.remove('loaded'); 114 115 // возвÑаÑаем wrapper в Ð¼Ð¾Ð´Ð°Ð»ÐºÑ 116 if (wrapper.classList.contains('fullscreen-fixed')) { 117 wrapper.classList.remove('fullscreen-fixed'); 118 placeholder.parentNode.insertBefore(wrapper, placeholder); 119 document.body.style.overflow = ''; 120 } 121 }); 122} 123 124function quizInit() { 125 // Only init quizzes not yet initialized (guard against re-runs after AJAX modal load) 126 const quizzes = Array.from(document.querySelectorAll('.quiz')).filter(function (q) { 127 return !q.dataset.vpQuizInited; 128 }); 129 if (!quizzes.length) return;
130 quizzes.forEach(function (q) { 131 q.dataset.vpQuizInited = '1'; 132 }); 133 134 const debouncedResizeHandler = debounce(() => { 135 quizzes.forEach((quiz) => { 136 console.log(quiz); 137 updateQuizDimensions(quiz); 138 }); 139 }, 250); 140 141 window.addEventListener('resize', debouncedResizeHandler); 142 window.addEventListener('orientationchange', debouncedResizeHandler); 143 144 quizzes.forEach((quiz) => { 145 const steps = Array.from(quiz.querySelectorAll('.quiz__step')); 146 const progressEl = quiz.querySelector('.quiz__progress'); 147 const titleEl = quiz.querySelector('.section__title'); 148 const totalSteps = steps.length; 149 const answers = []; 150 const form = quiz.querySelector('.quiz__form'); 151 152 updateQuizDimensions(quiz); 153 154 function updateHeader(stepIndex) { 155 const currentStep = steps[stepIndex]; 156 const title = currentStep.dataset.title; 157 const stepNum = stepIndex + 1; 158 159 if (titleEl) titleEl.textContent = title; 160 if (progressEl) { 161 progressEl.innerHTML = `${stepNum} / <span>${totalSteps}</span>`; 162 } 163 } 164 165 function fadeOutStep(step, callback) { 166 if (!step) return callback?.(); 167 step.style.transition = 'opacity 0.3s ease'; 168 step.style.opacity = '0'; 169 const onEnd = () => { 170 step.removeEventListener('transitionend', onEnd); 171 step.style.display = 'none'; 172 callback?.(); 173 }; 174 step.addEventListener('transitionend', onEnd); 175 } 176 177 function fadeInStep(step) { 178 if (!step) return; 179 step.style.display = 'block'; 180 void step.offsetWidth; 181 step.style.transition = 'opacity 0.3s ease'; 182 step.style.opacity = '1'; 183 } 184 185 function goToNextStep(currentStepIndex) { 186 const nextStepIndex = currentStepIndex + 1; 187 const nextStep = steps[nextStepIndex]; 188 if (!nextStep) return; 189 190 fadeOutStep(steps[currentStepIndex], () => { 191 fadeInStep(nextStep); 192 193 if (nextStep.classList.contains('quiz__step--final')) { 194 const stepsWrapper = quiz.querySelector('.quiz__steps-wrapper'); 195 if (stepsWrapper) stepsWrapper.style.height = ''; 196 197 setTimeout(() => { 198 const phoneInput = nextStep.querySelector('.phone'); 199 if (phoneInput) { 200 fixInputPadding(phoneInput); 201 } 202 }, 60); 203 } 204 205 nextStep.classList.add('quiz__step--active'); 206 steps[currentStepIndex].classList.remove('quiz__step--active'); 207 updateHeader(nextStepIndex); 208 }); 209 } 210 211 function handleOptionClick(e) { 212 const option = e.currentTarget; 213 const input = option.querySelector('input[type="radio"]'); 214 if (!input) return; 215 216 const step = option.closest('.quiz__step'); 217 const idx = steps.indexOf(step); 218 219 step 220 .querySelectorAll('.quiz__option') 221 .forEach((opt) => opt.classList.remove('quiz__option--selected')); 222 option.classList.add('quiz__option--selected'); 223 224 answers[idx] = { 225 title: step.dataset.title, 226 answer: input.value, 227 }; 228 229 setTimeout(() => goToNextStep(idx), 400); 230 } 231 232 // ÐавеÑиваем обÑабоÑÑики на опÑии 233 quiz 234 .querySelectorAll('.quiz__option') 235 .forEach((opt) => opt.addEventListener('click', handleOptionClick)); 236 237 // ð¥ ÐбÑабоÑка оÑпÑавки ÑоÑÐ¼Ñ 238 /*if (form) { 239 form.addEventListener('submit', function (e) { 240 e.preventDefault(); // ÐÑедоÑвÑаÑаем обÑÑнÑÑ Ð¾ÑпÑÐ°Ð²ÐºÑ 241 242 // 1. СобиÑаем вÑе оÑвеÑÑ 243 const payload = { 244 answers: answers, 245 timestamp: new Date().toISOString() 246 }; 247 248 // 2. ÐÐ°Ñ Ð¾Ð´Ð¸Ð¼ модалÑное окно ÑÑÐ¿ÐµÑ Ð° 249 const successModalId = quiz.dataset.successModalId; 250 const successModal = successModalId 251 ? document.getElementById(successModalId) 252 : null; 253 254 // 3. ÐакÑÑваем ÑодиÑелÑÑкÑÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ (еÑли квиз внÑÑÑи .modal) 255 const parentModal = quiz.closest('.modal'); 256 if (parentModal) { 257 parentModal.classList.remove('modal--active'); 258 // ÐобавлÑем задеÑÐ¶ÐºÑ Ð¿ÐµÑед оÑкÑÑÑием success-модалки 259 setTimeout(() => { 260 if (successModal) { 261 successModal.classList.add('modal--active'); 262 } 263 }, 300); 264 } else { 265 // ÐÑли квиз не в модалке â пÑоÑÑо оÑкÑÑваем success 266 if (successModal) { 267 successModal.classList.add('modal--active'); 268 } 269 } 270 271 // 4. ÐÑпÑавлÑем даннÑе на ÑеÑÐ²ÐµÑ (AJAX
271) 272 fetch('/submit-quiz', { 273 method: 'POST', 274 headers: { 275 'Content-Type': 'application/json', 276 // ÐобавÑÑе CSRF-Ñокен, еÑли нÑжно 277 // 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content 278 }, 279 body: JSON.stringify(payload) 280 }) 281 .then(response => { 282 if (response.ok) { 283 console.log('â Ðвиз ÑÑпеÑно оÑпÑавлен на ÑеÑвеÑ'); 284 // Ðожно оÑиÑÑиÑÑ answers, еÑли нÑжно 285 } else { 286 console.error('â ÐÑибка оÑпÑавки квиза'); 287 // ÐбÑабоÑка оÑибки â можно показаÑÑ Ð´ÑÑгÑÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ 288 } 289 }) 290 .catch(error => { 291 console.error('ð¥ ÐÑибка ÑеÑи:', error); 292 }); 293 }); 294 }*/ 295 296 steps.forEach((step, i) => { 297 step.style.display = i === 0 ? 'block' : 'none'; 298 step.style.opacity = i === 0 ? '1' : '0'; 299 step.classList.toggle('quiz__step--active', i === 0); 300 }); 301 updateHeader(0); 302 }); 303 304 function debounce(func, wait) { 305 let timeout; 306 return function executedFunction(...args) { 307 const later = () => { 308 clearTimeout(timeout); 309 func(...args); 310 }; 311 clearTimeout(timeout); 312 timeout = setTimeout(later, wait); 313 }; 314 } 315 316 function updateQuizDimensions(quiz) { 317 const steps = Array.from(quiz.querySelectorAll('.quiz__step')); 318 const stepsWrapper = quiz.querySelector('.quiz__steps-wrapper'); 319 320 if (!stepsWrapper || steps.length === 0) return; 321 322 const activeStep = quiz.querySelector('.quiz__step--active'); 323 if (activeStep && activeStep.classList.contains('quiz__step--final')) { 324 stepsWrapper.style.height = ''; 325 return; 326 } 327 328 let maxHeight = 0; 329 const stepsToMeasure = steps.filter((step) => !step.classList.contains('quiz__step--final')); 330 331 stepsToMeasure.forEach((step) => { 332 step.style.position = 'absolute'; 333 step.style.visibility = 'hidden'; 334 step.style.display = 'block'; 335 step.style.width = 'auto'; 336 maxHeight = Math.max(maxHeight, step.offsetHeight); 337 step.style.position = ''; 338 step.style.visibility = ''; 339 step.style.display = ''; 340 }); 341 342 stepsWrapper.style.height = maxHeight + 'px'; 343 } 344} 345 346function initVideoJS() { 347 const videoWrappers = document.querySelectorAll('.video-js'); 348 349 videoWrappers.forEach((wrapper) => { 350 const video = wrapper.querySelector('video'); 351 const playBtn = wrapper.querySelector('.video-js__play'); 352 353 if (!video || !playBtn) return; 354 355 const updateState = () => { 356 if (video.paused) { 357 wrapper.classList.remove('video-js--playing'); 358 wrapper.classList.add('video-js--paused'); 359 } else { 360 wrapper.classList.add('video-js--playing'); 361 wrapper.classList.remove('video-js--paused'); 362 } 363 }; 364 365 playBtn.addEventListener('click', () => { 366 if (video.paused) video.play(); 367 else video.pause(); 368 }); 369 370 playBtn.addEventListener('keydown', (e) => { 371 if (e.key === 'Enter' || e.key === ' ') { 372 e.preventDefault(); 373 playBtn.click(); 374 } 375 }); 376 377 // СобÑÑÐ¸Ñ Ð²Ð¸Ð´ÐµÐ¾ 378 video.addEventListener('play', updateState); 379 video.addEventListener('pause', updateState); 380 381 wrapper.resetVideo = () => { 382 video.pause(); 383 video.currentTime = 0; 384 wrapper.classList.remove('video-js--playing', 'video-js--paused'); 385 }; 386 }); 387} 388 389function initObservers() { 390 const observer = new IntersectionObserver( 391 (entries, obs) => { 392 entries.forEach((entry) => { 393 if (!entry.isIntersecting) return; 394 395 const el = entry.target; 396 397 if (el.classList.contains('corners--animated')) { 398 el.classList.add('active'); 399 } 400 401 if (el.classList.contains('animated-numbers')) { 402 animateNumbers?.(el); 403 } 404 405 if (el.classList.contains('decor-appear') || el.classList.contains('animated')) { 406 handleImageLoad(el, () => el.classList.add('_visible'), obs); 407 return; 408 } 409 410 obs.unobserve(el); 411 }); 412 }, 413 { threshold: 0.3 } 414 ); 415 416 document 417 .querySelectorAll( 418 ` 419 .animated, 420 .corners--animated, 421 .about__stats, 422 .decor-appear, 423 .animated-numbers 424 ` 425 ) 426 .forEach((el) => observer.observe(el)); 427} 428 429function animateNumbers(container) { 430 const numbers = container.querySelectorAll('.animated--number');
431 numbers.forEach((counter) => { 432 const target = +counter.getAttribute('data-target'); 433 let current = 0; 434 const increment = Math.ceil(target / 150); 435 436 const updateCount = () => { 437 current += increment; 438 if (current < target) { 439 counter.innerText = current; 440 requestAnimationFrame(updateCount); 441 } else { 442 counter.innerText = target; 443 } 444 }; 445 446 updateCount(); 447 }); 448} 449 450function handleImageLoad(element, onAllLoaded, observer) { 451 const images = element.querySelectorAll('img'); 452 453 if (!images.length) { 454 // Ð½ÐµÑ ÐºÐ°ÑÑинок â вÑзÑваем callback ÑÑÐ°Ð·Ñ Ð¸ пеÑеÑÑаÑм наблÑдаÑÑ 455 onAllLoaded(); 456 if (observer) observer.unobserve(element); 457 return; 458 } 459 460 let loadedCount = 0; 461 const markLoaded = () => { 462 loadedCount++; 463 if (loadedCount === images.length) { 464 onAllLoaded(); 465 if (observer) observer.unobserve(element); 466 } 467 }; 468 469 images.forEach((img) => { 470 // еÑли каÑÑинка Ñже загÑÑжена (ÐºÐµÑ Ð¸Ð»Ð¸ бÑÑÑÑÑй загÑÑз) 471 if (img.complete && img.naturalWidth !== 0) { 472 markLoaded(); 473 } else { 474 // ÑлÑÑаем ÑобÑÑие load и error (error Ñоже ÑÑиÑаем "загÑÑженной", ÑÑÐ¾Ð±Ñ Ð½Ðµ веÑно ждаÑÑ) 475 const onFinish = () => markLoaded(); 476 img.addEventListener('load', onFinish, { once: true }); 477 img.addEventListener('error', onFinish, { once: true }); 478 } 479 }); 480} 481 482function initPhoneInput() { 483 // Skip already-initialized inputs (guard for re-run after AJAX modal load) 484 const inputs = Array.from(document.querySelectorAll('.phone')).filter(function (el) { 485 return !el.dataset.vpPhoneInited; 486 }); 487 inputs.forEach(function (el) { 488 el.dataset.vpPhoneInited = '1'; 489 }); 490 const masks = { 491 ru: '(999) 999-99-99', 492 kz: '(999) 999-99-99', 493 bg: '(999) 999-999', 494 }; 495 496 inputs.forEach((input) => {
497 let currentMask = masks.ru; 498 499 const iti = intlTelInput(input, { 500 initialCountry: 'ru', 501 i18n: { 502 searchPlaceholder: 'ÐоиÑк...', 503 zeroSearchResults: 'ÐиÑего не найдено', 504 oneSearchResult: 'Ðайден 1 ваÑианÑ', 505 multipleSearchResults: '${count} ваÑианÑов найдено', 506 }, 507 preferredCountries: ['ru', 'kz', 'ua', 'by'], 508 separateDialCode: false, 509 utilsScript: 'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js', 510 }); 511 512 function updateMask() { 513 const countryData = iti.getSelectedCountryData(); 514 const iso2 = countryData.iso2.toLowerCase(); 515 516 if (masks[iso2]) { 517 currentMask = masks[iso2]; 518 } else { 519 try { 520 const exampleNumber = intlTelInputUtils.getExampleNumber(iso2, false, 2); 521 const formatted = intlTelInputUtils.formatNumber(exampleNumber, iso2, 2); 522 const dialCode = countryData.dialCode; 523 let maskStr = formatted.replace('+' + dialCode + ' ', '').replace(/[0-9]/g, '9'); 524 currentMask = maskStr; 525 } catch (e) { 526 currentMask = '9999999999999'; 527 } 528 } 529 530 input.placeholder = currentMask.replace(/9/g, '0'); 531 input.value = ''; 532 } 533 534 function updateCountryCodeDisplay() { 535 const data = iti.getSelectedCountryData(); 536 const iso = data.iso2.toUpperCase(); 537 const dial = data.dialCode; 538 const btn = input.closest('.iti').querySelector('.iti__selected-country'); 539 if (!btn) return; 540 541 let label = btn.querySelector('.country-code-label'); 542 if (!label) { 543 const primaryBlock = btn.querySelector('.iti__selected-country-primary'); 544 label = document.createElement('span'); 545 label.className = 'country-code-label'; 546 primaryBlock.appendChild(label); 547 } 548 label.textContent = `${iso} (+${dial})`; 549 } 550 551 function formatPhoneInput(e) { 552 let value = e.target.value.replace(/\D/g, ''); 553 let formattedValue = ''; 554 let index = 0; 555 556 for (let i = 0; i < currentMask.length && index < value.length; i++) { 557 if (currentMask[i] === '9') { 558 formattedValue += value[index]; 559 index++; 560 } else { 561 formattedValue += currentMask[i]; 562 } 563 } 564 input.value = formattedValue; 565 } 566 567 function getMaskDigitCount() { 568 return (currentMask.match(/9/g) || []).length; 569 } 570 571 function isPhoneMaskComplete() { 572 const digits = input.value.replace(/\D/g, ''); 573 return digits.length > 0 && digits.length === getMaskDigitCount(); 574 } 575 576 function getPhoneWrap() { 577 return ( 578 input.closest('.wpcf7-form-control-wrap') || 579 input.closest('.form__group') || 580 input.parentElement 581 ); 582 } 583 584 // Ð ÑоÑÐ¼Ð°Ñ Ñ Ñабами ТелеÑон/Telegram/Max валидиÑÑем ÑолÑко акÑивнÑй ÑпоÑоб ÑвÑзи 585 function isActiveContactField(field) { 586 const changeWrap = field.closest('.change'); 587 if (!changeWrap) return true; 588 589 const formEl = field.closest('form'); 590 if (!formEl || !formEl.querySelector('.group [data-show]')) return true; 591 592 return changeWrap.classList.contains('sel'); 593 } 594 595 function showPhoneError(message) { 596 input.classList.add('wpcf7-not-valid'); 597 input.setAttribute('aria-invalid', 'true'); 598 599 const wrap = getPhoneWrap(); 600 if (!wrap) return; 601 602 let tip = wrap.querySelector('.wpcf7-not-valid-tip'); 603 if (!tip) { 604 tip = document.createElement('span'); 605 tip.className = 'wpcf7-not-valid-tip'; 606 tip.setAttribute('aria-hidden', 'true'); 607 wrap.appendChild(tip); 608 } 609 tip.textContent = message; 610 } 611 612 function clearPhoneError() { 613 input.classList.remove('wpcf7-not-valid'); 614 input.removeAttribute('aria-invalid'); 615 616 const wrap = getPhoneWrap(); 617 const tip = wrap && wrap.querySelector('.wpcf7-not-valid-tip'); 618 if (tip) tip.remove(); 619 } 620 621 // ÐкÑивнÑй ÑелеÑон обÑзаÑелен; неполнÑй по маÑке â оÑибка. ÐеакÑивнÑе ÑÐ°Ð±Ñ â пÑопÑÑкаем. 622 input._vpValidatePhone = function () { 623 if (!isActiveContactField(input)) { 624 clearPhoneError(); 625 return true; 626 } 627 628 const digits = input.value.replace(/\D/g, ''); 629 630 if (!digits.length) { 631 showPhoneError('ÐбÑзаÑелÑное поле'); 632 return false;
633 } 634 635 if (!isPhoneMaskComplete()) { 636 showPhoneError('ÐведиÑе Ð½Ð¾Ð¼ÐµÑ Ð¿Ð¾Ð»Ð½Ð¾ÑÑÑÑ'); 637 return false; 638 } 639 640 clearPhoneError(); 641 return true; 642 }; 643 644 input.setAttribute('aria-required', 'true'); 645 input.classList.add('wpcf7-validates-as-required'); 646 if (!input.hasAttribute('required')) { 647 input.required = true; 648 } 649 650 updateMask(); 651 updateCountryCodeDisplay(); 652 fixInputPadding(input); 653 654 input.addEventListener('countrychange', () => { 655 updateMask(); 656 updateCountryCodeDisplay(); 657 fixInputPadding(input); 658 clearPhoneError(); 659 }); 660 661 input.addEventListener('input', (e) => { 662 formatPhoneInput(e); 663 clearPhoneError(); 664 }); 665 666 input.addEventListener('blur', () => { 667 if (!isActiveContactField(input)) { 668 clearPhoneError(); 669 return; 670 } 671 672 const digits = input.value.replace(/\D/g, ''); 673 if (!digits.length) { 674 showPhoneError('ÐбÑзаÑелÑное поле'); 675 } else if (!isPhoneMaskComplete()) { 676 showPhoneError('ÐведиÑе Ð½Ð¾Ð¼ÐµÑ Ð¿Ð¾Ð»Ð½Ð¾ÑÑÑÑ'); 677 } 678 }); 679 680 const form = input.closest('form'); 681 if (form && !form.dataset.vpPhoneValidateBound) { 682 form.dataset.vpPhoneValidateBound = '1'; 683 684 const syncContactMethodFields = () => { 685 if (!form.querySelector('.group [data-show]')) return; 686 687 form.querySelectorAll('.change').forEach((wrap) => { 688 const active = wrap.classList.contains('sel'); 689 wrap.querySelectorAll('input, textarea, select').forEach((field) => { 690 // disabled â CF7 и бÑаÑÐ·ÐµÑ Ð½Ðµ валидиÑÑÑÑ ÑкÑÑÑÑе ÑпоÑÐ¾Ð±Ñ ÑвÑзи 691 field.disabled = !active; 692 if (!active) { 693 field.classList.remove('wpcf7-not-valid'); 694 field.removeAttribute('aria-invalid'); 695 const tip = field 696 .closest('.wpcf7-form-control-wrap') 697 ?.querySelector('.wpcf7-not-valid-tip'); 698 if (tip) tip.remove(); 699 } 700 }); 701 }); 702 }; 703 704 const getFormFields = () => 705 Array.from( 706 form.querySelectorAll( 707 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not(:disabled), textarea:not(:disabled), select:not(:disabled)' 708 ) 709 ); 710 711 const isEmptyRequired = (field) => { 712 if (field.classList.contains('phone')) return false; 713 if (field.disabled) return false; 714 const required = 715 field.required || 716 field.getAttribute('aria-required') === 'true' || 717 field.classList.contains('wpcf7-validates-as-required'); 718 return required && !String(field.value || '').trim(); 719 }; 720 721 const showFieldError = (field, message) => { 722 field.classList.add('wpcf7-not-valid'); 723 field.setAttribute('aria-invalid', 'true'); 724 725 const wrap = 726 field.closest('.wpcf7-form-control-wrap') || 727 field.closest('.wpcf7-acceptance') || 728 field.parentElement; 729 if (!wrap) return; 730 731 let tip = wrap.querySelector('.wpcf7-not-valid-tip'); 732 if (!tip) { 733 tip = document.createElement('span'); 734 tip.className = 'wpcf7-not-valid-tip'; 735 tip.setAttribute('aria-hidden', 'true'); 736 wrap.appendChild(tip); 737 } 738 tip.textContent = message; 739 }; 740 741 const clearFieldError = (field) => { 742 if (!field) return; 743 field.classList.remove('wpcf7-not-valid'); 744 field.removeAttribute('aria-invalid'); 745 746 const wrap = 747 field.closest('.wpcf7-form-control-wrap') || 748 field.closest('.wpcf7-acceptance') || 749 field.parentElement; 750 const tip = wrap && wrap.querySelector('.wpcf7-not-valid-tip'); 751 if (tip) tip.remove(); 752 }; 753 754 // ÐодÑвеÑка оÑÑалÑнÑÑ Ð¾Ð±ÑзаÑелÑнÑÑ Ð¿Ð¾Ð»ÐµÐ¹ в ÑÑиле CF7 (когда CF7 Ð¼Ñ Ð±Ð»Ð¾ÐºÐ¸ÑÑем) 755 const highlightOtherInvalidFields = () => {
756 getFormFields().forEach((field) => { 757 if (field.classList.contains('phone')) return; 758 if (isEmptyRequired(field)) { 759 showFieldError(field, 'ÐбÑзаÑелÑное поле'); 760 } 761 }); 762 763 form.querySelectorAll('input[type="checkbox"]:not(:disabled)').forEach((cb) => { 764 const isAcceptance = 765 cb.name === 'consent' || 766 cb.name === 'your-accept' || 767 cb.classList.contains('vp-consent') || 768 cb.closest('.wpcf7-acceptance'); 769 const required = 770 cb.required || cb.getAttribute('aria-required') === 'true' || isAcceptance; 771 772 if (required && !cb.checked) { 773 showFieldError(cb, 'ÐбÑзаÑелÑное поле'); 774 } 775 }); 776 }; 777 778 const focusField = (field) => { 779 if (!field || field.disabled) return; 780 field.focus(); 781 setTimeout(() => field.focus(), 0); 782 setTimeout(() => field.focus(), 50); 783 setTimeout(() => field.focus(), 150); 784 }; 785 786 // Ðогика ÑелеÑона наÑа; пÑи оÑибке ÑелеÑона CF7 не запÑÑкаеÑÑÑ â 787 // поÑÑÐ¾Ð¼Ñ Ð¾ÑÑалÑнÑе обÑзаÑелÑнÑе Ð¿Ð¾Ð»Ñ Ð¿Ð¾Ð´ÑвеÑиваем Ñами, как CF7. 788 const blockInvalidPhones = (e) => { 789 syncContactMethodFields(); 790 791 const phones = form.querySelectorAll('.phone'); 792 let hasInvalidPhone = false; 793 794 phones.forEach((phoneEl) => { 795 if (phoneEl.disabled) return; 796 if (typeof phoneEl._vpValidatePhone === 'function' && !phoneEl._vpValidatePhone()) { 797 hasInvalidPhone = true; 798 } 799 }); 800 801 // ТелеÑон ок â оÑдаÑм CF7 (ФÐÐ, ÑоглаÑие и Ñ.д. подÑвеÑÑÑÑÑ ÐºÐ°Ðº ÑанÑÑе) 802 if (!hasInvalidPhone) return false; 803 804 highlightOtherInvalidFields(); 805 806 e.preventDefault(); 807 e.stopImmediatePropagation(); 808 809 const firstInvalid = [ 810 ...getFormFields(), 811 ...form.querySelectorAll('input[type="checkbox"]:not(:disabled)'), 812 ].find((field) => field.classList.contains('wpcf7-not-valid')); 813 814 focusField(firstInvalid || form.querySelector('.phone.wpcf7-not-valid:not(:disabled)')); 815 return true; 816 }; 817 818 syncContactMethodFields(); 819 820 // Снимаем оÑибки ÑÑÐ°Ð·Ñ Ð¿Ñи заполнении / ÑекбокÑе â не ждаÑÑ blur 821 form.addEventListener('input', (e) => { 822 const field = e.target; 823 if (!(field instanceof HTMLElement)) return; 824 if (field.classList.contains('phone')) return; 825 if (field.matches('input, textarea, select')) { 826 clearFieldError(field); 827 } 828 }); 829 830 form.addEventListener('change', (e) => { 831 const field = e.target; 832 if (!(field instanceof HTMLElement)) return; 833 if (field.matches('input[type="checkbox"], input[type="radio"], select')) { 834 clearFieldError(field); 835 } 836 }); 837 838 form.addEventListener('submit', blockInvalidPhones, true); 839 840 form 841 .querySelectorAll('button:not([type="button"]), input[type="submit"], .wpcf7-submit') 842 .forEach((btn) => { 843 btn.addEventListener('click', blockInvalidPhones, true); 844 }); 845 846 form.querySelectorAll('.group [data-show]').forEach((tab) => { 847 tab.addEventListener('click', () => { 848 // scripts.js пеÑеклÑÑÐ°ÐµÑ .sel ÑÐ¸Ð½Ñ Ñонно; ÑÐ¸Ð½Ñ ÑонизиÑÑем поÑле него 849 setTimeout(syncContactMethodFields, 0); 850 }); 851 }); 852 } 853 854 setTimeout(fixInputPadding, 200); 855 }); 856 857 // ÐÑли CF7 вÑÑ Ð¶Ðµ пÑоÑÑл и ÑÑокÑÑиÑовал поле поÑле ÑелеÑона â веÑнÑм ÑокÑÑ 858 if (!document.documentElement.dataset.vpPhoneCf7Bound) { 859 document.documentElement.dataset.vpPhoneCf7Bound = '1'; 860 861 document.addEventListener('wpcf7invalid', (event) => { 862 const form = event.target; 863 if (!(form instanceof HTMLFormElement)) return; 864 865 const fields = Array.from( 866 form.querySelectorAll( 867 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not(:disabled), textarea:not(:disabled), select:not(:disabled)' 868 ) 869 ); 870 871 form.querySelectorAll('.phone:not(:disabled)').forEach((phoneEl) => { 872 if (typeof phoneEl._vpValidatePhone === 'function') { 873 phoneEl._vpValidatePhone(); 874 } 875 }); 876 877 const firstInvalid = fields.find((field) => { 878 if (field.classList.contains('phone')) { 879 return field.classList.contains('wpcf7-not-valid'); 880 } 881 return ( 882 field.classList.contains('wpcf7-not-valid') || 883 field.getAttribute('aria-invalid') === 'true' 884 ); 885 }); 886 887 if (firstInvalid && firstInvalid.classList.contains('phone')) { 888 setTimeout(() => firstInvalid.focus(), 0); 889 setTimeout(() => firstInvalid.focus(), 50); 890 } 891 }); 892 } 893} 894 895function fixInputPadding(input) { 896 if (!input) return; 897 898 const itiWrapper = input.closest('.iti'); 899 if (!itiWrapper) return; 900 901 if (getComputedStyle(itiWrapper).display === 'none') return; 902 903 const btn = itiWrapper.querySelector('.iti__selected-country'); 904 if (!btn) return; 905 906 const width = btn.getBoundingClientRect().width; 907 if (!width) return; 908 909 input.style.paddingLeft = width + 6 + 'px'; 910} 911 912function initializeSliders() { 913 document.querySelectorAll('.js-before-after')?.forEach(initBeforeAfterSlider); 914 initCasesSlider(); 915 initWhySlider(); 916 initAwardsSlider(); 917 initServicesSlider(); 918 initShowcaseSlider(); 919 initNewMediaSlider(); 920 initAchievementSlider(); 921 initHeroAwardsSlider(); 922 initStagesSlider(); 923 initStagesSlider2(); 924 initStagesSlider3(); 925 initStagesSlider4(); 926 initStagesSlider5(); 927 initVpCardsSliders(); 928 initCompositionSlider(); 929 initResultSlider(); 930 initResultSlider2(); 931 initProcessSlider(); 932 initWorkflowSlider(); 933 initStepsSlider(); 934 initAuditSlider(); 935 //initSingleSlider(); 936 initMaterialsSlider(); 937 initPhotosSlider(); 938 initBlogPostSlider(); 939 initCategoryHeroSlider(); 940 941 const responsiveSwipers = []; 942 943 responsiveSwipers.push( 944 setupResponsiveSwiper({ 945 selector: '.consultation__awards', 946 breakpoint: 768, 947 options: { 948 slidesPerView: 1, 949 spaceBetween: 20, 950 loop: false, 951 pagination: { 952 el: '.swiper-pagination', 953 clickable: true, 954 }, 955 }, 956 }) 957 ); 958 959 document.addEventListener('click', (e) => { 960 const btn = e.target.closest('.slide-to-button'); 961 if (!btn) return; 962 963 const sliderName = btn.dataset.slider; 964 const index = +btn.dataset.index - 1; 965 966 const container = btn.dataset.container; 967 const component = btn.closest(container) || btn.closest('.section'); 968
969 const sliderEl = component.querySelector(`.swiper[data-slider-name="${sliderName}"]`); 970 971 if (!sliderEl || !sliderEl.swiper) return; 972 973 const swiper = sliderEl.swiper; 974 975 const slideCount = swiper.slides.length; 976 if (index < 0 || index >= slideCount) return; 977 978 if (swiper.params.loop) { 979 swiper.slideToLoop(index); 980 } else { 981 swiper.slideTo(index); 982 } 983 }); 984} 985 986function tabsInit() { 987 const tabSections = document.querySelectorAll('.tab'); 988 989 tabSections.forEach(function (tab) { 990 const tabButtons = tab.querySelectorAll('.tab-button'); 991 const tabSelect = tab.querySelector('select'); 992 993 tabButtons.forEach(function (button) { 994 button.addEventListener('click', function () { 995 if (this.classList.contains('tab-button--active')) return; 996 997 const contentID = this.dataset.content; 998 const activeContent = tab.querySelector('.tab-content--active'); 999 const activeButton = tab.querySelector('.tab-button--active'); 1000 const contentToActivate = document.getElementById(contentID); 1001 1002 if (contentToActivate) { 1003 activeContent?.classList.remove('tab-content--active'); 1004 activeButton?.classList.remove('tab-button--active'); 1005 1006 this.classList.add('tab-button--active'); 1007 contentToActivate.classList.add('tab-content--active'); 1008 1009 if (tabSelect) { 1010 tabSelect.value = contentID; 1011 tabSelect.dispatchEvent(new Event('change', { bubbles: true })); 1012 } 1013 } 1014 }); 1015 }); 1016 1017 if (tabSelect) { 1018 tabSelect.addEventListener('change', function () { 1019 const selectedValue = this.value; 1020 const correspondingButton = tab.querySelector( 1021 `.tab-button[data-content="${selectedValue}"]` 1022 ); 1023 if (correspondingButton) { 1024 correspondingButton.click(); 1025 } 1026 }); 1027 } 1028 }); 1029} 1030 1031function setupResponsiveSwiper({ selector, breakpoint = 768, options = {} }) { 1032 let swiperInstance; 1033 1034 function checkSwiper() { 1035 const windowWidth = window.innerWidth; 1036 1037 if (windowWidth < breakpoint && !swiperInstance) { 1038 swiperInstance = new Swiper(selector, options); 1039 } else if (windowWidth >= breakpoint && swiperInstance) { 1040 swiperInstance.destroy(true, true); 1041 swiperInstance = undefined; 1042 } 1043 } 1044 1045 checkSwiper(); 1046 1047 window.addEventListener('resize', checkSwiper); 1048 window.addEventListener('orientationchange', () => setTimeout(checkSwiper, 200)); 1049 1050 return { 1051 getInstance: () => swiperInstance, 1052 }; 1053} 1054 1055function initPhotosSlider() { 1056 document.querySelectorAll('.photos__slider').forEach((sliderEl, i) => { 1057 const swiper = new Swiper(sliderEl, { 1058 freeMode: true, 1059 speed: 600, 1060 spaceBetween: 0, 1061 breakpoints: { 1062 0: { 1063 slidesPerView: 'auto', 1064 }, 1065 768: { 1066 slidesPerView: 'auto', 1067 }, 1068 1024: { 1069 slidesPerView: 4, 1070 }, 1071 }, 1072 }); 1073 }); 1074} 1075 1076function initShowcaseSlider() { 1077 document.querySelectorAll('.showcase').forEach(showcase => { 1078 if (showcase.dataset.inited) return; 1079 showcase.dataset.inited = '1'; 1080 1081 const thumbsEl = showcase.querySelector('.showcase__thumbs-slider'); 1082 const mainEl = showcase.querySelector('.showcase__slider'); 1083 if (!mainEl) return; 1084 1085 const SPEED = 600; 1086 const prevBtns = showcase.querySelectorAll('.showcase__nav--prev'); 1087 const nextBtns = showcase.querySelectorAll('.showcase__nav--next'); 1088 1089 let showcaseThumbs = null; 1090 let showcaseMain = null; 1091 let activeThumbIndex = 0; 1092 1093 function updateNav() { 1094 const target = showcaseThumbs || showcaseMain; 1095 if (!target) return; 1096 const atStart = target.isBeginning; 1097 const atEnd = target.isEnd; 1098 prevBtns.forEach(btn => { 1099 btn.classList.toggle('swiper-button-disabled', atStart); 1100 btn.setAttribute('aria-disabled', atStart ? 'true' : 'false'); 1101 }); 1102 nextBtns.forEach(btn => { 1103 btn.classList.toggle('swiper-button-disabled', atEnd); 1104 btn.setAttribute('aria-disabled', atEnd ? 'true' : 'false'); 1105 }); 1106 } 1107 1108 function setActiveThumb(idx) { 1109 activeThumbIndex = idx; 1110 if (!showcaseThumbs) return;
1111 showcaseThumbs.slides.forEach((slide, i) => { 1112 slide.classList.toggle('swiper-slide-thumb-active', i === idx); 1113 }); 1114 } 1115 1116 if (thumbsEl) { 1117 const wrapper = thumbsEl.querySelector('.swiper-wrapper'); 1118 Array.from(wrapper.children).forEach((slide, i) => { 1119 slide.dataset.thumbIndex = i; 1120 }); 1121 1122 showcaseThumbs = new Swiper(thumbsEl, { 1123 slidesPerView: 4, 1124 spaceBetween: 36, 1125 loop: false, 1126 watchOverflow: true, 1127 speed: SPEED, 1128 initialSlide: 1, 1129 watchSlidesProgress: true, 1130 breakpoints: { 1131 0: { slidesPerView: 2 }, 1132 576: { slidesPerView: 3 }, 1133 992: { slidesPerView: 4 }, 1134 }, 1135 }); 1136 } 1137 1138 showcaseMain = new Swiper(mainEl, { 1139 slidesPerView: 'auto', 1140 spaceBetween: 0, 1141 effect: 'slide', 1142 speed: SPEED, 1143 loop: false, 1144 watchOverflow: true, 1145 // ÐеÑÑ Ð½Ð¸Ð¹ ÑÐ»Ð°Ð¹Ð´ÐµÑ Ð¼ÐµÐ½ÑеÑÑÑ ÑолÑко кликом по пÑевÑÑ 1146 allowTouchMove: !showcaseThumbs, 1147 on: { 1148 slideChange: () => { 1149 showcase.querySelectorAll('.video-js').forEach(wrapper => { 1150 if (wrapper.resetVideo) wrapper.resetVideo(); 1151 }); 1152 if (!showcaseThumbs) updateNav(); 1153 }, 1154 }, 1155 }); 1156 1157 if (showcaseThumbs) { 1158 setActiveThumb(0); 1159 1160 showcaseThumbs.on('click', (s) => { 1161 const slide = s.clickedSlide; 1162 if (!slide) return; 1163 const idx = Number(slide.dataset.thumbIndex); 1164 if (Number.isNaN(idx) || idx === activeThumbIndex) return; 1165 setActiveThumb(idx); 1166 showcaseMain.slideTo(idx); 1167 }); 1168 1169 showcaseThumbs.on('slideChange', updateNav); 1170 showcaseThumbs.on('reachBeginning', updateNav); 1171 showcaseThumbs.on('reachEnd', updateNav); 1172 showcaseThumbs.on('fromEdge', updateNav); 1173 } 1174 1175 nextBtns.forEach(btn => 1176 btn.addEventListener('click', () => { 1177 if (btn.classList.contains('swiper-button-disabled')) return; 1178 if (showcaseThumbs) showcaseThumbs.slideNext(); 1179 else showcaseMain.slideNext(); 1180 }) 1181 ); 1182 prevBtns.forEach(btn => 1183 btn.addEventListener('click', () => { 1184 if (btn.classList.contains('swiper-button-disabled')) return; 1185 if (showcaseThumbs) showcaseThumbs.slidePrev(); 1186 else showcaseMain.slidePrev(); 1187 }) 1188 ); 1189 1190 updateNav(); 1191 }); 1192} 1193 1194function initNewMediaSlider() { 1195 const wrap = document.querySelector('.new-media__slider-wrap'); 1196 const el = wrap?.querySelector('.new-media__slider'); 1197 if (!el) return; 1198 1199 const mq = window.matchMedia('(max-width: 1024px)'); 1200 let swiper = null; 1201 1202 const sync = () => { 1203 if (mq.matches) { 1204 if (swiper) return; 1205 swiper = new Swiper(el, { 1206 slidesPerView: 'auto', 1207 centeredSlides: true, 1208 loop: true, 1209 speed: 500, 1210 spaceBetween: 24, 1211 navigation: { 1212 nextEl: wrap.querySelector('.new-media__nav--next'), 1213 prevEl: wrap.querySelector('.new-media__nav--prev'), 1214 }, 1215 }); 1216 return; 1217 } 1218 1219 if (swiper) { 1220 swiper.destroy(true, true); 1221 swiper = null; 1222 } 1223 }; 1224 1225 if (mq.addEventListener) { 1226 mq.addEventListener('change', sync); 1227 } else { 1228 mq.addListener(sync); 1229 } 1230 1231 sync(); 1232} 1233 1234function initAchievementSlider() { 1235 document.querySelectorAll('.achievement__items').forEach((items) => { 1236 if (items.closest('.achievement__slider-wrap')) return; 1237 1238 const wrap = document.createElement('div'); 1239 wrap.className = 'achievement__slider-wrap'; 1240 1241 const slider = document.createElement('div'); 1242 slider.className = 'swiper achievement__slider'; 1243 1244 items.parentNode.insertBefore(wrap, items); 1245 wrap.appendChild(slider); 1246 slider.appendChild(items); 1247 1248 items.classList.add('swiper-wrapper'); 1249 items.querySelectorAll('.achievement__item').forEach((item) => { 1250 item.classList.add('swiper-slide'); 1251 }); 1252 1253 const nav = document.createElement('div'); 1254 nav.className = 'slider-nav slider-nav--absolute achievement__nav show-tablet'; 1255 nav.innerHTML = ` 1256 <button type="button" class="btn-reset slider-nav__button slider-nav__button--prev achievement__nav--prev" aria-label="ÐÑедÑдÑÑий Ñлайд"></button> 1257 <button type="button" class="btn-reset slider-nav__button slider-nav__button--next achievement__nav--next" aria-label="СледÑÑÑий Ñлайд"></button> 1258 `; 1259 wrap.appendChild(nav); 1260 1261 const mq = window.matchMedia('(max-width: 1024px)'); 1262 let swiper = null; 1263 1264 const sync = () => { 1265 if (mq.matches) { 1266 if (swiper) return; 1267 swiper = new Swiper(slider, { 1268 slidesPerView: 'auto', 1269 centeredSlides: true, 1270 loop: true, 1271 speed: 500, 1272 spaceBetween: 24, 1273 navigation: { 1274 nextEl: wrap.querySelector('.achievement__nav--next'), 1275 prevEl: wrap.querySelector('.achievement__nav--prev'), 1276 }, 1277 }); 1278 return; 1279 } 1280 1281 if (swiper) { 1282 swiper.destroy(true, true); 1283 swiper = null; 1284 } 1285 }; 1286 1287 if (mq.addEventListener) { 1288 mq.addEventListener('change', sync); 1289 } else { 1290 mq.addListener(sync); 1291 } 1292 1293 sync(); 1294 }); 1295} 1296 1297function initHeroAwardsSlider() { 1298 document.querySelectorAll('.new-hero__awards--after .new-hero__awards-inner').forEach((items) => { 1299 if (items.closest('.new-hero__awards-slider-wrap')) return; 1300 1301 const wrap = document.createElement('div'); 1302 wrap.className = 'new-hero__awards-slider-wrap'; 1303 1304 const slider = document.createElement('div'); 1305 slider.className = 'swiper new-hero__awards-slider'; 1306 1307 items.parentNode.insertBefore(wrap, items); 1308 wrap.appendChild(slider); 1309 slider.appendChild(items); 1310 1311 items.classList.add('swiper-wrapper'); 1312 items.querySelectorAll('.new-hero__award').forEach((item) => { 1313 item.classList.add('swiper-slide'); 1314 }); 1315 1316 const nav = document.createElement('div'); 1317 nav.className = 'slider-nav slider-nav--absolute new-hero__awards-nav show-tablet'; 1318 nav.innerHTML = ` 1319 <button type="button" class="btn-reset slider-nav__button slider-nav__button--prev new-hero__awards-nav--prev" aria-label="ÐÑедÑдÑÑий Ñлайд"></button> 1320 <button type="button" class="btn-reset slider-nav__button slider-nav__button--next new-hero__awards-nav--next" aria-label="СледÑÑÑий Ñлайд"></button> 1321 `; 1322 wrap.appendChild(nav); 1323 1324 const mq = window.matchMedia('(max-width: 1024px)'); 1325 let swiper = null; 1326 1327 const sync = () => { 1328 if (mq.matches) { 1329 if (swiper) return; 1330 swiper = new Swiper(slider, { 1331 slidesPerView: 'auto', 1332 centeredSlides: true, 1333 loop: true, 1334 loopedSlides: 2, 1335 speed: 500, 1336 spaceBetween: 24, 1337 navigation: { 1338 nextEl: wrap.querySelector('.new-hero__awards-nav--next'), 1339 prevEl: wrap.querySelector('.new-hero__awards-nav--prev'), 1340 }, 1341 }); 1342 return; 1343 } 1344 1345 if (swiper) { 1346 swiper.destroy(true, true); 1347 swiper = null; 1348 } 1349 }; 1350 1351 if (mq.addEventListener) { 1352 mq.addEventListener('change', sync); 1353 } else { 1354 mq.addListener(sync); 1355 } 1356 1357 sync(); 1358 }); 1359} 1360 1361function initCompositionSlider() { 1362 const compositionSlider = new Swiper('.composition__slider', { 1363 slidesPerView: 1, 1364 centeredSlides: true, 1365 spaceBetween: 20, 1366 loop: true, 1367 speed: 400, 1368 pagination: { 1369 el: '.composition__pagination', 1370 clickable: true, 1371 }, 1372 1373 navigation: { 1374 nextEl: '.composition__nav--next', 1375 prevEl: '.composition__nav--prev', 1376 }, 1377 1378 breakpoints: { 1379 768: { 1380 slidesPerView: 1.2, 1381 spaceBetween: 40, 1382 }, 1383 1025: { 1384 slidesPerView: 1.4, 1385 spaceBetween: 40, 1386 }, 1387 1366: { 1388 slidesPerView: 1.6, 1389 spaceBetween: 100, 1390 }, 1391 }, 1392 }); 1393} 1394 1395function initStagesSlider() { 1396 const sliderEl = document.querySelector('.stages__slider'); 1397 1398 if (!sliderEl) return; 1399 1400 const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length; 1401 1402 const slidesPerView = 3; 1403 const isEnoughSlides = slidesCount > slidesPerView; 1404 1405 const stagesSlider = new Swiper(sliderEl, { 1406 grabCursor: true, 1407 loop: isEnoughSlides, 1408 centeredSlides: isEnoughSlides, 1409 speed: 500, 1410 loop: true, 1411 pagination: { 1412 el: '.stages__pagination', 1413 clickable: true, 1414 }, 1415 navigation: { 1416 nextEl: '.stages__nav--next', 1417 prevEl: '.stages__nav--prev', 1418 }, 1419 1420 slidesPerView: slidesPerView, 1421 spaceBetween: 74, 1422 1423 breakpoints: { 1424 0: { 1425 slidesPerView: 1, 1426 centeredSlides: true, 1427 spaceBetween: 10, 1428 }, 1429 768: { 1430 spaceBetween: 30, 1431 }, 1432 1440: { 1433 spaceBetween: 74, 1434 }, 1435 }, 1436 }); 1437} 1438function initStagesSlider3() { 1439 const sliderEl = document.querySelector('.stages__slider3'); 1440 1441 if (!sliderEl) return; 1442 1443 const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length; 1444 1445 const slidesPerView = 4; 1446 const isEnoughSlides = slidesCount > slidesPerView; 1447 1448 const stagesSlider = new Swiper(sliderEl, { 1449 grabCursor: true, 1450 loop: isEnoughSlides, 1451 centeredSlides: isEnoughSlides, 1452 speed: 500, 1453 loop: true, 1454 navigation: { 1455 nextEl: '.stages3__nav--next', 1456 prevEl: '.stages3__nav--prev', 1457 }, 1458 1459 slidesPerView: slidesPerView, 1460 spaceBetween: 0, 1461 1462 breakpoints: { 1463 0: { 1464 slidesPerView: 1, 1465 centeredSlides: true, 1466 }, 1467 768: { 1468 slidesPerView: 2, 1469 }, 1470 1024: { 1471 slidesPerView: 3, 1472 }, 1473 1440: { 1474 slidesPerView: 4, 1475 }, 1476 }, 1477 }); 1478} 1479function initStagesSlider4() { 1480 const sliderEl = document.querySelector('.stages__slider4'); 1481 1482 if (!sliderEl) return; 1483 1484 const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length; 1485 1486 const slidesPerView = 4; 1487 const isEnoughSlides = slidesCount > slidesPerView; 1488 1489 const stagesSlider = new Swiper(sliderEl, { 1490 grabCursor: true, 1491 loop: isEnoughSlides, 1492 centeredSlides: isEnoughSlides, 1493 speed: 500, 1494 loop: true, 1495 navigation: { 1496 nextEl: '.stages4__nav--next', 1497 prevEl: '.stages4__nav--prev', 1498 }, 1499 1500 slidesPerView: slidesPerView, 1501 spaceBetween: 0, 1502 1503 breakpoints: { 1504 0: { 1505 slidesPerView: 1, 1506 centeredSlides: true, 1507 }, 1508 768: { 1509 slidesPerView: 2, 1510 }, 1511 1024: { 1512 slidesPerView: 3, 1513 }, 1514 1440: { 1515 slidesPerView: 4, 1516 }, 1517 }, 1518 }); 1519} 1520function markStagesCurrentSlides(swiper) { 1521 const desktop = window.innerWidth >= 1024; 1522 const perView = Number(swiper.params.slidesPerView); 1523 const count = desktop && Number.isFinite(perView) ? perView : 0; 1524 const start = swiper.activeIndex; 1525
1526 swiper.slides.forEach((slide, index) => { 1527 slide.classList.toggle('is-current', count > 0 && index >= start && index < start + count); 1528 }); 1529} 1530function initStagesSlider2() { 1531 const sliderEl = document.querySelector('.stages__slider2'); 1532 1533 if (!sliderEl) return; 1534 1535 const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length; 1536 1537 const slidesPerView = 3; 1538 1539 let initialSlide = 0; 1540 if (slidesCount > 4) initialSlide = 2; 1541 else if (slidesCount > 2) initialSlide = 1; 1542 1543 const stagesSlider = new Swiper(sliderEl, { 1544 grabCursor: true, 1545 centeredSlides: true, 1546 speed: 500, 1547 loop: false, 1548 initialSlide, 1549 pagination: { 1550 el: '.stages2__pagination', 1551 clickable: true, 1552 }, 1553 navigation: { 1554 nextEl: '.stages2__nav--next', 1555 prevEl: '.stages2__nav--prev', 1556 }, 1557 1558 slidesPerView: slidesPerView, 1559 spaceBetween: 74, 1560 1561 breakpoints: { 1562 0: { 1563 spaceBetween: 0, 1564 }, 1565 1024: { 1566 spaceBetween: 30, 1567 }, 1568 1440: { 1569 spaceBetween: 74, 1570 }, 1571 }, 1572 }); 1573} 1574function initStagesSlider5() { 1575 //const sliderEl = document.querySelector('.stages__slider5'); 1576 1577 document.querySelectorAll('.stages_slider5')?.forEach((sliderEl) => { 1578 if (!sliderEl) return; 1579 1580 const sliderElswiper = sliderEl.querySelector('.stages__slider5'); 1581 1582 const prevBtn = sliderEl.querySelectorAll('.steps__nav--prev'); 1583 const nextBtn = sliderEl.querySelectorAll('.steps__nav--next, .slide-next'); 1584 1585 // Catalog keeps auto-width side peeks below 1024px. From 1024px both skins 1586 // show three slides and step by three, without centering. 1587 const isCatalog = sliderEl.classList.contains('stages--catalog'); 1588 1589 const stagesSlider = new Swiper(sliderElswiper, { 1590 grabCursor: true, 1591 centeredSlides: true, 1592 speed: 500, 1593 loop: false, 1594 initialSlide: 0, 1595 slidesPerGroup: 1, 1596 navigation: { 1597 nextEl: sliderEl.querySelector('.stages2__nav--next'), 1598 prevEl: sliderEl.querySelector('.stages2__nav--prev'), 1599 }, 1600 pagination: { 1601 el: sliderEl.querySelector('.stages5__pagination'), 1602 clickable: true, 1603 }, 1604 1605 slidesPerView: isCatalog ? 'auto' : 1, 1606 spaceBetween: isCatalog ? 14 : 0, 1607 1608 breakpoints: isCatalog 1609 ? { 1610 0: { spaceBetween: 14, slidesPerView: 'auto', slidesPerGroup: 1, centeredSlides: true }, 1611 1024: { spaceBetween: 36, slidesPerView: 3, slidesPerGroup: 3, centeredSlides: false }, 1612 1440: { spaceBetween: 56, slidesPerView: 3, slidesPerGroup: 3, centeredSlides: false }, 1613 } 1614 : { 1615 0: { 1616 spaceBetween: 0, 1617 slidesPerView: 1, 1618 slidesPerGroup: 1, 1619 centeredSlides: true, 1620 }, 1621 1024: { 1622 spaceBetween: 30, 1623 slidesPerView: 3, 1624 slidesPerGroup: 3, 1625 centeredSlides: false, 1626 }, 1627 1440: { 1628 spaceBetween: 74, 1629 slidesPerView: 3, 1630 slidesPerGroup: 3, 1631 centeredSlides: false, 1632 }, 1633 }, 1634 1635 on: { 1636 init: markStagesCurrentSlides, 1637 slideChange: markStagesCurrentSlides, 1638 breakpoint: markStagesCurrentSlides, 1639 resize: markStagesCurrentSlides, 1640 }, 1641 }); 1642 nextBtn?.forEach((btn) => { 1643 btn.addEventListener('click', () => stagesSlider.slideNext()); 1644 }); 1645 1646 prevBtn?.forEach((btn) => { 1647 btn.addEventListener('click', () => stagesSlider.slidePrev()); 1648 }); 1649 }); 1650} 1651 1652// Card sliders on catalog terms (blocks A/B, brief 4.6). Two skins share one 1653// init: "concepts" centers the active framed slide, "features" is a plain 1654// multi-card loop. Fraction counter is shown on mobile only. 1655function initVpCardsSliders() { 1656 document.querySelectorAll('.vp-cards').forEach((sectionEl) => { 1657 const swiperEl = sectionEl.querySelector('.vp-cards__slider'); 1658 if (!swiperEl) return; 1659 1660 const isConcepts = sectionEl.classList.contains('vp-cards--concepts'); 1661 const total = 1662 parseInt(swiperEl.dataset.total || '0', 10) || 1663 swiperEl.querySelectorAll('.swiper-slide').length; 1664 const currentEl = sectionEl.querySelector('.slider-nums__current'); 1665 const totalEl = sectionEl.querySelector('.slider-nums__total'); 1666 1667 const slider = new Swiper(swiperEl, { 1668 grabCursor: true, 1669 speed: 500, 1670 centeredSlides: isConcepts, 1671 loop: false, 1672 rewind: true, 1673 initialSlide: isConcepts && window.innerWidth >= 1024 ? 1 : 0, 1674 breakpoints: isConcepts 1675 ? { 1676 0: { slidesPerView: 'auto', spaceBetween: 24 }, 1677 768: { slidesPerView: 'auto', spaceBetween: 36 }, 1678 1024: { slidesPerView: 'auto', spaceBetween: 29 }, 1679 1700: { slidesPerView: 'auto', spaceBetween: 34 }, 1680 1920: { slidesPerView: 'auto', spaceBetween: 40 }, 1681 } 1682 : { 1683 0: { slidesPerView: 'auto', spaceBetween: 4 }, 1684 768: { slidesPerView: 'auto', spaceBetween: 24 }, 1685 1200: { slidesPerView: 'auto', spaceBetween: 68 }, 1686 1800: { slidesPerView: 'auto', spaceBetween: 85 }, 1687 1920: { slidesPerView: 'auto', spaceBetween: 91 }, 1688 }, 1689 on: { 1690 init(sw) { 1691 if (totalEl) totalEl.textContent = total; 1692 if (currentEl) currentEl.textContent = (sw.realIndex % total) + 1; 1693 }, 1694 slideChange(sw) { 1695 if (currentEl) currentEl.textContent = (sw.realIndex % total) + 1; 1696 }, 1697 }, 1698 }); 1699
1700 sectionEl.querySelectorAll('.vp-cards__nav--prev, .vp-cards__nums-btn--prev').forEach((btn) => { 1701 btn.addEventListener('click', () => slider.slidePrev()); 1702 }); 1703 sectionEl.querySelectorAll('.vp-cards__nav--next, .vp-cards__nums-btn--next').forEach((btn) => { 1704 btn.addEventListener('click', () => slider.slideNext()); 1705 }); 1706 }); 1707} 1708 1709function initAuditSlider() { 1710 document.querySelectorAll('.audit__slider').forEach((sliderEl) => { 1711 const wrapper = sliderEl.closest('.audit__slider-wrapper') || sliderEl.closest('.section'); 1712 1713 const swiper = new Swiper(sliderEl, { 1714 grabCursor: true, 1715 slidesPerView: 3, 1716 spaceBetween: 30, 1717 loop: true, 1718 speed: 500, 1719 navigation: { 1720 nextEl: '.audit__nav--next', 1721 prevEl: '.audit__nav--prev', 1722 }, 1723 on: { 1724 init() { 1725 toggleLockedClass(this, wrapper); 1726 }, 1727 resize() { 1728 toggleLockedClass(this, wrapper); 1729 }, 1730 }, 1731 breakpoints: { 1732 0: { 1733 slidesPerView: 1, 1734 spaceBetween: 50, 1735 }, 1736 1025: { 1737 spaceBetween: 30, 1738 }, 1739 1440: { 1740 spaceBetween: 100, 1741 }, 1742 }, 1743 }); 1744 }); 1745} 1746 1747function initStepsSlider() { 1748 document.querySelectorAll('.steps')?.forEach((stepsBlock) => { 1749 const sliderEl = stepsBlock.querySelector('.steps__slider'); 1750 const prevBtn = stepsBlock.querySelectorAll('.steps__nav--prev'); 1751 const nextBtn = stepsBlock.querySelectorAll('.steps__nav--next, .slide-next'); 1752 const numsPagination = stepsBlock.querySelector('.slider-nums'); 1753 1754 const currentEl = numsPagination.querySelector('.slider-nums__current'); 1755 const totalEl = numsPagination.querySelector('.slider-nums__total'); 1756 1757 let swiper; 1758 1759 const getActiveInnerSlider = () => { 1760 const activeSlide = swiper.slides[swiper.activeIndex]; 1761 return activeSlide?.querySelector('.single-slider')?.swiper || null; 1762 }; 1763 1764 const handleStepsNav = (direction) => { 1765 const innerSwiper = getActiveInnerSlider(); 1766 1767 if (innerSwiper) { 1768 if (direction === 'next' && !innerSwiper.isEnd) { 1769 innerSwiper.slideNext(); 1770 return; 1771 } 1772 if (direction === 'prev' && !innerSwiper.isBeginning) { 1773 innerSwiper.slidePrev(); 1774 return; 1775 } 1776 } 1777 1778 if (direction === 'next') { 1779 swiper.slideNext(); 1780 } else { 1781 swiper.slidePrev(); 1782 } 1783 }; 1784 1785 const updateArrows = () => { 1786 const innerSwiper = getActiveInnerSlider(); 1787 const atStart = swiper.isBeginning && (!innerSwiper || innerSwiper.isBeginning); 1788 const atEnd = swiper.isEnd && (!innerSwiper || innerSwiper.isEnd); 1789
1790 prevBtn.forEach((btn) => { 1791 btn.classList.toggle('swiper-button-disabled', atStart); 1792 btn.setAttribute('aria-disabled', atStart ? 'true' : 'false'); 1793 }); 1794 nextBtn.forEach((btn) => { 1795 if (!btn.classList.contains('steps__nav--next')) return; 1796 btn.classList.toggle('swiper-button-disabled', atEnd); 1797 btn.setAttribute('aria-disabled', atEnd ? 'true' : 'false'); 1798 }); 1799 }; 1800 1801 swiper = new Swiper(sliderEl, { 1802 grabCursor: true, 1803 centeredSlides: true, 1804 slidesPerView: 'auto', 1805 speed: 500, 1806 spaceBetween: 90, 1807 1808 breakpoints: { 1809 0: { spaceBetween: 15 }, 1810 768: { spaceBetween: 30 }, 1811 1024: { spaceBetween: 90 }, 1812 }, 1813 1814 on: { 1815 init(swiper) { 1816 totalEl.textContent = swiper.slides.length; 1817 currentEl.textContent = swiper.realIndex + 1; 1818 }, 1819 slideChange(swiper) { 1820 currentEl.textContent = swiper.realIndex + 1; 1821 }, 1822 }, 1823 }); 1824 1825 swiper.on('slideChange', updateArrows); 1826 updateArrows(); 1827 1828 setTimeout(() => { 1829 sliderEl.querySelectorAll('.single-slider').forEach((el) => { 1830 if (!el.swiper || el.dataset.stepsArrowsBound) return; 1831 el.dataset.stepsArrowsBound = '1'; 1832 el.swiper.on('slideChange', updateArrows); 1833 }); 1834 updateArrows(); 1835 }, 0); 1836 1837 nextBtn?.forEach((btn) => { 1838 btn.addEventListener('click', () => handleStepsNav('next')); 1839 }); 1840 1841 prevBtn?.forEach((btn) => { 1842 btn.addEventListener('click', () => handleStepsNav('prev')); 1843 }); 1844 }); 1845} 1846 1847function initWorkflowSlider() { 1848 document.querySelectorAll('.workflow__slider')?.forEach((sliderEl) => { 1849 const wrapper = sliderEl.closest('.workflow') || sliderEl.closest('.section'); 1850 const pagination = wrapper.querySelector('.workflow__pagination'); 1851 1852 const swiper = new Swiper(sliderEl, { 1853 grabCursor: true, 1854 centeredSlides: true, 1855 slidesPerView: 1, 1856 loop: true, 1857 speed: 500, 1858 spaceBetween: 50, 1859 pagination: { 1860 el: pagination, 1861 clickable: true, 1862 }, 1863 navigation: { 1864 nextEl: '.workflow__nav--next', 1865 prevEl: '.workflow__nav--prev', 1866 }, 1867 breakpoints: { 1868 0: { 1869 spaceBetween: 20, 1870 }, 1871 1024: { 1872 spaceBetween: 50, 1873 }, 1874 }, 1875 }); 1876 }); 1877} 1878 1879function initCategoryHeroSlider() { 1880 document.querySelectorAll('.js-category-hero-slider').forEach((sliderEl) => { 1881 const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length; 1882 if (!slidesCount) return; 1883 1884 const rawInterval = Number(sliderEl.dataset.interval); 1885 const delay = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 5000; 1886 1887 new Swiper(sliderEl, { 1888 effect: 'fade', 1889 fadeEffect: { 1890 crossFade: true, 1891 }, 1892 loop: slidesCount > 1, 1893 speed: 800, 1894 allowTouchMove: slidesCount > 1, 1895 autoplay: 1896 slidesCount > 1 1897 ? { 1898 delay, 1899 disableOnInteraction: false, 1900 } 1901 : false, 1902 }); 1903 }); 1904} 1905 1906function initAwardsSlider() { 1907 const awardsSlider = new Swiper('.awards__slider:not(.awards__slider2)', { 1908 grabCursor: true, 1909 centeredSlides: true, 1910 slidesPerView: 1, 1911 // slidesPerView: 'auto', 1912 // effect: 'coverflow', 1913 effect: 'fade', 1914 fadeEffect: { 1915 crossFade: true, 1916 }, 1917 loop: true, 1918 speed: 500, 1919 autoplay: { 1920 delay: 6000, 1921 disableOnInteraction: false, 1922 }, 1923 // coverflowEffect: { 1924 // rotate: 0, 1925 // stretch: document.querySelector('.awards__slider')?.offsetWidth * 0.86, 1926 // depth: 200, 1927 // modifier: 1, 1928 // slideShadows: true, 1929 // }, 1930 pagination: { 1931 el: '.awards__pagination', 1932 clickable: true, 1933 }, 1934 navigation: { 1935 nextEl: '.awards__slider-nav--next', 1936 prevEl: '.awards__slider-nav--prev', 1937 }, 1938 breakpoints: { 1939 0: { 1940 slidesPerView: 1, 1941 centeredSlides: false, 1942 // effect: 'slide', 1943 }, 1944 // 576: { 1945 // effect: 'coverflow', 1946 // }, 1947 }, 1948 on: { 1949 slideChange: updateActiveItems, 1950 init: updateActiveItems, 1951 // resize: function () { 1952 // if (this.params.effect === 'coverflow') { 1953 // this.params.coverflowEffect.stretch = 1954 // document.querySelector('.awards__slider').offsetWidth * 0.86; 1955 // this.update(); 1956 // } 1957 // }, 1958 }, 1959 }); 1960} 1961 1962function updateActiveItems() { 1963 const slides = document.querySelectorAll('.awards__slide');
1964 slides.forEach((slide) => { 1965 const items = slide.querySelectorAll('.award-card__item'); 1966 if (slide.classList.contains('swiper-slide-active')) { 1967 items.forEach((item, i) => { 1968 item.style.setProperty('--index', i); 1969 item.classList.add('active'); 1970 }); 1971 } else { 1972 items.forEach((item) => item.classList.remove('active')); 1973 } 1974 }); 1975} 1976 1977function initServicesSlider() { 1978 const servicesThumbs = new Swiper('.services__thumbs', { 1979 slidesPerView: 3, 1980 spaceBetween: 2, 1981 watchSlidesProgress: true, 1982 slideToClickedSlide: true, 1983 breakpoints: { 1984 0: { 1985 spaceBetween: 8, 1986 slidesPerView: 'auto', 1987 direction: 'vertical', 1988 }, 1989 992: { 1990 slidesPerView: 3, 1991 direction: 'horizontal', 1992 }, 1993 }, 1994 }); 1995 1996 const servicesSlider = new Swiper('.services__slider', { 1997 slidesPerView: 1, 1998 spaceBetween: 30, 1999 loop: true, 2000 speed: 600, 2001 pagination: { 2002 el: '.services__pagination--desktop', 2003 clickable: true, 2004 type: 'bullets', 2005 }, 2006 thumbs: { 2007 swiper: servicesThumbs, 2008 }, 2009 breakpoints: { 2010 0: { 2011 pagination: { 2012 el: '.services__pagination--mobile', 2013 clickable: true, 2014 type: 'bullets', 2015 }, 2016 }, 2017 993: { 2018 pagination: { 2019 el: '.services__pagination--desktop', 2020 clickable: true, 2021 type: 'bullets', 2022 }, 2023 }, 2024 }, 2025 }); 2026 2027 document.querySelectorAll('.services__slider-nav--next')?.forEach((btn) => { 2028 btn.addEventListener('click', () => servicesSlider.slideNext()); 2029 }); 2030 2031 document.querySelectorAll('.services__slider-nav--prev')?.forEach((btn) => { 2032 btn.addEventListener('click', () => servicesSlider.slidePrev()); 2033 }); 2034} 2035 2036function initCasesSlider() { 2037 document.querySelectorAll('.cases').forEach((cases) => { 2038 const sliderEl = cases.querySelector('.cases__slider'); 2039 const nextBtn = cases.querySelector('.cases__slider-nav--next'); 2040 const prevBtn = cases.querySelector('.cases__slider-nav--prev'); 2041 2042 if (!sliderEl) return; 2043 2044 sliderEl.querySelectorAll('.case-card__preview').forEach((img) => { 2045 img.loading = 'eager'; 2046 }); 2047 2048 const slidesCount = sliderEl.querySelectorAll('.cases__slide').length; 2049 let initialSlide = 0; 2050 if (slidesCount > 4) initialSlide = 2; 2051 else if (slidesCount > 2) initialSlide = 1; 2052 2053 const casesSlider = new Swiper(sliderEl, { 2054 speed: 500, 2055 slidesPerView: 4, 2056 spaceBetween: 100, 2057 loop: false, 2058 initialSlide, 2059 slideToClickedSlide: true, 2060 centeredSlides: true, 2061 watchOverflow: true, 2062 noSwiping: true, 2063 noSwipingSelector: '[data-video-volume-range], [data-video-volume]', 2064 navigation: { 2065 nextEl: nextBtn, 2066 prevEl: prevBtn, 2067 }, 2068 breakpoints: { 2069 0: { slidesPerView: 1.5, spaceBetween: 20 }, 2070 576: { slidesPerView: 2, spaceBetween: 50 }, 2071 992: { slidesPerView: 3, spaceBetween: 50 }, 2072 1170: { slidesPerView: 3, spaceBetween: 100 }, 2073 1500: { slidesPerView: 4, spaceBetween: 100 }, 2074 }, 2075 on: { 2076 init: (swiper) => { 2077 requestAnimationFrame(() => { 2078 requestAnimationFrame(() => { 2079 updateCasesNavWidth(swiper); 2080 }); 2081 }); 2082 }, 2083 slideChange: updateCasesNavWidth, 2084 resize: updateCasesNavWidth, 2085 breakpoint: updateCasesNavWidth, 2086 }, 2087 }); 2088 2089 initCasesSlideHydration(cases, casesSlider); 2090 2091 // ÐаÑза видео ТÐÐЬÐРвнÑÑÑи ÑекÑÑего cases 2092 casesSlider.on('slideChange', () => { 2093 cases.querySelectorAll('[data-video-card]').forEach((card) => { 2094 const video = card.querySelector('[data-video]'); 2095 if (video && !video.paused) { 2096 video.pause(); 2097 card.classList.remove('is-playing'); 2098 } 2099 }); 2100 }); 2101 }); 2102} 2103 2104const vpCasesSlideHtmlCache = {}; 2105 2106function initCasesSlideHydration(cases, swiper) { 2107 const sliderEl = cases.querySelector('.cases__slider'); 2108 if (!sliderEl || !sliderEl.querySelector('.cases__slide.is-empty')) return; 2109 2110 const layout = cases.dataset.casesLayout || 'inner'; 2111 const ajaxUrl = (window.VPAjax && window.VPAjax.url) || '/wp-admin/admin-ajax.php'; 2112 const queued = new Set(); 2113 2114 const originals = () => Array.from(sliderEl.querySelectorAll('.cases__slide')); 2115 2116 const applyHtml = (id, html) => { 2117 vpCasesSlideHtmlCache[layout + ':' + id] = html;
2118 sliderEl.querySelectorAll('.cases__slide[data-post-id="' + id + '"]').forEach((slide) => { 2119 if (slide.querySelector('[data-video-card]')) return; 2120 slide.innerHTML = html; 2121 slide.classList.remove('is-empty'); 2122 slide.querySelectorAll('.case-card__preview').forEach((img) => { 2123 img.loading = 'eager'; 2124 }); 2125 }); 2126 }; 2127 2128 const fetchIds = (ids) => { 2129 const unique = []; 2130 ids.forEach((id) => { 2131 if (!id) return; 2132 const key = layout + ':' + id; 2133 if (vpCasesSlideHtmlCache[key]) { 2134 applyHtml(id, vpCasesSlideHtmlCache[key]); 2135 return; 2136 } 2137 const filled = sliderEl.querySelector( 2138 '.cases__slide[data-post-id="' + id + '"]:not(.is-empty)' 2139 ); 2140 if (filled && filled.querySelector('[data-video-card]')) { 2141 applyHtml(id, filled.innerHTML); 2142 return; 2143 } 2144 if (queued.has(key) || unique.indexOf(id) !== -1) return; 2145 unique.push(id); 2146 }); 2147 if (!unique.length) return; 2148 2149 unique.forEach((id) => queued.add(layout + ':' + id)); 2150 const body = new URLSearchParams(); 2151 body.set('action', 'vp_cases_slide_html'); 2152 body.set('layout', layout); 2153 unique.forEach((id) => body.append('ids[]', id)); 2154 2155 fetch(ajaxUrl, { 2156 method: 'POST', 2157 credentials: 'same-origin', 2158 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 2159 body, 2160 }) 2161 .then((r) => r.json()) 2162 .then((json) => { 2163 const map = json && json.success && json.data ? json.data : {}; 2164 unique.forEach((id) => { 2165 queued.delete(layout + ':' + id); 2166 const html = map[id] || map[String(id)]; 2167 if (html) applyHtml(id, html); 2168 }); 2169 }) 2170 .catch(() => { 2171 unique.forEach((id) => queued.delete(layout + ':' + id)); 2172 }); 2173 }; 2174 2175 const hydrateAround = () => { 2176 const slides = originals(); 2177 const total = slides.length; 2178 if (!total) return; 2179 2180 const BUFFER = 3; 2181 const spv = Number(swiper.params.slidesPerView); 2182 const visible = Number.isFinite(spv) && spv > 0 ? Math.ceil(spv) : 4; 2183 const toEdge = Math.max(1, Math.ceil(visible / 2)); 2184 const active = swiper.activeIndex; 2185 const firstVisible = Math.max(0, active - toEdge); 2186 const lastVisible = Math.min(total - 1, active + toEdge); 2187 2188 const ids = []; 2189 const takeEmpty = (from, to, step) => { 2190 for (let i = from; step > 0 ? i <= to : i >= to; i += step) { 2191 if (i < 0 || i >= total) break; 2192 const slide = slides[i]; 2193 if (slide && slide.classList.contains('is-empty') && slide.dataset.postId) { 2194 ids.push(slide.dataset.postId); 2195 return; 2196 } 2197 } 2198 }; 2199 2200 takeEmpty(firstVisible, lastVisible, 1); 2201 takeEmpty(lastVisible + 1, lastVisible + BUFFER, 1); 2202 takeEmpty(firstVisible - 1, firstVisible - BUFFER, -1); 2203 2204 fetchIds(ids); 2205 }; 2206 2207 let moveTick = false; 2208 const hydrateOnMove = () => { 2209 if (moveTick) return; 2210 moveTick = true; 2211 requestAnimationFrame(() => { 2212 moveTick = false; 2213 hydrateAround(); 2214 }); 2215 }; 2216 2217 hydrateAround(); 2218 swiper.on('slideChange', hydrateAround); 2219 swiper.on('slideChangeTransitionStart', hydrateAround); 2220 swiper.on('sliderMove', hydrateOnMove); 2221 swiper.on('breakpoint', hydrateAround); 2222} 2223 2224function initWhySlider() { 2225 document.querySelectorAll('.why__slider')?.forEach((sliderEl) => { 2226 const swiper = new Swiper(sliderEl, { 2227 slidesPerView: 1, 2228 spaceBetween: 10, 2229 speed: 600, 2230 loop: true, 2231 autoHeight: true, 2232 autoplay: { 2233 delay: 5000000, 2234 }, 2235 pagination: { 2236 el: sliderEl.closest('.why')?.querySelector('.why__pagination'), 2237 clickable: true, 2238 type: 'bullets', 2239 }, 2240 }); 2241 2242 sliderEl 2243 .closest('.why') 2244 ?.querySelectorAll('.why__slider-nav--next') 2245 .forEach((btn) => { 2246 btn.addEventListener('click', () => swiper.slideNext()); 2247 }); 2248 2249 sliderEl 2250 .closest('.why') 2251 ?.querySelectorAll('.why__slider-nav--prev') 2252 .forEach((btn) => { 2253 btn.addEventListener('click', () => swiper.slidePrev()); 2254 }); 2255 }); 2256} 2257 2258function initBlogPostSlider() { 2259 document.querySelectorAll('.blog-post__swiper')?.forEach((sliderEl) => { 2260 const swiper = new Swiper(sliderEl, { 2261 slidesPerView: 1, 2262 spaceBetween: 0, 2263 speed: 600,
2264 effect: 'slide', 2265 autoHeight: true, 2266 loop: true, 2267 pagination: { 2268 el: sliderEl.parentElement.querySelector('.blog-post__swiper-pagination'), 2269 clickable: true, 2270 }, 2271 }); 2272 2273 sliderEl.querySelectorAll('img').forEach((img) => { 2274 if (img.complete) return; 2275 img.addEventListener('load', () => swiper.updateAutoHeight(0), { once: true }); 2276 }); 2277 }); 2278} 2279 2280function initResultSlider() { 2281 const preloadSlideImages = (slide) => { 2282 if (!slide) return; 2283 2284 slide.querySelectorAll('img').forEach((img) => { 2285 img.loading = 'eager'; 2286 2287 if (img.dataset.src && img.src !== img.dataset.src) { 2288 img.src = img.dataset.src; 2289 } 2290 2291 const src = img.currentSrc || img.src; 2292 if (src) { 2293 const preloader = new Image(); 2294 preloader.src = src; 2295 } 2296 }); 2297 }; 2298 2299 const preloadAdjacentSlides = (swiper) => { 2300 preloadSlideImages(swiper.slides[swiper.activeIndex]); 2301 preloadSlideImages(swiper.slides[swiper.activeIndex + 1]); 2302 preloadSlideImages(swiper.slides[swiper.activeIndex - 1]); 2303 }; 2304 2305 const resultSlider = new Swiper('.result__slider', { 2306 slidesPerView: 1, 2307 spaceBetween: 10, 2308 speed: 600, 2309 loop: true, 2310 allowTouchMove: false, 2311 pagination: { 2312 el: '.result__pagination', 2313 clickable: true, 2314 type: 'bullets', 2315 }, 2316 on: { 2317 init(swiper) { 2318 preloadAdjacentSlides(swiper); 2319 }, 2320 slideChangeTransitionStart(swiper) { 2321 preloadAdjacentSlides(swiper); 2322 }, 2323 }, 2324 }); 2325 2326 document.querySelectorAll('.result__nav--next')?.forEach((btn) => { 2327 btn.addEventListener('click', () => resultSlider.slideNext()); 2328 }); 2329 2330 document.querySelectorAll('.result__nav--prev')?.forEach((btn) => { 2331 btn.addEventListener('click', () => resultSlider.slidePrev()); 2332 }); 2333} 2334 2335function initResultSlider2() { 2336 const resultSlider = new Swiper('.result__slider2', { 2337 slidesPerView: 1, 2338 spaceBetween: 10, 2339 speed: 600, 2340 loop: true, 2341 allowTouchMove: true, 2342 pagination: { 2343 el: '.result__pagination', 2344 clickable: true, 2345 type: 'bullets', 2346 }, 2347 2348 breakpoints: { 2349 0: { 2350 direction: 'horizontal', 2351 }, 2352 577: { 2353 direction: 'vertical', 2354 }, 2355 }, 2356 }); 2357 2358 document.querySelectorAll('.result2__nav--next')?.forEach((btn) => { 2359 btn.addEventListener('click', () => resultSlider.slideNext()); 2360 }); 2361 2362 document.querySelectorAll('.result2__nav--prev')?.forEach((btn) => { 2363 btn.addEventListener('click', () => resultSlider.slidePrev()); 2364 }); 2365 2366 const element = document.querySelector('.result__slider2'); 2367 if (element) { 2368 element.classList.add('result__slider'); 2369 } 2370} 2371 2372function initProcessSlider() { 2373 const wrapper = document.querySelector('.process'); 2374 2375 if (!wrapper) return; 2376 2377 const nextBtn = wrapper.querySelectorAll('.process__next, .process__nav--next'); 2378 const prevBtn = wrapper.querySelectorAll('.process__nav--prev'); 2379 const paginationEl = wrapper.querySelector('.process__pagination'); 2380 2381 const processSlider = new Swiper('.process__slider', { 2382 slidesPerView: 'auto', 2383 spaceBetween: 70, 2384 speed: 600, 2385 loop: true, 2386 allowTouchMove: false, 2387 pagination: { 2388 el: paginationEl, 2389 clickable: true, 2390 }, 2391 }); 2392 2393 nextBtn?.forEach((btn) => { 2394 btn.addEventListener('click', () => processSlider.slideNext()); 2395 }); 2396 2397 prevBtn?.forEach((btn) => { 2398 btn.addEventListener('click', () => processSlider.slidePrev()); 2399 }); 2400 2401 const resizeHandler = () => { 2402 if (window.innerWidth < 768) { 2403 paginationEl.classList.replace('slider-pagination-line', 'slider-pagination'); 2404 } else { 2405 paginationEl.classList.replace('slider-pagination', 'slider-pagination-line'); 2406 } 2407 }; 2408 2409 resizeHandler(); 2410 2411 let resizeTimer; 2412 window.addEventListener('resize', () => { 2413 clearTimeout(resizeTimer); 2414 resizeTimer = setTimeout(resizeHandler, 100); 2415 }); 2416 2417 wrapper.querySelectorAll('.process__gallery-wrapper').forEach((galleryWrapper) => { 2418 const slider = galleryWrapper.querySelector('.process__gallery-slider'); 2419 const slidesCount = slider.querySelectorAll('.swiper-slide').length; 2420 2421 if (slidesCount <= 1) { 2422 galleryWrapper.classList.add('is-single'); 2423 } 2424 2425 new Swiper(slider, { 2426 slidesPerView: 'auto', 2427 spaceBetween: 10, 2428 speed: 600,
2429 loop: slidesCount > 1, 2430 pagination: { 2431 el: galleryWrapper.querySelector('.process__gallery-pagination'), 2432 clickable: true, 2433 }, 2434 navigation: { 2435 nextEl: galleryWrapper.querySelector('.process__gallery-nav--next'), 2436 prevEl: galleryWrapper.querySelector('.process__gallery-nav--prev'), 2437 }, 2438 }); 2439 }); 2440} 2441 2442function initGallerySlider() { 2443 document.querySelectorAll('.gallery-slider').forEach((sliderEl) => { 2444 const wrapper = sliderEl.closest('.zone-slider') || sliderEl.parentElement; 2445 2446 const nextBtn = wrapper.querySelectorAll('.gallery-slider__nav--next'); 2447 const prevBtn = wrapper.querySelectorAll('.gallery-slider__nav--prev'); 2448 const pagination = sliderEl.querySelector('.gallery-slider__pagination'); 2449 2450 const swiper = new Swiper(sliderEl, { 2451 centeredSlides: true, 2452 slidesPerView: 'auto', 2453 spaceBetween: 0, 2454 loop: true, 2455 speed: 600, 2456 2457 pagination: { 2458 el: pagination, 2459 clickable: true, 2460 }, 2461 2462 breakpoints: { 2463 0: { 2464 slidesPerView: 1, 2465 }, 2466 768: { 2467 slidesPerView: 'auto', 2468 }, 2469 }, 2470 }); 2471 2472 nextBtn?.forEach((btn) => { 2473 btn.addEventListener('click', () => swiper.slideNext()); 2474 }); 2475 2476 prevBtn?.forEach((btn) => { 2477 btn.addEventListener('click', () => swiper.slidePrev()); 2478 }); 2479 }); 2480} 2481 2482function initSingleSlider() { 2483 document.querySelectorAll('.single-slider').forEach((sliderEl, i) => { 2484 let wrapper = sliderEl.closest('.single-slider-outer') || sliderEl.parentElement; 2485 2486 if (sliderEl.hasAttribute('data-container-class')) { 2487 wrapper = sliderEl.closest(sliderEl.getAttribute('data-container-class')); 2488 } 2489 2490 const nextBtn = wrapper.querySelectorAll( 2491 '.single-slider__nav--next:not(.inc), .slide-next:not(.inc)' 2492 ); 2493 const prevBtn = wrapper.querySelectorAll('.single-slider__nav--prev:not(.inc)'); 2494 const pagination = wrapper.querySelector('.single-slider__pagination:not(.inc)'); 2495 2496 const swiper = new Swiper(sliderEl, { 2497 centeredSlides: true, 2498 slidesPerView: 'auto', 2499 spaceBetween: 0, 2500 loop: false, 2501 speed: 600, 2502 pagination: { 2503 el: pagination, 2504 clickable: true, 2505 }, 2506 2507 on: { 2508 init() { 2509 toggleLockedClass(this, wrapper); 2510 }, 2511 resize() { 2512 toggleLockedClass(this, wrapper); 2513 }, 2514 slideChange() { 2515 syncButtons(this); 2516 syncBadge(this); 2517 }, 2518 }, 2519 }); 2520 2521 nextBtn?.forEach((btn) => { 2522 btn.addEventListener('click', () => { 2523 swiper.slideNext(); 2524 }); 2525 }); 2526 2527 prevBtn?.forEach((btn) => { 2528 btn.addEventListener('click', () => { 2529 swiper.slidePrev(); 2530 }); 2531 }); 2532 }); 2533} 2534 2535function initMaterialsSlider() { 2536 document.querySelectorAll('.materials__slider').forEach((sliderEl, i) => { 2537 const wrapper = sliderEl.closest('.materials'); 2538 2539 const nextBtn = wrapper.querySelectorAll('.materials__nav--next, .slide-next'); 2540 const prevBtn = wrapper.querySelectorAll('.materials__nav--prev'); 2541 const pagination = wrapper.querySelector('.materials__pagination'); 2542 2543 const swiper = new Swiper(sliderEl, { 2544 slidesPerView: 'auto', 2545 spaceBetween: 50, 2546 loop: true, 2547 speed: 600, 2548 pagination: { 2549 el: pagination, 2550 clickable: true, 2551 }, 2552 on: { 2553 slideChange() { 2554 syncButtons(this); 2555 }, 2556 }, 2557 }); 2558 2559 nextBtn?.forEach((btn) => { 2560 btn.addEventListener('click', () => swiper.slideNext()); 2561 }); 2562 2563 prevBtn?.forEach((btn) => { 2564 btn.addEventListener('click', () => swiper.slidePrev()); 2565 }); 2566 }); 2567} 2568 2569function syncBadge(swiper) { 2570 const activeIndex = swiper.realIndex + 1; 2571 2572 const step = swiper.el.closest('.section'); 2573 if (!step) return; 2574 2575 const badgeSpans = step.querySelectorAll('.section__badge span'); 2576 if (badgeSpans) { 2577 badgeSpans.forEach((badgeSpan) => { 2578 badgeSpan.textContent = activeIndex; 2579 }); 2580 } 2581} 2582 2583function syncButtons(swiper) { 2584 const sliderClass = swiper.el.dataset.sliderName; 2585 const scope = swiper.el.closest('.swiper-slide') || swiper.el.closest('.section'); 2586 const activeIndex = swiper.realIndex; 2587
2588 scope.querySelectorAll(`.slide-to-button[data-slider="${sliderClass}"]`).forEach((btn) => { 2589 btn.classList.toggle('active', +btn.dataset.index - 1 === activeIndex); 2590 }); 2591} 2592 2593function toggleLockedClass(swiper, wrapper) { 2594 wrapper.classList.toggle('is-locked', swiper.isLocked); 2595} 2596 2597function updateCasesNavWidth() { 2598 const activeSlide = document.querySelector('.cases__slide.swiper-slide-active'); 2599 const nav = document.querySelector('.cases__slider-nav'); 2600 2601 if (activeSlide && nav) { 2602 const navWidth = activeSlide.offsetWidth + 100; 2603 nav.style.width = navWidth + 'px'; 2604 } 2605} 2606 2607function initLazyVideo(selector = '.lazy-video') { 2608 document.querySelectorAll(selector)?.forEach((item) => { 2609 const video = item.querySelector('.lazy-video__video'); 2610 2611 if (video) { 2612 video.addEventListener( 2613 'playing', 2614 () => { 2615 requestAnimationFrame(() => { 2616 item.classList.add('lazy-video--loaded'); 2617 }); 2618 }, 2619 { once: true } 2620 ); 2621 2622 video.play().catch((err) => console.log('Autoplay blocked:', err)); 2623 } 2624 }); 2625} 2626 2627function initBeforeAfterSlider(slider) { 2628 const beforeItem = slider.querySelector('.before-after__item--before'); 2629 const afterItem = slider.querySelector('.before-after__item--after'); 2630 const handle = slider.querySelector('.before-after__handle'); 2631 const drag = slider.querySelector('.before-after__drag'); 2632 2633 if (!beforeItem || !afterItem || !handle || !drag) return; 2634 2635 const beforeOverlay = beforeItem.querySelector('.before-after__overlay'); 2636 const afterOverlay = afterItem.querySelector('.before-after__overlay'); 2637 2638 let isDragging = false; 2639 let currentPosition = 50; 2640 2641 if (!drag.hasAttribute('tabindex')) { 2642 drag.setAttribute('tabindex', '0'); 2643 } 2644 drag.style.pointerEvents = 'all'; 2645 2646 updatePosition(); 2647 2648 drag.addEventListener('mousedown', startDrag); 2649 drag.addEventListener('touchstart', startDrag); 2650 drag.addEventListener('keydown', handleKeyboard); 2651 2652 function handleKeyboard(e) { 2653 const step = e.shiftKey ? 10 : 1; 2654 2655 switch (e.key) { 2656 case 'ArrowLeft': 2657 case 'Left': 2658 e.preventDefault(); 2659 currentPosition = Math.max(0, currentPosition - step); 2660 updatePosition(); 2661 break; 2662 case 'ArrowRight': 2663 case 'Right': 2664 e.preventDefault(); 2665 currentPosition = Math.min(100, currentPosition + step); 2666 updatePosition(); 2667 break; 2668 case 'Home': 2669 e.preventDefault(); 2670 currentPosition = 0; 2671 updatePosition(); 2672 break; 2673 case 'End': 2674 e.preventDefault(); 2675 currentPosition = 100; 2676 updatePosition(); 2677 break; 2678 } 2679 } 2680 2681 function startDrag(e) { 2682 isDragging = true; 2683 e.preventDefault(); 2684 document.addEventListener('mousemove', move); 2685 document.addEventListener('mouseup', stopDrag); 2686 document.addEventListener('touchmove', move); 2687 document.addEventListener('touchend', stopDrag); 2688 } 2689 2690 function move(e) { 2691 if (!isDragging) return; 2692 e.preventDefault(); 2693 2694 const rect = slider.getBoundingClientRect(); 2695 const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX; 2696 const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); 2697 2698 currentPosition = (x / rect.width) * 100; 2699 updatePosition(); 2700 } 2701 2702 function stopDrag() { 2703 isDragging = false; 2704 document.removeEventListener('mousemove', move); 2705 document.removeEventListener('mouseup', stopDrag); 2706 document.removeEventListener('touchmove', move); 2707 document.removeEventListener('touchend', stopDrag); 2708 } 2709 2710 function updatePosition() { 2711 beforeItem.style.clipPath = `inset(0% ${100 - currentPosition}% 0% 0%)`; 2712 handle.style.left = `${currentPosition}%`; 2713 updateOverlays(); 2714 } 2715 2716 function updateOverlays() { 2717 const factor = currentPosition / 100; 2718 const afterOpacity = factor < 0.5 ? factor * 2 : 1; 2719 const beforeOpacity = factor >= 0.5 ? (1 - factor) * 2 : 1; 2720 2721 if (afterOverlay) afterOverlay.style.opacity = afterOpacity; 2722 if (beforeOverlay) beforeOverlay.style.opacity = beforeOpacity; 2723 } 2724} 2725 2726function initHeaderNav() { 2727 const header = document.getElementById('header'); 2728 if (!header) return; 2729 2730 const MOBILE_BREAKPOINT = 1201; 2731 const SUBMENU_DURATION = 350; 2732 let wasMobile = window.innerWidth < MOBILE_BREAKPOINT; 2733 let isAnimating = false;
2734 let submenuAnimating = false; 2735 2736 const burgers = header.querySelectorAll('.header__burger'); 2737 const submenuToggles = header.querySelectorAll('.header__nav-submenu-toggle'); 2738 2739 if (!burgers.length) return; 2740 2741 syncSubmenuStructure(); 2742 2743 burgers.forEach((burger) => { 2744 burger.addEventListener('click', () => { 2745 if (isAnimating) return; 2746 2747 isAnimating = true; 2748 const willOpen = !header.classList.contains('header--nav-active'); 2749 2750 burgers.forEach((b) => b.classList.toggle('burger--active', willOpen)); 2751 if (isMobileViewport()) { 2752 document.body.classList.toggle('overflow-hidden', willOpen); 2753 } else { 2754 document.body.classList.remove('overflow-hidden'); 2755 } 2756 header.classList.toggle('header--nav-active', willOpen); 2757 2758 if (!willOpen) { 2759 resetSubmenuStyles(); 2760 } 2761 2762 setTimeout(() => { 2763 isAnimating = false; 2764 }, 300); 2765 }); 2766 }); 2767 2768 // ÐеÑкÑоп: нижнее Ð¼ÐµÐ½Ñ Ð¾ÑкÑÑÑо 2769 if (!isMobileViewport()) { 2770 header.classList.add('header--nav-active'); 2771 burgers.forEach((b) => b.classList.add('burger--active')); 2772 } 2773 2774 submenuToggles.forEach((toggle) => { 2775 toggle.addEventListener('click', (e) => { 2776 e.preventDefault(); 2777 e.stopPropagation(); 2778 if (!isMobileViewport()) return; 2779 toggleSubmenu(toggle); 2780 }); 2781 }); 2782 2783 header 2784 .querySelectorAll('.header__nav-item--has-children > .header__nav-item-inner') 2785 .forEach((inner) => { 2786 inner.addEventListener('click', (e) => { 2787 if (!isMobileViewport()) return; 2788 const toggle = inner.querySelector('.header__nav-submenu-toggle'); 2789 if (!toggle) return; 2790 e.preventDefault(); 2791 e.stopPropagation(); 2792 toggleSubmenu(toggle); 2793 }); 2794 }); 2795 2796 document.addEventListener('click', (e) => { 2797 if (!isMobileViewport()) return; 2798 if (header.classList.contains('header--nav-active') && !header.contains(e.target)) { 2799 closeMobileMenu(); 2800 } 2801 }); 2802 2803 let resizeTimer; 2804 window.addEventListener('resize', () => { 2805 clearTimeout(resizeTimer); 2806 resizeTimer = setTimeout(() => { 2807 handleBreakpointChange(); 2808 }, 250); 2809 }); 2810 2811 window.addEventListener('orientationchange', () => { 2812 setTimeout(() => { 2813 handleBreakpointChange(); 2814 }, 300); 2815 }); 2816 2817 function isMobileViewport() { 2818 return window.innerWidth < MOBILE_BREAKPOINT; 2819 } 2820 2821 function syncSubmenuStructure() { 2822 if (isMobileViewport()) { 2823 prepareSubmenuInners(); 2824 } else { 2825 unwrapSubmenuInners(); 2826 } 2827 } 2828 2829 function prepareSubmenuInners() { 2830 header.querySelectorAll('.header__nav-submenu').forEach((submenu) => { 2831 if (submenu.dataset.accordionReady) return; 2832 const inner = document.createElement('div'); 2833 inner.className = 'header__nav-submenu-inner'; 2834 while (submenu.firstChild) { 2835 inner.appendChild(submenu.firstChild); 2836 } 2837 submenu.appendChild(inner); 2838 submenu.dataset.accordionReady = '1'; 2839 }); 2840 } 2841 2842 function unwrapSubmenuInners() { 2843 header.querySelectorAll('.header__nav-submenu').forEach((submenu) => { 2844 const inner = submenu.querySelector(':scope > .header__nav-submenu-inner'); 2845 if (inner) { 2846 while (inner.firstChild) { 2847 submenu.appendChild(inner.firstChild); 2848 } 2849 inner.remove(); 2850 } 2851 delete submenu.dataset.accordionReady; 2852 submenu.style.maxHeight = ''; 2853 submenu.style.transition = ''; 2854 submenu.style.display = ''; 2855 }); 2856 } 2857 2858 function handleBreakpointChange() { 2859 const isMobile = isMobileViewport(); 2860 if (wasMobile === isMobile) return; 2861 2862 resetSubmenuStyles(); 2863 syncSubmenuStructure(); 2864 2865 if (isMobile) { 2866 header.classList.remove('header--nav-active'); 2867 burgers.forEach((b) => b.classList.remove('burger--active')); 2868 document.body.classList.remove('overflow-hidden'); 2869 } else { 2870 header.classList.add('header--nav-active'); 2871 burgers.forEach((b) => b.classList.add('burger--active')); 2872 document.body.classList.remove('overflow-hidden'); 2873 } 2874 2875 wasMobile = isMobile; 2876 } 2877 2878 function toggleSubmenu(toggle) { 2879 if (submenuAnimating || !isMobileViewport()) return; 2880 2881 const parentItem = toggle.closest('.header__nav-item--has-children'); 2882 if (!parentItem?.querySelector('.header__nav-submenu')) return; 2883 2884 const isOpen = parentItem.classList.contains('header__nav-item--submenu-open'); 2885 submenuAnimating = true; 2886 2887 const animations = []; 2888 header.querySelectorAll('.header__nav-item--submenu-open').forEach((item) => { 2889 if (item === parentItem) return; 2890 animations.push(closeSubmenu(item)); 2891 }); 2892 2893 if (isOpen) { 2894 animations.push(closeSubmenu(parentItem)); 2895 Promise.all(animations).finally(() => { 2896 submenuAnimating = false;
2897 }); 2898 return; 2899 } 2900 2901 animations.push(openSubmenu(parentItem)); 2902 Promise.all(animations) 2903 .then(() => scrollTitleIntoView(parentItem)) 2904 .finally(() => { 2905 submenuAnimating = false; 2906 }); 2907 } 2908 2909 function openSubmenu(item) { 2910 const submenu = item.querySelector('.header__nav-submenu'); 2911 const toggle = item.querySelector('.header__nav-submenu-toggle'); 2912 if (!submenu) return Promise.resolve(); 2913 2914 item.classList.add('header__nav-item--submenu-open'); 2915 toggle?.classList.add('header__nav-submenu-toggle--active'); 2916 2917 submenu.style.transition = 'none'; 2918 submenu.style.maxHeight = 'none'; 2919 const targetHeight = submenu.scrollHeight; 2920 submenu.style.maxHeight = '0px'; 2921 void submenu.offsetHeight; 2922 2923 return new Promise((resolve) => { 2924 requestAnimationFrame(() => { 2925 submenu.style.transition = ''; 2926 submenu.style.maxHeight = `${targetHeight}px`; 2927 waitForTransition(submenu).then(() => { 2928 if (item.classList.contains('header__nav-item--submenu-open')) { 2929 submenu.style.maxHeight = 'none'; 2930 } 2931 resolve(); 2932 }); 2933 }); 2934 }); 2935 } 2936 2937 function closeSubmenu(item) { 2938 const submenu = item.querySelector('.header__nav-submenu'); 2939 const toggle = item.querySelector('.header__nav-submenu-toggle'); 2940 if (!submenu) return Promise.resolve(); 2941 2942 submenu.style.transition = 'none'; 2943 submenu.style.maxHeight = `${submenu.scrollHeight}px`; 2944 void submenu.offsetHeight; 2945 2946 return new Promise((resolve) => { 2947 requestAnimationFrame(() => { 2948 item.classList.remove('header__nav-item--submenu-open'); 2949 toggle?.classList.remove('header__nav-submenu-toggle--active'); 2950 submenu.style.transition = ''; 2951 submenu.style.maxHeight = '0px'; 2952 waitForTransition(submenu).then(resolve); 2953 }); 2954 }); 2955 } 2956 2957 function waitForTransition(el) { 2958 return new Promise((resolve) => { 2959 let done = false; 2960 const finish = () => { 2961 if (done) return; 2962 done = true; 2963 el.removeEventListener('transitionend', onEnd); 2964 resolve(); 2965 }; 2966 const onEnd = (e) => { 2967 if (e.target !== el || e.propertyName !== 'max-height') return; 2968 finish(); 2969 }; 2970 el.addEventListener('transitionend', onEnd); 2971 setTimeout(finish, SUBMENU_DURATION + 50); 2972 }); 2973 } 2974 2975 function getScrollParent(el) { 2976 let parent = el.parentElement; 2977 while (parent && parent !== document.body) { 2978 const { overflowY } = getComputedStyle(parent); 2979 if ( 2980 (overflowY === 'auto' || overflowY === 'scroll') && 2981 parent.scrollHeight > parent.clientHeight 2982 ) { 2983 return parent; 2984 } 2985 parent = parent.parentElement; 2986 } 2987 return document.scrollingElement || document.documentElement; 2988 } 2989 2990 function scrollTitleIntoView(item) { 2991 const title = item.querySelector('.header__nav-item-inner') || item; 2992 const rect = title.getBoundingClientRect(); 2993 const viewportTop = 0; 2994 const viewportBottom = window.innerHeight; 2995 2996 if (rect.top >= viewportTop && rect.bottom <= viewportBottom) { 2997 return Promise.resolve(); 2998 } 2999 3000 const scrollParent = getScrollParent(title); 3001 const parentRect = scrollParent.getBoundingClientRect(); 3002 const padding = 12; 3003 let nextTop = scrollParent.scrollTop; 3004 3005 if (rect.top < Math.max(viewportTop, parentRect.top) + padding) { 3006 nextTop += rect.top - Math.max(viewportTop, parentRect.top) - padding; 3007 } else if (rect.bottom > Math.min(viewportBottom, parentRect.bottom) - padding) { 3008 nextTop += rect.bottom - Math.min(viewportBottom, parentRect.bottom) + padding; 3009 } 3010 3011 return animateScrollLinear(scrollParent, nextTop, SUBMENU_DURATION); 3012 } 3013 3014 function animateScrollLinear(el, to, duration) { 3015 const start = el.scrollTop; 3016 const change = to - start; 3017 if (Math.abs(change) < 1) return Promise.resolve(); 3018 3019 const startTime = performance.now(); 3020 return new Promise((resolve) => { 3021 const step = (now) => { 3022 const progress = Math.min(1, (now - startTime) / duration); 3023 el.scrollTop = start + change * progress; 3024 if (progress < 1) { 3025 requestAnimationFrame(step); 3026 } else { 3027 resolve(); 3028 } 3029 }; 3030 requestAnimationFrame(step); 3031 }); 3032 } 3033 3034 function resetSubmenuStyles() { 3035 header.querySelectorAll('.header__nav-submenu').forEach((submenu) => { 3036 submenu.style.maxHeight = ''; 3037 submenu.style.display = ''; 3038 submenu.style.transition = ''; 3039 }); 3040 3041 header.querySelectorAll('.header__nav-item--submenu-open').forEach((item) => { 3042 item.classList.remove('header__nav-item--submenu-open'); 3043 }); 3044 3045 header.querySelectorAll('.header__nav-submenu-toggle').forEach((toggle) => { 3046 toggle.classList.remove('header__nav-submenu-toggle--active'); 3047 }); 3048 3049 submenuAnimating = false;
3050 } 3051 3052 function closeMobileMenu() { 3053 burgers.forEach((b) => b.classList.remove('burger--active')); 3054 header.classList.remove('header--nav-active'); 3055 document.body.classList.remove('overflow-hidden'); 3056 resetSubmenuStyles(); 3057 } 3058} 3059 3060function initFileUpload(selector = '.form__file') { 3061 document.querySelectorAll(selector).forEach((wrapper) => { 3062 if (wrapper.dataset.vpFileInited) return; // guard against re-init after AJAX modal load 3063 wrapper.dataset.vpFileInited = '1'; 3064 const input = wrapper.querySelector('.form__file-input'); 3065 const textEl = wrapper.querySelector('.form__file-text'); 3066 const removeBtn = wrapper.querySelector('.form__file-remove'); 3067 3068 if (!input) return; 3069 3070 const defaultText = wrapper.dataset.text || textEl.textContent; 3071 3072 input.addEventListener('change', () => { 3073 if (input.files.length > 0) { 3074 const file = input.files[0]; 3075 textEl.textContent = shortenFileName(file.name); 3076 wrapper.classList.add('form__file--uploaded'); 3077 } else { 3078 resetFile(); 3079 } 3080 }); 3081 3082 removeBtn?.addEventListener('click', resetFile); 3083 3084 function resetFile() { 3085 input.value = ''; 3086 textEl.textContent = defaultText; 3087 wrapper.classList.remove('form__file--uploaded'); 3088 removeBtn.hidden = true; 3089 } 3090 }); 3091} 3092 3093function shortenFileName(filename, maxLength = 20) { 3094 if (filename.length <= maxLength) return filename; 3095 3096 const parts = filename.split('.'); 3097 const ext = parts.pop(); 3098 const name = parts.join('.'); 3099 const charsToShow = Math.floor((maxLength - ext.length - 3) / 2); 3100 3101 return `${name.substring(0, charsToShow)}...${name.substring(name.length - charsToShow)}.${ext}`; 3102} 3103 3104function faqInit() { 3105 document.querySelectorAll('.faq-item-toggle').forEach((toggle) => { 3106 toggle.addEventListener('click', (e) => { 3107 e.preventDefault(); 3108 3109 const container = toggle.closest('.faq'); 3110 const parent = toggle.closest('.faq-item'); 3111 const content = parent.querySelector('.faq-item-content'); 3112 3113 container.querySelectorAll('.faq-item--active').forEach((item) => { 3114 if (item !== parent) { 3115 item.classList.remove('faq-item--active'); 3116 item.querySelector('.faq-item-content')?.slideToggle(300, 'flex'); 3117 } 3118 }); 3119 3120 parent.classList.toggle('faq-item--active'); 3121 content?.slideToggle(300, 'flex'); 3122 }); 3123 }); 3124} 3125 3126function newShowcaseAccordionInit() { 3127 document.querySelectorAll('.new-showcase__item-head').forEach((head) => { 3128 head.addEventListener('click', () => { 3129 const item = head.closest('.new-showcase__item'); 3130 const items = item?.closest('.new-showcase__items'); 3131 if (!item || !items) return; 3132 3133 items.querySelectorAll('.new-showcase__item.is-open').forEach((openItem) => { 3134 if (openItem !== item) { 3135 openItem.classList.remove('is-open'); 3136 openItem 3137 .querySelector('.new-showcase__item-head') 3138 ?.setAttribute('aria-expanded', 'false'); 3139 } 3140 }); 3141 3142 const isOpen = item.classList.toggle('is-open'); 3143 head.setAttribute('aria-expanded', String(isOpen)); 3144 }); 3145 }); 3146} 3147 3148const VP_CASE_VOLUME = 0.5; 3149 3150function vpVideoAudioContext() { 3151 const Ctx = window.AudioContext || window.webkitAudioContext; 3152 if (!Ctx) return null; 3153 if (!vpVideoAudioContext.ctx) { 3154 vpVideoAudioContext.ctx = new Ctx(); 3155 } 3156 return vpVideoAudioContext.ctx; 3157} 3158 3159function vpVideoLevel(video) { 3160 const slider = video.closest('[data-video-card]')?.querySelector('[data-video-volume-range]'); 3161 const value = slider ? Number(slider.value) : VP_CASE_VOLUME; 3162 if (!Number.isFinite(value)) return VP_CASE_VOLUME; 3163 return Math.max(0, Math.min(1, value)); 3164} 3165 3166function vpApplyVideoLevel(video, level) { 3167 const next = Math.max(0, Math.min(1, level)); 3168 const ctx = vpVideoAudioContext(); 3169 if (ctx) { 3170 try { 3171 if (!video._vpGain) { 3172 const source = ctx.createMediaElementSource(video); 3173 const gain = ctx.createGain(); 3174 source.connect(gain).connect(ctx.destination); 3175 video._vpGain = gain; 3176 } 3177 video._vpGain.gain.value = next; 3178 if (ctx.state === 'suspended') { 3179 ctx.resume().catch(() => {}); 3180 } 3181 return; 3182 } catch (e) {} 3183 } 3184 try { 3185 video.volume = next; 3186 } catch (e) {} 3187} 3188 3189function vpSyncVolumeIcon(card, video) {
3190 const icon = card.querySelector('[data-video-volume] use'); 3191 if (!icon) return; 3192 const href = icon.getAttribute('href') || icon.getAttribute('xlink:href') || ''; 3193 const base = href.split('#')[0] || ''; 3194 const hash = video.muted ? 'volume-muted' : 'volume'; 3195 icon.setAttribute('href', base + '#' + hash); 3196 if (icon.hasAttribute('xlink:href')) { 3197 icon.setAttribute('xlink:href', base + '#' + hash); 3198 } 3199} 3200 3201function initVideoCards() { 3202 if (initVideoCards.bound) return; 3203 initVideoCards.bound = true; 3204 3205 document.addEventListener( 3206 'input', 3207 (e) => { 3208 const range = e.target.closest('[data-video-volume-range]'); 3209 if (!range) return; 3210 const card = range.closest('[data-video-card]'); 3211 const video = card && card.querySelector('[data-video]'); 3212 if (!video) return; 3213 const level = Number(range.value); 3214 const next = Number.isFinite(level) ? level : VP_CASE_VOLUME; 3215 vpApplyVideoLevel(video, next); 3216 video.muted = next <= 0; 3217 card.classList.toggle('is-muted', video.muted); 3218 vpSyncVolumeIcon(card, video); 3219 }, 3220 true 3221 ); 3222 3223 document.addEventListener( 3224 'click', 3225 (e) => { 3226 if (e.target.closest('[data-video-volume-range]')) { 3227 e.stopPropagation(); 3228 return; 3229 } 3230 3231 const volumeBtn = e.target.closest('[data-video-volume]'); 3232 if (volumeBtn) { 3233 const card = volumeBtn.closest('[data-video-card]'); 3234 const video = card && card.querySelector('[data-video]'); 3235 if (!video) return; 3236 e.preventDefault(); 3237 e.stopPropagation(); 3238 video.muted = !video.muted; 3239 if (!video.muted) { 3240 const range = card.querySelector('[data-video-volume-range]'); 3241 if (range && Number(range.value) <= 0) {
3242 range.value = String(VP_CASE_VOLUME); 3243 } 3244 vpApplyVideoLevel(video, vpVideoLevel(video)); 3245 } 3246 card.classList.toggle('is-muted', video.muted); 3247 vpSyncVolumeIcon(card, video); 3248 return; 3249 } 3250 3251 const playBtn = e.target.closest('[data-video-play]'); 3252 const card = 3253 (playBtn && playBtn.closest('[data-video-card]')) || 3254 e.target.closest('[data-video-card].is-playing'); 3255 if (!card) return; 3256 3257 const video = card.querySelector('[data-video]'); 3258 if (!video) return; 3259 3260 e.preventDefault(); 3261 e.stopPropagation(); 3262 if (video.paused) { 3263 const level = vpVideoLevel(video); 3264 video.muted = level <= 0; 3265 card.classList.toggle('is-muted', video.muted); 3266 vpApplyVideoLevel(video, level); 3267 vpSyncVolumeIcon(card, video); 3268 const playPromise = video.play(); 3269 if (playPromise && typeof playPromise.catch === 'function') { 3270 playPromise.catch(() => {}); 3271 } 3272 card.classList.add('is-playing'); 3273 } else { 3274 video.pause(); 3275 card.classList.remove('is-playing'); 3276 } 3277 }, 3278 true 3279 ); 3280} 3281 3282function initPortfolioBreadcrumbsOffset() { 3283 const header = document.getElementById('header'); 3284 const hero = document.querySelector('.hero-new-portfolio--detail'); 3285 if (!header || !hero) return; 3286 3287 const mediaQuery1024 = window.matchMedia('(max-width: 1024px)'); 3288 const mediaQuery767 = window.matchMedia('(max-width: 767px)'); 3289 3290 const setOffset = () => { 3291 const gap = mediaQuery767.matches ? 18 : mediaQuery1024.matches ? 44 : 30; 3292 hero.style.setProperty('--hero-breadcrumbs-top', `${header.offsetHeight + gap}px`); 3293 }; 3294 3295 setOffset(); 3296 window.addEventListener('resize', setOffset); 3297 mediaQuery1024.addEventListener('change', setOffset); 3298 mediaQuery767.addEventListener('change', setOffset); 3299 3300 if (typeof ResizeObserver !== 'undefined') { 3301 const observer = new ResizeObserver(setOffset); 3302 observer.observe(header); 3303 } 3304} 3305 3306function initBlogPostOffset() { 3307 const header = document.getElementById('header'); 3308 const post = document.querySelector('.blog-post'); 3309 if (!header || !post) return; 3310 3311 const setOffset = () => { 3312 post.style.setProperty('--blog-post-header-height', `${header.offsetHeight}px`); 3313 }; 3314 3315 setOffset(); 3316 window.addEventListener('resize', setOffset); 3317 3318 if (typeof ResizeObserver !== 'undefined') { 3319 const observer = new ResizeObserver(setOffset); 3320 observer.observe(header); 3321 } 3322} 3323 3324function initBlogPostNav() { 3325 const post = document.querySelector('.blog-post'); 3326 if (!post) return; 3327 3328 const leftCol = 3329 post.querySelector('.blog-post__sidebar > .blog-post__col:first-child') || 3330 post.querySelector('.blog-post__row > .blog-post__col'); 3331 const content = post.querySelector('.how-apply__footer-text'); 3332 if (!leftCol || !content) return; 3333 3334 const headings = Array.from(content.querySelectorAll('h2')); 3335 if (!headings.length) return; 3336 3337 const usedIds = new Set(); 3338 3339 const escapeHtml = (value) => 3340 value 3341 .replace(/&/g, '&') 3342 .replace(/</g, '<') 3343 .replace(/>/g, '>') 3344 .replace(/"/g, '"'); 3345 3346 const slugify = (text, index) => { 3347 const base = text 3348 .toLowerCase() 3349 .replace(/Ñ/g, 'е') 3350 .replace(/[^a-zа-Ñ0-9]+/gi, '-') 3351 .replace(/^-+|-+$/g, '') 3352 .slice(0, 60); 3353 let id = base || `blog-post-section-${String(index).padStart(2, '0')}`; 3354 if (usedIds.has(id) || document.getElementById(id)) { 3355 id = `${id}-${String(index).padStart(2, '0')}`; 3356 } 3357 usedIds.add(id); 3358 return id; 3359 }; 3360 3361 const items = headings.map((heading, index) => { 3362 const title = heading.textContent.replace(/\s+/g, ' ').trim(); 3363 if (!heading.id) { 3364 heading.id = slugify(title, index + 1); 3365 } else { 3366 usedIds.add(heading.id); 3367 } 3368 3369 const num = String(index + 1).padStart(2, '0'); 3370 return { id: heading.id, title, num }; 3371 }); 3372 3373 const nav = document.createElement('nav'); 3374 nav.className = 'blog-post__toc'; 3375 nav.setAttribute('aria-label', 'СодеÑжание'); 3376 nav.innerHTML = ` 3377 <div class="blog-post__toc-head">
3378 <span class="blog-post__toc-line" aria-hidden="true"></span> 3379 <div class="blog-post__toc-title">СодеÑжание</div> 3380 </div> 3381 <ol class="blog-post__toc-list list-reset"> 3382 ${items 3383 .map( 3384 (item, index) => ` 3385 <li class="blog-post__toc-item${index === 0 ? ' is-active' : ''}"> 3386 <a class="blog-post__toc-link" href="#${item.id}"> 3387 <span class="blog-post__toc-num">${item.num}</span> 3388 <span class="blog-post__toc-text">${escapeHtml(item.title)}</span> 3389 </a> 3390 </li>` 3391 ) 3392 .join('')} 3393 </ol> 3394 `; 3395 3396 leftCol.appendChild(nav); 3397 3398 const tocItems = Array.from(nav.querySelectorAll('.blog-post__toc-item')); 3399 3400 const setActive = (index) => { 3401 tocItems.forEach((el, i) => { 3402 el.classList.toggle('is-active', i === index); 3403 }); 3404 }; 3405 3406 const getNavOffset = () => { 3407 const header = document.getElementById('header'); 3408 return (header ? header.offsetHeight : 0) + 30; 3409 }; 3410 3411 const updateActiveFromScroll = () => { 3412 const offset = getNavOffset() + 1; 3413 let activeIndex = 0; 3414 3415 for (let i = 0; i < headings.length; i++) { 3416 if (headings[i].getBoundingClientRect().top <= offset) { 3417 activeIndex = i; 3418 } else { 3419 break; 3420 } 3421 } 3422 3423 setActive(activeIndex); 3424 }; 3425 3426 nav.addEventListener('click', (event) => { 3427 const link = event.target.closest('.blog-post__toc-link'); 3428 if (!link) return; 3429 const item = link.closest('.blog-post__toc-item'); 3430 if (!item) return; 3431 const index = tocItems.indexOf(item); 3432 if (index !== -1) setActive(index); 3433 }); 3434 3435 window.addEventListener('scroll', updateActiveFromScroll, { passive: true }); 3436 window.addEventListener('resize', updateActiveFromScroll); 3437 updateActiveFromScroll(); 3438} 3439 3440function initPortfolioHeroMobileHeight() { 3441 const hero = document.querySelector('section.hero.hero-new-portfolio--detail'); 3442 if (!hero) return; 3443 3444 const actions = document.querySelector('.new-portfolio__hero-panel-actions'); 3445 const aboutSection = document.querySelector('.new-portfolio__section-about'); 3446 const mediaQuery = window.matchMedia('(max-width: 767px)'); 3447 let cachedActionsHeight = 0; 3448 3449 const reset = () => { 3450 hero.style.removeProperty('--hero-actions-height'); 3451 hero.style.height = ''; 3452 hero.style.minHeight = ''; 3453 }; 3454 3455 const cacheActionsHeight = () => { 3456 if (!actions || getComputedStyle(actions).display === 'none') return cachedActionsHeight; 3457 3458 const height = actions.offsetHeight; 3459 if (height > 0) cachedActionsHeight = height; 3460 return cachedActionsHeight; 3461 }; 3462 3463 const measureHiddenActionsHeight = () => { 3464 if (!actions) return 0; 3465 if (cachedActionsHeight > 0) return cachedActionsHeight; 3466 3467 const prevDisplay = actions.style.display; 3468 const prevVisibility = actions.style.visibility; 3469 const prevPosition = actions.style.position; 3470 const prevPointerEvents = actions.style.pointerEvents; 3471 3472 actions.style.display = 'flex'; 3473 actions.style.visibility = 'hidden'; 3474 actions.style.position = 'absolute'; 3475 actions.style.pointerEvents = 'none'; 3476 3477 const height = actions.offsetHeight; 3478 if (height > 0) cachedActionsHeight = height; 3479 3480 actions.style.display = prevDisplay; 3481 actions.style.visibility = prevVisibility; 3482 actions.style.position = prevPosition; 3483 actions.style.pointerEvents = prevPointerEvents; 3484 3485 return cachedActionsHeight; 3486 }; 3487 3488 const update = () => { 3489 // Strictly mobile: never touch hero height outside â¤767px 3490 if (!mediaQuery.matches || !actions) { 3491 reset(); 3492 return; 3493 } 3494 3495 // At â¤1024 the duplicate block is hidden; real buttons live in the hero panel. 3496 if (getComputedStyle(actions).display === 'none') { 3497 reset(); 3498 return; 3499 } 3500 3501 const aboutOpen = 3502 aboutSection?.classList.contains('is-about-open') || hero.classList.contains('is-about-open'); 3503 3504 // When about opens, buttons are display:none â shrink hero by that block's height 3505 const actionsHeight = aboutOpen ? measureHiddenActionsHeight() : cacheActionsHeight(); 3506 3507 hero.style.setProperty('--hero-actions-height', `${actionsHeight}px`); 3508 }; 3509 3510 update(); 3511 window.addEventListener('resize', update); 3512 mediaQuery.addEventListener('change', update); 3513 3514 if (typeof ResizeObserver !== 'undefined' && actions) { 3515 new ResizeObserver(() => { 3516 if (!mediaQuery.matches) return; 3517 if ( 3518 aboutSection?.classList.contains('is-about-open') || 3519 hero.classList.contains('is-about-open') 3520 ) { 3521 return; 3522 } 3523 cacheActionsHeight(); 3524 update(); 3525 }).observe(actions); 3526 } 3527 3528 const observeAboutClass = (target) => { 3529 if (!target || typeof MutationObserver === 'undefined') return; 3530 new MutationObserver(update).observe(target, { 3531 attributes: true, 3532 attributeFilter: ['class'], 3533 }); 3534 }; 3535 3536 observeAboutClass(aboutSection); 3537 observeAboutClass(hero); 3538 3539 // Expose for sync before display:none so height is cached while buttons are visible 3540 initPortfolioHeroMobileHeight.cacheBeforeOpen = () => { 3541 if (!mediaQuery.matches) return; 3542 cacheActionsHeight(); 3543 }; 3544 initPortfolioHeroMobileHeight.update = update; 3545} 3546 3547function initNewExperienceTextToggle() { 3548 const text = document.querySelector('.new-experience__text'); 3549 const btn = text?.querySelector('.new-experience__more'); 3550 const icon = btn?.querySelector('.new-experience__more-icon'); 3551 if (!text || !btn) return; 3552 3553 const mediaQuery = window.matchMedia('(max-width: 1024px)'); 3554 3555 const setExpanded = (open) => { 3556 text.classList.toggle('is-expanded', open); 3557 btn.setAttribute('aria-expanded', String(open)); 3558 if (icon) icon.textContent = open ? 'â' : '+'; 3559 }; 3560 3561 btn.addEventListener('click', () => { 3562 if (!mediaQuery.matches) return; 3563 setExpanded(btn.getAttribute('aria-expanded') !== 'true'); 3564 }); 3565 3566 const sync = () => { 3567 if (!mediaQuery.matches) setExpanded(false); 3568 }; 3569 3570 if (typeof mediaQuery.addEventListener === 'function') { 3571 mediaQuery.addEventListener('change', sync); 3572 } else if (typeof mediaQuery.addListener === 'function') { 3573 mediaQuery.addListener(sync); 3574 } 3575} 3576 3577function initNewExperienceCountriesToggle() {
3578 const block = document.querySelector('.new-experience__countries-block'); 3579 const list = block?.querySelector('.new-experience__countries'); 3580 const head = block?.querySelector('.new-experience__countries-head'); 3581 const btn = block?.querySelector('.new-experience__countries-toggle'); 3582 if (!block || !list || !head || !btn) return; 3583 3584 const mediaQuery = window.matchMedia('(max-width: 1024px)'); 3585 3586 const getRowHeight = () => { 3587 const first = list.querySelector('.new-experience__country'); 3588 if (!first) return 37; 3589 return Math.ceil(first.getBoundingClientRect().height); 3590 }; 3591 3592 const applyRowHeightVar = () => { 3593 block.style.setProperty('--countries-row-height', `${getRowHeight()}px`); 3594 }; 3595 3596 const setListHeight = (open, animate = true) => { 3597 if (!mediaQuery.matches) { 3598 list.style.maxHeight = ''; 3599 return; 3600 } 3601 3602 applyRowHeightVar(); 3603 3604 if (open) { 3605 list.style.maxHeight = `${list.scrollHeight}px`; 3606 return; 3607 } 3608 3609 if (!animate) { 3610 list.style.maxHeight = `${getRowHeight()}px`; 3611 return; 3612 } 3613 3614 list.style.maxHeight = `${list.scrollHeight}px`; 3615 requestAnimationFrame(() => { 3616 list.style.maxHeight = `${getRowHeight()}px`; 3617 }); 3618 }; 3619 3620 const setExpanded = (open, animate = true) => { 3621 block.classList.toggle('is-expanded', open); 3622 btn.setAttribute('aria-expanded', String(open)); 3623 setListHeight(open, animate); 3624 }; 3625 3626 btn.addEventListener('click', (event) => { 3627 if (!mediaQuery.matches) return; 3628 event.stopPropagation(); 3629 setExpanded(btn.getAttribute('aria-expanded') !== 'true'); 3630 }); 3631 3632 head.addEventListener('click', () => { 3633 if (!mediaQuery.matches) return; 3634 if (!block.classList.contains('is-expanded')) return; 3635 setExpanded(false); 3636 }); 3637 3638 const sync = () => { 3639 if (!mediaQuery.matches) { 3640 setExpanded(false, false); 3641 list.style.maxHeight = ''; 3642 return; 3643 } 3644 setListHeight(block.classList.contains('is-expanded'), false); 3645 }; 3646 3647 sync(); 3648 3649 if (typeof mediaQuery.addEventListener === 'function') { 3650 mediaQuery.addEventListener('change', sync); 3651 } else if (typeof mediaQuery.addListener === 'function') { 3652 mediaQuery.addListener(sync); 3653 } 3654 3655 window.addEventListener('resize', () => { 3656 if (!mediaQuery.matches) return; 3657 applyRowHeightVar(); 3658 if (block.classList.contains('is-expanded')) { 3659 list.style.maxHeight = `${list.scrollHeight}px`; 3660 } else { 3661 list.style.maxHeight = `${getRowHeight()}px`; 3662 } 3663 }); 3664} 3665 3666function initPortfolioAboutSync() { 3667 const hero = document.querySelector('.hero-new-portfolio--detail'); 3668 const aboutSection = document.querySelector('.new-portfolio__section-about'); 3669 const text = document.querySelector('.new-portfolio__about-text'); 3670 const aboutBtn = document.querySelector('.new-portfolio__about-toggle'); 3671 const openAboutBtns = document.querySelectorAll( 3672 '.hero-new-portfolio--detail .hero__panel .hero__panel-btn--about, .new-portfolio__about-panel-btn, .new-portfolio__hero-panel-actions .hero__panel-btn--about' 3673 ); 3674 const heroCloseBtn = hero?.querySelector('.hero__content-close'); 3675 const aboutCollapseBtn = document.querySelector('.new-portfolio__about-collapse'); 3676 if (!hero && !aboutSection) return; 3677 3678 const setOpen = (open) => { 3679 if (open) { 3680 initPortfolioHeroMobileHeight.cacheBeforeOpen?.(); 3681 } 3682 3683 hero?.classList.toggle('is-about-open', open); 3684 aboutSection?.classList.toggle('is-about-open', open); 3685 text?.classList.toggle('is-open', open); 3686 aboutBtn?.setAttribute('aria-expanded', String(open)); 3687 openAboutBtns.forEach((btn) => btn.setAttribute('aria-expanded', String(open))); 3688 3689 requestAnimationFrame(() => { 3690 initPortfolioHeroMobileHeight.update?.(); 3691 }); 3692 }; 3693 3694 openAboutBtns.forEach((btn) => { 3695 btn.addEventListener('click', (event) => { 3696 event.preventDefault(); 3697 const open = btn.getAttribute('aria-expanded') !== 'true'; 3698 setOpen(open); 3699 }); 3700 }); 3701 3702 if (heroCloseBtn) { 3703 heroCloseBtn.addEventListener('click', () => { 3704 setOpen(false); 3705 }); 3706 } 3707 3708 if (aboutCollapseBtn) { 3709 aboutCollapseBtn.addEventListener('click', () => { 3710 setOpen(false); 3711 }); 3712 } 3713 3714 if (aboutBtn) { 3715 aboutBtn.addEventListener('click', () => { 3716 const open = aboutBtn.getAttribute('aria-expanded') !== 'true'; 3717 setOpen(open); 3718 }); 3719 } 3720} 3721 3722function initPortfolioGallerySlider() { 3723 const section = document.querySelector('.new-portfolio__section-gallery'); 3724 if (!section) return; 3725 3726 const formatIndex = (index) => String(index + 1).padStart(2, '0'); 3727 3728 section.querySelectorAll('.new-portfolio__gallery').forEach((block) => { 3729 const sliderEl = block.querySelector('.new-portfolio__gallery-slider'); 3730 const currentEl = block.querySelector('.new-portfolio__gallery-counter-current'); 3731 const totalEl = block.querySelector('.new-portfolio__gallery-counter-total'); 3732 const prevBtn = block.querySelector('.new-portfolio__gallery-nav-btn--prev'); 3733 const nextBtn = block.querySelector('.new-portfolio__gallery-nav-btn--next'); 3734 3735 if (!sliderEl) return; 3736 3737 const slides = sliderEl.querySelectorAll('.swiper-slide'); 3738 const total = slides.length; 3739 3740 if (totalEl) { 3741 totalEl.textContent = String(total).padStart(2, '0'); 3742 } 3743 3744 const updateCounter = (swiper) => { 3745 if (currentEl) { 3746 currentEl.textContent = formatIndex(swiper.activeIndex); 3747 } 3748 }; 3749 3750 const swiper = new Swiper(sliderEl, { 3751 slidesPerView: 'auto', 3752 slidesPerGroup: 1, 3753 spaceBetween: 20, 3754 speed: 900, 3755 loop: false, 3756 breakpoints: { 3757 0: { 3758 spaceBetween: 12, 3759 }, 3760 768: { 3761 spaceBetween: 20, 3762 }, 3763 }, 3764 on: { 3765 init: updateCounter, 3766 slideChange: updateCounter, 3767 }, 3768 }); 3769 3770 sliderEl.querySelectorAll('img').forEach((img) => { 3771 if (img.complete) return; 3772 img.addEventListener('load', () =>
3772 swiper.update(), { once: true }); 3773 }); 3774 3775 prevBtn?.addEventListener('click', () => swiper.slidePrev()); 3776 nextBtn?.addEventListener('click', () => swiper.slideNext()); 3777 }); 3778} 3779 3780function initPortfolioReasonsSlider() { 3781 const sliderEl = document.querySelector('.new-portfolio__reasons-cards-slider'); 3782 if (!sliderEl) return; 3783 3784 const cards = sliderEl.closest('.new-portfolio__reasons-cards'); 3785 const prevBtn = cards?.querySelector('.new-portfolio__reasons-nav-btn--prev'); 3786 const nextBtn = cards?.querySelector('.new-portfolio__reasons-nav-btn--next'); 3787 const paginationEl = cards?.querySelector('.new-portfolio__reasons-pagination'); 3788 3789 setupResponsiveSwiper({ 3790 selector: '.new-portfolio__reasons-cards-slider', 3791 breakpoint: 1025, 3792 options: { 3793 slidesPerView: 'auto', 3794 spaceBetween: 16, 3795 speed: 600, 3796 breakpoints: { 3797 768: { 3798 spaceBetween: 20, 3799 }, 3800 }, 3801 navigation: { 3802 prevEl: prevBtn, 3803 nextEl: nextBtn, 3804 }, 3805 pagination: { 3806 el: paginationEl, 3807 clickable: true, 3808 }, 3809 }, 3810 }); 3811} 3812 3813function initSmoothAnchorScroll() { 3814 const header = document.getElementById('header'); 3815 if (!header) return; 3816 3817 let scrollAnimationId = null; 3818 3819 const getHeaderOffset = () => header.getBoundingClientRect().height; 3820 3821 const getScrollTopForTarget = (target) => { 3822 const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset(); 3823 return Math.max(0, top); 3824 }; 3825 3826 const easeInOutCubic = (progress) => 3827 progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2; 3828 3829 const playActiveShowcaseVideo = (scope) => { 3830 const activeSlide = scope.querySelector('.showcase__slide.swiper-slide-active'); 3831 if (!activeSlide) return; 3832 3833 const video = activeSlide.querySelector('video'); 3834 const playBtn = activeSlide.querySelector('.video-js__play'); 3835 if (!video || !video.paused) return; 3836 3837 if (playBtn) { 3838 playBtn.click(); 3839 return; 3840 } 3841 3842 video.play().catch(() => {}); 3843 }; 3844 3845 // play() поÑле rAF-ÑкÑолла ÑеÑÑÐµÑ user-gesture; ÑдеÑживаем медиа до конÑа ÑкÑолла. 3846 let holdShowcaseVideoUntilScrollEnd = false; 3847 3848 const prepareShowcaseVideoForScrollPlay = (scope) => { 3849 const activeSlide = scope.querySelector('.showcase__slide.swiper-slide-active'); 3850 const video = activeSlide?.querySelector('video'); 3851 if (!video) return; 3852 3853 holdShowcaseVideoUntilScrollEnd = true; 3854 3855 const playPromise = video.play(); 3856 if (!playPromise?.then) return; 3857 3858 playPromise 3859 .then(() => { 3860 if (!holdShowcaseVideoUntilScrollEnd) return; 3861 video.pause(); 3862 video.currentTime = 0; 3863 }) 3864 .catch(() => {}); 3865 }; 3866 3867 const smoothScrollTo = (top, duration = 600, onComplete) => { 3868 if (scrollAnimationId) { 3869 cancelAnimationFrame(scrollAnimationId); 3870 scrollAnimationId = null; 3871 } 3872 3873 const start = window.scrollY; 3874 const distance = top - start; 3875 3876 if (Math.abs(distance) < 1) { 3877 window.scrollTo(0, top); 3878 onComplete?.(); 3879 return; 3880 } 3881 3882 const startTime = performance.now(); 3883 3884 const step = (currentTime) => { 3885 const elapsed = currentTime - startTime; 3886 const progress = Math.min(elapsed / duration, 1); 3887 window.scrollTo(0, start + distance * easeInOutCubic(progress)); 3888 3889 if (progress < 1) { 3890 scrollAnimationId = requestAnimationFrame(step); 3891 } else { 3892 scrollAnimationId = null; 3893 onComplete?.(); 3894 } 3895 }; 3896 3897 scrollAnimationId = requestAnimationFrame(step); 3898 }; 3899 3900 const scrollToHashTarget = (target, { updateHash = true, smooth = true } = {}) => { 3901 if (!target) return; 3902 3903 const top = getScrollTopForTarget(target); 3904 const afterScroll = () => { 3905 if (target.id === 'video') { 3906 holdShowcaseVideoUntilScrollEnd = false; 3907 playActiveShowcaseVideo(target); 3908 } 3909 }; 3910 3911 if (smooth) { 3912 smoothScrollTo(top, 600, afterScroll); 3913 } else { 3914 window.scrollTo(0, top); 3915 afterScroll(); 3916 } 3917 3918 if (updateHash && target.id) { 3919 history.pushState(null, '', `#${target.id}`); 3920 } 3921 }; 3922 3923 const getHashFromLink = (link) => { 3924 const href = link.getAttribute('href'); 3925 if (!href) return ''; 3926 3927 if (href.startsWith('#')) { 3928 return href; 3929 } 3930 3931 try { 3932 const url = new URL(href, window.location.href); 3933 if (url.origin !== window.location.origin) return ''; 3934 if (url.pathname !== window.location.pathname) return ''; 3935 return url.hash; 3936 } catch { 3937 return ''; 3938 } 3939 }; 3940 3941 const handleAnchorClick = (event) => { 3942 const link = event.target.closest('a[href*="#"]'); 3943 if (!link || link.target === '_blank' || link.classList.contains('hero__panel-btn--about')) { 3944 return; 3945 } 3946 3947 const hash = getHashFromLink(link); 3948 if (!hash || hash === '#') return; 3949 3950 let target; 3951 try { 3952 target = document.querySelector(hash); 3953 } catch { 3954 return; 3955 } 3956 3957 if (!target) return; 3958 3959 event.preventDefault(); 3960 event.stopPropagation(); 3961 3962 if (target.id === 'video') { 3963 prepareShowcaseVideoForScrollPlay(target); 3964 } 3965 3966 scrollToHashTarget(target); 3967 }; 3968 3969 document.addEventListener('click', handleAnchorClick, true); 3970 3971 if (window.location.hash && window.location.hash !== '#') { 3972 if ('scrollRestoration' in history) { 3973 history.scrollRestoration = 'manual'; 3974 } 3975 window.scrollTo(0, 0); 3976 } 3977 3978 const scrollOnLoad = () => { 3979 const { hash } = window.location; 3980 if (!hash || hash === '#') return; 3981 3982 let target; 3983 try { 3984 target = document.querySelector(hash); 3985 } catch { 3986 return; 3987 } 3988 3989 if (!target) return; 3990 3991 scrollToHashTarget(target, { updateHash: false, smooth: false }); 3992 }; 3993 3994 if (document.readyState === 'complete') { 3995 scrollOnLoad(); 3996 } else { 3997 window.addEventListener('load', scrollOnLoad, { once: true }); 3998 } 3999} 4000 4001function initToggleActive(buttonSelector, containerSelector, activeClass = 'is-active') { 4002 document.querySelectorAll(buttonSelector).forEach((button) => { 4003 button.addEventListener('click', () => { 4004 const container = button.closest(containerSelector); 4005 if (container) { 4006 container.classList.toggle(activeClass); 4007 } 4008 }); 4009 }); 4010} 4011 4012function hydrateMirrors() { 4013 const groups = {}; 4014 document.querySelectorAll('[data-mirror]').forEach((el) => {
4015 const key = el.dataset.mirror; 4016 (groups[key] = groups[key] || []).push(el); 4017 }); 4018 Object.values(groups).forEach((els) => { 4019 const src = els.find((e) => e.innerHTML.trim() !== ''); 4020 if (!src) return; 4021 els.forEach((e) => { 4022 if (e !== src && e.innerHTML.trim() === '') e.innerHTML = src.innerHTML; 4023 }); 4024 }); 4025} 4026 4027function hydrateMobileCountries() { 4028 document.querySelectorAll('[data-mobile-countries]').forEach((container) => { 4029 const tpl = document.querySelector( 4030 `template[data-mobile-countries-tpl="${container.dataset.mobileCountries}"]` 4031 ); 4032 if (!tpl) return; 4033 container.appendChild(tpl.content.cloneNode(true)); 4034 tpl.remove(); 4035 }); 4036} 4037 4038function countriesMapInit() { 4039 const buttons = document.querySelectorAll('.experience__country'); 4040 const regions = document.querySelectorAll('.experience__map .mapsvg-region'); 4041 4042 buttons.forEach((btn) => { 4043 btn.addEventListener('mouseenter', () => { 4044 const regionId = btn.dataset.countryId; 4045 const region = document.querySelector(`.mapsvg-region[data-name="${regionId}"]`); 4046 4047 regions.forEach((r) => r.classList.remove('active')); 4048 buttons.forEach((b) => b.classList.remove('active')); 4049 btn.classList.add('active'); 4050 4051 if (region) { 4052 region.classList.add('active'); 4053 4054 const bbox = region.getBBox(); 4055 const cx = bbox.x + bbox.width / 2; 4056 const cy = bbox.y + bbox.height / 2; 4057 4058 let marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); 4059 marker.setAttribute('cx', cx); 4060 marker.setAttribute('cy', cy); 4061 marker.setAttribute('r', 5); 4062 marker.setAttribute('fill', 'red'); 4063 marker.classList.add('region-marker'); 4064 4065 region.parentNode.appendChild(marker); 4066 } 4067 }); 4068 4069 btn.addEventListener('mouseleave', () => { 4070 btn.classList.remove('active'); 4071 4072 const regionId = btn.dataset.countryId; 4073 const region = document.querySelector(`.mapsvg-region[data-name="${regionId}"]`); 4074 4075 if (region) { 4076 region.classList.remove('active'); 4077 4078 const marker = document.querySelector('.region-marker'); 4079 if (marker) marker.remove(); 4080 } 4081 }); 4082 }); 4083} 4084 4085// AJAX-loaded modals: keep modal scaffolding out of the static HTML for crawlers. 4086// Modals live in modals.php and are fetched on demand via /wp-admin/admin-ajax.php?action=vp_modals. 4087window.vpModalsState = window.vpModalsState || { loaded: false, promise: null }; 4088 4089function vpLoadModals() { 4090 if (window.vpModalsState.loaded) return Promise.resolve(true); 4091 if (window.vpModalsState.promise) return window.vpModalsState.promise; 4092 const ajaxUrl = (window.VPAjax && window.VPAjax.url) || '/wp-admin/admin-ajax.php'; 4093 window.vpModalsState.promise = fetch(ajaxUrl + '?action=vp_modals', { 4094 credentials: 'same-origin', 4095 }) 4096 .then((r) => r.text()) 4097 .then((html) => { 4098 const mount = document.getElementById('vp-modals-mount'); 4099 if (!mount) return false; 4100 mount.insertAdjacentHTML('beforeend', html); 4101 mount.removeAttribute('aria-hidden'); 4102 window.vpModalsState.loaded = true; 4103 4104 // Re-init CF7 forms inside newly mounted modals (v5+ API) 4105 if (window.wpcf7 && typeof window.wpcf7.init === 'function') { 4106 mount.querySelectorAll('form.wpcf7-form, .wpcf7 form').forEach(function (f) { 4107 try { 4108 window.wpcf7.init(f); 4109 } catch (e) {} 4110 }); 4111 } 4112 4113 // Run modal-related init hooks now that elements exist 4114 try { 4115 if (typeof initProject360Modal === 'function') initProject360Modal(); 4116 } catch (e) {} 4117 try { 4118 if (typeof quizInit === 'function') quizInit(); 4119 } catch (e) {} 4120 try { 4121 if (typeof initPhoneInput === 'function') initPhoneInput(); 4122 } catch (e) {} 4123 try { 4124 if (typeof initFileUpload === 'function') initFileUpload(); 4125 } catch (e) {} 4126 try { 4127 if (typeof hydrateMirrors === 'function') hydrateMirrors(); 4128 } catch (e) {} 4129 4130 return true; 4131 }) 4132 .catch(function (err) { 4133 // Reset so retry is possible 4134 window.vpModalsState.promise = null; 4135 console.warn('[vp] modals AJAX failed', err); 4136 return false;
4137 }); 4138 return window.vpModalsState.promise; 4139} 4140 4141// Eager-load modals after page is idle (without blocking critical render). 4142// Most users get modals ready before clicking. Falls back to on-click load if this fails. 4143(function eagerLoadModals() { 4144 if (typeof window === 'undefined') return; 4145 const trigger = function () { 4146 vpLoadModals(); 4147 }; 4148 if ('requestIdleCallback' in window) { 4149 window.addEventListener('load', function () { 4150 requestIdleCallback(trigger, { timeout: 2500 }); 4151 }); 4152 } else { 4153 window.addEventListener('load', function () { 4154 setTimeout(trigger, 800); 4155 }); 4156 } 4157 // Also trigger on first user interaction in case 'load' is delayed 4158 ['pointerdown', 'keydown', 'touchstart'].forEach(function (ev) { 4159 document.addEventListener(ev, trigger, { once: true, passive: true }); 4160 }); 4161})(); 4162 4163function modalInit() { 4164 function closeModal(modal) { 4165 if (!modal) return; 4166 const source = modal.querySelector('input[name="source"]'); 4167 modal.classList.remove('modal--active'); 4168 if (source) source.value = ''; 4169 if (!document.querySelector('.modal--active')) { 4170 document.body.classList.remove('overflow-hidden'); 4171 } 4172 } 4173 4174 function activateModal(button, modal) { 4175 document.body.classList.add('overflow-hidden'); 4176 modal.classList.add('modal--active'); 4177 4178 if (button.hasAttribute('data-title')) { 4179 const sourceInput = modal.querySelector('input[name="source"]'); 4180 if (sourceInput) sourceInput.value = button.getAttribute('data-title'); 4181 } 4182 4183 const title = button.dataset.title; 4184 const img = button.dataset.img; 4185 const decorIndex = button.dataset.decorIndex; 4186 const decorLeftSrc = button.dataset.decorLeft; 4187 const decorRightSrc = button.dataset.decorRight; 4188 4189 const modalTitle = modal.querySelector('.js-modal-title'); 4190 const modalImg = modal.querySelector('.js-modal-img'); 4191 const decorLeft = modal.querySelector('.js-modal-decor-left'); 4192 const decorRight = modal.querySelector('.js-modal-decor-right'); 4193 const decorWrappers = modal.querySelectorAll('.modal__decor-wrapper'); 4194 4195 if (title && modalTitle) modalTitle.innerHTML = title; 4196 if (img && modalImg) modalImg.src = img; 4197 4198 if (decorLeft && decorRight) { 4199 decorLeft.className = 'decor modal__decor js-modal-decor-left'; 4200 decorRight.className = 'decor modal__decor js-modal-decor-right'; 4201 4202 decorWrappers.forEach(function (wrapper) { 4203 wrapper.className = 'decor-wrapper modal__decor-wrapper hide-md'; 4204 }); 4205 4206 if (decorIndex) { 4207 decorLeft.classList.add('modal__decor-' + decorIndex + '--left'); 4208 decorRight.classList.add('modal__decor-' + decorIndex + '--right'); 4209 decorWrappers.forEach(function (wrapper) { 4210 wrapper.classList.add('modal__decor-wrapper-' + decorIndex); 4211 }); 4212 } 4213 4214 const leftImg = decorLeft.querySelector('img'); 4215 const rightImg = decorRight.querySelector('img'); 4216 if (decorLeftSrc && leftImg) leftImg.src = decorLeftSrc; 4217 if (decorRightSrc && rightImg) rightImg.src = decorRightSrc; 4218 } 4219 } 4220 4221 // Delegated OPEN handler. Modals are AJAX-loaded into #vp-modals-mount; wait for that 4222 // before activating. Falls back gracefully if AJAX hasn't finished. 4223 document.addEventListener('click', function (e) { 4224 const button = e.target.closest('[data-modal]'); 4225 if (!button) return; 4226 if (e.target.closest('[data-close-modal]')) return; // edge case: click on close inside an active modal 4227 e.preventDefault(); 4228 4229 const modalId = button.dataset.modal; 4230 // Try immediate open (modals may already be mounted from eager-load) 4231 let modal = document.getElementById(modalId); 4232 if (modal) { 4233 activateModal(button, modal); 4234 return; 4235 } 4236 // Modals not yet loaded â fetch and then open 4237 if (typeof vpLoadModals === 'function') { 4238 vpLoadModals().then(function () { 4239 const m = document.getElementById(modalId); 4240 if (m) activateModal(button, m); 4241 }); 4242 } 4243 }); 4244 4245 // Delegated CLOSE handler: works for close buttons inside dynamically mounted modals 4246 document.addEventListener('click', function (e) { 4247 const btn = e.target.closest('[data-close-modal]'); 4248 if (!btn) return; 4249 closeModal(btn.closest('.modal')); 4250 }); 4251 4252 // Esc to close (веÑÑ Ð½ÑÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÐ°, еÑли оÑкÑÑÑо неÑколÑко) 4253 document.addEventListener('keydown', function (e) { 4254 if (e.key === 'Escape' || e.key === 'Esc') { 4255 const actives = document.querySelectorAll('.modal.modal--active'); 4256 closeModal(actives[actives.length - 1]); 4257 } 4258 }); 4259 4260 // Click outside (on backdrop) to close 4261 document.addEventListener('click', function (e) { 4262 if (e.target.classList && e.target.classList.contains('modal--active')) { 4263 closeModal(e.target); 4264 } 4265 }); 4266} 4267 4268HTMLElement.prototype.slideToggle = function (duration = 400, displayType = 'block') { 4269 this.clientHeight === 0 ? slideDown(this, duration, displayType) : slideUp(this, duration); 4270}; 4271 4272function slideDown(el, duration, displayType) { 4273 el.style.overflow = 'hidden'; 4274 el.style.display = displayType; 4275 4276 const elStyles = window.getComputedStyle(el); 4277 const original = { 4278 height: parseFloat(elStyles.height), 4279 paddingTop: parseFloat(elStyles.paddingTop), 4280 paddingBottom: parseFloat(elStyles.paddingBottom), 4281 marginTop: parseFloat(elStyles.marginTop), 4282 marginBottom: parseFloat(elStyles.marginBottom), 4283 }; 4284 4285 animate(el, original, duration, true); 4286} 4287 4288function slideUp(el, duration) { 4289 const elStyles = window.getComputedStyle(el); 4290 const original = { 4291 height: parseFloat(elStyles.height), 4292 paddingTop: parseFloat(elStyles.paddingTop), 4293 paddingBottom: parseFloat(elStyles.paddingBottom), 4294 marginTop: parseFloat(elStyles.marginTop), 4295 marginBottom: parseFloat(elStyles.marginBottom), 4296 }; 4297 4298 animate(el, original, duration, false); 4299} 4300 4301function animate(el, original, duration, isDown) { 4302 let start; 4303 4304 function step(timestamp) { 4305 if (start === undefined) start = timestamp; 4306 const progress = Math.min((timestamp - start) / duration, 1); 4307 const factor = isDown ? progress : 1 - progress; 4308 4309 el.style.height = original.height * factor + 'px'; 4310 el.style.paddingTop = original.paddingTop * factor + 'px'; 4311 el.style.paddingBottom = original.paddingBottom * factor + 'px'; 4312 el.style.marginTop = original.marginTop * factor + 'px'; 4313 el.style.marginBottom = original.marginBottom * factor + 'px'; 4314 4315 if (progress >= 1) { 4316 el.style.height = ''; 4317 el.style.paddingTop = ''; 4318 el.style.paddingBottom = ''; 4319 el.style.marginTop = ''; 4320 el.style.marginBottom = ''; 4321 el.style.overflow = ''; 4322 if (!isDown) el.style.display = 'none'; 4323 } else { 4324 requestAnimationFrame(step); 4325 } 4326 } 4327 4328 requestAnimationFrame(step); 4329} 4330 4331function initFormField() { 4332 const currentUrl = window.location.href; 4333 const pageUrlFields = document.querySelectorAll('input[name="your-page-url"]'); 4334 4335 if (pageUrlFields.length) { 4336 const url = new URL(currentUrl); 4337 const searchParams = new URLSearchParams(url.search);
4338 const utmParams = new URLSearchParams(); 4339 4340 // СобиÑаем ÑолÑко UTM-паÑамеÑÑÑ 4341 for (const [key, value] of searchParams.entries()) { 4342 if (key.startsWith('utm_')) { 4343 utmParams.set(key, value); 4344 } 4345 } 4346 4347 // ÐÑли еÑÑÑ UTM-паÑамеÑÑÑ, добавлÑем Ð¸Ñ Ð² URL 4348 if (utmParams.size > 0) { 4349 // ÐобавлÑем UTM к ÑÑÑеÑÑвÑÑÑим паÑамеÑÑам 4350 for (const [key, value] of utmParams.entries()) { 4351 searchParams.set(key, value); 4352 } 4353 url.search = searchParams.toString(); 4354 4355 // ÐбновлÑем currentUrl Ñ Ð½Ð¾Ð²Ñм URL 4356 const updatedUrl = url.toString(); 4357 // ÐаполнÑем вÑе найденнÑе Ð¿Ð¾Ð»Ñ 4358 pageUrlFields.forEach((field) => (field.value = updatedUrl)); 4359 } else { 4360 // ÐÑли UTM-паÑамеÑÑов неÑ, пÑоÑÑо заполнÑем ÑекÑÑим URL 4361 pageUrlFields.forEach((field) => (field.value = currentUrl)); 4362 } 4363 } 4364} 4365 4366document.addEventListener('click', (e) => { 4367 const modalBtn = e.target.closest('#header .header__bottom-mobile [data-modal]'); 4368 if (!modalBtn) return; 4369 4370 const header = document.getElementById('header'); 4371 if (!header?.classList.contains('header--nav-active')) return; 4372 4373 // ÑолÑко мобилÑное Ð¼ÐµÐ½Ñ 4374 if (window.innerWidth >= 1201) return; 4375 4376 const burger = header.querySelector('.header__burger'); 4377 if (burger) burger.click(); 4378}); 4379 4380function initHeroVideo() { 4381 document.querySelectorAll('.hero__slider .before-after__handle button').forEach((button) => { 4382 button.addEventListener('pointerup', () => { 4383 const slider = button.closest('.hero__slider'); 4384 if (!slider) return; 4385 slider.querySelectorAll('video').forEach((video) => { 4386 video.play().catch((err) => console.log('Play blocked:', err)); 4387 }); 4388 slider.querySelectorAll('.before-after__icon--hand').forEach((el) => { 4389 el.classList.remove('btn-hidden'); 4390 }); 4391 slider.querySelectorAll('.before-after__icon--play').forEach((el) => { 4392 el.classList.add('btn-hidden'); 4393 }); 4394 }); 4395 }); 4396} 4397 4398/* ============================================================ 4399 YANDEX METRIKA â Ñели (ÑÑÑÑÑик 23472505) 4400 ============================================================ */ 4401(function initMetrikaGoals() { 4402 const COUNTER_ID = 23472505; 4403 const reach = (g, p) => { try { window.ym(COUNTER_ID, 'reachGoal', g, p || {}); } catch (e) {} }; 4404 4405 // ---------- 1. СоÑÑеÑи (BEM-клаÑÑÑ) ---------- 4406 const SOCIAL = { 4407 'social__link--whatsapp': 'whatsapp', 4408 'social__link--telegram': 'telegram', 4409 'social__link--youtube': 'youtube', 4410 'header__contacts-messenger--telegram': 'telegram', 4411 }; 4412 document.addEventListener('click', (e) => { 4413 const a = e.target.closest('a[class*="social__link--"], a[class*="header__contacts-messenger--"]'); 4414 if (!a) return; 4415 for (const c in SOCIAL) if (a.classList.contains(c)) { reach(SOCIAL[c]); return; } 4416 }, true); 4417 4418 // ---------- 2. СоÑÑеÑи/плоÑадки по href ---------- 4419 document.addEventListener('click', (e) => { 4420 const a = e.target.closest('a[href]'); 4421 if (!a) return; 4422 const h = (a.getAttribute('href') || '').toLowerCase(); 4423 if (/youtube\.com|youtu\.be/.test(h)) reach('youtube'); 4424 else if (/houzz\./.test(h)) reach('houzz'); 4425 else if (/profi\.ru/.test(h)) reach('profi'); 4426 else if (/contacts/.test(h)) reach('contacts'); 4427 }, true); 4428 4429 // ---------- 3. on_phone ---------- 4430 document.addEventListener('click', (e) => { 4431 const a = e.target.closest('a[href^="tel:"]'); 4432 if (a) reach('on_phone', { phone: a.getAttribute('href') }); 4433 }, true); 4434 4435 // ---------- 4. ÐпÑеделÑем ÑÐµÐ»Ñ ÐºÐ½Ð¾Ð¿ÐºÐ¸ ---------- 4436 const MODAL_ID_TO_GOAL = { 4437 'modal-rasschitat-stoimost': 'smeta', 4438 'modal-ostavit-zayavku-3-vedomost-smeta': 'smeta', 4439 'modal-kvartiry-doma': 'zakaz_dp', 4440 'modal-kommercheskie-interery': 'zakaz_dp', 4441 'modal-zhilye-interery': 'zakaz_dp', 4442 'modal-sotrudnichestvo': 'napishite', 4443 'modal-prezentacziya-mopov-i-lobbi': 'prez_gold', 4444 'modal-prezentacziya-arhitekturnyh-proektov': 'prez_chrome', 4445 'modal-prezentacziya-ofisnyh-intererov': 'prez_white', 4446 'modal-premialnyj-stil': 'prez_black', 4447 }; 4448 4449 const TEXT_TO_GOAL = { 4450 'пеÑезвониÑе мне': 'call_back', 4451 'заказаÑÑ Ð·Ð²Ð¾Ð½Ð¾Ðº': 'call_back', 4452 'заказаÑÑ ÐºÐ¾Ð½ÑÑлÑÑаÑиÑ': 'consult', 4453 'полÑÑиÑÑ ÐºÐ¾Ð½ÑÑлÑÑаÑиÑ': 'consult', 4454 'заказаÑÑ ÑмеÑÑ': 'smeta', 4455 'ÑаÑÑÑиÑаÑÑ ÑÑоимоÑÑÑ': 'smeta', 4456 'заказаÑÑ Ð´ÐµÐ½Ñ Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑом': 'day_wd', 4457 'заказаÑÑ ÑеÑÑ-дÑайв': 'test_drive', 4458 'заказаÑÑ Ð´Ð¸Ð·Ð°Ð¹Ð½ и надзоÑ': 'des_nadzor', 4459 'заказаÑÑ Ð´Ð¸Ð·Ð°Ð¹Ð½ под клÑÑ': 'des_kluch', 4460 'заÑвка на авÑоÑÑкий надзоÑ': 'aut_nadzor', 4461 'заÑвка на комплекÑаÑиÑ': 'komplekt', 4462 'заÑвка на ÑпÑавление': 'uprav', 4463 'напиÑиÑе нам оÑзÑв': 'otzyv', 4464 'напиÑиÑе нам': 'napishite', 4465 'оÑÑавиÑÑ Ð·Ð°ÑвкÑ': 'zayavka', 4466 'подаÑÑ Ð·Ð°ÑвкÑ': 'zayavka', 4467 'полÑÑиÑÑ Ð°Ð»Ñбом Ñо ÑÑилем': 'album_style', 4468 }; 4469 4470 function detectGoal(btn) { 4471 if (!btn) return null; 4472 if (btn.dataset.goal) return btn.dataset.goal; 4473 4474 // --- СпеÑ-ÑлÑÑай: заÑвка внÑÑÑи .cta = «ÐаказаÑÑ ÑмеÑÑ» --- 4475 if (btn.closest('.cta')) return 'smeta'; 4476 4477 const title = (btn.dataset.title || '').trim().toLowerCase(); 4478 if (TEXT_TO_GOAL[title]) return TEXT_TO_GOAL[title]; 4479 4480 const text = (btn.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase(); 4481 if (TEXT_TO_GOAL[text]) return TEXT_TO_GOAL[text]; 4482 4483 const modalId = btn.dataset.modal; 4484 if (MODAL_ID_TO_GOAL[modalId]) return MODAL_ID_TO_GOAL[modalId]; 4485 4486 return null; 4487 } 4488 4489 let pendingGoal = null; 4490 document.addEventListener('click', (e) => { 4491 const btn = e.target.closest('[data-modal]'); 4492 if (!btn) return; 4493 pendingGoal = detectGoal(btn) || pendingGoal; 4494 }, true); 4495 4496 // ---------- 5. ÐÑпÑавка CF7 ---------- 4497 document.addEventListener('wpcf7mailsent', (e) => { 4498 const form = e.target; 4499 const goal = form.dataset.goal || pendingGoal || 'zayavka'; 4500 reach(goal, { source: form.querySelector('input[name="source"]')?.value || '' }); 4501 pendingGoal = null; 4502 }); 4503 4504 // ---------- 6. ÐÑпÑавка квиза ---------- 4505 document.addEventListener('submit', (e) => { 4506 const form = e.target.closest('.quiz__form'); 4507 if (!form) return; 4508 const quiz = form.closest('.quiz'); 4509 const goal = quiz?.dataset.goal || 'style_q'; // style_q / album_style 4510 reach(goal); 4511 }, true); 4512})();
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.