たねやつの木

Photos and Programming

はてなブログにボタン一つでダークモードを実装する

こんばんは、たねやつです。

以前、ブログのテーマを変更したときによりちゃんとしたダークモードを搭載した という話をしましたが、今日はそれについてもう少し詳しく & ソース付きで解説してみようと思います。

基本思想

基本的には、背景色変更のボタンを押したときに、JavaScriptで各要素にstyle属性を追加し、主に文字色と背景色を変更します。

style属性で指定した値は、!importantで指定されている値の次に優先的に適用されるのでほとんどの場合に設定値を上書きできます。

背景色を元に戻す時には逆にstyle属性を取り除きます。

制限

ここで紹介しているソースは、Haruniというテーマでのみ動作を確認しています。ほかの公式テーマやカスタムテーマでもある程度(6-7割)は背景色を変更してくれますが、追加で手直しが必要になってきます。。。

http://blog.hatena.ne.jp/-/store/theme/17391345971624603155

といってもChromeなどから対象の要素のクラスやIDを取得して、適応するだけなのでCSSをちょこっといじることのできる方であればそこまで難易度は高くないと思います。

ソース

/**
 * 背景色を切り替えます。
 * @param {boolean} flg true を渡すと黒背景色に変更します。
 *                      falseを渡すと元の背景色に戻します。
 */
function changeBgColor(flg) {
    
    if (flg) {
        // 背景を暗くする
        
        const color_veryblack   = '#000000';
        const color_black       = '#121212';
        const color_darkgray    = '#202020';
        const color_white       = '#e5e5e5';
        const border_radius_6px = '6px';

        /**
         * 画面内に一つしか存在しない要素、クラス
         */
        setAttributeStyle('#content',                       'background-color:'     + color_black       + ';'); // 一番背景の背景色
        setAttributeStyle('#blog-title',                    'background-color:'     + color_veryblack   + ';'); // タイトルの背景色
        setAttributeStyle('.hatena-module-related-entries', 'background-color:'     + color_veryblack   + ';'); // 関連記事
        setAttributeStyle('.table-of-contents',             'background-color:'     + color_veryblack   + ';'
                                                          + 'box-shadow:'           + 'none'            + ';'); // 目次
        setAttributeStyle('#main-inner',                    'color:'                + color_white       + ';'); // 本文文字色
        setAttributeStyle('blockquote',                     'color:'                + color_veryblack   + ';'); // 引用
        
        /**
         * 画面内に複数存在する要素、クラス
         */
        setAttributeStyleAll('code',                        'background-color:'     + color_veryblack   + ';'); // code
        setAttributeStyleAll('.archive-entry',              'background-color:'     + color_darkgray    + ';'
                                                          + 'border-radius:'        + border_radius_6px + ';'); // アーカイブ
        setAttributeStyleAll('h5',                          'color:'                + color_white       + ';'); // h5
        setAttributeStyleAll('th',                          'color:'                + color_veryblack   + ';'); // th
        setAttributeStyleAll('.entry-title-link',           'color:'                + color_white       + ';'); // 記事タイトル
        setAttributeStyleAll('.comment-content',            'color:'                + color_white       + ';'); // comment
        setAttributeStyleAll('.hatena-asin-detail',         'color:'                + color_veryblack   + ';'); // asin

        /**
         * 例外
         */
        // .entry-dateとarchive-dateの存在チェックでTOPページか、各記事のページかを判定
        var date_elem = document.querySelector('.entry-date');
        if (date_elem) {
            setAttributeStyle('#main', 'background-color:' + color_darkgray + ';');     // トップツールバーの背景色
            date_elem.children[0].setAttribute('style', 'color:' + color_white + ';');  // タイトルの日付
        }

        var date_elems = document.querySelectorAll('.archive-date');
        if (date_elems) {
            for (var i = 0; i < date_elems.length; i++) {
                date_elems[i].children[0].setAttribute('style', 'color:' + color_white + ';');  // タイトルの日付
            }
        }

        // サイドバーのモジュール
        var module_elem = document.querySelector('#box2-inner');
        if (module_elem) {
            for (var i = 0; i < module_elem.childElementCount; i++) {
                var child = module_elem.children[i];
                child.setAttribute('style', 'background-color:' + color_darkgray + ';');
            }
        }
    
    } else {
        // 背景を明るくする
        
        /**
         * 画面内に一つしか存在しない要素、クラス
         */
        removeAttributeStyle('#content');                       // 一番背景の背景色
        removeAttributeStyle('#blog-title');                    // タイトルの背景色
        removeAttributeStyle('.hatena-module-related-entries'); // 関連記事
        removeAttributeStyle('.table-of-contents');             // 目次
        removeAttributeStyle('#main-inner');                    // 本文文字色
        removeAttributeStyle('blockquote');                     // 引用

        /**
         * 画面内に複数存在する要素、クラス
         */
        removeAttributeStyleAll('code');                // code
        removeAttributeStyleAll('.archive-entry');      // アーカイブ
        removeAttributeStyleAll('h5');                  // h5
        removeAttributeStyleAll('th');                  // th
        removeAttributeStyleAll('.entry-title-link');   // 記事タイトル
        removeAttributeStyleAll('comment-content');     // comment
        removeAttributeStyleAll('.hatena-asin-detail'); // asin

        /**
         * 例外
         */
        // .entry-dateとarchive-dateの存在チェックでTOPページか、各記事のページかを判定
        var date_elem = document.querySelector('.entry-date');
        if (date_elem) {
            removeAttributeStyle('#main');                  // トップツールバーの背景色
            date_elem.children[0].removeAttribute('style'); // タイトルの日付
        }

        var date_elems = document.querySelectorAll('.archive-date');
        if (date_elems) {
            for (var i = 0; i < date_elems.length; i++) {
                date_elems[i].children[0].removeAttribute('style'); // タイトルの日付
            }
        }

        // サイドバーのモジュール
        var module_elem = document.querySelector('#box2-inner');
        if (module_elem) {
            for (var i = 0; i < module_elem.childElementCount; i++) {
                var child = module_elem.children[i];
                child.removeAttribute('style');
            }
        }
    }
}

