|
634 | 634 |
|
635 | 635 | function scrollToTarget(hash) { |
636 | 636 | if (!hash || hash === "#") { |
637 | | - return; |
| 637 | + return null; |
638 | 638 | } |
639 | 639 |
|
640 | 640 | const target = document.querySelector(hash); |
641 | 641 | if (!target) { |
642 | | - return; |
| 642 | + return null; |
643 | 643 | } |
644 | 644 |
|
645 | 645 | if (hash === '#hero'){ |
646 | 646 | window.scrollTo({ |
647 | 647 | top: 0, |
648 | 648 | behavior: prefersReducedMotion ? "auto" : "smooth" |
649 | 649 | }); |
650 | | - return; |
| 650 | + return 0; |
651 | 651 | } |
652 | 652 |
|
653 | 653 | const destination = Math.max(0, target.getBoundingClientRect().top + window.scrollY - getNavOffset()); |
|
656 | 656 | top: destination, |
657 | 657 | behavior: prefersReducedMotion ? "auto" : "smooth" |
658 | 658 | }); |
| 659 | + |
| 660 | + return destination; |
659 | 661 | } |
660 | 662 |
|
661 | 663 | function initAnchorScroll(){ |
|
670 | 672 | if (!target) return; |
671 | 673 |
|
672 | 674 | event.preventDefault(); |
673 | | - scrollToTarget(hash); |
| 675 | + const destination = scrollToTarget(hash); |
| 676 | + window.dispatchEvent(new CustomEvent('modelized:navautoscroll', { |
| 677 | + detail: { hash, destination } |
| 678 | + })); |
674 | 679 |
|
675 | 680 | const nav = document.querySelector('.nav'); |
676 | 681 | if (nav?.classList.contains('nav--open')){ |
|
792 | 797 | }; |
793 | 798 |
|
794 | 799 | const homeSection = document.querySelector("#hero"); |
795 | | - let lastViewportHash = location.hash === "#hero" ? "" : location.hash; |
796 | | - |
797 | | - const syncUrlToSection = (section) => { |
798 | | - if (!section || !section.id) { |
799 | | - return; |
| 800 | + let lockedHash = ""; |
| 801 | + let lockedDestination = null; |
| 802 | + let lockFrame = 0; |
| 803 | + let lockTimer = 0; |
| 804 | + |
| 805 | + const clearScrollLock = () => { |
| 806 | + if (lockFrame) { |
| 807 | + cancelAnimationFrame(lockFrame); |
| 808 | + lockFrame = 0; |
800 | 809 | } |
801 | | - |
802 | | - const sectionHash = `#${section.id}`; |
803 | | - const nextHash = section === homeSection ? "" : sectionHash; |
804 | | - if (nextHash === lastViewportHash) { |
805 | | - return; |
| 810 | + if (lockTimer) { |
| 811 | + clearTimeout(lockTimer); |
| 812 | + lockTimer = 0; |
806 | 813 | } |
| 814 | + lockedHash = ""; |
| 815 | + lockedDestination = null; |
| 816 | + }; |
| 817 | + |
| 818 | + const releaseScrollLock = () => { |
| 819 | + clearScrollLock(); |
| 820 | + syncFromViewport(); |
| 821 | + }; |
| 822 | + |
| 823 | + const watchScrollLock = () => { |
| 824 | + if (!lockedHash) return; |
| 825 | + |
| 826 | + let lastY = getScrollTop(); |
| 827 | + let stillFrames = 0; |
| 828 | + |
| 829 | + const tick = () => { |
| 830 | + lockFrame = 0; |
| 831 | + if (!lockedHash) return; |
| 832 | + |
| 833 | + const currentY = getScrollTop(); |
| 834 | + const delta = Math.abs(currentY - lastY); |
| 835 | + const nearDestination = lockedDestination !== null && Math.abs(currentY - lockedDestination) <= 2; |
807 | 836 |
|
808 | | - const nextUrl = nextHash |
809 | | - ? `${location.pathname}${location.search}${nextHash}` |
810 | | - : `${location.pathname}${location.search}`; |
| 837 | + lastY = currentY; |
| 838 | + stillFrames = (nearDestination || delta < 0.5) ? (stillFrames + 1) : 0; |
811 | 839 |
|
812 | | - history.replaceState(null, "", nextUrl); |
813 | | - lastViewportHash = nextHash; |
| 840 | + if (stillFrames >= 3) { |
| 841 | + releaseScrollLock(); |
| 842 | + return; |
| 843 | + } |
| 844 | + |
| 845 | + lockFrame = requestAnimationFrame(tick); |
| 846 | + }; |
| 847 | + |
| 848 | + lockFrame = requestAnimationFrame(tick); |
814 | 849 | }; |
815 | 850 |
|
816 | 851 | setActiveByHash(location.hash); |
|
823 | 858 | map.forEach((_link, section) => sectionRatios.set(section, 0)); |
824 | 859 |
|
825 | 860 | const syncFromViewport = () => { |
| 861 | + if (lockedHash) { |
| 862 | + setActiveByHash(lockedHash); |
| 863 | + return; |
| 864 | + } |
| 865 | + |
826 | 866 | const viewportFocus = window.innerHeight * 0.45; |
827 | 867 | let currentSection = null; |
828 | 868 | let bestScore = -Infinity; |
|
853 | 893 |
|
854 | 894 | const currentHash = currentSection.id ? `#${currentSection.id}` : ""; |
855 | 895 | setActiveByHash(currentHash); |
856 | | - syncUrlToSection(currentSection); |
857 | 896 | }; |
858 | 897 |
|
859 | 898 | const observer = new IntersectionObserver( |
|
872 | 911 |
|
873 | 912 | map.forEach((_link, section) => observer.observe(section)); |
874 | 913 | requestAnimationFrame(syncFromViewport); |
| 914 | + |
| 915 | + window.addEventListener('modelized:navautoscroll', (event) => { |
| 916 | + const nextHash = event.detail?.hash; |
| 917 | + if (!nextHash) return; |
| 918 | + |
| 919 | + clearScrollLock(); |
| 920 | + lockedHash = nextHash; |
| 921 | + lockedDestination = typeof event.detail?.destination === 'number' ? event.detail.destination : null; |
| 922 | + setActiveByHash(lockedHash); |
| 923 | + |
| 924 | + if (prefersReducedMotion) { |
| 925 | + lockTimer = window.setTimeout(releaseScrollLock, 0); |
| 926 | + return; |
| 927 | + } |
| 928 | + |
| 929 | + lockTimer = window.setTimeout(watchScrollLock, 120); |
| 930 | + }); |
| 931 | + |
| 932 | + window.addEventListener('orientationchange', clearScrollLock); |
| 933 | + window.addEventListener('resize', () => { |
| 934 | + if (!lockedHash) return; |
| 935 | + lockTimer = window.setTimeout(releaseScrollLock, 0); |
| 936 | + }); |
875 | 937 | } |
876 | 938 |
|
877 | 939 | function initSectionDepth() { |
|
1211 | 1273 | initParallax(); |
1212 | 1274 | initHoverTracking(); |
1213 | 1275 |
|
1214 | | - if (location.hash){ |
1215 | | - requestAnimationFrame(() => { |
1216 | | - scrollToTarget(location.hash); |
1217 | | - }); |
1218 | | - }else{ |
1219 | | - requestAnimationFrame(() => { |
1220 | | - window.scrollTo({ top: 0, behavior: 'auto' }); |
1221 | | - }); |
1222 | | - } |
1223 | 1276 | } |
1224 | 1277 |
|
1225 | 1278 | if (document.readyState === "loading") { |
|
0 commit comments