はてなブログテーマ開発(8)スマホ用 offcanvasmenu の作り方

はてなブログテーマ開発(8)スマホ用 offcanvasmenu の作り方

はてなブログテーマ開発(8)スマホ用 offcanvasmenu の作り方

別ブログ「@半径とことこ60分」のテーマ変更を機に、はてなブログのテーマ開発について書いています。今回はスマホ用のナビゲーションメニューを作ります。

はてなブログテーマ開発(1)
はてなブログテーマ開発(2)
はてなブログテーマ開発(3)
はてなブログテーマ開発(4)
はてなブログテーマ開発(5)
はてなブログテーマ開発(6)(Javascript 記載の漏れがありました)
はてなブログテーマ開発(7)

01スマホのナビゲーションメニュー

ブログは、日々記事を書き連ねていきますので、どうしてもサイト内の見通しが悪くなります。特にスマホですと、新着情報などの、デスクトップではサイドバーに表示しているものが記事の下にいってしまいます。

そのため、何らかのボタンをタップしますと上下左右のいずれかからナビゲーションメニューが飛び出してくる offcanvas menu を使う場合が多いようです。はてなブログにも実装しようと思います。

実装方法はいろいろありますので、その一例です。このサイトでも過去に他の方法を記事にしたことがあります。

まだ他にもあるような気がしますが、思い出せません(笑)。

なお、以下の内容は「@半径とことこ60分」に実装しています。

ナビゲーションメニュー実装の構想

以下、ブレイクポイントを 768px ひとつで作成していますので、768px 以上をデスクトップと表示しています。

  • スマホでは、タップで右から飛び出す offcanvas menu 、デスクトップでは、ブログタイトル右に常時表示
  • サイドバーにナビゲーションメニューの HTML コードを書き、javascript で #blog-title-content 内に移動する
  • サイドバーのカテゴリーモジュールを javascript でナビゲーションメニュー内に移動する
    (直書きでもいいが、カテゴリーを追加変更する予定があるため)
  • デスクトップではカテゴリーリストをサブメニューとしているので、タッチデバイス用にタッチで hover クラスを追加する

02サイドバーにナビゲーションメニューを作成

今回は、タイトルブロックに移動してしまいますのでどこに入れてもいいのですが、ヘッダのタイトル下は他の用途に使う予定があり、フッタもあれこれ詰め込んでいますのでサイドバーに作成しています。

<a class="offcanvastoggle" href="javascript:void(0);" onclick="javascript:toggleoffcanvas();return false;">MENU</a>
<ul class="offcanvasmenu">
    <li>
        <a class="search-title" href="#">検索</a>
        <div class="search-body">
            <!-- 検索モジュールのコードを直書きしている -->
            <form class="search-form" role="search" action="サイトアドレス/search" method="get">
                <input type="text" name="q" class="search-module-input" value="" placeholder="記事を検索" required>
                <input type="submit" value="検索" class="search-module-button" />
            </form>
            <!-- ここまで -->
        </div>
    </li>
    <li id="category-wrapper">
        <a class="category-title" href="#">カテゴリー</a>
    </li>
    <li>
        <ul class="sub-offcanvasmenu">
            <li><a href="/archive">記事一覧</a></li>
            <!-- フッタへスムーズスクロールする -->
            <li><a href="javascript:void(0);" onclick="javascript:scrollToContent('#contactus');return false;">Contuct Us</a></li>
            <!-- ここまで -->
        </ul>
    </li>
</ul>

サイドバーの HTML モジュールに放り込みます。

03Javascript で要素を移動する

<script>
function toggleoffcanvas(){
    if( !document.body.classList.contains('offcanvas')) {
        document.body.classList.add('offcanvas');
        // iPhoneの場合、背景のスクロールを止める
        var elem = document.getElementsByClassName('offcanvasmenu')[0]; // このクラス名を間違えていました(下記参照)
        elem.addEventListener('touchmove', function(e) {
            var scroll = elem.scrollTop;
            var range = elem.scrollHeight - elem.offsetHeight - 1;
            if (scroll < 1) {
                e.preventDefault();
                elem.scrollTop = 1;
            } else if(scroll > range) {
                e.preventDefault();
                elem.scrollTop = range;
            }
        });
    } else {
        document.body.classList.remove('offcanvas');
    }


}


