さぁ!検索しよう!

ECサイト等で使われているサムネイルをクリックするとメイン画像が切り替わる画像ビューアの作り方です。レスポンシブ対応です。

画像ビューアとは、Amazonの商品詳細ページにある以下のようなものです。

イメージ12465

DEMO

始めに完成したものを以下に挙げます。

こういったものを作ります。

画像ビューアの骨組みを作る

始めに以下のHTMLを書いて画像ビューアの骨組みを作ります。

HTML

<div class="overlay"></div>
<div class="modal-window"></div>
<div class="image-viewer">
    <div class="main-image">
    </div>
    <ul class="nav">
    </ul>
</div>

HTMLはオーバーレイ.overlay・モーダルウィンドウ.modal-window・画像ビューア全体.image-viewer・メイン画像.main-image・ナビゲーション.navで構成されています。それから後でJavaScriptで.modal-window.main-image.nav内にそれぞれ要素を生成して挿入するため今は空にしています。

CSS(SCSS)で画像ビューアの見た目を作る

次に以下のCSSでそれぞれの要素の配置等、画像ビューアの見た目を作ります。併せてレスポンシブ対応も行います。

CSS(SCSS)

img {
    height: auto;
    width: 100%;
}

.image-viewer {
    background: #dfdfdf;
    margin: 0 auto;
    width: 40%;
    @media screen and (max-width: 768px) {
        width: 100%;
    }
}

.main-image {
    background: #777;
    padding-bottom: 56.25%;
    margin-bottom: 10px;
    overflow: hidden;
    position: relative;
    > img {
        //opacity: 0;
        position: absolute;
        top: 0;
        left: 0;
        transition: 0.5s;
        //visibility: hidden;
        /*
    &.active {
      opacity: 1;
      visibility: visible;
      z-index: 100;
    }
    */
    }
}

.nav {
    font-size: 0;
    list-style: none;
    margin: 0;
    padding: 0;
    &-item {
        background: #ccc;
        display: inline-block;
        overflow: hidden;
        position: relative;
        padding-bottom: 14.0625%;
        width: 25%;
        /*
    @media screen and (max-width: 768px) {
      padding-bottom: 28.125%;
      width: 50%;
    }
    */
        > img {
            position: absolute;
            top: 0;
            left: 0;
        }
    }
}

.overlay {
    background: #000;
    height: 100%;
    opacity: 0;
    position: fixed;
    top: 0;
    left: 0;
    transition: 0.5s;
    -webkit-visibility: hidden;
    visibility: hidden;
    width: 100%;
    z-index: 1000;
    &.active {
        opacity: 0.6;
        visibility: visible;
    }
}

.modal-window {
    opacity: 0;
    position: fixed;
    top: 50%;
    left: 50%;
    transition: 0.5s;
    transform: translate(-50%, -50%);
    visibility: hidden;
    z-index: 2000;
    &.active {
        opacity: 1;
        visibility: visible;
    }
}

レスポンシブ対応

始めにimgのスタイルを以下のようにしてフルードイメージにします。

img {
    height: auto;
    width: 100%;
}

続けてメディアクエリを使って.image-viewerをウィンドウの幅が768px以下では画面いっぱいに表示させ、それより大きければ40%にします。

.image-viewer {
    width: 40%;
    @media screen and (max-width: 768px) {
        width: 100%;
    }
}

最後に.main-image.nav-itemとそれらの直近の子要素であるimgを以下のようにしてウィンドウ幅の変化に応じて常に16:9の比率を維持したまま伸縮させるようにします。

.main-imageとそのimg

.main-image {
    padding-bottom: 56.25%;
    overflow: hidden;
    position: relative;
    > img {
        position: absolute;
        top: 0;
        left: 0;
    }
}

.nav-itemとそのimg

.nav {
    &-item {
        display: inline-block;
        overflow: hidden;
        position: relative;
        padding-bottom: 14.0625%;
        width: 25%;
        > img {
            position: absolute;
            top: 0;
            left: 0;
        }
    }
}

http://gakublog.com/archives/34

オーバーレイとモーダルウィンドウの初期状態

オーバーレイとモーダルウィンドウはJavaScriptで表示させるため、初めは以下のようにして非表示にします。

.overlay

.overlay {
    opacity: 0;
    visibility: hidden;
}

.modal-window

.modal-window {
    opacity: 0;
    visibility: hidden;
}

次にJavaScriptでオーバーレイとモーダルウィンドウを表示するために必要な.activeクラスを作成します。

.overlay

.overlay {
    &.active {
        opacity: 0.6;
        visibility: visible;
    }
}

.modal-window

.modal-window {
    &.active {
        opacity: 1;
        visibility: visible;
    }
}

モーダルウィンドウは完全に表示しますが、オーバーレイはopacityを0.6にして半透明で表示します。

