【第8回】スティッキーヘッダーを作ってみよう【jQuery講座】

jQuery講座

本シリーズは、jQueryの書き方を勉強し、その結果をまとめたものになる。

今回も引き続き、具体的な機能の実装だ。

画面をスクロールしてもついてくるヘッダー、スティッキーヘッダーを実装してみよう。

前回と同様に、レベルを2段階に分けて実装を行う

初級編は、シンプルに元々表示されているものを画面についてくるように。

上級編は、画面追従時に見た目を少し変えて、さらに見た目には分からない細工も少し入れてみる。

前回同様、まずは初級編をやってみて、余裕があれば上級編にもチャレンジしてみよう。

スポンサーリンク

前回の復習

前回は、スライドショーを初級編・上級編の二つ実装してみた。

新しいメソッドや関数もいくつか出てきているが、それらが分かってしまえば難しくなかったはず。

今回も使うものはあるので、しっかり見直しておきたい。

以下がその記事だ。

スティッキーヘッダー初級編

では、初級編から見ていこう。

冒頭にも書いた通り、初級編では最初は定位置にあるが、画面をスクロールしていくと上部にくっついてくるようなものを実装していく。

非常にシンプルではあるが、上級編も共通でcssの知識も多少必要になってくる。

そこも併せて解説していこう。

サンプルページ

先にサンプルページの紹介を。

リンクはこちら、ディレクトリ構造もいつも通りで以下。

  • jquery
    • course-08_1.html
    • js
      • jquery-08_1.js
    • css
      • style-08_1.css

ページを開いてもらうと、タイトルのすぐ下に水色背景のメニューが出てきており、その下にページの中身がある。

中身に書いてある通りで、スクロールしてメニューが画面外に行こうとすると、隠れるのではなくページの上部でくっついてくる

また、画面下部でページ更新してもそこに表示されていることを確認しておいてほしい。

ソースは以下の通りで、やはり今回も短め。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>jQuery講座第8回サンプルページ初級編</title>

        <!-- jQuery読み込み -->
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script type="text/javascript" src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>

        <!-- 各回ごとのJavaScriptファイル読み込み -->
        <script type="text/javascript" src="./js/jquery-08_1.js"></script>

        <!-- 必要に応じてcssファイル読み込み -->
        <link rel="stylesheet" type="text/css" href="./css/style-08_1.css">
    </head>
    <body>
        <!-- ヘッダー -->
        <div class="header">
            <h1 id="page-title">jQuery講座第8回サンプルページ初級編</h1>
        </div>

        <!-- スティッキーヘッダー -->
        <div class="header-menu">
            <ul>
                <li>メニュー1</li>
                <li>メニュー2</li>
                <li>メニュー3</li>
                <li>メニュー4</li>
                <li>メニュー5</li>
            </ul>
        </div>

        <!-- メインコンテンツ -->
        <div class="main-contents">

            <!-- 改行でページの長さを稼ぐ -->
            <div class="contents">
                <p>今回は中身はないよ</p>
                <p>スクロールしてもついてくることを確認してね</p>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <p>ページ最下部</p>
            </div>
        </div>
    </body>
</html>
$(function(){
    /* --- 変数定義パート --- */

    /* ページ全体を保持する変数 */
    var $window = $(window);

    /* スティッキーヘッダー部分を保持する変数 */
    var $stickyHeader = $(".header-menu");

    /* ヘッダーのデフォルトの位置を保持する変数 */
    var defaultHeaderTop = $stickyHeader.offset().top;


    /* --- 初期設定パート --- */

    /* スクロールイベントを発生させる */
    $window.trigger("scroll");

    
    /* --- イベント登録パート --- */

    /* 画面のスクロールを監視する */
    $window.on("scroll", toggleSticky);    
    

    /* --- 関数定義パート --- */

    /* 現在のスクロール位置を取得し、クラスの付け外しを行う */
    function toggleSticky(){
        if($window.scrollTop() > defaultHeaderTop){
            $stickyHeader.addClass("sticky");
        }else{
            $stickyHeader.removeClass("sticky");
        }
    }

    /* オマケ:メニューをマウスオーバーで変化させる */
    var durationMenuChange = 300;
    $stickyHeader
        .find("ul li")
        .on({
            "mouseover": function(){
                $(this)
                    .stop(true)
                    .animate(
                        {
                            "color": "white",
                            "background-color": "blue"
                        },
                        durationMenuChange
                    );
            },
            "mouseout": function(){
                $(this)
                    .stop(true)
                    .animate(
                        {
                            "color": "black",
                            "background-color": "transparent"
                        },
                        durationMenuChange
                    );
            }
        });
});
/* ページ全体の設定 */
body {
    margin: 0;
    font-family: "Avenir Next";
}