(function(){
    // サイドバーのひとつ目の要素をブログタイトルボックスに移動する
    var elements = document.getElementById('box2-inner').children;
    var titleDiv = document.getElementById('blog-title-content');
    titleDiv.appendChild(elements[0]);


    // メニュー内にカテゴリーを移動する
    // [2]はサイドバー内で3つ目のモジュール、ただし、上でひとつ移動させているので実際は4つ目
    var catWrap = document.getElementById('category-wrapper');
    catWrap.appendChild(elements[2].getElementsByClassName('hatena-urllist')[0]);


    // メニュー内の li 全てに touch または mouseover イベントを付加し、hover クラスをトグル設定する
    var menulists = document.getElementsByClassName('offcanvasmenu')[0].getElementsByTagName('li');
    Array.prototype.forEach.call(menulists, function(menulist){
        if((('createTouch' in document) || ('ontouchstart' in document)) && ('orientation' in window)) {
            menulist.addEventListener('touchstart', function(e) {
                menulist.classList.toggle('hover');
            });
        } else {
            menulist.addEventListener('mouseover', function(e) {
                menulist.classList.add('hover');
            });
            menulist.addEventListener('mouseout', function(e) {
                menulist.classList.remove('hover');
            });
        }
    });
}());
</script>

フッタに入れます。

「iPhoneの場合、背景のスクロールを止める」は、以下の記事の時はうまくいっていたのですが、今確認したところうまくいっていません。背景がスクロールしていしまいます。iPhone は面倒くさい(涙)。

(2018.10.14)iPhoneの件は勘違いでした。クラス名を offcanvasmenu に変更したことを忘れていただけでした(ペコリ)。上のスクリプトで背景はスクロールしません。

iPhoneでモーダルの背景のスクロールを止める – IMUZA.com

04CSS

.offcanvastoggle {
    display: block;
    color: #fff;
    background: #fff;
    height: 50px;
    width: 50px;
    border-radius: 50%;
    position: relative;
}
.offcanvastoggle:hover {
    color: #fff;
}
.offcanvastoggle::before, .offcanvastoggle::after {
    content: "";
    background: #87140d;
    position: absolute;
    right: 0;
    left: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 30px;
    height: 3px;
    -webkit-transition: all 300ms;
    transition: all 300ms;
}
.offcanvastoggle::before {
    -webkit-box-shadow: 0px -10px 0px #87140d;
            box-shadow: 0px -10px 0px #87140d;
}
.offcanvastoggle::after {
    -webkit-box-shadow: 0px 10px 0px #87140d;
            box-shadow: 0px 10px 0px #87140d;
}
@media (min-width: 768px) {
    .offcanvastoggle {
        display: none;
    }
}

まず、ハンバーガーメニューです。

  • テキストは text-indent: -9999px; で飛ばす方法もありますが、ハンバーガーメニューにイメージを使いませんので色を背景と同じにしています。SEO 上はよくないのか? でも、MENU ですからね…。
  • ハンバーガーメニューのボーダーは、before, after の疑似要素を使い、box-shadow で3本にしています。
  • タップで、box-shadow を消し、ボーダーを 45度回転させています。
  • デスクトップでは、要素自体を非表示にしています。
