ガイドラインページの作成時、目次を静的にコーディングしていたのですが、見出しが増えれば増えるほど修正が大変…
- 本文内の見出し修正時に目次の修正を忘れる
- アンカー用 ID が連番だと見出し削除時に全ての ID を変更しなければならない
- ページが長くなれば長くなるほど html 上の見出しと目次の位置が離れる
というわけで、JavaScript で目次を自動生成させてみました!
仕様
目次の起点要素
h1
はページタイトルで h2
から見出しというパターンが多いかと思いますが、自由に設定できるように指定した要素以下が対象となるようにしました。
hx 要素の親要素
hx
要素の親要素には目次に必要となる見出しが含まれるであろうセクショニングコンテツ section
article
が必須となります。
セクショニングコンテツの親要素
section
article
の親要素となれるのは div
のみとしました。
本当はなんでもOKにしたかったのですが、うまくいかず…
アンカー用 ID の付与要素
hx
要素に付与するとデザインによっては見出し上のスペース設定ができず、アンカーリンクのクリック時に見出しとウィンドウ上部がピチピチになってしまうという経験がありました。
そこで、以下のような仕様としました。
デフォルトではセクショニングコンテツ section
article
とし、isAddAnchorIdToHeading
というプロパティを true
にすることで hx
要素に変更可能。
アンカー用 ID の付与要素 | |
---|---|
デフォルト | section と article |
isAddAnchorIdToHeading = true | hx |
JavaScript
class TableOfContents {
constructor() {
this.rootElement = document.getElementById('js-mainInner')
this.toc = document.getElementById('js-toc')
this.anchorIdPrefix = 'section'
this.anchorIdSeparator = '-'
this.isAddAnchorIdToHeading = false
this.tocListItemHtml = ''
}
init() {
this.createHtml(this.rootElement)
this.insertHtml()
}
/**
* 目次の HTML の生成
* セクショニングコンテツ にアンカー用 id 付与
* @param {Object} rootElem 起点となる要素
* @param {String} anchorStr アンカー用 id の文字列
*/
createHtml(rootElem, anchorStr) {
const targetSelector = ':scope > section, :scope > article, :scope > div'
const targetElements = rootElem.querySelectorAll(targetSelector)
// 子要素に targetSelector がない場合は処理を抜ける
if (targetElements.length === 0) return
// 子要素の targetSelector をループ
targetElements.forEach((tag, i) => {
switch(tag.tagName) {
// 子要素がセクショニングコンテツ
case 'SECTION':
case 'ARTICLE':
const index = i + 1
const anchorId =
typeof anchorStr === 'undefined' ? this.anchorIdPrefix + index : anchorStr + this.anchorIdSeparator + index
const childElementAll = tag.querySelectorAll('*')
const childTargetElements = tag.querySelectorAll(targetSelector)
const heading = (() => {
for (const el of childElementAll) {
if (/^H[1-6]$/.test(el.tagName)) return el
}
})()
const headingText = heading.innerText
const aTag = `<a href="#${anchorId}">${headingText}</a>`
const addAnchorIdTarget = this.isAddAnchorIdToHeading ? heading : tag
// HTML を生成
if (childTargetElements.length) {
this.tocListItemHtml += `<li>${aTag}<ul>`
this.createHtml(tag, anchorId)
this.tocListItemHtml += '</ul></li>'
} else {
this.tocListItemHtml += `<li>${aTag}</li>`
}
// アンカー id 付与
addAnchorIdTarget.setAttribute('id', anchorId)
break
// 子要素が div
case 'DIV':
this.createHtml(tag, anchorStr)
}
})
}
/**
* 目次の HTML を挿入
* @param {Object} target 目次の HTML を挿入したい要素
*/
insertHtml() {
this.toc.insertAdjacentHTML(
'afterbegin',
`
<div class="toc_inner">
<h2 class="toc_heading">目次</h2>
<ul class="tocList">
${this.tocListItemHtml}
</ul>
</div>`
)
}
}
const tableOfContents = new TableOfContents()
tableOfContents.init()
DEMO
See the Pen Automatic Table of Contents by Takuya Mori (@taqumo) on CodePen.