Aleš Sýkora / November 25, 2024 / 0 comments

Bricks: Change Menu Anchor Link Active status on scroll

Post summary: If you want to have onepager with menu created from anchor links ─ and you want to change the active styles by scrolling over sections, you should try this snippet.

I have found a way how to do this in Github repo od Werkmind, so credit goes to werkmind Github: https://github.com/werkmind/Bricks-Anchor-Navigation.

However, I have found some difficulties, so I did a little upgrade. You can now modify the menu structure without touching the JavaScript code, and everything will work automatically as long as the href attributes in the menu items are properly formatted.

For the menu items, please use full link with hash:

Upgraded scroller plugin code

The main changes focused on making the code more dynamic by automatically extracting hashes from menu items instead of using a static list.

This code:

  1. Gets all menu items from the DOM
  2. Extracts hash from href attribute
  3. Adds valid hashes to the Set
  4. Creates a mapping of hash to DOM section

Key Benefits:

  1. No need to maintain a static list of hashes
  2. Automatically works with any menu structure
  3. Only processes hashes that actually exist in the menu
  4. More flexible and maintainable
  5. Reduces chance of errors from manual hash list maintenance

Configuration simplified:

  1. Removed static hash list from config
  2. Only essential configuration remains

Error Handling:

  1. Checks if hash exists before adding
  2. Ignores empty hashes (‘#’)
  3. Uses optional chaining for safer property access
  4. More robust against malformed menu structures

Performance Considerations:

  1. Hash extraction happens once at initialization
  2. Uses Set for O(1) lookup performance
  3. Caches section elements for faster access
  4. Reduces DOM queries during scroll events

Snippet for Bricks Builder menu automatic hash link active status

document.addEventListener("DOMContentLoaded", function() {
    const config = {
        menuSelector: ".bricks-nav-menu li",
        scrollThreshold: 0.2,
        rootPath: '/'
    };

    const menuItems = document.querySelectorAll(config.menuSelector);
    const validHashes = new Set();
    const sections = {};

    menuItems.forEach(item => {
        const link = item.querySelector("a");
        if (link) {
            const href = link.getAttribute("href");
            const hash = href?.includes('#') ? '#' + new URL(href).hash.slice(1) : '';
            if (hash && hash !== '#') {
                validHashes.add(hash);
                sections[hash] = document.querySelector(hash);
            }
        }
    });

    const state = {
        isScrolling: false,
        targetHash: null
    };

    const utils = {
        getFullPath: (anchor) => window.location.origin + window.location.pathname + anchor,
        
        updateHistory: (hash) => {
            history.pushState({}, '', hash);
            utils.setActiveClass(hash);
        },

        setActiveClass: (anchor) => {
            const expectedHref = utils.getFullPath(anchor);
            menuItems.forEach(item => {
                const link = item.querySelector("a");
                item.classList.toggle("current-menu-item", 
                    link?.getAttribute("href") === expectedHref);
            });
        },

        getSectionInView: () => {
            const viewportThreshold = window.innerHeight * config.scrollThreshold;
            
            for (const [hash, section] of Object.entries(sections)) {
                if (!section) continue;
                const rect = section.getBoundingClientRect();
                if (rect.top >= 0 && rect.top <= viewportThreshold) {
                    return hash;
                }
            }
            return null;
        }
    };

    const handlers = {
        scroll: () => {
            const currentSection = utils.getSectionInView();
            
            if (state.isScrolling) {
                if (currentSection && currentSection === state.targetHash) {
                    state.isScrolling = false;
                    sessionStorage.removeItem('programmaticScroll');
                    state.targetHash = null;
                    utils.updateHistory(currentSection);
                }
            } else if (currentSection) {
                utils.updateHistory(currentSection);
            }
        },

        click: (e, link) => {
            const href = link.getAttribute("href");
            const targetHash = href.includes('#') ? '#' + href.split('#')[1] : '';

            if (!validHashes.has(targetHash)) {
                window.location.href = href;
                return;
            }

            e.preventDefault();
            
            if (window.location.pathname === config.rootPath) {
                const targetSection = sections[targetHash];
                if (targetSection) {
                    const offset = targetSection.getBoundingClientRect().top + window.pageYOffset;
                    state.isScrolling = true;
                    state.targetHash = targetHash;
                    window.scrollTo({
                        top: offset,
                        behavior: 'smooth'
                    });
                }
            } else {
                sessionStorage.setItem('programmaticScroll', targetHash);
                window.location.href = window.location.origin + config.rootPath + targetHash;
            }
        }
    };

    window.addEventListener('scroll', handlers.scroll);
    menuItems.forEach(menuItem => {
        const link = menuItem.querySelector("a");
        if (link) {
            link.addEventListener("click", (e) => handlers.click(e, link));
        }
    });

    const initialHash = window.location.hash;
    if (validHashes.has(initialHash)) {
        if (sessionStorage.getItem('programmaticScroll') === initialHash) {
            state.isScrolling = true;
            state.targetHash = initialHash;
        }
        utils.setActiveClass(initialHash);
    } else {
        utils.setActiveClass("");
    }
});

Fuel my passion for writing with a beer🍺

Your support not only makes me drunk but also greatly motivates me to continue creating content that helps. Cheers to more discoveries and shared success. 🍻

0 comments

Share Your Thoughts