JavaScriptで画像ビューアに機能を実装する

最後に以下のJavaScriptを書いてHTMLとCSSで作った画像ビューアに機能を実装します。

JavaScript

(() => {
    //画像に関するクラス
    class Asset {
        constructor() {
            this.urls = [
                "https://tsukulog.net/wp-content/uploads/2014/09/https-www.pakutaso.com-assets_c-2015-05-PED_narandaapple2-thumb-1000xauto-13287.jpg",
                "https://tsukulog.net/wp-content/uploads/2015/07/IMG_44521.jpg",
                "https://tsukulog.net/wp-content/uploads/2015/11/bsHIR96_boxneko.jpg",
                "https://tsukulog.net/wp-content/uploads/2016/04/shared-img-thumb-SHIO16032459IMG_6994_TP_V.jpg"
            ];

            this._images = [];
        }

        get images() {
            return this._images;
        }

        //初期化するメソッド
        init() {
            this.createImage();
        }

        //画像を生成するメソッド
        createImage() {
            for (let i = 0; i < this.urls.length; i++) {
                const image = new Image();
                image.src = this.urls[i];
                this._images[i] = image;
            }
        }
    }

    //ナビゲーションに関するクラス
    class Nav {
        constructor() {
            this.elem = document.querySelector(".nav");
            this.navItems = null;
        }

        //ナビゲーションをクリックできる状態にするメソッド
        setupListener(mainImage) {
            this.navItems.forEach((navItem, index) =>
                navItem.addEventListener("click", e => {
                    e.preventDefault();

                    //クリックされたナビの画像
                    const target = e.target;

                    //複製
                    const clone = target.cloneNode(true);

                    //表示されている画像を削除
                    mainImage.removeImage();

                    //複製した画像を挿入
                    mainImage.addImage(clone);
                })
            );
        }

        //ナビゲーションにサムネイルを挿入するメソッド
        insertThumbnail(images) {
            //.navに画像を挿入(画像が親要素を持たなければouterHTMLはエラーを発生する)
            images.forEach(image => this.elem.appendChild(image));

            //画像を.nav-itemで包む
            for (let i = 0; i < images.length; i++) {
                images[i].outerHTML = `<li class="nav-item">${images[i].outerHTML}</li>`;
            }

            this.navItems = document.querySelectorAll(".nav-item");
            //this.navItems.forEach(navItem => this.elem.appendChild(navItem));
        }
    }

    //メイン画像に関するクラス
    class MainImage {
        constructor() {
            this.elem = document.querySelector(".main-image");
        }

        //初期化するメソッド
        init(nav) {
            //先頭のサムネイル画像を複製
            const clone = nav.navItems[0].firstChild.cloneNode(true);

            //複製したサムネイル画像を表示
            this.addImage(clone);
        }

        //メイン画像をクリックできる状態にするメソッド
        setupListener(modalWindow, overlay) {
            this.elem.addEventListener("click", e => {
                e.preventDefault();
                const target = e.target;
                //console.log(target);

                //メイン画像を複製
                const clone = target.cloneNode(true);

                //それまで挿入されていたモーダルウィンドウの画像を削除
                modalWindow.removeImage();

                //複製したメイン画像をモーダルウィンドウに挿入
                modalWindow.addImage(clone);

                //オーバーレイを表示
                overlay.show();
            });
        }

        //.main-imageにメイン画像を挿入するメソッド
        addImage(image) {
            this.elem.appendChild(image);
            //image.classList.add("active");
        }

        //.main-imageからメイン画像を削除するメソッド
        removeImage() {
            this.elem.removeChild(this.elem.firstChild);
            //this.elem.firstChild.classList.remove("active");
        }
    }

    //モーダルウィンドウに関するクラス
    class ModalWindow {
        constructor() {
            this.elem = document.querySelector(".modal-window");
        }

        //モーダルウィンドウを表示するメソッド
        show() {
            this.elem.classList.add("active");
        }

        //モーダルウィンドウを非表示にするメソッド
        hide() {
            this.elem.classList.remove("active");
        }

        //.modal-windowに画像を挿入するメソッド
        addImage(image) {
            this.elem.appendChild(image);
            this.elem.classList.add("active");
        }

        //.modal-windowから画像を削除するメソッド
        removeImage() {
            //モーダルウィンドウ内に画像があれば(初回は無いためそのまま書くとエラーが発生する)
            if (this.elem.firstChild) {
                this.elem.removeChild(this.elem.firstChild);
                this.elem.classList.remove("active");
            }
        }
    }

    //オーバーレイに関するクラス
    class Overlay {
        constructor() {
            this.elem = document.querySelector(".overlay");
        }

        //オーバーレイをクリックできる状態にするメソッド
        setupListener(modalWindow) {
            this.elem.addEventListener("click", e => {
                e.preventDefault();

                //オーバーレイを非表示
                this.hide();

                //モーダルウィンドウを非表示
                modalWindow.hide();

                //モーダルウィンドウ内の画像を削除
                modalWindow.removeImage(modalWindow.elem.firstChild);
            });
        }

        //オーバーレイを表示するメソッド
        show() {
            this.elem.classList.add("active");
        }

        //オーバーレイを非表示にするメソッド
        hide() {
            this.elem.classList.remove("active");
        }
    }

    //インスタンス化
    const asset = new Asset();
    const nav = new Nav();
    const mainImage = new MainImage();
    const modalWindow = new ModalWindow();
    const overlay = new Overlay();

    //全体を初期化する関数
    function init() {
        asset.init();
        nav.insertThumbnail(asset.images);
        mainImage.init(nav);
        setupListeners();
    }

    //画像ビューアを操作できる状態にする関数
    function setupListeners() {
        nav.setupListener(mainImage);
        mainImage.setupListener(modalWindow, overlay);
        overlay.setupListener(modalWindow);
    }

    //起動
    init();
})();

