|
<style> |
|
/* CSS is not necessary. That is for look better and easy to test. */ |
|
|
|
/* Longer pages, so you can test to see if you can actually get to the specified location after clicking. */ |
|
body { |
|
min-height: 160rem |
|
} |
|
|
|
/* similar to the bootstrap */ |
|
.fixed-top { |
|
position: fixed; |
|
top: 0; |
|
right: 50vw; |
|
z-index: 1000; |
|
} |
|
</style> |
|
|
|
<div id="target"> |
|
<h1 id="my-app">App1</h1> |
|
<h2>Video</h2> |
|
<h3>mp4</h3> |
|
<h3>webm</h3> |
|
|
|
<h2>Audio</h2> |
|
<h3>Mp3</h3> |
|
<h3>m4a</h3> |
|
|
|
<h1>App2</h1> |
|
<h2>Overview</h2> |
|
</div> |
|
|
|
<script> |
|
|
|
class TOC { |
|
/** |
|
* @param {[HTMLHeadingElement]} headingSet |
|
* */ |
|
static parse(headingSet) { |
|
const tocData = [] |
|
let curLevel = 0 |
|
let preTocItem = undefined |
|
|
|
headingSet.forEach(heading => { |
|
const hLevel = heading.outerHTML.match(/<h([\d]).*>/)[1] |
|
const titleText = heading.innerText |
|
|
|
switch (hLevel >= curLevel) { |
|
case true: |
|
if (preTocItem === undefined) { |
|
preTocItem = new TocItem(titleText, hLevel) |
|
tocData.push(preTocItem) |
|
} else { |
|
const curTocItem = new TocItem(titleText, hLevel) |
|
const parent = curTocItem.level > preTocItem.level ? preTocItem : preTocItem.parent |
|
curTocItem.parent = parent |
|
parent.children.push(curTocItem) |
|
preTocItem = curTocItem |
|
} |
|
break |
|
case false: |
|
// We need to find the appropriate parent node from the preTocItem |
|
const curTocItem = new TocItem(titleText, hLevel) |
|
while (1) { |
|
if (preTocItem.level < curTocItem.level) { |
|
preTocItem.children.push(curTocItem) |
|
preTocItem = curTocItem |
|
break |
|
} |
|
preTocItem = preTocItem.parent |
|
|
|
if (preTocItem === undefined) { |
|
tocData.push(curTocItem) |
|
preTocItem = curTocItem |
|
break |
|
} |
|
} |
|
break |
|
} |
|
|
|
curLevel = hLevel |
|
|
|
if (heading.id === "") { |
|
heading.id = titleText.replace(/ /g, "-").toLowerCase() |
|
} |
|
preTocItem.id = heading.id |
|
}) |
|
|
|
return tocData |
|
} |
|
|
|
/** |
|
* @param {[TocItem]} tocData |
|
* @return {string} |
|
* */ |
|
static build(tocData) { |
|
let result = "<ul>" |
|
tocData.forEach(toc => { |
|
result += `<li><a href=#${toc.id}>${toc.text}</a></li>` |
|
if (toc.children.length) { |
|
result += `${TOC.build(toc.children)}` |
|
} |
|
}) |
|
return result + "</ul>" |
|
} |
|
} |
|
|
|
/** |
|
* @param {string} text |
|
* @param {int} level |
|
* @param {TocItem} parent |
|
* */ |
|
function TocItem(text, level, parent = undefined) { |
|
this.text = text |
|
this.level = level |
|
this.id = undefined |
|
this.parent = parent |
|
this.children = [] |
|
} |
|
|
|
window.onload = () => { |
|
|
|
const headingSet = document.querySelectorAll("h1, h2, h3, h4, h5, h6") // You can also select only the titles you are interested in. |
|
const tocData = TOC.parse(headingSet) |
|
|
|
console.log(tocData) |
|
|
|
const tocHTMLContent = TOC.build(tocData) |
|
const frag = document.createRange().createContextualFragment(`<fieldset class="fixed-top"><legend>TOC</legend>${tocHTMLContent}</fieldset>`) |
|
document.querySelector(`body`).insertBefore(frag, document.querySelector(`body`).firstChild) |
|
} |
|
</script> |