グリッドレイアウトの使用とJavaScriptによる子要素の更新 ― スケッチブック②

はじめに

前回に引き続き、『スケッチブック』という画像をギャラリー表示する自作ページの実装について解説していきます。前回の記事では、主に非同期通信周りの処理について説明しました。↓

Fetch APIによる非同期通信でページを一部だけ更新 ― スケッチブック①

今回はその続きとして、CSSによるグリッドレイアウトと、その中身をJavaScriptで操作して更新する方法について解説します。

ソースコードはコチラです。↓

mitaka1962/sketchbook (GitHub)

CSSの全体像

先に今回解説するCSSのコード全体を示しておきます。

sketchbook_style.css
#sketchbook_grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    column-gap: 0.5em;
    row-gap: 0.8em;
    margin-top: 1em;
}

.sketchbook_month {
    grid-column: 1 / -1;
    margin-top: 1em;
}

.sketchbook_image_container {
    overflow: hidden;
    aspect-ratio: 4 / 3;
}

.sketchbook_image_container > img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    transition: all 0.3s ease-in-out;
    opacity: 1;
}

.sketchbook_image_container:hover > img {
    transform: scale(1.2);
    opacity: 0.7;
}

@media screen and (min-width: 576px) and (max-width: 768px) {
    #sketchbook_grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

@media screen and (max-width: 576px)    {
    #sketchbook_grid {
        grid-template-columns: 1fr;
    }
}

@media screen and (max-width: 460px) {
    .lum-lightbox-inner img {
        max-width: 160vw !important;  /* 左右の幅 */
        max-height: 85vh !important;  /* 上下に余白 */
    }
}

グリッドレイアウト

グリッドレイアウトを使った画像ギャラリーのページ

グリッドレイアウト(Grid Layout)はCSSで使用できるレイアウト手法の1つです。その名の通り、行と列で作られる格子状のグリッドをもとに子要素を整列・配置できます。各行、各列の大きさを指定することや、配置する要素の幅や高さをグリッド単位で指定することなどができるので、かなり柔軟な要素の配置が可能です。

複雑なレイアウトを実現する手段としては、似たようなものにFlexboxもあります。どちらも要素を自由に配置できますが、作りたいレイアウトの形によってそれぞれ向き不向きがあるようです(ただ、グリッドレイアウトのほうが簡潔に書ける印象です)。今までは何となくFlexboxの方を使っていたのですが(ヘッダーやサイドバーの配置といったこのサイト全体のレイアウト含め)、今回は画像ギャラリーとして画像をグリッド状に一覧表示するため、グリッドレイアウトを使用することにしました。

CSSファイル内のグリッドレイアウトに関する部分は以下の通りです。

sketchbook_style.css
#sketchbook_grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    column-gap: 0.5em;
    row-gap: 0.8em;
    margin-top: 1em;
}

.sketchbook_month {
    grid-column: 1 / -1;
    margin-top: 1em;
}

@media screen and (min-width: 576px) and (max-width: 768px) {
    #sketchbook_grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

@media screen and (max-width: 576px) {
    #sketchbook_grid {
        grid-template-columns: 1fr;
    }
}

グリッドレイアウトの使い方については、MDNの方でわかりやすくまとめられているので(ありがたい…)、正直そちらを読むほうが早いと思います(グリッドレイアウトの基本概念)。ここでは、ソースコードの簡単な注釈をつけるだけにとどめておきます。

sketchbook_gridがグリッドレイアウトを適用する要素(のid)です。グリッドレイアウトはdisplay: gridで設定できます。

列の大きさや数の指定はgrid-template-columnsで行います。今回は画像を4列に配置したいので、1frfrは新しい単位で、それぞれの列が占める大きさの比を表す)を4つ並べます。repeat()記法というものを使って簡略化していますが、以下のように書くのと同じです。

grid-template-columns: 1fr 1fr 1fr 1fr;

列間、行間の余白は、column-gaprow-gapで指定します。

今回は行の方の指定(grid-template-row)を行っていませんが、この場合、「暗黙的なグリッド」と言って、必要に応じて行が勝手に追加されて行きます。行の幅も中身の要素のサイズに合わせて勝手に変化します。

