JavaScript: Building an automatic submenu
You might have noticed the navigation menu that appears on the top right of the article pages on The Art of Web. This menu is dynamically generated using JavaScript (DHTML) when the page is loaded so we don't have to adjust it every time the page changes.
All second level (H2) headings on a page are included, and numbers and anchor links for the headings generated automatically.
How does it work?
We use a "self-executing anonymous function" to create the menu. It accepts two parameters: the id of the element that is to contain the generated menu; and the HTML tag used to identify headings on the page (defaults to H2 if not supplied).
All you need to do is include a placeholder where you want the menu to be inserted:
<div id="submenu">building menu...</div>
and then include the following code at the bottom of your HTML page. The parameters are specifed in the last line:
<script>
(function(targetId, headingTag) {
// Original JavaScript code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
var target = document.getElementById(targetId);
var headings = document.getElementsByTagName(headingTag || "h2");
if(target && (headings.length > 1)) {
/* construct an ordered list of links */
var menuList = document.createElement("ol");
for(let i=0; i < headings.length; i++) {
var headingText = headings[i].innerText;
var anchorName = "";
if(headings[i].id) {
anchorName = headings[i].id;
} else {
anchorName = "section_" + i;
headings[i].setAttribute("id", anchorName);
}
/* add numbering to headings */
headings[i].firstChild.nodeValue = (i+1) + ". " + headingText;
/* add link to menu list */
var listItem = document.createElement("li");
var menuLink = document.createElement("a");
menuLink.setAttribute("href", "#" + anchorName);
menuLink.appendChild(document.createTextNode(headingText));
listItem.appendChild(menuLink);
menuList.appendChild(listItem);
}
/* insert our generated menu into the target element */
target.replaceChildren(menuList);
} else {
/* remove the target element from the DOM */
target.parentNode.removeChild(target);
}
})("submenu", "h2");
</script>
You can copy the above code or download it as a separate javascript file using the link further down the page.
Instructions for implementation
First you need to include a placeholder for the submenu. Normally this would be a DIV element. The submenu will replace any existing contents of the selected element with an unordered list (OL) listing and linking to all the headings on the page.
<div id="submenu"><!-- --></div>
The submenu can be positioned and styled using CSS on the #submenu element (the name can change, but must match that in the function call).
Then all you need to to is include the above code at the end of your HTML page (or anywhere after the last heading that you want to be included in the submenu).
You can also include it as a separate file, again at the bottom of the page:
<script src="buildmenu.js"></script>
Or you can create an onload event that triggers the script after the DOM has finished loading.
That's all. The script will run as the page loads, scan the page for H2 headings (or other tags if specified), add numbers and anchors to them and populate the submenu with links. Normally this happens in a fraction of a second once the page has loaded.
If there are no headings found, or only one, the submenu element is removed from the DOM and will not be displayed.
How to style the submenu
On page load the script dynamically replaces the contents of the #submenu element as follows:
Before:
<div id="submenu">building menu...</div>
After:
<div id="submenu">
<ol>
<li><a href="#section_0">How does it work?</a></li>
<li><a href="#section_1">Instructions for implementation</a></li>
<li><a href="#section_2">Sample CSS styles</a></li>
<li><a href="#section_3">References</a></li>
<li><a href="#send_feedback">Send Feedback</a></li></ol>
</ol>
</div>
You can see here that when an id has already been assigned to one of the headings, it is used automatically as the anchor link. Otherwise a new, numbered, id is assigned to the headings.
So all we need to do now is apply some styles to the DIV and OL elements. The CSS styles we're currently using are:
<style>
#submenu {
float: right;
margin: 1em 0;
padding: 4px;
background: #fcfaf0;
border: 2px solid #E0D8B7;
border-radius: 1em;
font-size: 11px;
}
#submenu ol {
margin: 0;
padding: 0 0 0 30px;
}
</style>
This places the menu at the top right of the page as you can see. On The Art of Web we've also added a transition and hover effect using CSS transforms to make the menu zoom out.
Other options could be used to place the menu inline at the top of the page as a "Table of Contents" or to have it instead displayed as a dropdown menu - especially useful for mobile devices.
Using an onload event
The 'correct' way to fire this kind of JavaScript function is to
attach a handler to the DOMContentLoaded event. This is not
supported in Internet Explorer 8, so a fallback is included using
attachEvent.
In addition to using the onload event we have rewritten the main function into ES6 notation, and modified the generated links that that they use scrollIntoView to take you to the selected heading rather than navigating to the HTML anchor points.
<script>
const buildMenu = (targetId, headingTag) => {
"use strict";
// Original JavaScript code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
const target = document.getElementById(targetId);
const headings = document.querySelectorAll(headingTag || "h2");
if(target && (headings.length > 1)) {
/* construct an ordered list of links */
let menuList = document.createElement("ol");
headings.forEach((current, idx) => {
let headingText = current.innerText;
let anchorName = "";
if(current.id) {
anchorName = current.id;
} else {
anchorName = "section_" + idx;
current.setAttribute("id", anchorName);
}
/* add numbering to headings */
current.firstChild.nodeValue = (idx + 1) + ". " + headingText;
/* add link to menu list */
let listItem = document.createElement("li");
let menuLink = document.createElement("a");
menuLink.setAttribute("href", "#" + anchorName);
menuLink.appendChild(document.createTextNode(headingText));
listItem.appendChild(menuLink);
menuLink.addEventListener("click", (e) => {
current.scrollIntoView({behavior: "smooth"});
e.preventDefault();
});
menuList.appendChild(listItem);
});
/* insert our generated menu into the target element */
target.replaceChildren(menuList);
} else {
/* remove the target element from the DOM */
target.parentNode.removeChild(target);
}
};
/* generate the menu only after the page has finished loading */
window.addEventListener("DOMContentLoaded", (e) => {
buildMenu("submenu", "h2");
});
</script>
The difference is that the script will execute only after all the DOM elements have been rendered rather than during the page load when the script is encountered.
Tech4EN 21 August, 2018
Hi,
Can you please give the complete code with implementation.
Gideon 22 October, 2015
This code seems to be what I am looking for. Actually I want to be able create menus and sub sub menus and the labeling must come from database. currently I did it where on load the menus are filled with data from database. But I want to be able to update menu list prior to changes in the database without having to code again. Good job and I count on you, thanks.