<table-of-contents>
Create a multi-level table of contents from the page headings.
Usage
Include a <table-of-contents>
element in your HTML.
By default, it will automatically generate a list of anchor links to all of the h2
through h6
heading elements. If a heading doesn’t have an ID, it will generate one.
<table-of-contents></table-of-contents>
Options
The <table-of-contents>
component can be customized with a handful of attributes…
levels
- Change which heading levels are used (default:h2, h3, h4, h5, h6
)list-type
- Adjust the list type used for the table of contents (default:ul
).heading
- The label to use for the heading (default:Table of Contents
).heading-level
- Theh*
level to use for heading (default:h2
).content
- Limit the headings used to a specific element by passing in a selector string for the content wrapper (ex.#main
).
<table-of-contents
levels="h4, h5, h6"
list-type="ol"
heading="On This Page..."
heading-level="h3"
content="#sidebar"
></table-of-contents>
Methods
If the content of the HTML changes, you can dynamically render an updated table of contents by running the render()
method on the <table-of-contents>
element that you want to update.
let toc = document.querySelector('table-of-contents');
toc.render();
The Web Component
customElements.define('table-of-contents', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Define properties
this.content = this.getAttribute('content') || document;
this.levels = this.getAttribute('levels') || 'h2, h3, h4, h5, h6';
this.heading = this.getAttribute('heading') || 'Table of Contents';
this.headingLevel = this.getAttribute('heading-level') || 'h2';
this.listType = this.getAttribute('list-type') || 'ul';
// Get the headings
// If none are found, don't render a list
this.headings = this.content.querySelectorAll(this.levels);
if (!this.headings.length) return;
// Render the list
this.render();
}
/**
* Inject the table of contents into the DOM
*/
render () {
// Track the current heading level
let level = this.headings[0].tagName.slice(1);
let startingLevel = level;
// Cache the number of headings
let len = this.headings.length - 1;
// Inject the HTML into the DOM
this.innerHTML =
`<${this.headingLevel}>${this.heading}</${this.headingLevel}>
<${this.listType}>
${Array.from(this.headings).map((heading, index) => {
// Add an ID if one is missing
this.createID(heading);
// Check the heading level vs. the current list
let currentLevel = heading.tagName.slice(1);
let levelDifference = currentLevel - level;
level = currentLevel;
let html = this.getStartingHTML(levelDifference, index);
// Generate the HTML
html +=
`<li>
<a href="#${heading.id}">${heading.innerHTML.trim()}</a>`;
// If the last item, close it all out
if (index === len) {
html += this.getOutdent(Math.abs(startingLevel - currentLevel));
}
return html;
}).join('')}
</${this.listType}>`;
}
/**
* Create an ID for a heading if one does not exist
* @param {Node} heading The heading element
*/
createID (heading) {
if (heading.id.length) return;
heading.id = `toc_${crypto.randomUUID()}`;
}
/**
* Get the HTML to indent a list a specific number of levels
* @param {Integer} count The number of times to indent the list
* @return {String} The HTML
*/
getIndent (count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `<${this.listType}>`;
}
return html;
}
/**
* Get the HTML to close an indented list a specific number of levels
* @param {Integer} count The number of times to "outdent" the list
* @return {String} The HTML
*/
getOutdent (count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `</${this.listType}></li>`;
}
return html;
}
/**
* Get the HTML string to start a new list of headings
* @param {Integer} diff The number of levels in or out from the current level the list is
* @param {Integer} index The index of the heading in the "headings" NodeList
* @return {String} The HTML
*/
getStartingHTML (diff, index) {
// If indenting
if (diff > 0) {
return this.getIndent(diff);
}
// If outdenting
if (diff < 0) {
return this.getOutdent(Math.abs(diff));
}
// If it's not the first item and there's no difference
if (index && !diff) {
return '</li>';
}
return '';
}
});