エクセルのセル結合のように複数の列をまたがる要素を配置したい場合、その子要素にgrid-columnを指定します。(始点)/(終点)で範囲を指定しますが、この時、グリッド線の番号で指定するということに注意です。(ブラウザの検証ツールで可視化できます↓)

グリッドラインの図示

今回は月表示ラベル(クラス名がsketchbook_month)をグリッド線1番目から-1番目(最後のグリッド線)まで範囲指定することで、1行すべてをラベルに割り当てています。

後半のメディアクエリが付いた部分は、グリッドレイアウトをレスポンシブ対応させるための記述です。画面の幅が小さくなるごとに、列の数を4→3→1に減らしています。これにより、画像の表示が小さくなりすぎるのを防ぐことができます。

個々の画像のスタイル設定

グリッドレイアウト内の個々の画像は、すべて縦横比3:4に統一され、マウスホバー時に拡大するアニメーションが付いています。これらの効果を実現するためのCSSが以下のコードです。

sketchbook_style.css
.sketchbook_image_container {
    overflow: hidden;
    aspect-ratio: 4 / 3;
}

.sketchbook_image_container > img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    transition: all 0.3s ease-in-out;
    opacity: 1;
}

.sketchbook_image_container:hover > img {
    transform: scale(1.2);
    opacity: 0.7;
}

拡大アニメーションをつけるために、sketchbook_image_containerクラスを持つdiv要素の外枠を作り、その中にimg要素を配置しています(これらのHTML要素は後述するJSで動的に生成します)。ホバー時のスタイルを:hoverで用意し、img要素にtransitionを設定することでアニメーションを簡単につけることができます(拡大した画像がはみ出ないように、外枠には忘れずoverflow: hiddenを記述します)。

ちなみに似たようなアニメーションを以前CSSで実装したことがあったので、その時の方法をそのまま使用しました。↓

CSSで画像に鏡面反射をつける方法(ホバーアニメーション付き)

画像の縦横比はうまい具合に調整するのが難しかったのですが、最終的に以下の指定で望み通りの結果を得ることができました。

width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;

まず、width: 100%img要素の幅をグリッドレイアウトの列幅に合わせ、その上で縦横比をaspect-ratioで指定することで、img要素の高さが決まります。このままだと中身の画像そのもののアスペクト比が変わってしまうので、object-fit: coverと設定することで中身の画像のアスペクト比を維持したまま拡大縮小させます(ちなみに、何も指定しない場合、object-fit: fillとなっています)。

object-fitの設定の比較

Luminousの使用

画像をクリックすると画面全体に画像を拡大表示する、というよくある機能を付けることにしたのですが、さすがに自作するのは手間がかかるので外部ライブラリを使用することにしました。

