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';

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