.offcanvasmenu {
    margin: 0;
    position: fixed;
    overflow-y: auto;
    height: 100%;
    display: block;
    right: -100vw;
    z-index: 10;
    padding: 50px 20px;
    -webkit-box-sizing: border-box;
            box-sizing: border-box;
    width: 100vw;
    background: #fff;
    -webkit-transition: right 500ms;
    transition: right 500ms;
}
@media (min-width: 768px) {
    .offcanvasmenu {
        display: block;
        position: relative;
        right: auto;
        width: auto;
        background: transparent;
        padding: 0;
        overflow-y: visible;
    }
}
.offcanvasmenu li {
    list-style: none;
}
@media (min-width: 768px) {
    .offcanvasmenu li {
        display: inline;
    }
    .offcanvasmenu li a {
        padding: 5px 10px;
        text-decoration: none;
        color: #454545;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu li.hover > div {
        -webkit-transform: scale(1);
                transform: scale(1);
    }
}
.offcanvasmenu > li {
    position: relative;
}
.offcanvasmenu .search-title {
    display: none;
}
@media (min-width: 768px) {
    .offcanvasmenu .search-title {
        display: inline-block;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu .search-body {
        position: absolute;
        width: 50vw;
        max-width: 350px;
        right: 0;
        background: rgba(135, 20, 13, 0.8);
        padding: 30px;
        border-radius: 10px;
        -webkit-transform: scale(0);
                transform: scale(0);
        -webkit-transition: all 300ms;
        transition: all 300ms;
        -webkit-transform-origin: top right;
                transform-origin: top right;
    }
}
.offcanvasmenu .search-body .search-form {
    background: #fff;
}
.offcanvasmenu #category-wrapper {
    margin: 0;
    padding: 0;
    margin: 20px 0 40px;
}
.offcanvasmenu #category-wrapper .category-title {
    text-align: center;
    pointer-events: none;
    display: block;
    text-decoration: none;
    color: #454545;
    font-weight: 700;
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper .category-title {
        display: inline-block;
        pointer-events: auto;
        font-weight: 100;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper .hatena-urllist {
        position: absolute;
        width: 300px;
        background: rgba(135, 20, 13, 0.8);
        right: 0;
        border-radius: 10px;
        padding: 20px;
        -webkit-transform: scale(0);
                transform: scale(0);
        -webkit-transition: all 300ms;
        transition: all 300ms;
        -webkit-transform-origin: top right;
                transform-origin: top right;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper .hatena-urllist li {
        display: block;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper .hatena-urllist li a {
        display: block;
        color: #fff;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper .hatena-urllist li a:hover {
        background: rgba(135, 20, 13, 0.8);
        color: rgba(255, 255, 255, 0.5);
        text-decoration: none;
    }
}
@media (min-width: 768px) {
    .offcanvasmenu #category-wrapper.hover .hatena-urllist {
        -webkit-transform: scale(1);
                transform: scale(1);
    }
}
.offcanvasmenu .sub-offcanvasmenu {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-pack: distribute;
        justify-content: space-around;
}
@media (min-width: 768px) {
    .offcanvasmenu .sub-offcanvasmenu {
        display: -webkit-inline-box;
        display: -ms-inline-flexbox;
        display: inline-flex;
        padding: 0;
    }
}
.offcanvasmenu .sub-offcanvasmenu li:first-child a::before {
    content: "\f022";
    font-size: 55px;
}
.offcanvasmenu .sub-offcanvasmenu li:last-child a::before {
    content: "\f04b";
    font-size: 45px;
    margin-top: 10px;
}
.offcanvasmenu .sub-offcanvasmenu li a {
    display: block;
    text-decoration: none;
}
.offcanvasmenu .sub-offcanvasmenu li a::before {
    display: block;
    line-height: 1;
    text-align: center;
    color: #87140d;
    font-family: blogicon;
}
@media (min-width: 768px) {
    .offcanvasmenu .sub-offcanvasmenu li a::before {
        display: none;
    }
}
/* ハンバーガーメニューをタップで body 要素にクラス offcanvas が追加される */
body.offcanvas {
    overflow: hidden;
}
.offcanvas .offcanvasmenu {
    right: 0;
}
.offcanvas .offcanvastoggle::before {
    -webkit-transform: rotate(45deg);
            transform: rotate(45deg);
    -webkit-box-shadow: none;
            box-shadow: none;
}
.offcanvas .offcanvastoggle::after {
    -webkit-box-shadow: none;
            box-shadow: none;
    -webkit-transform: rotate(-45deg);
            transform: rotate(-45deg);
}
/* 中身を移動しているので抜け殻非表示 */
.hatena-module-category {
    display: none;
}
  • offcanvas menu は、これもいろいろ方法はありますが、今回は、position:absolute; を指定して、デバイス幅分右にずらして非表示にし、ハンバーガーメニューをタップで右からスライドさせています。
  • デスクトップでは、検索、またはカテゴリーをマウスオーバーまたはタップしますと、検索窓、またはカテゴリーメニューを表示します。

かなり煩雑になってしまいました。