/* ページタイトルエリアの設定 */
.header {
    padding-top: 1rem;
    padding-bottom: 1rem;
    background-color: black;
    color: white;
    text-align: center;
}

/* スティッキーヘッダー部分、処理に関わらない設定 */
.header-menu {
    width: 100%;
    background-color: lightskyblue;
    font-size: 1.2rem;
    text-align: center;
}
.header-menu ul {
    margin: 0;
    padding: 0;
}
.header-menu ul li {
    display: inline-block;
    list-style: none;
    width: 15%;
    min-width: 80px;
    margin: 0;
    padding: 1rem 0;
}

/* コンテンツ領域全体の設定 */
.main-contents {
    width: 50%;
    margin: 5rem auto 0;
}

/* スティッキーヘッダー部分、処理用の設定 */
/* スクロールしていない時の状態 */
.header-menu:not(.sticky) {
    position: absolute;
}
/* スクロールしている時の状態 */
.header-menu.sticky {
    position: fixed;
    top: 0;
}

では、HTMLとcssの設定から見ていこう。

HTMLでは、シンプルにスティッキーヘッダーにする部分をheader-menuというクラスをつけている

処理に関する部分はこれだけで、中身は一応ulで各メニューを入れている。

cssも、今回は処理に関わる部分を切り出してみた。

一番下の2つが該当の記述で、一つ目はstickyクラスがついていない時にposition: absolute;を設定しているのみ

二つ目はそのstickyクラスがついている時で、ここでposition: fixed;を指定してまずは画面に対する配置に変更と、top: 0;で最上部に表示するようになっている。

実際の処理では、単にクラス名を付け外しするような処理をすればOKの状態、ということだ。

軽く補足で、最初にposition: absolute;を指定している部分。

これでtopleftなどの座標を指定していないので、領域自体は通常の場所に出てくる。

しかし、他の内容(今回はメインコンテンツ部分)にはその影響は与えず、何もしなければ重なって表示される。

このメインコンテンツはmarginで上幅を調整しており、見た目は重なっていない。

で、ここにposition: absolute;を設定していないとどうなるかだが、画面をスクロールした時に、メインコンテンツが少し上に移動してしまうのだ。

クラスをつけてposition: fixed;にすると、元あったメニュー部分がそこから無くなってしまい、上に詰めることでこんな現象が起こる。

それを防ぐために、先にabsoluteで他に影響を与えないようにしているのだ。

この他は単なる見た目の設定なので、基本的なcssの知識があれば問題ないはず。

処理の流れ

では、実際の処理を見ていこう。

前回と同じくパートに分けており、今回は5つ。

うち、スティッキーヘッダーに関わるのは上から4パートだ。

まず変数宣言パート、ここで3つの変数を宣言している。

一つ目はウィンドウを保持するjQueryオブジェクト、ここで特殊なセレクタを使っている。

今回は、ページのスクロールに対するイベントを監視したく、そのためにはウィンドウに対してonメソッドを適用する必要がある。

そこで、$(window)というセレクタを使ってそれを取得しているのだ。

二つ目はスティッキーヘッダー部分のjQueryオブジェクト、これは特に問題ないと思う。

三つ目、デフォルトのスティッキーヘッダー部分の位置を取得している。

この値を使って、後でスクロールがこれ以上されているかどうかで場合分けを行う。

ここで使っているoffsetメソッドは初出だ。

これは、その要素のドキュメント上の位置を取得するメソッドで、戻り値は二つの値を保持するオブジェクトになる。

ドキュメント上というのは、今表示されている部分ではなく、ページ全体を意味している。

使い方はシンプルに引数無しで呼び出し、戻り値のオブジェクトの中身は以下の通り。

{
    "top": ドキュメントの一番上からの距離,
    "left": ドキュメントの一番左からの距離
}

ということで、これを取得してから後ろに.topを繋げることで、上からの距離を求めることができる。

次に、初期設定パートを見ていく。

ここでは1つだけ、$window.trigger("scroll");と書かれている。

このtriggerメソッドイベントを任意に発生させるもので、引数に発生させるイベントタイプを指定する。

trigger(イベントタイプ)

そのため、ここではscrollというタイプのイベントを一度発生させていることになる。

なぜこれをするのかは、後で解説することにしよう。

第三ブロックは、イベント処理の登録

先ほども少し触れたが、今回監視したい画面スクロールのイベントは、ウィンドウに対して登録する必要がある

