Created
November 3, 2020 12:57
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="bl-toc-container" class="bl-toc-transparent"> | |
<div class="bl-toc-title-container"> | |
<p class="bl-toc-title">Contents</p> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ======================== | |
// 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(/\ /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(/\ /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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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