| | |
| | | (()=>{class t{constructor(t="dl.glossary",e="nav.glossary-index"){this.glossary=document.querySelector(t),this.nav=document.querySelector(e),this.glossary&&this.nav&&(this.terms=this.glossary.querySelectorAll("dt[id]"),this.navList=this.nav.querySelector("ul"),this.activeClass="active",this.currentActive=null,this.init(),this.setupResizeHandler())}init(){const t={root:null,rootMargin:this.getRootMargin(),threshold:0};this.observer=new IntersectionObserver((t=>this.handleIntersection(t)),t),this.terms.forEach((t=>this.observer.observe(t))),this.handleScroll=this.debounce((()=>this.checkActiveTerm()),100),window.addEventListener("scroll",this.handleScroll,{passive:!0})}getRootMargin(){return"-50% 0px -50% 0px"}setupResizeHandler(){let t;window.addEventListener("resize",(()=>{clearTimeout(t),t=setTimeout((()=>{this.reinitialize()}),250)}))}reinitialize(){this.observer&&this.observer.disconnect(),this.init()}handleIntersection(t){const e=t.find((t=>t.isIntersecting));e&&this.setActive(e.target)}checkActiveTerm(){parseFloat(getComputedStyle(document.documentElement).fontSize);let t=null,e=1/0;this.terms.forEach((i=>{const s=i.getBoundingClientRect();if(s.top+s.height/2>=0&&s.top+s.height/2<=window.innerHeight){const n=window.innerHeight/2,r=Math.abs(s.top-n);r<e&&(e=r,t=i)}})),t&&this.setActive(t)}setActive(t){this.currentActive!==t&&(this.currentActive&&this.currentActive.classList.remove(this.activeClass),t.classList.add(this.activeClass),this.currentActive=t,this.updateNavigation(t.id))}updateNavigation(t){this.nav.querySelectorAll("a").forEach((t=>t.classList.remove(this.activeClass)));const e=this.nav.querySelector(`a[href="#${t}"]`);e&&(e.classList.add(this.activeClass),this.centerNavItem(e))}centerNavItem(t){const e=this.navList.getBoundingClientRect(),i=t.getBoundingClientRect(),s=this.navList.scrollTop,n=i.top-e.top,r=e.height/2-i.height/2;this.navList.scrollTo({top:s+n-r,behavior:"smooth"})}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout((()=>{clearTimeout(i),t(...s)}),e)}}destroy(){this.observer&&this.observer.disconnect(),window.removeEventListener("scroll",this.handleScroll)}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{new t})):new t})(); |
| | | /******/ (() => { // webpackBootstrap |
| | | /*!******************************!*\ |
| | | !*** ./src/glossary/view.js ***! |
| | | \******************************/ |
| | | /** |
| | | * Glossary Navigation Active State Manager |
| | | * Handles highlighting active terms as they scroll into view |
| | | * and syncing navigation with scroll position |
| | | */ |
| | | class GlossaryNavigator { |
| | | constructor(glossarySelector = 'dl.glossary', navSelector = 'nav.glossary-index') { |
| | | this.glossary = document.querySelector(glossarySelector); |
| | | this.nav = document.querySelector(navSelector); |
| | | if (!this.glossary || !this.nav) return; |
| | | this.terms = this.glossary.querySelectorAll('dt[id]'); |
| | | this.navList = this.nav.querySelector('ul'); |
| | | this.activeClass = 'active'; |
| | | this.currentActive = null; |
| | | this.init(); |
| | | this.setupResizeHandler(); |
| | | } |
| | | init() { |
| | | // Set up Intersection Observer with screen-size appropriate margins |
| | | const observerOptions = { |
| | | root: null, |
| | | // viewport |
| | | rootMargin: this.getRootMargin(), |
| | | threshold: 0 |
| | | }; |
| | | this.observer = new IntersectionObserver(entries => this.handleIntersection(entries), observerOptions); |
| | | |
| | | // Observe all terms |
| | | this.terms.forEach(term => this.observer.observe(term)); |
| | | |
| | | // Also handle manual scroll for edge cases |
| | | this.handleScroll = this.debounce(() => this.checkActiveTerm(), 100); |
| | | window.addEventListener('scroll', this.handleScroll, { |
| | | passive: true |
| | | }); |
| | | } |
| | | getRootMargin() { |
| | | // On larger screens: centered (50% from top and bottom) |
| | | return '-50% 0px -50% 0px'; |
| | | } |
| | | setupResizeHandler() { |
| | | let resizeTimer; |
| | | window.addEventListener('resize', () => { |
| | | clearTimeout(resizeTimer); |
| | | resizeTimer = setTimeout(() => { |
| | | // Reinitialize observer with new margins on resize |
| | | this.reinitialize(); |
| | | }, 250); |
| | | }); |
| | | } |
| | | reinitialize() { |
| | | // Disconnect old observer |
| | | if (this.observer) { |
| | | this.observer.disconnect(); |
| | | } |
| | | |
| | | // Create new observer with updated margins |
| | | this.init(); |
| | | } |
| | | handleIntersection(entries) { |
| | | // Find the entry that's intersecting |
| | | const intersecting = entries.find(entry => entry.isIntersecting); |
| | | if (intersecting) { |
| | | this.setActive(intersecting.target); |
| | | } |
| | | } |
| | | checkActiveTerm() { |
| | | // Fallback method to find which term is in the trigger zone |
| | | const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize); |
| | | const margin = remInPixels * 4; |
| | | let closestTerm = null; |
| | | let closestDistance = Infinity; |
| | | this.terms.forEach(term => { |
| | | const rect = term.getBoundingClientRect(); |
| | | const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight; |
| | | if (isInZone) { |
| | | // Find closest to the trigger point |
| | | const triggerPoint = window.innerHeight / 2; |
| | | const distance = Math.abs(rect.top - triggerPoint); |
| | | if (distance < closestDistance) { |
| | | closestDistance = distance; |
| | | closestTerm = term; |
| | | } |
| | | } |
| | | }); |
| | | if (closestTerm) { |
| | | this.setActive(closestTerm); |
| | | } |
| | | } |
| | | setActive(term) { |
| | | if (this.currentActive === term) return; |
| | | |
| | | // Remove active class from previous term |
| | | if (this.currentActive) { |
| | | this.currentActive.classList.remove(this.activeClass); |
| | | } |
| | | |
| | | // Add active class to current term |
| | | term.classList.add(this.activeClass); |
| | | this.currentActive = term; |
| | | |
| | | // Update navigation |
| | | this.updateNavigation(term.id); |
| | | } |
| | | updateNavigation(termId) { |
| | | // Remove active from all nav links |
| | | const navLinks = this.nav.querySelectorAll('a'); |
| | | navLinks.forEach(link => link.classList.remove(this.activeClass)); |
| | | |
| | | // Find and activate corresponding nav link |
| | | const activeLink = this.nav.querySelector(`a[href="#${termId}"]`); |
| | | if (activeLink) { |
| | | activeLink.classList.add(this.activeClass); |
| | | |
| | | // Scroll the nav list to center the active link |
| | | this.centerNavItem(activeLink); |
| | | } |
| | | } |
| | | centerNavItem(link) { |
| | | const listRect = this.navList.getBoundingClientRect(); |
| | | const linkRect = link.getBoundingClientRect(); |
| | | |
| | | // Calculate position to center the link in the nav container |
| | | const scrollTop = this.navList.scrollTop; |
| | | const linkOffset = linkRect.top - listRect.top; |
| | | const centerOffset = listRect.height / 2 - linkRect.height / 2; |
| | | this.navList.scrollTo({ |
| | | top: scrollTop + linkOffset - centerOffset, |
| | | behavior: 'smooth' |
| | | }); |
| | | } |
| | | debounce(func, wait) { |
| | | let timeout; |
| | | return function executedFunction(...args) { |
| | | const later = () => { |
| | | clearTimeout(timeout); |
| | | func(...args); |
| | | }; |
| | | clearTimeout(timeout); |
| | | timeout = setTimeout(later, wait); |
| | | }; |
| | | } |
| | | destroy() { |
| | | if (this.observer) { |
| | | this.observer.disconnect(); |
| | | } |
| | | window.removeEventListener('scroll', this.handleScroll); |
| | | } |
| | | } |
| | | |
| | | // Initialize when DOM is ready |
| | | if (document.readyState === 'loading') { |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | new GlossaryNavigator(); |
| | | }); |
| | | } else { |
| | | new GlossaryNavigator(); |
| | | } |
| | | /******/ })() |
| | | ; |
| | | //# sourceMappingURL=view.js.map |