neighbour-loader.js

On every note page, neighbour-loader.js, a lightweight script is loaded via the partial neighbour-panel.html. It has no external dependencies compared to graph.js .

The panel is populated on demand when the user clicks “Neighbours” — or immediately on page load if sessionStorage records it was previously open. It renders a neighbour list directly into #neighbour-panel in the aside, above the TOC. Closing the panel clears its contents and removes the sessionStorage flag.

The Neighbour panel topbar includes a graph icon linking to the note’s entry point in the graphical view.

(function () {
    const trigger = document.getElementById("neighbour-trigger");
    if (!trigger) return;

    const lnk = trigger.dataset.lnk;
    let _fetchAborted = false;

    function renderList() {
        const panel = document.getElementById("neighbour-panel");
        if (!panel) return;
        _fetchAborted = false;
        panel.innerHTML = '<div class="np-loading">Loading…</div>';

        GraphData.fetch()
            .then(data => {
                if (_fetchAborted) return;
                const current = GraphData.findNode(data, lnk);
                if (!current) {
                    panel.innerHTML = '<div class="np-empty">This note has no connections yet.</div>';
                    return;
                }
                const map        = GraphData.buildDirectionMap(data, current);
                const neighbours = GraphData.getNeighbours(data, current, map);

                panel.innerHTML = "";

                // Header
                const topBar = document.createElement("div");
                topBar.className = "np-topbar";
                const count = document.createElement("span");
                count.className = "np-count";
                count.textContent = neighbours.length
                    ? `${neighbours.length} neighbour${neighbours.length !== 1 ? "s" : ""}`
                    : "No connections";
                // graph icon (left)
                const graphLink = document.createElement("a");
                graphLink.href      = `/map/?focus=${encodeURIComponent(lnk)}#graph-wrapper`;
                graphLink.title     = "Explore visually";
                graphLink.className = "np-graph-link";
                graphLink.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
    viewBox="0 0 24 24" fill="none" stroke="currentColor"
    stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
    aria-hidden="true">
  <circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/>
  <circle cx="18" cy="19" r="3"/>
  <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
  <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>`;
                const closeBtn = document.createElement("button");
                closeBtn.textContent = "✕";
                closeBtn.className = "np-close";
                closeBtn.setAttribute("aria-label", "Close neighbour panel");
                closeBtn.addEventListener("click", destroy, { once: true });

                topBar.appendChild(graphLink);
                topBar.appendChild(count);
                topBar.appendChild(closeBtn);
                panel.appendChild(topBar);

                if (!neighbours.length) {
                    const msg = document.createElement("div");
                    msg.className = "np-empty";
                    msg.textContent = "This note has no connections yet.";
                    panel.appendChild(msg);
                } else {
                    const links     = neighbours.filter(nb => map.get(nb.id).out)
                                                .sort((a, b) => a.label.localeCompare(b.label));
                    const backlinks = neighbours.filter(nb => map.get(nb.id).in)
                                                .sort((a, b) => a.label.localeCompare(b.label));

                    [["Links", links], ["Backlinks", backlinks]].forEach(([heading, group]) => {
                        if (!group.length) return;
                        const h = document.createElement("div");
                        h.className = "np-group-heading";
                        h.textContent = heading;
                        panel.appendChild(h);
                        const ul = document.createElement("ul");
                        ul.className = "np-list";
                        group.forEach(nb => {
                            const li = document.createElement("li");
                            const a  = document.createElement("a");
                            a.href        = `/docs/${nb.lnk}/`;
                            a.textContent = nb.label;
                            li.appendChild(a);
                            ul.appendChild(li);
                        });
                        panel.appendChild(ul);
                    });
                }
            })
            .catch(() => {
                if (_fetchAborted) return;
                panel.innerHTML = '<div class="np-loading">Could not load.</div>';
            });
    }

    function destroy() {
        _fetchAborted = true;
        const panel = document.getElementById("neighbour-panel");
        if (panel) panel.innerHTML = "";
        sessionStorage.removeItem("neighbourPanelOpen");
    }

    if (sessionStorage.getItem("neighbourPanelOpen") === "1") {
        renderList();
    }

    trigger.addEventListener("click", () => {
        const panel = document.getElementById("neighbour-panel");
        if (panel && panel.innerHTML !== "") {
            destroy();
        } else {
            sessionStorage.setItem("neighbourPanelOpen", "1");
            renderList();
        }
    });
})();

© Prabu Anand K 2020-2026