Search code examples
javascriptjquerytableofcontents

Dynamic Table of Contents - For Loop Does Not Finish


I am working on some code that loops through <section> and <header> elements to create a table of contents. Once created, the table of contents should highlight which section the user scrolls into.

The code seems to work, but stops on item 10 when looping through the created table of contents. I am wondering if there is too much happening in the loop, which causes a timeout? I am fairly new to JS and jQuery, any help appreciated!

I have the working example here: https://codepen.io/higginbg/pen/jONzrYG

TOC code:

function tableOfContents(tocList) {

    $(tocList).empty()

    let prevItem = null
    let prevList = null

    $( 'section' ).each(function() {

        const text = $(this).children( 'header' ).text()

        const id = text.replace(/ /g, '_')

        const li = `
          <li>
            <a class='nav-link'
               href='#${id}'
               onclick='menuToggle()'
            >${text}
            </a>
          </li>
        `

        $(this).attr('id', id)

        prevList = $("<ul></ul>")
        prevItem = $(li)
        prevItem.append(prevList)
                .appendTo(tocList)
    })
}       

Code to highlight section when scrolled to:

function highlightToc() {

  const elements = $( 'section' )
  const scrollPosition = ($(window).height() * 0.25) + $(window).scrollTop()

  for (let i = 0; i < elements.length; i++) {

    const thisId = '#' + elements[i].id
    const nextId = (i <= elements.length) ? `#${elements[i+1].id}` : '#'

    const thisOffset = $(thisId).offset().top
    const nextOffset = (i <= elements.length) ? $(nextId).offset().top : 0

    const listItem = $( `a[href='${thisId}']` )

    const isSelected = ((scrollPosition > thisOffset) && (scrollPosition < nextOffset))

      isSelected ? listItem.addClass( 'selected' ) : listItem.removeClass( 'selected' )
  }
}

Solution

  • Came up with a solution that creates a condensed array of only the section id and offset value. Seems to prevent the timeout. Also decided not to use jQuery.

    Code to create sections:

    const fullSections = document.querySelectorAll('section')
    const sections = []
    
    const createSections = () => {
      for (const sec of fullSections) {
        const header = sec.querySelector('header')
        const text = header.innerText
        const id = text.replace(/ /g, '_')
    
        // Set section id
        sec.id = id
    
        sections.push({
          id,
          text,
          href: `#${id}`,
          header,
          offsetTop: sec.offsetTop,
        })
      }
    }
    

    TOC code:

    const createToc = () => {
      const toc = document.getElementById('toc')
    
      for (const sec of sections) {
        const { id, text, href } = sec
    
        // Create list item
        const li = document.createElement('li')
        const a = document.createElement('a')
    
        a.innerText = text
        a.href = href
        a.className = 'nav-link'
    
        li.append(a)
        toc.append(li)
      }
    }
    

    Highlight on scroll:

    const highlightToc = () => {
      const scrollPosition = (window.innerHeight * 0.50) + window.pageYOffset
      const numSections = sections.length
    
      for (let i = 0; i < sections.length; i++) {
        const { id, offsetTop } = sections[i]
        const thisClasses = document.querySelector(`#toc a[href='#${id}']`).classList
    
        // Next item (only up to last section)
        const n = (i + 1 < numSections) ? (i + 1) : i
        const nextOffset = sections[n].offsetTop
    
        // Highlight toc item if within scroll window
        if ((scrollPosition > offsetTop) && (scrollPosition < nextOffset)) {
          thisClasses.add('selected')
        } else {
          thisClasses.remove('selected')
        }
    
        // Highlight last toc item if near bottom of page
        if (sections[numSections - 1].offsetTop < window.innerHeight + window.pageYOffset) {
          const p = (i - 1 < 0) ? 0 : (i - 1)
          const prevId = sections[p].id
          const prevClasses = document.querySelector(`#toc a[href='#${prevId}']`).classList
    
          thisClasses.add('selected')
          prevClasses.remove('selected')
        }
      }
    }
    

    Remap offsets on window resize:

    const newOffsets = () => {
      for (let i = 0; i < fullSections.length; i++) {
        sections[i].offsetTop = fullSections[i].offsetTop
      }
    }