そのため、上で定義した$windowに登録するという記述になっているのだ。

で、初期設定パートでも出してしまったが、画面スクロールが発生した時のイベントタイプはscrollだ。

第四ブロックの関数定義パート、イベントハンドラで使っているtoggleSticky関数を見ていこう。

/* 現在のスクロール位置を取得し、クラスの付け外しを行う */
function toggleSticky(){
    if($window.scrollTop() > defaultHeaderTop){
        $stickyHeader.addClass("sticky");
    }else{
        $stickyHeader.removeClass("sticky");
    }
}

まず、ウィンドウのjQueryオブジェクトから、scrollTopメソッドを使って現在どの程度スクロールされているかを取得する。

scrollTopメソッドスクロールが一番上にある状態を0とし、現在表示されている位置の上端との差を求めるメソッドだ。

これが、スティッキーヘッダー部分のデフォルトの位置よりも大きければ…言い換えると、スティッキーヘッダーのデフォルト位置の上が画面外に行っている状態であれば、クラスにstickyを追加する。

cssの説明で述べた通り、これで画面についてくる状態になる。

逆に、それよりも上が表示されているなら、クラスstickyを取り除く。

なお、このaddClassメソッドは、元々そのクラスが無ければ追加する、という動作をする。

そのため、連続で同じクラスを追加しようとしても何も起こらないだけなので、その部分の場合分けは不要だ。

removeClassメソッドも同様、そのクラスがあれば削除する動作となっている。

以上で、画面についてくるスティッキーヘッダーの完成だ。

…最後、オマケパートがあるが、これはメニューの見た目をそれっぽくするだけのものだ。

スティッキーヘッダーの動作には関係ないので、解説は省略させてもらう。

前回までの内容で十分理解できるはずなので、分からなければ復習しておきたい。

さて、初期設定パートで、最初に一度イベントを発生させている理由について見てみよう。

これは、ページの途中で画面を更新した際の挙動を調整するためのものだ。

もしこれがないと、例えばページの一番下で更新した時、スクロールしなければ現在位置のチェックが行われない。

つまり、その時はスティッキーヘッダーが表示されなくなってしまい、スクロールして初めて表示されるようになってしまう。

それを防ぐため、わざと一回イベントを発生させ、元々スクロールされている状態であればスティッキーヘッダーが表示されるようにしている

ちなみにだが、今回の場合はここでイベントを発生させる代わりにtoggleSticky関数を直接実行しても処理的には何も問題ない。

以上で、初級編の完成。

今回やった、あらかじめcssで幾つかのクラスのスタイルを決めておき、jQueryではクラス名の変更のみを行う方法はよく使われるようだ。

このメリットは、スタイルはcss側で、処理はjQuery側で切り分けることが可能となり、メンテナンスがしやすくなるという点。

知っておくと非常に便利なので、是非使いこなせるようにしておこう。

スティッキーヘッダー上級編

さて、上級編と書いたが、実際やることはあまり変わらない。

変わるのは以下二つ。

  • スクロールでついてくるときに見た目を変える
  • イベント発生の頻度を抑える

一つ目は見た目に分かりやすいが、二つ目は見た目にはあまり影響を与えず、ページの動作を軽くするためのもの。

なぜそれをするかは、処理解説のところで触れよう。

サンプルページ

では、こちらもサンプルページから。

リンクはこちら、ディレクトリ構造はやはり名前以外は同じになっている。

  • jquery
    • course-08_2.html
    • js
      • jquery-08_2.js
    • css
      • style-08_2.css

さて、サンプルを開いて、画面を下にスクロールしてみてほしい。

すると、やはりスクロールについてくるのは同じだが、スクロールしている状態だと少し見た目が小さくなっていると思う。

こんな感じで、変化させられるというのが一つ目の変更点だ。

二つ目の変更点は…ちょっと見た目では分からないと思う。

が、この見た目には分からない、というのが重要だったりする。

