TableOfContents CSS
When scrolling, my tutorial would highlight the current the current section in the TOC. The TOC sidebar feels a little unpolished atm as the edit page buttons are bundled with the page's TOC.
Here's the modified CSS + JS of my tutorials scroll effect. Just drop in any .md
to test. I added a seperator line between Edit Page and the TOC as well.
<style>
#TableOfContents {
/* same as .row */
margin-left: -15px;
margin-right: -15px;
}
#TableOfContents ul li > a {
padding-left: 15px;
padding-right: 15px;
border-left: 4px solid transparent;
}
#TableOfContents > ul > li > ul > li {
margin-left: 0;
}
#TableOfContents > ul > li > ul > li > a {
padding-left: calc(15px + .5rem);
padding-right: calc(15px + .5rem);
}
#TableOfContents > ul > li.active > a {
border-left-color: #54a3d8;
background: #54a3d820;
}
#TableOfContents > ul > li > ul > li.active > a {
border-left-color: #54a3d8c0;
background: #54a3d810;
}
.td-page-meta + #TableOfContents {
border-top: 1px solid #bcbebf; /* same as .td-toc */
padding-top: .25rem;
}
</style>
<script>
function getOffsetSum(elem) {
var top=0, left=0
while (elem) {
top = top + parseInt(elem.offsetTop)
left = left + parseInt(elem.offsetLeft)
elem = elem.offsetParent
}
return { top: top, left: left }
}
function setActiveMenuItem(li) {
console.log('setActiveMenuItem', li)
var isMenuItem = li.matches('#TableOfContents > ul > li')
var isSubMenuItem = li.matches('#TableOfContents > ul > li > ul > li')
var wasActive = li.classList.contains('active')
if (isMenuItem || !wasActive) {
// Unset current active item
var menuItem = document.querySelector('#TableOfContents > ul > li.active')
if (menuItem) {
menuItem.classList.remove('active')
}
var menuItem = document.querySelector('#TableOfContents > ul > li > ul > li.active')
if (menuItem) {
menuItem.classList.remove('active')
}
}
li.classList.add('active')
if (isMenuItem) {
var firstSubMenuItem = li.querySelector('ul > li')
if (firstSubMenuItem) {
firstSubMenuItem.classList.add('active')
}
} else if (isSubMenuItem) {
var menuItem = li.parentNode.parentNode // TODO: parentUntil('li')
menuItem.classList.add('active')
}
// Scroll to item
// li.scrollIntoView()
}
function getMenuItemElement(li) {
var id = li.querySelector('a').href.split('#', 2)[1]
return document.getElementById(id)
}
function updateTOC() {
console.log('updateTOC')
var viewTop = window.scrollY
var tocList = document.querySelectorAll('#TableOfContents li')
// Binary Search maybe (since elements are in order)?
for (var i = 0; i < tocList.length; i++) {
var curMenuItem = tocList[i]
var curElement = getMenuItemElement(curMenuItem)
var offset = getOffsetSum(curElement)
var offsetDiff = viewTop - offset.top
if (offsetDiff < 0) {
console.log(i, viewTop, offset.top, curElement)
if (i <= 1) {
console.log('selectTop')
// At top, select the first item
setActiveMenuItem(curMenuItem)
} else {
var prevMenuItem = tocList[i - 1]
var prevElement = getMenuItemElement(prevMenuItem)
var prevOffset = getOffsetSum(prevElement)
var prevOffsetDiff = viewTop - prevOffset.top
var sectionReadRatio = (prevOffsetDiff)/prevMenuItem.offsetHeight
if (sectionReadRatio >= 0.7) {
console.log('selectCurrent', sectionReadRatio)
// Current item is being read
setActiveMenuItem(curMenuItem)
} else {
console.log('selectPrev', sectionReadRatio)
// Previous item is still being read
setActiveMenuItem(prevMenuItem)
}
}
break
} else if (i == tocList.length - 1) {
// At bottom, select last item
setActiveMenuItem(curMenuItem)
}
}
}
var updating = false
function queueUpdateTOC() {
console.log('queueUpdateTOC', updating)
if (!updating) {
updating = true
requestAnimationFrame(function() {
updateTOC()
updating = false
})
}
}
queueUpdateTOC()
window.addEventListener('scroll', queueUpdateTOC)
</script>