| | |
| | | class OnThisPage extends window.UIHandler { |
| | | constructor() { |
| | | super(); |
| | | class OnThisPage { |
| | | constructor() { |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | } |
| | | |
| | | // Initialize state tracking |
| | | this.navOpen = false; |
| | | initElements() { |
| | | this.selectors = { |
| | | nav: 'nav.on-this-page', |
| | | toggle: 'button.toggle', |
| | | icon: 'button.toggle .icon' |
| | | } |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | |
| | | // Bind methods |
| | | this.toggleNav = this.toggleNav.bind(this); |
| | | let items = this.ui.nav.querySelectorAll('li a'); |
| | | this.ui.items = {}; |
| | | this.ui.sections = {}; |
| | | this.selectors.items = []; |
| | | |
| | | // Bind elements first |
| | | this.bindElements(); |
| | | for (let item of items) { |
| | | let id = item.getAttribute('href'); |
| | | this.ui.items[id.replace('#', '')] = item.closest('li'); |
| | | this.selectors.items.push(id); |
| | | this.ui.sections[id.replace('#', '')] = document.querySelector(id); |
| | | } |
| | | this.selectors.items = this.selectors.items.join(','); |
| | | } |
| | | |
| | | initListeners() { |
| | | this.ui.toggle.addEventListener('click', () => { |
| | | let icon = this.ui.nav.classList().contains('open') ? 'icon-x-square' : 'icon-plus-square'; |
| | | console.log('Changing icon to: '+icon); |
| | | this.ui.icon.className = 'icon '+icon; |
| | | }); |
| | | |
| | | if (this.elements.nav) { |
| | | if(this.elements.toggle){ |
| | | // Bind click directly |
| | | this.elements.toggle.addEventListener('click', this.toggleNav); |
| | | this.observer = new IntersectionObserver( |
| | | (entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | |
| | | // Bind UIHandler events for escape and outside clicks |
| | | this.bindEvents(); |
| | | } |
| | | let index = Object.keys(this.ui.items).indexOf(entry.target.id); |
| | | let i = 0; |
| | | for (let item of Object.values(this.ui.items)) { |
| | | item.classList.toggle('active', index === i); |
| | | // item.classList.toggle('adj', i === index +1 || i === index -1); |
| | | i++; |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | { |
| | | rootMargin: '-50% 0px -50% 0px', |
| | | threshold: 0 |
| | | } |
| | | ); |
| | | |
| | | |
| | | // Set up section observer |
| | | this.setupSectionObserver(); |
| | | } |
| | | } |
| | | |
| | | bindElements() { |
| | | const nav = document.querySelector('nav.on-this-page'); |
| | | |
| | | if (!nav) return; |
| | | this.elements = { |
| | | nav, |
| | | links: nav.querySelectorAll('a'), |
| | | sections: Array.from(nav.querySelectorAll('a')) |
| | | .map(a => { |
| | | const id = a.getAttribute('href'); |
| | | return document.querySelector(id); |
| | | }) |
| | | .filter(Boolean) |
| | | }; |
| | | if(nav.querySelector('button.toggle')){ |
| | | this.elements.toggle = nav.querySelector('button.toggle'); |
| | | } |
| | | } |
| | | |
| | | // Override to prevent UIHandler's component event binding |
| | | bindComponentEvents() { |
| | | // Intentionally empty |
| | | } |
| | | |
| | | toggleNav(event) { |
| | | event?.preventDefault(); |
| | | event?.stopPropagation(); |
| | | |
| | | const { nav, toggle } = this.elements; |
| | | if (!nav || !toggle) return; |
| | | |
| | | // Toggle state |
| | | this.navOpen = !this.navOpen; |
| | | |
| | | // Update DOM |
| | | if (this.navOpen) { |
| | | nav.classList.add('open'); |
| | | toggle.setAttribute('aria-label', 'Hide Index'); |
| | | toggle.setAttribute('aria-expanded', 'true'); |
| | | this.bindLinkHandlers(); |
| | | } else { |
| | | nav.classList.remove('open'); |
| | | toggle.setAttribute('aria-label', 'Show Index'); |
| | | toggle.setAttribute('aria-expanded', 'false'); |
| | | this.cleanupLinkHandlers(); |
| | | } |
| | | } |
| | | |
| | | bindLinkHandlers() { |
| | | const { links } = this.elements; |
| | | links?.forEach(link => { |
| | | link._boundHandler = () => { |
| | | this.navOpen = false; |
| | | this.elements.nav.classList.remove('open'); |
| | | this.elements.toggle.setAttribute('aria-label', 'Show Index'); |
| | | this.elements.toggle.setAttribute('aria-expanded', 'false'); |
| | | this.cleanupLinkHandlers(); |
| | | }; |
| | | link.addEventListener('click', link._boundHandler); |
| | | }); |
| | | } |
| | | |
| | | cleanupLinkHandlers() { |
| | | const { links } = this.elements; |
| | | links?.forEach(link => { |
| | | if (link._boundHandler) { |
| | | link.removeEventListener('click', link._boundHandler); |
| | | delete link._boundHandler; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setupSectionObserver() { |
| | | const { sections } = this.elements; |
| | | |
| | | if (!sections?.length) return; |
| | | |
| | | this.initializeObserver( |
| | | 'sections', |
| | | sections, |
| | | { |
| | | rootMargin: '-50% 0% -50% 0%', |
| | | threshold: 0 |
| | | }, |
| | | (entries) => { |
| | | entries.forEach(entry => { |
| | | if (!entry.isIntersecting) return; |
| | | |
| | | const id = entry.target.id; |
| | | const link = this.elements.nav?.querySelector(`a[href="#${id}"]`); |
| | | if (link) { |
| | | this.updateActiveClasses(link); |
| | | } |
| | | }); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | updateActiveClasses(activeLink) { |
| | | const listItem = activeLink.closest('li'); |
| | | if (!listItem) return; |
| | | |
| | | // Remove existing active and adjacent classes |
| | | const allItems = this.elements.nav.querySelectorAll('li'); |
| | | allItems.forEach(item => { |
| | | item.classList.remove('active', 'adj'); |
| | | }); |
| | | |
| | | // Add new classes |
| | | listItem.classList.add('active'); |
| | | |
| | | // Add adjacent classes |
| | | if (listItem.previousElementSibling) { |
| | | listItem.previousElementSibling.classList.add('adj'); |
| | | } |
| | | if (listItem.nextElementSibling) { |
| | | listItem.nextElementSibling.classList.add('adj'); |
| | | } |
| | | } |
| | | |
| | | // Use local state for component active check |
| | | isComponentActive(componentKey) { |
| | | if (componentKey === 'nav') { |
| | | return this.navOpen; |
| | | } |
| | | return super.isComponentActive(componentKey); |
| | | } |
| | | |
| | | // UIHandler event handlers |
| | | handleOutsideClick(event) { |
| | | if (this.navOpen && !this.elements.nav.contains(event.target)) { |
| | | this.toggleNav(event); |
| | | } |
| | | } |
| | | |
| | | handleEscapeKey(event) { |
| | | if (event.key === 'Escape' && this.navOpen) { |
| | | this.toggleNav(event); |
| | | event.preventDefault(); |
| | | } |
| | | } |
| | | |
| | | cleanup() { |
| | | this.cleanupLinkHandlers(); |
| | | super.cleanup(); |
| | | } |
| | | for (let section of Object.values(this.ui.sections)) { |
| | | console.log('Observing section: ', section); |
| | | this.observer.observe(section); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Initialize |