では、サンプルソースを。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>jQuery講座第8回サンプルページ上級編</title>

        <!-- jQuery読み込み -->
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script type="text/javascript" src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>

        <!-- jQuery throttle / debounceプラグイン読み込み -->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js"></script>

        <!-- 各回ごとのJavaScriptファイル読み込み -->
        <script type="text/javascript" src="./js/jquery-08_2.js"></script>

        <!-- 必要に応じてcssファイル読み込み -->
        <link rel="stylesheet" type="text/css" href="./css/style-08_2.css">
    </head>
    <body>
        <!-- ヘッダー -->
        <div class="header">
            <h1 id="page-title">jQuery講座第8回サンプルページ上級編</h1>
        </div>

        <!-- スティッキーヘッダー -->
        <div class="header-menu">
            <ul>
                <li>メニュー1</li>
                <li>メニュー2</li>
                <li>メニュー3</li>
                <li>メニュー4</li>
                <li>メニュー5</li>
            </ul>
        </div>

        <!-- メインコンテンツ -->
        <div class="main-contents">

            <!-- 改行でページの長さを稼ぐ -->
            <div class="contents">
                <p>今回は中身はないよ</p>
                <p>スクロールしてもついてくることを確認してね</p>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <br><br><br><br><br><br><br><br><br><br>
                <p>ページ最下部</p>
            </div>
        </div>
    </body>
</html>
$(function(){
    /* --- 変数定義パート --- */

    /* ページ全体を保持する変数 */
    var $window = $(window);

    /* スティッキーヘッダー部分を保持する変数 */
    var $stickyHeader = $(".header-menu");

    /* ヘッダーのデフォルトの位置を保持する変数 */
    var defaultHeaderTop = $stickyHeader.offset().top;


    /* --- 初期設定パート --- */

    /* スクロールイベントを発生させる */
    $window.trigger("scroll");

    
    /* --- イベント登録パート --- */

    /* 画面のスクロールを監視する */
    $window.on("scroll", $.throttle(1000 / 15, toggleSticky));

    /* --- 関数定義パート --- */

    /* 現在のスクロール位置を取得し、クラスの付け外しを行う */
    function toggleSticky(){
        if($window.scrollTop() > defaultHeaderTop){
            $stickyHeader.addClass("sticky");
        }else{
            $stickyHeader.removeClass("sticky");
        }
    }

    /* オマケ:メニューをマウスオーバーで変化させる */
    var durationMenuChange = 300;
    $stickyHeader
        .find("ul li")
        .on({
            "mouseover": function(){
                $(this)
                    .stop(true)
                    .animate(
                        {
                            "color": "white",
                            "background-color": "blue"
                        },
                        durationMenuChange
                    );
            },
            "mouseout": function(){
                $(this)
                    .stop(true)
                    .animate(
                        {
                            "color": "black",
                            "background-color": "transparent"
                        },
                        durationMenuChange
                    );
            }
        });
});
/* ページ全体の設定 */
body {
    margin: 0;
    font-family: "Avenir Next";
}

/* ページタイトルエリアの設定 */
.header {
    padding-top: 1rem;
    padding-bottom: 1rem;
    background-color: black;
    color: white;
    text-align: center;
}

/* スティッキーヘッダー部分、処理に関わらない設定 */
.header-menu {
    width: 100%;
    background-color: lightskyblue;
    text-align: center;
}
.header-menu ul {
    margin: 0;
    padding: 0;
}
.header-menu ul li {
    display: inline-block;
    list-style: none;
    min-width: 80px;
    margin: 0;
}

/* コンテンツ領域全体の設定 */
.main-contents {
    width: 50%;
    margin: 5rem auto 0;
}

/* スティッキーヘッダー部分、処理用の設定 */
/* 文字サイズ変更にかける時間の設定 */
.header-menu {
    transition: font-size 0.25s;
}

/* 領域サイズ変更にかける時間の設定 */
.header-menu ul li {
    transition: width 0.25s, padding 0.25s;
}

/* スクロールしていない時の状態 */
.header-menu:not(.sticky) {
    font-size: 1.2rem;
    position: absolute;
}
.header-menu:not(.sticky) ul li {
    width: 15%;
    padding: 1rem 0;
}

/* スクロールしている時の状態 */
.header-menu.sticky {
    position: fixed;
    top: 0;
    font-size: 1rem;
}
.header-menu.sticky ul li {
    width: 10%;
    padding: 0.5rem;
}

さて、よく見てもらえれば分かると思うが、実は初級編からあまりHTMLやjQueryのソースは変化していない

初級編との差で中身を見ていこう。

まずHTMLでは、1つ外部プラグインを追加しているだけ。

jQuery throttle / debounceというプラグインで、これを使って処理を軽くする。

公式ページはこちら、使い方やこれでどうなるかは後で解説しよう。

cssについて、ここがちょっと色々細工している。

まず、transitionプロパティを幾つか指定し、これに指定したプロパティの値が変化した時にアニメーションで変化をするようにした。

そして、スクロールでついてきているとき、ついてきていない時それぞれの設定に、文字サイズや領域サイズの変化を追加

というわけで、こちらの見た目の変化はcss側で設定している。

