Last active
June 21, 2022 09:54
-
-
Save manabuyasuda/0b1ac7fab3ba66ec21b7f093235a6650 to your computer and use it in GitHub Desktop.
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
/** | |
* @classdesc 対象要素内の見出しを検索して目次を生成します。 | |
* @author Manabu Yasuda <info@manabuyasuda.com> | |
* @example | |
* import Toc from '@lib/Toc' | |
* const toc = new Toc({ | |
* tocSelector: '.toc', | |
* contentSelector: '.content', | |
* headingSelector: 'h2', | |
* listClass: 'list', | |
* itemClass: 'item', | |
* linkClass: 'link', | |
* textClass: 'text', | |
* insertAfterbegin: (currentLevel) => { | |
* if (currentLevel === 2) { | |
* return ` | |
* <svg role="img"> | |
* <use xlink:href="/assets/svg/sprite.svg#arrow-circle-down"></use> | |
* </svg> | |
* ` | |
* } | |
* | |
* return '' | |
* } | |
* }) | |
* toc.init() | |
*/ | |
export default class Toc { | |
/** | |
* @param {object} options | |
* @param {string} options.tocSelector ['[data-toc]'] 目次を出力するセレクター | |
* @param {string} options.contentSelector ['[data-toc-content]'] 目次を検索するセレクター | |
* @param {string} options.headingSelector ['h2, h3'] 検索対象の見出し | |
* @param {string} options.prefix ['heading-'] 見出しとリンクに設定するid属性値とhref属性値の接頭辞(1から始まる数字が続く) | |
* @param {boolean|string} options.listClass [false] olタグのクラス名 | |
* @param {boolean|string} options.itemClass [false] liタグのクラス名 | |
* @param {boolean|string} options.linkClass [false] aタグのクラス名 | |
* @param {boolean|string} options.textClass [false] aタグ内のテキストを囲っているspanのクラス名 | |
* @param {boolean|string} options.insertAfterbegin(currentLevel) [false] リンクの最初の子要素 | |
* @param {boolean|string} options.insertBeforeend(currentLevel) [false] リンクの最後の子要素 | |
* @param {boolean|string} options.beforeInit() [false] 目次を生成後に実行するコールバック関数 | |
*/ | |
constructor(options) { | |
const defaultOptions = { | |
tocSelector: '[data-toc]', | |
contentSelector: '[data-toc-content]', | |
headingSelector: 'h2, h3', | |
prefix: 'heading-', | |
listClass: false, | |
itemClass: false, | |
linkClass: false, | |
textClass: false, | |
insertAfterbegin: false, | |
insertBeforeend: false, | |
beforeInit: false, | |
} | |
this.options = Object.assign(defaultOptions, options) | |
Object.keys(this.options).forEach(key => { | |
this[key] = this.options[key] | |
}) | |
this.selector = { | |
toc: document.querySelector(this.tocSelector), | |
content: document.querySelector(this.contentSelector), | |
headings: document.querySelector(this.contentSelector) | |
? document.querySelector(this.contentSelector).querySelectorAll(this.headingSelector) | |
: null, | |
} | |
} | |
init() { | |
if (!this.selector.toc) return | |
if (!this.selector.content) return | |
this.createToc() | |
} | |
createToc() { | |
const listClass = this.listClass ? ` class="${this.listClass}"` : '' | |
const itemClass = this.itemClass ? ` class="${this.itemClass}"` : '' | |
const linkClass = this.linkClass ? ` class="${this.linkClass}"` : '' | |
const textClass = this.textClass ? ` class="${this.textClass}"` : '' | |
let baseLevel = 2 | |
let html = '' | |
Array.from(this.selector.headings).forEach((heading, index) => { | |
const count = index + 1 | |
const titleId = `${this.prefix}${count}` | |
const currentLevel = Number(heading.nodeName.toLowerCase().split('h')[1]) | |
const isEqualLevel = currentLevel === baseLevel | |
const isFallLevel = currentLevel > baseLevel | |
const isRiseLevel = currentLevel < baseLevel | |
const firstOpeningTag = count === 1 | |
const lastClosingTag = count === this.selector.headings.length | |
let openingTag = '' | |
let closingTag = '' | |
// 挿入するリンク。 | |
const link = `<a${linkClass} href="#${titleId}">${ | |
this.insertAfterbegin !== false ? this.insertAfterbegin(currentLevel, index) : '' | |
}<span${textClass}>${heading.textContent.trim()}</span>${ | |
this.insertBeforeend !== false ? this.insertBeforeend(currentLevel, index) : '' | |
}</a>` | |
// 見出しにid属性を追加する。 | |
heading.setAttribute('id', titleId) | |
// 見出しレベルが同じ場合は、liのままにする。 | |
if (isEqualLevel) { | |
openingTag = `</li><li${itemClass}>` | |
} | |
// 見出しレベルが下がったら、olタグで入れ子にする。 | |
if (isFallLevel) { | |
openingTag = `<ol${listClass}><li${itemClass}>` | |
baseLevel++ | |
} | |
// 見出しレベルが上がったら、olタグを閉じる。 | |
if (isRiseLevel) { | |
const closeTag = Array.from({ length: baseLevel - currentLevel }).reduce(acc => { | |
acc += `</li></ol${listClass}>` | |
return acc | |
}, '') | |
openingTag = closeTag + `</li><li${itemClass}>` | |
baseLevel += Number(currentLevel - baseLevel) | |
} | |
// 最初の要素の開始タグ | |
if (firstOpeningTag) { | |
openingTag = `<li${itemClass}>` | |
} | |
// 最後の要素の終了タグ | |
if (lastClosingTag) { | |
const closeTag = Array.from({ length: baseLevel - 2 }).reduce(acc => { | |
acc += '</li></ol>' | |
return acc | |
}, '') | |
closingTag = currentLevel === 2 ? '</li>' : closeTag + '</li>' | |
} | |
html += openingTag + link + closingTag | |
}) | |
this.selector.toc.insertAdjacentHTML('beforeend', `<ol${listClass}>` + html + '</ol>') | |
if (this.beforeInit !== false) { | |
this.beforeInit() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment