Skip to content

Instantly share code, notes, and snippets.

@vlavrynovych
Last active May 12, 2024 20:11
Show Gist options
  • Save vlavrynovych/4111095383229db74e71aedf72652fc9 to your computer and use it in GitHub Desktop.
Save vlavrynovych/4111095383229db74e71aedf72652fc9 to your computer and use it in GitHub Desktop.
Script for the Ghost blog which builds a Table of Contents where it is placed
<!-- <toc> element is used to inject a TOC to it -->
<toc></toc>
<style>
toc #toc-container { width: 100%; }
toc .toc-empty-item { display: block }
toc .toc-caption { text-align: center; margin-bottom: 15px }
</style>
<script>
class TOC {
caption = {
enabled: true,
text: 'Table of contents'
}
constructor() {
document.addEventListener('DOMContentLoaded', () => this.onLoad());
}
onLoad() {
const article = document.querySelector('article');
// find all <h2> and <h3> elements within the <article>
this.headers = Array.from(article.querySelectorAll('h2,h3'));
// defines if the structure is complex or not
this.isMultilevel = this.headers
.map(el => el.tagName)
.filter(isUnique)
.length > 1;
// create container
const container = this.el("div", 'toc-container');
this.addCaption(container);
// create navigation element
const nav = this.el('nav', 'table-of-contents');
nav.setAttribute('role', 'navigation');
nav.appendChild(this.buildList());
container.appendChild(nav);
article.querySelector('toc').appendChild(container);
}
/**
* Creates one item
* @param el - defines H2 or H3 element
* @returns {HTMLLIElement} <li> element with link inside
*/
createItem(el) {
if(!el) return this.el('li', 'toc-empty-item');
const item = this.el('li'),
link = this.el('a');
link.setAttribute('href', `#${el.id}`);
link.textContent = el.textContent;
item.appendChild(link);
return item;
}
/**
* Builds a table of contents as a list of elements
* @returns {HTMLUListElement} list of items
*/
buildList() {
const list = this.el('ul');
this.prepareStructure().forEach(item => {
const li = this.createItem(item.el);
if (item.list.length) {
const ul = this.el('ul');
item.list.forEach(el => ul.appendChild(this.createItem(el)))
li.appendChild(ul)
}
list.appendChild(li)
})
return list;
}
prepareStructure() {
const list = [];
this.headers.forEach(el => {
if(el.tagName === 'H2' || !this.isMultilevel) {
list.push(new ListItem(el));
} else {
let item = list[list.length - 1];
if(item == null) {
item = new ListItem();
list.push(item);
}
item.list.push(el)
}
})
return list;
}
/**
* Add a caption if enabled
* @param container - defines a container element
*/
addCaption(container) {
if(!this.caption.enabled) return;
const caption = this.el('H2', 'toc-caption');
caption.textContent = this.caption.text;
container.appendChild(caption);
}
/**
* Create new element of provided tag name with class if specified
* @param tagName - defines the tag name of element
* @param clazz - defines what will be set to class attribute
* @returns {*}
*/
el(tagName, clazz) {
const el = document.createElement(tagName);
if(clazz) el.setAttribute('class', clazz);
return el;
}
}
// --- Utils
function isUnique(value, index, array) {
return array.indexOf(value) === index;
}
class ListItem {
list = [];
constructor(el) {
this.el = el;
}
}
new TOC();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment