javascript

Highlight menu item with top section scrolling into view including last non-top visible sections


JSFiddle: https://jsfiddle.net/8xo4aghn/

In this Fiddle, I have a menu which highlights the item corresponding to the current top section as the page is being scrolled. The last few sections may be short (as in this example) and they never get the menu selection. It's also possible to click the A-links to go to the section, but for the last few, the clicking won't select the item either.

I need to dynamically force any last short sections (if they exist) to at least correspond to menu clicks, if they'll never be activated via scrolling, so that the menu clicks make sense. Is this possible, or are there any better solutions to this scrolling problem? If I were to do it the other way - the last section being shown -- then clicking any non-last sections could conflict with that as well, and there would be similar discrepancies.

function updateMenu(){

    const menus = document.querySelectorAll('div.sticky a');

    const sectionsAll = document.querySelectorAll('div.card');
    // Exclude any Sections that don't have an H2
    const sections = [...sectionsAll].filter(section => {
        const sectionH2 = section.querySelectorAll('h2');
        const sectionHeader = 
                                                (sectionH2.length ? sectionH2[0] :
                                                    null);
        if (!sectionHeader) {
            return false; //exclude
        } else {
            return true;
        }
    });

    let currentSection = null;
    for (var i = 0; i < sections.length; i++) {
        const rect = sections[i].getBoundingClientRect();
        
        // Find current section; if not last in the list, stop immediately when found
        if(rect.top >= 0) {
            currentSection = sections[i];
            if (i < sections.length - 1) {
                break;
            }
        }
  }
    
    if(currentSection){
        // Try to find a matching menu based on the A innerText
        menus.forEach(menu => {
            const menuInnerText = menu.innerText;
        
            const currentSectionH2 = currentSection.querySelectorAll('h2');
            const currentSectionHeader = 
                                            (currentSectionH2.length ? currentSectionH2[0] :
                                                null);
            
            if (currentSectionHeader && menuInnerText == currentSectionHeader.innerText) {
                // Select menu
                menu.classList.add('active');
            } else {
                // Clear menu
                menu.classList.remove('active');
            }
        });
    } else {
        // Deselect all menus
        menus.forEach(menu => {
            menu.classList.remove('active');
        });
    }
}

window.addEventListener('scroll', updateMenu);
updateMenu(); // initial call

I was playing with this condition but it didn't work,

        // Find current section; if not last in the list, stop immediately when found
        if(rect.top >= 0) {
            currentSection = sections[i];
            if (i < sections.length - 1) {
                break;
            }
        }

