commit 20260703
This commit is contained in:
+183
-1
@@ -1,10 +1,183 @@
|
||||
/**
|
||||
* Home hero carousel — autoplay + dot navigation + mobile swipe
|
||||
* Home hero carousel — autoplay + dot navigation + mobile swipe + layout sync
|
||||
*
|
||||
* Layout contract (extensible per banner):
|
||||
* data-hero-layout-reference — slide that defines carousel height
|
||||
* data-hero-layout-anchor — element whose bottom anchors dot position
|
||||
* --hero-coach-dots-gap — gap between anchor bottom and dots top (default 35px)
|
||||
* Mobile: .is-banner-active fixes dots to viewport bottom while banner is visible
|
||||
*/
|
||||
(function () {
|
||||
var AUTOPLAY_MS = 5000;
|
||||
var SWIPE_THRESHOLD = 48;
|
||||
var MOBILE_MQ = '(max-width: 991.98px)';
|
||||
var DESKTOP_MQ = '(min-width: 992px)';
|
||||
|
||||
function readPx(styles, name, fallback) {
|
||||
var value = parseFloat(styles.getPropertyValue(name));
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function getLayoutScope(root) {
|
||||
return root.closest('#homepage') || root;
|
||||
}
|
||||
|
||||
function clearMeasuredLayout(scope, root) {
|
||||
[
|
||||
'--hero-reference-content-bottom',
|
||||
'--hero-reference-viewport-height',
|
||||
'--hero-reference-dots-top',
|
||||
'--hero-mobile-dots-reserve',
|
||||
'--hero-dots-y',
|
||||
].forEach(function (name) {
|
||||
scope.style.removeProperty(name);
|
||||
});
|
||||
|
||||
if (root) {
|
||||
var dots = root.querySelector('.home-hero-carousel__dots');
|
||||
if (dots) {
|
||||
dots.removeAttribute('data-hero-dots-positioned');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncHeroLayout(root) {
|
||||
var inner = root.querySelector('.home-hero-carousel__inner');
|
||||
var viewport = root.querySelector('.home-hero-carousel__viewport');
|
||||
if (!inner || !viewport) return;
|
||||
|
||||
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|
||||
|| root.querySelector('[data-home-hero-slide]');
|
||||
if (!referenceSlide) return;
|
||||
|
||||
var anchor = referenceSlide.querySelector('[data-hero-layout-anchor]')
|
||||
|| referenceSlide.querySelector('.home-hero-carousel__ai-coach')
|
||||
|| referenceSlide.querySelector('.home-hero-carousel__actions :last-child');
|
||||
if (!anchor) return;
|
||||
|
||||
var scope = getLayoutScope(root);
|
||||
var styles = getComputedStyle(scope);
|
||||
var coachDotsGap = readPx(styles, '--hero-coach-dots-gap', 35);
|
||||
var dotsHeight = readPx(styles, '--hero-dots-height', 10);
|
||||
var innerPaddingBottom = readPx(styles, '--hero-inner-padding-bottom', 40);
|
||||
var isDesktop = window.matchMedia(DESKTOP_MQ).matches;
|
||||
|
||||
if (isDesktop) {
|
||||
var innerRect = inner.getBoundingClientRect();
|
||||
var viewportRect = viewport.getBoundingClientRect();
|
||||
var anchorRect = anchor.getBoundingClientRect();
|
||||
|
||||
var contentBottom = Math.ceil(anchorRect.bottom - viewportRect.top);
|
||||
var viewportHeight = contentBottom + coachDotsGap;
|
||||
var dotsTop = Math.ceil(anchorRect.bottom - innerRect.top + coachDotsGap);
|
||||
var dotsBottomY = dotsTop + dotsHeight + innerPaddingBottom;
|
||||
|
||||
scope.style.setProperty('--hero-reference-content-bottom', contentBottom + 'px');
|
||||
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
|
||||
scope.style.setProperty('--hero-reference-dots-top', dotsTop + 'px');
|
||||
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
|
||||
root.setAttribute('data-hero-layout-synced', 'desktop');
|
||||
|
||||
var dots = root.querySelector('.home-hero-carousel__dots');
|
||||
if (dots) {
|
||||
dots.setAttribute('data-hero-dots-positioned', 'true');
|
||||
}
|
||||
root.classList.remove('is-banner-active');
|
||||
return;
|
||||
}
|
||||
|
||||
clearMeasuredLayout(scope, root);
|
||||
|
||||
var viewportRect = viewport.getBoundingClientRect();
|
||||
var slideGrid = referenceSlide.querySelector('.home-hero-carousel__slide-grid');
|
||||
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
|
||||
var mobileFixedDotsBottom = readPx(styles, '--hero-mobile-fixed-dots-bottom', 16);
|
||||
var imageScale = readPx(styles, '--hero-visual-image-scale', 1.06);
|
||||
|
||||
if (!slideGrid) return;
|
||||
|
||||
var visualBottom = visual
|
||||
? visual.getBoundingClientRect().bottom - viewportRect.top
|
||||
: slideGrid.getBoundingClientRect().bottom - viewportRect.top;
|
||||
|
||||
if (visual) {
|
||||
var img = visual.querySelector('img');
|
||||
if (img && imageScale > 1) {
|
||||
var imgHeight = img.getBoundingClientRect().height;
|
||||
visualBottom += Math.ceil((imgHeight * imageScale - imgHeight) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
var viewportHeight = Math.ceil(visualBottom);
|
||||
var dotsReserve = dotsHeight + mobileFixedDotsBottom;
|
||||
var homepageRect = scope.getBoundingClientRect();
|
||||
var dotsBottomY = Math.ceil(
|
||||
viewportRect.top - homepageRect.top + viewportHeight + dotsReserve + innerPaddingBottom
|
||||
);
|
||||
|
||||
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
|
||||
scope.style.setProperty('--hero-mobile-dots-reserve', dotsReserve + 'px');
|
||||
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
|
||||
|
||||
root.setAttribute('data-hero-layout-synced', 'mobile');
|
||||
}
|
||||
|
||||
function bindMobileFixedDots(root) {
|
||||
if (typeof IntersectionObserver === 'undefined') return;
|
||||
|
||||
var mobileMq = window.matchMedia(MOBILE_MQ);
|
||||
var observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
if (!mobileMq.matches) {
|
||||
root.classList.remove('is-banner-active');
|
||||
return;
|
||||
}
|
||||
root.classList.toggle('is-banner-active', entries[0].isIntersecting);
|
||||
},
|
||||
{
|
||||
threshold: 0,
|
||||
rootMargin: '0px 0px -8% 0px',
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(root);
|
||||
|
||||
mobileMq.addEventListener('change', function () {
|
||||
if (!mobileMq.matches) {
|
||||
root.classList.remove('is-banner-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindHeroLayoutSync(root) {
|
||||
var sync = function () {
|
||||
syncHeroLayout(root);
|
||||
};
|
||||
|
||||
sync();
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
var observer = new ResizeObserver(sync);
|
||||
observer.observe(root);
|
||||
var inner = root.querySelector('.home-hero-carousel__inner');
|
||||
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|
||||
|| root.querySelector('[data-home-hero-slide]');
|
||||
if (inner) observer.observe(inner);
|
||||
if (referenceSlide) observer.observe(referenceSlide);
|
||||
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
|
||||
if (visual) observer.observe(visual);
|
||||
}
|
||||
|
||||
var resizeTimer;
|
||||
window.addEventListener('resize', function () {
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(sync, 100);
|
||||
});
|
||||
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(sync);
|
||||
}
|
||||
}
|
||||
|
||||
function initHomeHeroCarousel(root) {
|
||||
if (root.getAttribute('data-carousel-ready') === 'true') return;
|
||||
@@ -133,6 +306,8 @@
|
||||
});
|
||||
|
||||
bindTouchSwipe();
|
||||
bindHeroLayoutSync(root);
|
||||
bindMobileFixedDots(root);
|
||||
setSlide(0);
|
||||
startAutoplay();
|
||||
}
|
||||
@@ -142,6 +317,13 @@
|
||||
}
|
||||
|
||||
window.AifoHomeHeroCarouselInit = initHomeHeroCarousel;
|
||||
window.AifoHomeHeroCarouselSyncLayout = function (root) {
|
||||
if (root) {
|
||||
syncHeroLayout(root);
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('[data-home-hero-carousel]').forEach(syncHeroLayout);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
|
||||
Reference in New Issue
Block a user