事前準備

機能を実装する前に以下の準備をする必要があります。

  • 画像を生成する。
  • ナビゲーション内にサムネイルを挿入する。
  • メインパネル内にメイン画像を挿入する。

画像を生成する

AssetクラスのコンストラクタでURLを格納した配列urlsを定義します。今回は4枚の画像を生成するため4つのURLを格納しています。

this.urls = [
    "https://tsukulog.net/wp-content/uploads/2014/09/https-www.pakutaso.com-assets_c-2015-05-PED_narandaapple2-thumb-1000xauto-13287.jpg",
        "https://tsukulog.net/wp-content/uploads/2015/07/IMG_44521.jpg",
        "https://tsukulog.net/wp-content/uploads/2015/11/bsHIR96_boxneko.jpg",
        "https://tsukulog.net/wp-content/uploads/2016/04/shared-img-thumb-SHIO16032459IMG_6994_TP_V.jpg"
];

そしてcreateImageメソッドで格納されているURLの数だけ画像を生成します。

//画像を生成するメソッド
createImage() {
    for (let i = 0; i < this.urls.length; i++) {
        const image = new Image();
        image.src = this.urls[i];
        this._images[i] = image;
    }
}

ナビゲーション内にサムネイルを挿入する

NavクラスのinsertThumbnailメソッドでナビゲーション内にサムネイルを挿入します。

//ナビゲーションにサムネイルを挿入するメソッド
insertThumbnail(images) {
    //.navに画像を挿入(画像が親要素を持たなければouterHTMLはエラーを発生する)
    images.forEach(image => this.elem.appendChild(image));

    //画像を.nav-itemで包む
    for (let i = 0; i < images.length; i++) {
        images[i].outerHTML = `<li class="nav-item">${images[i].outerHTML}</li>`;
    }

    this.navItems = document.querySelectorAll(".nav-item");
    //this.navItems.forEach(navItem => this.elem.appendChild(navItem));
}

サムネイルを挿入するまでの過程は次の通りです。

  1. forEachメソッドでimages内の画像を一つずつ取り出して.nav内に挿入します。
  2. images内の画像をfor文とouterHTMLで一つずつ.nav-item要素で包みます。

メインパネル内にメイン画像を挿入する

メインパネルにはクリックされたサムネイルの画像が挿入されますが、初期状態では先頭のサムネイルの画像を表示させます。これをMainImageクラスのinitメソッドで実現します。

//初期化するメソッド
init(nav) {
    //先頭のサムネイル画像を複製
    const clone = nav.navItems[0].firstChild.cloneNode(true);

    //複製したサムネイル画像を表示
    this.addImage(clone);
}

nav.navItems[0].firstChild.cloneNode(true);で先頭の.nav-item内の画像を複製し、複製した画像cloneaddImageメソッドに渡して.main-image内に挿入しています。

addImageメソッド

//.main-imageにメイン画像を挿入するメソッド
addImage(image) {
    this.elem.appendChild(image);
}

実装する機能

画像ビューアに実装する機能は以下の通りです。

  • クリックしたサムネイルと同じ画像をメインパネルに表示する
  • メイン画像をクリックするとオーバーレイとモーダルウィンドウを表示する。
  • オーバーレイをクリックするとオーバーレイとモーダルウィンドウを非表示にする。

クリックしたサムネイルと同じ画像をメインパネルに表示する

サムネイルをクリックするとクリックしたサムネイルと同じ画像をメイン画像として.main-image内に挿入します。この処理はNavクラスのsetupListenerメソッドで実現します。