Solution

  • Based on Differentiate user scroll from anchor scroll i came up with the following solution to your issue

    let userScrolling = true;
    
    function highlightSection(section) {
      const menus = document.querySelectorAll("div.sticky a");
    
      if (section) {
        // Try to find a matching menu based on the A innerText
        menus.forEach((menu) => {
          const menuInnerText = menu.innerText;
    
          const currentSectionH2 = section.querySelectorAll("h2");
          const currentSectionHeader = currentSectionH2.length ?
            currentSectionH2[0] :
            null;
    
          if (
            currentSectionHeader &&
            menuInnerText == currentSectionHeader.innerText
          ) {
            // Select menu
            menu.classList.add("active");
          } else {
            // Clear menu
            menu.classList.remove("active");
          }
        });
      } else {
        // Deselect all menus
        menus.forEach((menu) => {
          menu.classList.remove("active");
        });
      }
    }
    
    function updateMenu() {
      if (!userScrolling) return;
    
      // Attempt to find matching menu
      const sections = document.querySelectorAll("div.card:has(h2)");
    
      let currentSection = null;
      for (var i = 0; i < sections.length; i++) {
        const rect = sections[i].getBoundingClientRect();
    
        // Find current section; if not last in the list, stop immediately when found
        if (rect.top >= 0) {
          currentSection = sections[i];
          if (i < sections.length - 1) {
            break;
          }
        }
      }
    
      highlightSection(currentSection);
    }
    
    document.querySelector(".sticky").addEventListener("click", (e) => {
      const {
        target
      } = e;
      if (target.nodeName === "A") {
        userScrolling = false;
        const id = new URL(e.target.href).hash;
    
        if (id) {
          const section = document.querySelector(id).closest(".card");
          highlightSection(section);
        }
      }
    });
    
    window.addEventListener("wheel", () => (userScrolling = true));
    window.addEventListener("touchmove", () => (userScrolling = true));
    window.addEventListener("mousedown", () => (userScrolling = true));
    
    window.addEventListener("scroll", updateMenu, {
      passive: true
    });
    updateMenu(); // initial call
    div.sticky {
      position: sticky;
      top: 0;
      background-color: yellow;
      padding: 5px;
      font-size: 20px;
    }
    
    .card {
      border: 1px solid gray;
      margin-top: 20px;
    }
    
    .active {
      border: 2px solid red;
    }
    
    /* Prevent A-anchor scrolling behind sticky header */
    :target:before {
      content: "";
      display: block;
      height: 90px;
      /* fixed header height*/
      margin: -90px 0 0;
      /* negative fixed header height */
    }
    <div class="sticky">
      <a href="#sec1Header">Section 1</a> | <a href="#sec2Header">Section 2</a> |
      <a href="#sec3Header">Section 3</a> | <a href="#sec4Header">Section 4</a> |
      <a href="#sec5Header">Section 5</a> | <a href="#sec6Header">Section 6</a> |
    </div>
    
    <div class="card">
      <h2 id="sec1Header">Section 1</h2>
      Some text some text Some text some text Some text some text Some text some
      text Some text some text Some text some text Some text some text Some text
      some text Some text some text
    </div>
    <div class="card">
      <h2 id="sec2Header">Section 2</h2>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus imperdiet,
      nulla et dictum interdum, nisi lorem egestas odio, vitae scelerisque enim
      ligula venenatis dolor. Maecenas nisl est, ultrices nec congue eget, auctor
      vitae massa. Fusce luctus vestibulum augue ut aliquet. Mauris ante ligula,
      facilisis sed ornare eu, lobortis in odio. Praesent convallis urna a lacus
      interdum ut hendrerit risus congue. Nunc sagittis dictum nisi, sed ullamcorper
      ipsum dignissim ac. In at libero sed nunc venenatis imperdiet sed ornare
      turpis. Donec vitae dui eget tellus gravida venenatis. Integer fringilla
      congue eros non fermentum. Sed dapibus pulvinar nibh tempor porta. Cras ac leo
      purus. Mauris
    </div>
    <div class="card">
      <h2 id="sec3Header">Section 3</h2>
      Nunc sagittis dictum nisi, sed ullamcorper ipsum dignissim ac. In at libero
      sed nunc venenatis imperdiet sed ornare turpis. Donec vitae dui eget tellus
      gravida venenatis. Integer fringilla congue eros non fermentum. Sed dapibus
      pulvinar nibh tempor porta. Cras ac leo purus. Mauris quis diam velit. Lorem
      ipsum dolor sit amet, consectetur adipiscing elit. Phasellus imperdiet, nulla
      et dictum interdum, nisi lorem egestas odio, vitae scelerisque enim ligula
      venenatis dolor. Maecenas nisl est, ultrices nec congue eget, auctor vitae
      massa. Fusce luctus vestibulum augue ut aliquet. Mauris ante ligula, facilisis
      sed ornare eu
    </div>
    <div class="card">
      <h2 id="sec4Header">Section 4</h2>
      abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc
      abc abc abc abc abc
    </div>
    <div class="card">
      <h2 id="sec5Header">Section 5</h2>
      def def def def
    </div>
    <div class="card">
      <h2 id="sec6Header">Section 6</h2>
      ghijkslfkl ghijkslfkl ghijkslfkl ghijkslfkl
    </div>


    JSFiddle: https://jsfiddle.net/gaby/dfxw827b/35/