Open 9am opened 2 years ago
Ever been looking through a long article without a side navigation section? Take Wikipedia for example.
https://user-images.githubusercontent.com/1435457/164148438-c84918be-5e00-458d-b86a-188043134ac7.mp4
It has navigation contents below the brief description section. But after scrolling down a few sections. I lost the connection between what I had been reading and what I'm about to read. A simple add-on(or extension) should solve the problem. So I built one.
https://user-images.githubusercontent.com/1435457/164148561-e09fa18c-6975-4855-b8b8-65c7d3d1ad5d.mp4
Collect headlines on the page to create a navigation list.
const root = document.body; // DOM from which to collect headlines
const pick = ["h1", "h2", "h3", "h4"]; // tagNames to pick;
// collect headlines
const headlines = [...root.querySelectorAll(pick.join(","))];
// create a <li> for each headlines
const items = headlines.map(
(node, index) => `
<li>
${node.textContent}
</li>
`
);
// wrap <li> with <ul>
const nav = document.createElement("ul");
nav.id = "nav";
nav.innerHTML = items.join("");
document.body.prepend(nav);
https://user-images.githubusercontent.com/1435457/164149945-f603a1b5-b603-441f-93a8-4484d3bb1831.mp4
Handle the indent and jumping.
We add margin to show a different level of the item. And place \ in the navigation item with the headline id so that it anchors to the headline when clicking.
const root = document.body; // DOM from which to collect headlines
const pick = ["h1", "h2", "h3", "h4"]; // tagNames to pick;
// collect headlines
const headlines = [...root.querySelectorAll(pick.join(","))];
// create a <li> for each headlines
const items = headlines.map((node, index) => {
// respect the origin id
const id = node.id || `hopps-${index}`;
// use tagName to level
const lv = node.tagName.match(/\d/)?.[0] || 1;
return `
<li style="margin-left:${lv * 16}px">
<a href="#${id}">${node.textContent}</a>
</li>
`;
});
// wrap <li> with <ul>
const nav = document.createElement("ul");
nav.id = "nav";
nav.innerHTML = items.join("");
document.body.prepend(nav);
Notice: The actual situation to choose a level is more complicated. It might be like this:
h1
----h3
------h4
------h4
----h3
--------h5
----------h6
--------h5
which should be indented like this:
h1
--h3
----h4
----h4
--h3
----h5
------h6
----h5
So we just simplify it by the tagName \<h($level)>
Throw some CSS to make it look nicer. Whoa, We’re Halfway There.
scroll-behavior: smooth
to make the scroll fancy.
backdrop-filter: blur()
to make a glass effect.
https://user-images.githubusercontent.com/1435457/164150734-03c9fb16-58e1-4fea-9bf6-3969a8df2e74.mp4
Highlight the right item when scrolling the page.
Let's think about how to do this. There could be only one item highlighted. It should be the topmost in the viewport. So we listen to 'scroll', and loop through the headlines from top to bottom, and the first one in the viewport is the target. Then highlight the item in navigation whose href
anchor to id
.
document.addEventListener("scroll", () => {
const { height } = window.screen;
let activeID = null;
headlines
.map((node) => [node.id, node.getClientRects()?.[0] || {}])
.some(([id, rect]) => {
const { top, bottom } = rect;
if (bottom >= 0 && top < height) {
// first one in viewport
activeID = id;
return true;
}
return false;
});
if (!activeID) {
return;
}
[...nav.querySelectorAll("a")].forEach((node) => {
const [, match] = node.href.match(/[^#]*#([^#]*)$/) || [];
node.classList.toggle("highlight", match === activeID);
});
});
(Notice: It's better to add throttle to the scroll handler to prevent unnecessary calculation, we're not gonna do that since it's a simple version :)
https://user-images.githubusercontent.com/1435457/164150819-bb484da8-e88d-4ad4-9cc5-6dc22cf8d002.mp4
Choose your browser to install the add-on.
The source code is here.
Hope you enjoy it, I'll see you next time.
@9am 🕘