こうすることで、jQuery側はやはりクラス名の変更だけで、アニメーションまで実装できてしまう

で、そのjQuery側、ここも変更は1か所のみ。

イベント登録時の記述を$window.on("scroll", $.throttle(1000 / 15, toggleSticky));としたのみだ。

その他の動作は、全て初級編と同じ。

…今更ながら、一緒にやってしまってよかったのでは感が否めない。

処理回数を減らすスロットリング処理

さて、このスロットリング処理というものを解説しよう。

今回監視しているscrollイベントなのだが、実はとんでもない回数イベントが発生し、イベントハンドラが実行されている

試しに初級編のイベントハンドラでコンソールに文字列を出力するようにしてみたところ、スピードにもよるが一回画面の最下部までスクロールするだけで約30回も呼ばれた

今回はまだ中の処理が少ないのでそこまで影響はないが、ここで大量の処理をする場合、パフォーマンスに影響が出てきてしまう

で、そこまで細かい単位で処理しても、人の目には見えないレベルになってしまうことも多い。

…それなら見た目に影響がない範囲でイベントを無視してもいいのではないだろうか

そんな時に、このスロットリングという処理を行う。

これは処理を間引くようなイメージで、不必要な処理を無視して、必要最低限だけ実行させるものだ。

上にもちらっと出したが、今回はjQuery throttle / debounceというプラグインを使って実現している。

というわけで、HTML側でこれを読み込む部分が追加で必要となる。

CDNは以下だ。

https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js

やはり実体はJavaScriptなので、scriptタグで読み込もう。

また、jQueryプラグインなので、jQuery本体の読み込みの後にすることも忘れずに。

これには大きく二つの処理が含まれており、その一つがこのスロットリングだ。

書き方としては、イベントハンドラで関数を指定するときに、以下の書き方に変える

$.throttle(間隔, 処理)

この間隔の部分には、ミリ秒単位での時間を指定する。

動作としては、まずそれまでイベントが発生していなければ、通常通り処理部分に書いた関数を実行する。

その後、間隔に指定した時間以内に再度イベントが発生しなければ通常と動き方は変わらない

もしその間に再度イベントが発生すると、まずその間隔の時間が経過するまで待つようになる。

で、経過したら待っていた処理が行われるようになるのだ。

この一つの間隔で複数のイベントが発生しても、最後に行われるのは一回だけ。

図にすると以下のようなイメージだ。

スロットル処理の概要

上の矢印のタイミングでイベントが発生、赤線が処理の実行タイミング、灰色のブロックが間隔の時間だ。

上の説明と照らし合わせて、どんな動きになるかを把握しておきたい。

さて、今回の書き方を改めて見てみよう。

/* 画面のスクロールを監視する */
$window.on("scroll", $.throttle(1000 / 15, toggleSticky));

これで、スクロールイベントがいくら発生しようと、1000分の15秒ごと…言い換えると、最大でも秒間15回までしか実行されなくなる、という感じになる。

これで処理を減らし、ページの表示を軽くすることが可能だ。

特に今回のようなscrollイベントは非常に多く発生するので、スクロール時のページ動作がもっさりしていたらこれを使うことを検討してみよう。

なお、どのくらいまでこの間隔を開けていいかだが…もちろん、それは状況によって異なる

一つ言えそうなのは、見た目に影響が出るほど処理を間引いてしまってはいけないので、繰り返し試してちょうどいいポイントを探るようにしよう。

おわりに

今回は、画面をスクロールしてもついてくるスティッキーヘッダーを実装した。

また、イベント処理を間引きするスロットリングも、外部プラグインではあるが使用する方法を解説した。

スロットリングについて、サンプルは非常にシンプルなのでまだ恩恵は薄いが、いずれ必要な場面は来るはずなので頭の片隅に入れておいて欲しい。

さて、次回なのだが…ちょっと未定とさせてもらいたい。

というのも、そろそろ単体の要素ではなく、これまでの複合で一つの大きなページを作ってみたい。

しかし、その中でやはり個別で解説した方がいい内容が出てくる可能性もある。

一旦サンプルの作成を進め、その状況によって判断させてもらうことにしよう。

2021/2/26追記

次回の内容を更新した。

これまでの内容を使い、本格的なページっぽいものを作ってみた。

で、大まかな部分の解説はこれで以上、つまり次回を本シリーズの最終回にしよう。

ここまで理解できれば、あとは必要な情報を調べながら進められるくらいにはなっていると思う。

もちろん、補足等あれば随時追加はしていくつもりだ。

是非、色々カスタマイズしながら試してみてほしい。

コメント

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