mobile-menu.js
This literate configuration file tangles to mobile-menu.js↗ . This script handles the mobile navigation menu for myzettel . It builds a slide-in root panel from the existing topnav links, with optional Contents and Neighbours panels. Neighbour list rendering is delegated to neighbour-utils.js .
Initialisation Guard
Guards against running on pages without a mobile menu toggle or
overlay. Also skips the /map/ graph page which has its own
navigation.
const menuBtn = document.getElementById('mobile-menu-toggle');
const overlay = document.getElementById('menu-overlay');
const tocPanel = document.querySelector('.toc');
if (!menuBtn || !overlay) return;
if (document.getElementById('main-graph')) return;
Build root panel for menu
Build root panel from existing topnav-links. Clones the existing
.topnav-links anchors into a new mobile-root-panel div. Cloning
ensures the desktop nav and mobile nav stay in sync without
duplication in the HTML.
const topnavLinks = document.querySelector('.topnav-links');
if (!topnavLinks) return;
const rootPanel = document.createElement('div');
rootPanel.id = 'mobile-root-panel';
rootPanel.className = 'mobile-root-panel';
Add nav links
Nav links are always added.
topnavLinks.querySelectorAll('a').forEach(a => {
const link = a.cloneNode(true);
rootPanel.appendChild(link);
});
Add TOC
Add Contents trigger if TOC exists. A “Contents ›” button is added
only when the page has a TOC. Clicking it transitions to the .toc
panel. The › glyph signals a sub-panel transition rather than a
direct action.
if (tocPanel && tocPanel.querySelector('ul, nav, #TableOfContents')) {
const hr = document.createElement('hr');
rootPanel.appendChild(hr);
const tocTrigger = document.createElement('button');
tocTrigger.type = 'button';
tocTrigger.id = 'toc-trigger';
tocTrigger.textContent = 'Contents ›';
rootPanel.appendChild(tocTrigger);
}
Add Neighbours
Add Neighbours trigger if on a notes page. A “Neighbours ›” button is
added only on note pages where #neighbour-trigger is
present. Clicking it loads the neighbour list via loadNeighbours().
const neighbourTrigger = document.getElementById('neighbour-trigger');
if (neighbourTrigger) {
const neighbourHr = document.createElement('hr');
rootPanel.appendChild(neighbourHr);
const neighbourBtn = document.createElement('button');
neighbourBtn.type = 'button';
neighbourBtn.id = 'mobile-neighbour-trigger';
neighbourBtn.textContent = 'Neighbours ›';
rootPanel.appendChild(neighbourBtn);
}
document.body.appendChild(rootPanel);
Build neighbours panel
Creates the mobile-neighbours-panel container and appends it to the
body. loadNeighbours delegates to NeighbourUtils.buildList which
fetches, groups, and returns a DOM fragment via callback. The panel
owns its own loading and empty states.
const neighboursPanel = document.createElement('div');
neighboursPanel.id = 'mobile-neighbours-panel';
neighboursPanel.className = 'mobile-neighbours-panel';
document.body.appendChild(neighboursPanel);
function loadNeighbours() {
const lnk = neighbourTrigger && neighbourTrigger.dataset.lnk;
if (!lnk) return;
neighboursPanel.innerHTML = '<div class="np-loading">Loading…</div>';
NeighbourUtils.buildList(lnk, 'np-mobile-list',
(n, fragment) => {
neighboursPanel.innerHTML = '';
if (fragment) neighboursPanel.appendChild(fragment);
else { neighboursPanel.innerHTML = '<div class="np-empty">This note has no connections yet.</div>'; }
},
() => { neighboursPanel.innerHTML = '<div class="np-empty">Could not load.</div>'; }
);
}
Maintain state
Four state functions manage which panel is visible. Only one panel is
active at a time — activating any panel deactivates the others.
closeAll is also called on overlay click and nav link click.
const closeAll = () => {
rootPanel.classList.remove('active');
overlay.classList.remove('active');
if (tocPanel) tocPanel.classList.remove('active');
neighboursPanel.classList.remove('active');
};
const openRoot = () => {
rootPanel.classList.add('active');
overlay.classList.add('active');
if (tocPanel) tocPanel.classList.remove('active');
};
const openToc = () => {
rootPanel.classList.remove('active');
tocPanel.classList.add('active');
overlay.classList.add('active');
};
const openNeighbours = () => {
rootPanel.classList.remove('active');
if (tocPanel) tocPanel.classList.remove('active');
neighboursPanel.classList.add('active');
overlay.classList.add('active');
loadNeighbours();
};
Listeners
Wires up all event listeners. The hamburger button checks all three panels (root, toc, neighbours) for open state before deciding whether to open or close. TOC links close the menu on click since they navigate to anchors on the same page.
menuBtn.addEventListener('click', () => {
const isOpen = rootPanel.classList.contains('active') ||
(tocPanel && tocPanel.classList.contains('active'))||
neighboursPanel.classList.contains('active');
isOpen ? closeAll() : openRoot();
});
overlay.addEventListener('click', closeAll);
rootPanel.querySelectorAll('a').forEach(link => {
link.addEventListener('click', closeAll);
});
const tocTriggerBtn = document.getElementById('toc-trigger');
if (tocTriggerBtn) {
tocTriggerBtn.addEventListener('click', openToc);
}
const mobileNeighbourBtn = document.getElementById('mobile-neighbour-trigger');
if (mobileNeighbourBtn) {
mobileNeighbourBtn.addEventListener('click', openNeighbours);
}
if (tocPanel) {
tocPanel.querySelectorAll('a').forEach(link => {
link.addEventListener('click', closeAll);
});
}
Assembl
The complete tangled version can be found here↗ .
document.addEventListener('DOMContentLoaded', () => {
// guard → build_root → add_nav → add_toc → add_neighbours → build_neighbours → state → listeners
});
© Prabu Anand K 2020-2026