/**
 * 指定した要素にstyle属性を付加します。
 * @param {string} cond     querySelectorで取得する条件
 * @param {string} style    設定するstyle
 */
function setAttributeStyle(cond, style) {
    if (!cond || !style) return;
    
    var elem = document.querySelector(cond);
    if (!elem) return;
    
    elem.setAttribute('style', style);
}

/**
 * 指定した要素にstyle属性を付加します。
 * @param {string} cond     querySelectorAllで取得する条件
 * @param {string} style    設定するstyle
 */
function setAttributeStyleAll(cond, style) {
    if (!cond || !style) return;
    
    var elems = document.querySelectorAll(cond);
    if (!elems) return;

    for (var i = 0; i < elems.length; i++) {
        var elem = elems[i];
        elem.setAttribute('style', style);
    }
}

/**
 * 指定した要素のstyle属性を除去します。
 * @param {string} cond     querySelectorで取得する条件
 */
function removeAttributeStyle(cond) {
    if (!cond) return;
    
    var elem = document.querySelector(cond);
    if (!elem) return;
    
    elem.removeAttribute('style');
}

/**
 * 指定した要素のstyle属性を除去します。
 * @param {string} cond     querySelectorAllで取得する条件
 */
function removeAttributeStyleAll(cond) {
    if (!cond) return;
    
    var elems = document.querySelectorAll(cond);
    if (!elems) return;

    for (var i = 0; i < elems.length; i++) {
        var elem = elems[i];
        elem.removeAttribute('style');
    }
}

かなり長めで横に長いのでコメント部分が見たい方はスクロールしてみてください。大したことは書いていないですが( 一一)

背景色を元に戻すほうは、配列化してfor文にすればもう少しソースを減らすことができそうです。背景を暗くするときとコードの構造が変わってしまうのであまり好きではないですが。

これを、デザイン画面のヘッダータイトル下などに<style>...</style>で囲んでスクリプトを設定します。ほかのスクリプトをすでに書いている方はそこと仲良く混ぜ合わせてください。

やっていることはただひたすらに、IDやクラス名で要素を取得してstyle属性を設定しているだけです('ω')

changeBgColor()という関数を設定しているので、サイドバーのボタンのonClickイベントや、ページ読み込み時で呼び出すようにしてください。このブログでのサイドバーのボタンは以下のようなソースになっています。

    <a href="javascript:void(0)" onClick="changeBgColor(true)">背景を暗くする</a>
    <a href="javascript:void(0)" onClick="changeBgColor(false)">背景を元に戻す</a>

