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