Javascriptで目次のハイライト

サイドバーにある目次の現在項目を項目を分かりやすくするためハイライトするようにした。

更新

  • 2021/11: 折りたたみ要素の展開などでページの高さが変わってもハイライトがずれないようにしました。

経緯

長い記事を読むとき、目次にハイライトがないとどこを読んでるのか分からなくなりますね。このサイトではサイドバーに目次があるのですが、今まで特に読んでいるところをハイライトするようなことはしていなかったので、あれば分かりやすいなと思いました。

そこでサイドバーの目次をJavascriptでハイライトするようにしました。

環境

ワードプレスをCocoonを親テーマとして使っています。 しかし、jQeuryは使わないでJavascriptオンリーで実装しました。

あんまりjQueryは使いたくないという不思議なこだわりがあります。

コード全体

これらのスクリプトとCSSをコピペして、クラス名を調整すればOKです。 子テーマのjavascript.jsstyle.cssに追記します。

まずはスクリプトから:

// javascript.js
// ...
(function () {
    let tocList = [];
    let lastScrollY = 0;
    let lastBodyClientHeight = 0;
    function init() {
        tocList = [];
        const widget_toc_elm = document.querySelector('.widget_toc');
        if (widget_toc_elm == null) {
            return;
        }
        const alist = widget_toc_elm.querySelectorAll('a');
        for (let i = 0; i < alist.length; i++) {
            const a = alist[i];
            const theID = a.href.substring(a.href.lastIndexOf('#'));
            const theSectionElm = document.querySelector(theID);
            let top = theSectionElm.offsetTop;
            let parent = theSectionElm.offsetParent;
            while (parent != null) {
                top += parent.offsetTop;
                parent = parent.offsetParent;
            }
            tocList.push({ top: top, bottom: 1e30, itemdom: a.parentElement });
            if (i > 0) {
                tocList[i - 1].bottom = tocList[i].top;
            }
        }
        const aFooter = document.querySelector('footer');
        let footerTop = aFooter.offsetTop;
        let parent = aFooter.offsetParent;
        while (parent != null) {
            footerTop += parent.offsetTop;
            parent = parent.offsetParent;
        }
        tocList[alist.length - 1].bottom = footerTop;
        lastBodyClientHeight = document.body.clientHeight;
    }
    function updateSection(scrollY) {
        if (document.body.clientHeight !== lastBodyClientHeight) {
            init();
        }
        for (let sec of tocList) {
            sec.itemdom.classList.remove('reading-sec');
        }
        for (let i = 0; i < tocList.length; i++) {
            const sec = tocList[i];
            if (sec.top <= scrollY && scrollY < sec.bottom) {
                sec.itemdom.classList.add('reading-sec');
                break;
            }
        }
    }
    let ticking = false;
    document.addEventListener('scroll', function () {
        lastScrollY = window.scrollY;
        if (ticking === false) {
            window.requestAnimationFrame(function () {
                updateSection(lastScrollY + 50);
                ticking = false;
            });
            ticking = true;
        }
    });
})();

そしてCSSはシンプルに:

/* style.css */
/* 現在読んでいる章節に付与するクラス */
.reading-sec {
    background: beige;
    transition: background 0.3s ease;
}

/* 目次全体に付与されているクラス */
.widget_toc .toc-list {
    /* デフォルトが空白広めだったので狭く */
    padding-left: 0.2em;
    /* *を内側にして、背景色がずれてしまうのを防ぐ */
    list-style: decimal inside;
}

以降は実装方針や注意点などを書いておきます。

実装方針

縦方向であるy座標の範囲を章節ごとにオブジェクトの配列(tocList)として保持しておいて、スクロールするごとに現在のy座標を取得し、目次の.reading-secクラスの付与対象を決定します。

章節ごとのy座標範囲はイベントでいうロード(load)か初回スクロール(scroll)時に初期化します。 2021/11追記: lastBodyClientHeight変数で直前のページ全体の高さを保持しておいて、スクロール時に変化していれば初期化し直します。

各章節の上端と下端のy座標を保持します。下端は後続の章節の上端を設定して、最後尾の章節の下端はフッターの上端を使用します(最後尾は指定しなくても可)。

スクロールイベントで保持している配列と現在のy座標をもとに目次の章節に付与するクラス.reading-secの付け替えを行います。

グローバルにはあまり変数を残したくないので、無名関数に入れて即時実行させます。

また、高頻度でアップデート関数が呼ばれないようにするため、requestAnimationFrame()を使って呼び出しを抑制します。

実装時の注意

各章節の<h2>タグなどのリストは問題なく取得できますが、それぞれのページ全体での位置(左上を原点とするy座標)を取得するのがちょっと面倒でした。

offsetTopを使うとその要素の親から見たその要素のy座標が取得できるので、それを遡ってoffsetTopを足し合わせることにしました。 遡るには、offsetParentを使えばいいです。抜粋:

// 1つの章節のページ全体でのy座標の取得(要素上側)
let top = theSectionElm.offsetTop;
let parent = theSectionElm.offsetParent;
while (parent != null) {
    // 遡って足し合わせる
    top += parent.offsetTop;
    parent = parent.offsetParent;
}

クラス名にも注意します。サイドバーの目次に付与されるクラス.widget_tocが存在するか確認します。もしかしたらクラス名が異なっている可能性も十分あります。

また、目次が存在しないページでは、.widget_tocは存在しないので何もせずに終了するようにしています。

おわり

CSSのクラスを追加するくらいなら割と楽な作業でした。Cocoonのクラス名の仕様はよく分からないままですが。。もしかしたらワードプレスの仕様かもですが。.toc-数字みたいなのが謎だったので今回は使わないように実装しました。今度調べよう。

以上です。


Amazonアソシエイト

Bitly

コメント

タイトルとURLをコピーしました