//ナビゲーションをクリックできる状態にするメソッド
setupListener(mainImage) {
    this.navItems.forEach((navItem, index) =>                            
            navItem.addEventListener("click", e => {
            e.preventDefault();

            //クリックされたナビの画像
            const target = e.target;

            //複製
            const clone = target.cloneNode(true);

            //表示されている画像を削除
            mainImage.removeImage();

            //複製した画像を挿入
            mainImage.addImage(clone);
        })
    );
}

仕組みは以下の通りです。

  1. クリックされたサムネイル画像targetを複製する。
  2. 一旦.main-image内の画像を削除
  3. mainImageオブジェクトのaddImageメソッドで.main-image内に1.で複製した画像cloneを挿入する。

メイン画像をクリックするとオーバーレイとモーダルウィンドウを表示する

メイン画像をクリックするとMainImageクラスのsetupListenerメソッドを実行してオーバーレイとモーダルウィンドウをフェードイン表示するようにします。

//メイン画像をクリックできる状態にするメソッド
setupListener(modalWindow, overlay) {
    this.elem.addEventListener("click", e => {
        e.preventDefault();
        const target = e.target;

        //メイン画像を複製
        const clone = target.cloneNode(true);

        //それまで挿入されていたモーダルウィンドウの画像を削除
        modalWindow.removeImage();

        //複製したメイン画像をモーダルウィンドウに挿入
        modalWindow.addImage(clone);

        //オーバーレイを表示
        overlay.show();
    });
}

仕組みは以下の通りです。

  1. クリックしたメイン画像targetcloneNodeで複製する。
  2. modalWindowremoveImageメソッドで直前まで表示されていたモーダルウィンドウの画像を削除する。
  3. modalWindowaddImageメソッドでモーダルウィンドウに1.で複製した画像cloneを挿入する。
  4. overlayshowメソッドでオーバーレイを表示する。

オーバーレイをクリックするとオーバーレイとモーダルウィンドウを非表示にする

オーバーレイをクリックするとOverlayクラスのsetupListenerメソッドでオーバーレイ自身とモーダルウィンドウをフェードアウトで非表示にします。

//オーバーレイをクリックできる状態にするメソッド
setupListener(modalWindow) {
    this.elem.addEventListener("click", e => {
        e.preventDefault();

        //オーバーレイを非表示
        this.hide();

        //モーダルウィンドウを非表示
        modalWindow.hide();

        //モーダルウィンドウ内の画像を削除
        modalWindow.removeImage(modalWindow.elem.firstChild);
    });
}

setupListenerメソッドでは始めにOverlayクラスのhideメソッドでオーバーレイを非表示にします。

//オーバーレイを非表示にするメソッド
hide() {
    this.elem.classList.remove("active");
}

次にModalWindowクラスのhideメソッドでモーダルウィンドウを非表示にします。

//モーダルウィンドウを非表示にするメソッド
hide() {
    this.elem.classList.remove("active");
}

最後にModalWindowクラスのremoveImageメソッドでモーダルウィンドウ内の画像を削除する。

//.modal-windowから画像を削除するメソッド
removeImage() {
    //モーダルウィンドウ内に画像があれば(初回は無いためそのまま書くとエラーが発生する)
    if (this.elem.firstChild) {
        this.elem.removeChild(this.elem.firstChild);
        this.elem.classList.remove("active");
    }
}

画像ビューアを完成させる

最後に以下のメイン処理で実装した機能を働かせて画像ビューアを完成させます。

インスタンス化

作成した全てのクラスをインスタンス化して実装した機能を使えるようにします。

//インスタンス化
const asset = new Asset();
const nav = new Nav();
const mainImage = new MainImage();
const modalWindow = new ModalWindow();
const overlay = new Overlay();

画像ビューアを操作できる状態にする関数を定義

setupListeners関数を定義してナビゲーション・メイン画像・オーバーレイをクリックできる状態にします。

//画像ビューアを操作できる状態にする関数
function setupListeners() {
    nav.setupListener(mainImage);
    mainImage.setupListener(modalWindow, overlay);
    overlay.setupListener(modalWindow);
}

やっていることはnavmainImageoverlayオブジェクトのsetupListenerメソッドを呼び出しているだけです。

全体を初期化する関数を定義

init関数を定義して画像ビューア全体を初期化します。

//全体を初期化する関数
function init() {
    asset.init();
    nav.insertThumbnail(asset.images);
    mainImage.init(nav);
    setupListeners();
}

init関数でやっていることは
1. assetオブジェクトのinitメソッドで画像を生成する。
2. navオブジェクトのinsertThumbnailメソッドで.nav内にサムネイルを挿入する。
3. mainImageオブジェクトのinitメソッドで.main-image内にメイン画像を挿入する。
4. setupListeners関数で画像ビューアを操作できる状態にする。
です。

起動する

init関数を実行して画像ビューアを起動します。

//起動
init();

さいごに

以上でレスポンシブな画像ビューアの作り方を終わります。