ボタンのCSSや中央揃えは各々設定してみてください。以下のサイトを参考にしてボタンのCSSは作成しています。

解説

changeBgColor

ボタン押下時に渡される引数を判定して、背景を暗くする場合と背景を元に戻す場合に分岐します。

function changeBgColor(flg) {
    
    if (flg) {
        // 背景を暗くする
        ...
    
    } else {
        // 背景を明るくする
        ...
    }
}

setAttributeStyle

画面内でユニークな要素に対してはコチラの関数を使用します。

function setAttributeStyle(cond, style) {
    if (!cond || !style) return;
    
    var elem = document.querySelector(cond);
    if (!elem) return;
    
    elem.setAttribute('style', style);
}

最初の引数には、querySelector()で検索する条件の文字列を指定します。要素の場合には引数の頭には何もつけず(setAttributeStyle('code', 'hoge'))、idで検索する場合には#をつけ(setAttributeStyle('#inner-main', 'hoge'))、クラスで検索する場合には.をつけます(setAttributeStyle('.entry-date', 'hoge'))。

引数のどちらかがnullや空文字の場合、この関数は何も行いません。同様に指定した検索条件で要素を取得できない場合にも何もしません。取得できていない要素に対してsetAttribute()を実行するとJavaScriptでエラーが発生し、以降の処理を行わないため、もしエラーが発生したときに背景色が中途半端に変わってしまうことを防ぎます。

styleの引数にはそのままstyle属性に設定したい値を渡します。背景色を変えたい場合はbackground-color:black;を、文字色を変更した場合にはcolor:black;などを渡します。色コードに関しては定数化して置いたほうが後々色を変更したいときに便利でしょう。

setAttributeStyleAll

function setAttributeStyleAll(cond, style) {
    if (!cond || !style) return;
    
    var elems = document.querySelectorAll(cond);
    if (!elems) return;

    for (var i = 0; i < elems.length; i++) {
        var elem = elems[i];
        elem.setAttribute('style', style);
    }
}

基本は上記のsetAttributeStyleと同じです。コチラは一ページ内で複数取得できる要素が存在している場合に使用します。間違ってsetAttributeStyleを使用すると、画面上で一番最初に現れる要素に対してのみ背景色変更が適応されますのでご注意を。

例外

単純にquerySelectorで取得できる要素にstyleを追加するだけでなく、取得できた要素の子要素すべてにstyleを設定しなければならないときがあります。その場合には、各要素に対して独自の処理を追加してあげる必要があります。

例えばサイドバーの各島(最新記事や、カテゴリなど)一応各島ごとにIDは振られているのですが、ソースが膨れ上がったりモジュールを追加するたびにソースの修正が必要になってくるので、モジュールの親要素である<div id="box2-inner">を取得して、各子要素の背景色を変更するようにしています。

var module_elem = document.querySelector('#box2-inner');
if (module_elem) {
    for (var i = 0; i < module_elem.childElementCount; i++) {
        var child = module_elem.children[i];
        child.setAttribute('style', 'background-color:' + color_darkgray + ';');
    }
}

removeAttributeStyle

背景色を元に戻すときに使用する関数で、行っていることはsetAttributeremoveAttributeに変わっているだけです。こっちはstyleの値とか設定しなくていいのですっきりしたコードになりますね!

最初から背景色を暗くする

以下の処理を記事本文の最後尾に追加します。先頭でもいいのですが、記事の概要にちょろちょろソースが表示されるのでかっこ悪いです。

<script>
    //<--
    window.onload = function() {
        changeBgColor(true);
    }
    //-->
</script>

window.onloadに関数を設定してあげることで、HTMLの読み込みが完了したときにchangeBgColor(true);を実行します。読み込みが完了した後に実行しないと、サイドバーなどの部分(本文よりも後に読み込まれる部分)に対して処理が適応されません。

サイドバーなどのボタンから実行するときには、すでにHTMLの読み込みはすべて完了しているはずなので特に問題ないですが、本文に埋め込むときは注意が必要なようです。

最初にちょろっと元の背景色の画面が表示されてしまうのはやむなし。。orz

最後に

以上でダークモードの実装が完了です!完全に網羅できているとはいいがたいですがぱっと見た感じすべての要素に関して設定できているはずです!(笑)

背景が黒だと写真(特に夜景)やプログラミング記事でかなりそれっぽくなりますね('◇')ゞ