A good idea, especially if you have many elements, is to do a binary search. Here is a complete example. The goal of this code is to highlight in a sidebar the headings of sections which are currently visible in <main>
.
const asideTocItems = Array.from(asideToc.querySelectorAll('li'))
.map(item => ({ item: item, heading: document.getElementById(getItemTargetId(item)) })),
mainBlock = document.querySelector('main');
function updateAsideToc() {
// Binary search to find the first heading which is above the top
let searchTop = 0, searchBot = asideTocItems.length;
while (searchTop + 1 < searchBot) {
const searchMid = (searchTop + searchBot) >> 1, heading = asideTocItems[searchMid].heading;
if (heading.offsetTop - getElemFontSize(heading) <= mainBlock.scrollTop) {
searchTop = searchMid;
} else {
searchBot = searchMid;
}
}
// Previously marked items are unmarked
for (const item of asideToc.querySelectorAll('li.current')) {
item.classList.remove('current');
}
// We mark items starting from the found one
const visibleBottomY = mainBlock.scrollTop + mainBlock.clientHeight;
for (let itemNum = searchTop; itemNum < asideTocItems.length; ++itemNum) {
// If the current heading is below the bottom, it’s time to leave
const heading = asideTocItems[itemNum].heading;
if (heading.offsetTop + getElemFontSize(heading) >= visibleBottomY) {
break;
}
let item = asideTocItems[itemNum].item;
item.classList.add('current');
// Parents are expanded
do {
item.classList.remove('collapsed');
} while ((item = item.parentElement.closest('li')) != null);
}
// Previously expanded items are collapsed back, unless they still have current items
for (const item of asideToc.querySelectorAll('.hasChild:not(.current):not(.collapsed)')) {
if (item.querySelector('.current') == null) {
item.classList.add('collapsed');
}
}
// We scroll the sidebar so that all marked items are visible
const currentItems = asideToc.querySelectorAll('.current > a');
if (currentItems.length > 0) {
const first = currentItems[0], last = currentItems[currentItems.length - 1],
container = asideToc.closest('aside');
if (first.offsetTop < container.scrollTop) {
container.scrollTop = first.offsetTop;
} else if (last.offsetTop + last.offsetHeight > container.scrollTop + container.clientHeight) {
container.scrollTop = last.offsetTop + last.offsetHeight - container.clientHeight;
}
}
}
mainBlock.onscroll = updateAsideToc;
updateAsideToc();
It is for the HTML manual of a program of my teacher. It is currently live here but should be moved here soon.
I may miss some interesting or more optimized features of JavaScript. It is so complicated…!
As it is a common feature on websites, I am curious to know if they use that same technique or if they have a better (or worse?) one.