Skip to content

Instantly share code, notes, and snippets.

@asha23
Created November 3, 2020 12:57
Show Gist options
  • Save asha23/e886debe74d693bfb61ed267b6505bda to your computer and use it in GitHub Desktop.
Save asha23/e886debe74d693bfb61ed267b6505bda to your computer and use it in GitHub Desktop.
Table Of Contents - Dynamically creates contents based on heading tags in the page
<div id="bl-toc-container" class="bl-toc-transparent">
<div class="bl-toc-title-container">
<p class="bl-toc-title">Contents</p>
</div>
</div>
// ========================
// TABLE OF CONTENTS
// =========================
// Setup anchor tags on post content
let headerTitles = [];
let titleHashes = [];
let titleTags = [];
let tocArray = [];
/**
* Function to build the <ul> tag when there are nested items
*
* @param {num} index
* @param {boolean} callFlag
*/
window.buildUl = function(index, callFlag) {
if (callFlag === true) {
tocInnerUl = document.querySelector('.toc-li-' + index);
tocInnerUlTag = document.createElement('ul');
tocInnerUlTag.classList.add('nested-toc-list-' + index);
tocInnerUl.appendChild(tocInnerUlTag);
callFlag = false;
}
};
/**
* Builds the table of contents
*/
window.buildContents = function() {
tocDom = document.getElementById('bl-toc-container');
tocDom.lastChild.remove();
navTag = document.createElement('nav');
navTag.classList.add('toc-nav');
tocDom.appendChild(navTag);
tocUl = document.querySelector('.toc-nav');
tocUlTag = document.createElement('ul');
tocUlTag.classList.add('bl-toc-list');
tocUl.appendChild(tocUlTag);
callFlag = true;
arrCount = 0;
tempArr = [];
for (i=0; i < tocArray.length; i++) {
for (j = 0; j < tocArray.length; j++) {
if (j === tocArray[i].getParent) {
tempArr.push(tocArray[i].getParent);
}
}
}
var countIndex = tempArr.reduce(function(acc, curr) {
acc[curr] ? acc[curr]++ : acc[curr] = 1;
return acc;
}, []);
for (i = 0; i < tocArray.length; i ++) {
// Build the toplevel items
if (tocArray[i].titles && tocArray[i].tags == 'H2') {
tocLi = document.querySelector('.bl-toc-list');
tocLiTag = document.createElement('li');
tocLiTag.classList.add('toc-li-' + i);
tocLi.appendChild(tocLiTag);
tocA = document.querySelector('.toc-li-' + i);
tocATag = document.createElement('a');
tocATag.classList.add('bl-toc-link');
tocATag.classList.add('bl-toc-link-' + i);
tocATag.textContent = tocArray[i].titles;
tocA.appendChild(tocATag);
tocLink = document.querySelector('.bl-toc-link-' + i);
tocLink.href = tocArray[i].hash;
}
getParent = tocArray[i].getParent;
countKeyVal = countIndex[tocArray[i].getParent];
// Build the <ul>
if (countKeyVal === arrCount) {
window.buildUl(tocArray[i].getParent, true);
arrCount = 0;
}
arrCount ++;
}
// Build the submenus
for (l = 0; l < tocArray.length; l ++) {
getParent = tocArray[l].getParent;
if (document.querySelector('.nested-toc-list-' + getParent) && tocArray[l].titles) {
nestedTocLi = document.querySelector('.nested-toc-list-' + getParent);
nestedTocLiTag = document.createElement('li');
nestedTocLiTag.classList.add('nested-toc-li-' + l);
nestedTocLi.appendChild(nestedTocLiTag);
nestedTocA = document.querySelector('.nested-toc-li-' + l);
nestedTocATag = document.createElement('a');
nestedTocATag.classList.add('bl-toc-link');
nestedTocATag.classList.add('nested-toc-link-' + l);
nestedTocATag.textContent = tocArray[l].titles;
nestedTocA.appendChild(nestedTocATag);
nestedTocLink = document.querySelector('.nested-toc-link-' + l);
nestedTocLink.href = tocArray[l].hash;
}
}
};
/**
* Remove Emojis
*
* @param {string} string
* @return {string} string
*/
window.removeEmojis = function(string) {
var regex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g;
string = string.replace(regex, '');
string = string.trim();
return string;
};
/**
* Do all the hashing based on page headers then create an array for the table of contents.
*
* @param {array} tocArray
*/
window.doHashing = function() {
let parent = null;
let getParent = null;
let count = 0;
document.querySelectorAll('.post-content-inner h1, .post-content-inner h2, .post-content-inner h3, .post-content-inner h4, .post-content-inner h5, .post-content-inner h6 .posts-panel-content h1, .posts-panel-content h2, .posts-panel-content h3, .posts-panel-content h4, .posts-panel-content h5, .posts-panel-content h6').forEach(heading => {
let headingInner = heading.innerText.toLowerCase().replace(/[`~!@#%^&*()_|+\-=?;:'",<>\{\}\[\]\\\/]/gi, '').replace(/ +/g, '-').replace(/\&nbsp;/g, '');
headingInner = removeEmojis(headingInner);
let id = heading.getAttribute("id") || headingInner;
if (id[id.length-1] === ".") {
id = id.slice(0, -1);
}
// Remove initial numbers
if (!isNaN(id.charAt(0))) {
id = id.substring(3);
}
// remove all weird entities
id = id.replace(/["|'|’|‘|’|“|”|(|)|/]/g, "");
id = id.replace(/\./g, '-');
id = id.trim();
id = window.removeEmojis(id);
heading.setAttribute('id', id);
let headingTag = heading;
let tagName = headingTag.tagName;
// Create arrays of the content;
tags = heading.tagName;
hash = '#' + id;
titles = window.removeEmojis(heading.innerText);
if (tagName === 'H2') {
parent = count;
}
if (tagName === 'H3') {
getParent = parent;
} else {
getParent = null;
}
tocArray.push({getParent, tags, titles, hash});
if (tagName === 'H2') {
let div = document.createElement('div');
div.className = 'content-gap';
div.innerText = '';
headingTag.appendChild(div);
heading.classList.add('anchor-heading-h2');
} else {
heading.classList.add('anchor-heading');
}
count++;
});
};
/**
* Automatically create the accordion contents
*/
window.contentsAccordion = function() {
document.querySelectorAll('.bl-toc-list li').forEach(tocList => {
tocList.classList.add('toc-accordion');
});
document.querySelectorAll('.toc-accordion ul').forEach(tocListContent => {
tocListContent.classList.add('toc-accordion-content');
});
document.querySelectorAll('.toc-accordion ul li').forEach(tocListRemove => {
tocListRemove.classList.remove('toc-accordion');
});
document.querySelectorAll('.bl-toc-list li a').forEach(ariaSet => {
ariaSet.setAttribute('aria-expanded', 'false');
innerText = ariaSet.textContent;
ariaSet.innerHTML = innerText + '<span class="icon"></span>';
});
// if the item has no children then hide the icon
document.querySelectorAll('.toc-accordion').forEach(checkItem => {
if (checkItem.getElementsByTagName('UL').length > 0) {
// do nothing
} else {
// hide the icon
checkItem.classList.add('hide-icon');
}
});
const items = document.querySelectorAll(".toc-accordion a");
/**
* Toggle the accordion
*
* @param {array} item
*/
function toggleAccordion(item) {
const itemSiblings = item.path[2].querySelectorAll('.toc-accordion-content');
const itemToggle = this.getAttribute('aria-expanded');
// Only open/close the accordion if the items are top level.
if (typeof(itemSiblings) !== 'undefined') {
if (itemSiblings.length !== 0) {
for (i = 0; i < items.length; i++) {
items[i].setAttribute('aria-expanded', 'false');
}
if (itemToggle == 'false') {
this.setAttribute('aria-expanded', 'true');
}
}
}
}
items.forEach(item => {
item.addEventListener('click', toggleAccordion);
});
};
/**
* Observer for the contents nav
*
* @param {function} fn
*/
function ready(fn) {
document.addEventListener('DOMContentLoaded', fn, false);
}
ready(() => {
const motionQuery = window.matchMedia('(prefers-reduced-motion)');
const TableOfContents = {
container: document.querySelector('.bl-toc-transparent'),
links: null,
headings: null,
intersectionOptions: {
rootMargin: '0px',
threshold: 1
},
previousSection: null,
observer: null,
/**
* Initialise
*
*/
init() {
this.handleObserver = this.handleObserver.bind(this);
this.setUpObserver();
this.findLinksAndHeadings();
this.observeSections();
this.links.forEach(link => {
if (link) {
link.addEventListener('click', this.handleLinkClick.bind(this));
}
});
},
/**
* Handles the click event
*
* @param {event} evt
*/
handleLinkClick(evt) {
evt.preventDefault();
let id = evt.target.getAttribute('href').replace('#', '');
let section = this.headings.find(heading => {
if (heading) {
return heading.getAttribute('id') === id;
}
});
window.scroll({
behavior: motionQuery.matches ? 'instant' : 'smooth',
top: section.offsetTop - 80,
block: 'start'
});
if (this.container.classList.contains('is-active')) {
this.container.classList.remove('is-active');
}
},
/**
* Handle the intersection observer
*
* @param {array} entries
* @param {event} observer
*/
handleObserver(entries, observer) {
entries.forEach(entry => {
let href = `#${entry.target.getAttribute('id')}`;
let link = this.links.find(l => l.getAttribute('href') === href);
if (entry.isIntersecting && entry.intersectionRatio >= 1) {
link.classList.add('is-visible');
this.previousSection = entry.target.getAttribute('id');
} else {
link.classList.remove('is-visible');
}
this.highlightFirstActive();
});
},
/**
* Highlights the first active link
*
*/
highlightFirstActive() {
let firstVisibleLink = this.container.querySelector('.is-visible');
this.links.forEach(link => {
link.classList.remove('active');
});
if (firstVisibleLink) {
firstVisibleLink.classList.add('active');
}
if (!firstVisibleLink && this.previousSection) {
this.container.querySelector(`a[href="#${this.previousSection}"]`).classList.add('active');
}
},
/**
* Observe each section
*
*/
observeSections() {
this.headings.forEach(heading => {
if (heading) {
this.observer.observe(heading);
}
});
},
/**
* Sets up the IntersectionObserver class
*
*/
setUpObserver() {
this.observer = new IntersectionObserver(
this.handleObserver,
this.intersectionOptions
);
},
/**
* Find the links and headings in the table of contents
*
*/
findLinksAndHeadings() {
this.linkVals = [...this.container.querySelectorAll('.bl-toc-link')];
this.links = [...this.container.querySelectorAll('a')];
this.outputLinks = this.linkVals.map(linkVal => {
linkHash = linkVal.hash;
linkHash = decodeURIComponent(linkHash);
linkHash = linkHash.replace(/^#/, "").replace(/[`~!@#$%^&*()_|+\=?;:'",.<>\{\}\[\]\\\/]/gi, '').replace(/ +/g, '-').replace(/\&nbsp;/g, '');
// Remove initial numbers
if (!isNaN(linkHash.charAt(0))) {
linkHash = linkHash.substring(2);
}
// remove weird entities
linkHash = linkHash.replace(/["|'|’|‘|’|“|”|(|)|/]/g, "");
linkHash = linkHash.trim();
// put the hash back
linkHash = linkHash.replace (/^/, '#');
linkHref = linkVal.setAttribute('href', linkHash);
// return linkHref;
});
this.headings = this.links.map(link => {
let id = link.getAttribute('href');
return document.querySelector(id);
});
}
};
if (TableOfContents.container) {
TableOfContents.init();
}
});
// Build out the table of contents
window.doHashing();
window.buildContents();
// Table of contents
// -----------------------------------------
.bl-contents-container {
#bl-toc-container {
border-radius: 5px;
border:none;
box-shadow:none;
background:white;
box-shadow: 0 0 4px 0 rgba(13, 16, 7, 0.04), 0 7px 15px -2px rgba(13, 16, 7, 0.08);
padding:15px 20px 15px 20px;
.bl-toc-title-container {
p {
font-weight:bold;
@include font-size(20);
padding:0;
margin-bottom:10px;
}
}
ul {
list-style:none;
margin:0;
padding:0;
@include font-size(14);
li {
line-height:18px;
border-radius:0;
@include font-size(16);
margin:0;
ul {
list-style:none;
margin:0;
padding:0;
margin-left:5px;
//padding-top:10px;
li {
@include font-size(12);
margin:0;
margin-bottom:1px;
&:last-of-type {
}
}
}
}
li a {
font-weight:bold!important;
color:#547BBA;
padding:8px;
display:block;
margin-bottom:1px;
transition:all 0.5s ease;
padding-right:15px;
&:hover,
&:focus {
background: $lightblue;
}
}
li > a.active {
font-weight:bold;
color: #333;
font-weight: 500;
background:$seagray;
}
}
}
}
.bl-toc-list {
.toc-accordion {
a[aria-expanded='true'] {
}
&.hide-icon {
a {
.icon {
display:none;
}
}
}
}
a {
position:relative;
&:hover, &:focus {
cursor: pointer;
&::after {
cursor: pointer;
}
}
.icon {
display: inline-block;
position: absolute;
top: 8px;
right: 10px;
&::after {
display: block;
position: absolute;
content: '+';
top: 0px;
left: -5px;
}
}
}
a[aria-expanded='true'] {
.icon {
&::after {
content:"-";
top:-2px;
left: -3px;
}
}
+ .toc-accordion-content {
opacity: 1;
max-height: 30em;
transition: all 0.2s linear;
will-change: opacity, max-height;
}
}
.toc-accordion-content {
opacity: 0;
max-height: 0;
overflow: hidden;
transition: opacity 0.2s linear, max-height 0.2s linear;
will-change: opacity, max-height;
li {
a {
.icon {
display:none;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment