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' )
}
}
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
}
}