Jake Vanderwerf
2026-03-29 275c0d74cd68677622a5431505c5c870c473063d
assets/js/concise/on-this-page.js
@@ -1,178 +1,64 @@
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