9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

Headline Hopps #4

Open 9am opened 2 years ago

9am commented 2 years ago
Using 50 lines of plain javascript to build a browser add-on to show all the headlines on a page and navigate between them.
headline-hopps hits
9am commented 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.

Feature Preview

https://user-images.githubusercontent.com/1435457/164148561-e09fa18c-6975-4855-b8b8-65c7d3d1ad5d.mp4

  1. List all the headlines in the article.
  2. Click items to jump between sections.
  3. Highlight the section as you scroll the page.
  4. Toggle visibility of level.
  5. Re-select the area to collect headlines.
  6. Change the position.

Build a Simple Version in 50 Lines

  1. 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);

    Edit step1

https://user-images.githubusercontent.com/1435457/164149945-f603a1b5-b603-441f-93a8-4484d3bb1831.mp4

  1. 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.

    Edit step2

https://user-images.githubusercontent.com/1435457/164150734-03c9fb16-58e1-4fea-9bf6-3969a8df2e74.mp4

  1. 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);
        });
    });

    Edit step3

    (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

The Whole Package

Choose your browser to install the add-on.

The source code is hereheadline hopps.

Hope you enjoy it, I'll see you next time.


@9am 🕘