使用するのはLuminousというJSライブラリです。コチラで使い方などが詳しく解説されています。(→ライトボックス Luminous Lightbox の使い方 | Web Design Leaves

こういった機能を提供するライブラリとしては、Lightboxが有名なのですが、jQueryが必要とのことだったので、今回はjQuery不要で似たような機能が使えるLuminousを使用することにしました(巷で脱jQueryなどと言われているのを聞くので、あまり使わない方がいいかなと…)。

CDN経由で利用する場合、ページのHTMLに以下の記述を追加します。(LuminousをJS内で起動させる方法は、次章で説明します。)

<script src="https://cdn.jsdelivr.net/npm/luminous-lightbox@2.4.0/dist/luminous.min.js"></script>

ちなみに、横長の画像だとスマホの縦画面で表示した際に小さくなってしまうので、横幅が画面をはみ出すように、!importantを使ってCSSを上書きしています。これにより、大きい画像のまま横スクロールで全体を見れるようになります。(Luminousを起動させると、lum-lightbox-innerというクラスをもつdiv要素が自動生成されるので、その中のimg要素に対して上書きします)

sketchbook_style.css
@media screen and (max-width: 460px) {
    .lum-lightbox-inner img {
        max-width: 160vw !important;  /* 左右の幅 */
        max-height: 85vh !important;  /* 上下に余白 */
    }
}

画像の更新

最後にグリッドレイアウト内の画像をJSで動的に更新する方法について説明します。前回の記事で解説した通り、年表示の横の矢印ボタンを押すと、非同期通信によってサーバにリクエストが送られ、結果として次に表示する画像の情報が以下のようなJSON形式でJSに返されます。

[
  {
    "month": "10",
    "image_src": "/images/image-from-rawpixel-id-3283786-jpeg.jpg",
    "desc": "落ち葉"
  },
  {
    "month": "3",
    "image_src": "/images/image-from-rawpixel-id-3285646-jpeg.jpg",
    "desc": "海"
  },
  {
    "month": "3",
    "image_src": "/images/image-from-rawpixel-id-3305755-jpeg.jpg",
    "desc": "ゾウの親子"
  }
]

これをもとにJSでDOM要素を生成することで、画像の一覧表示を更新します。そのコードが以下になります(前回の記事で省略した関数です)。引数のimage_data_arrが、受け取ったJSONデータ(をJavaScriptオブジェクトに変換したもの)です。

sketchbook_fetch.js
function update_grid_layout(image_data_arr) {
    let current_month = 0;

    for (const image_data of image_data_arr) {
        // 月ラベルを追加
        if (current_month != image_data["month"]) {
            current_month = image_data["month"];
            const newMonthDiv = document.createElement("div");
            newMonthDiv.className = "sketchbook_month";
            newMonthDiv.textContent = current_month + "月";
            sketchbook_grid_elm.appendChild(newMonthDiv);
        }

        // Luminous.js用の<a>を用意
        const newLuminousA = document.createElement("a");
        newLuminousA.className = "luminous_gallery";
        newLuminousA.href = image_data["image_src"];

        // 画像を用意
        const newContainerDiv = document.createElement("div");
        newContainerDiv.className = "sketchbook_image_container";
        const newImg = document.createElement("img");
        newImg.src = image_data["image_src"];
        newImg.alt = image_data["desc"];

        // 子要素として追加
        newContainerDiv.appendChild(newImg);
        newLuminousA.appendChild(newContainerDiv);
        sketchbook_grid_elm.appendChild(newLuminousA);
    }

    // Luminous.jsの有効化(alt属性をキャプションに設定)
    new LuminousGallery(document.querySelectorAll(".luminous_gallery"), {}, {
        caption: (trigger) => {
            if (trigger.querySelector("img").hasAttribute("alt")) {
                return trigger.querySelector("img").alt;
            } else {
                return "";
            }
        }
    });
}

基本的には、必要な要素をdocument.createElement()で生成し、そのプロパティ(.classNameなど)を設定した上で、親要素にappendChild()で追加する、というのを繰り返すだけです。最終的に、生成した要素をグリッドレイアウトの要素であるsketchbook_grid_elm(あらかじめdocument.getElementById("sketchbook_grid")で取得済み)に子要素として追加します。

注意点としては、Luminousを使用するためにluminous_galleryクラス(※クラス名は任意)を持ったa要素を画像の親要素にしていることです。そのうえで、new LuminousGallery()(画像が単体のときはnew Luminous())を呼び出すことで、Luminousを有効化できます。

Luminous有効化のコードは入れ子にしたため複雑になってしまっていますが、指定している引数は次の3つだけです。

  • 第1引数:Luminousを適用する要素の集合を渡します。querySelectorAll()で、先ほど画像の親要素として生成したa要素をクラス名によって取得します。

  • 第2引数:ギャラリーに対する設定をオブジェクトとして渡します。今回は特に設定しないので空のオブジェクト{}を渡しています。

  • 第3引数:個々の画像に対する設定をオブジェクトとして渡します。今回は画像のキャプションを設定するために、captionにキャプション文字列を返す関数を指定しています。
    引数として受け取っているtriggerはLuminousを適用したa要素です。そこから子要素の画像を取得し、alt属性をキャプションとして返しています。

中身の要素の全削除

ちなみに、画面更新の際にはあらかじめグリッドレイアウト内のすべての要素を以下のコードで削除しています(前回の記事参照)。一括削除はできないようなので繰り返し処理で削除しています。

while (sketchbook_grid_elm.firstChild) {
    sketchbook_grid_elm.removeChild(sketchbook_grid_elm.firstChild);
}

おわりに

グリッドレイアウトはかなり使い道がありそうです。今後はFlexboxよりもこちらをメインで使っていこうかなと思います。