WebページでFetch中にローディング画面を表示する

Web開発

お疲れ様です。すぺきよです。

Webのシステムを実装していると非同期でサーバーにデータを取得することが多々あります。

たとえば、検索条件を指定してから検索ボタンをクリックすることで、検索結果の情報を取得したりするとかですね。

そんな時に、読み込み中に画面を固めてしまうのは格好悪いですし、だからと言ってそのままにして読み込み中に画面を操作されては想定外の不具合が出ることがあります。

そんな時に愛用していたのが、JQueryのプラグインであるBlockUIです。

ただ、このJQueryのBlockUIはここしばらくメンテナンスされていないようですし、個人的にも脱JQueryを進めています。(便利なんですけどねJQuery)

そこで、私がよく使っていたこのJQueryのプラグインであるBlockUIをブラウザ標準の書き方で、必要最低限の部分を実装しなおしてみました。

このBlockUIには多くの機能がありますが、今回は必要最低限のローディング画面の実装部分のみをしようと思います。

よろしければ参考にしてください。

では行ってみましょう。

この記事では

この記事で実装するものは以下のものを目標としています。

  • JQueryとBlockUIプラグインを使わない
  • 機能がたくさんあるBlockUIの全機能の実装を目標としない
  • 画面をブロックしている間はローディング画面の様な画面を表示すること
  • awaitとasyncを使って、UIをブロックできること
  • 画面全体をオーバーラップして、マウスやタッチ操作を受け付けない様にすること
  • キーボード操作によるボタンやリンクのクリックなども禁止すること

実装コード

CSS

以下のコードを「blockUI.css」として保存します。

.block_ui--overlay
{
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;
    width: 100vw;
    height:100vh;
    display: block;
    background: rgba(0,0,0,0.6);
}

.block_ui--spinner_wrapper
{
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.block_ui--spinner
{
    width: 80px;
    height: 80px;
    border: 4px #ddd solid;
    border-top: 4px #999 solid;
    border-radius: 50%;
    animation: block_ui--spinner_anime 0.8s infinite linear;
}

@keyframes block_ui--spinner_anime
{
    0% { transform: rotate(0deg); }
    100% { transform: rotate(359deg); }
}

JavaScript

以下のコードを「blockUI.js」として保存します

class blockUI
{
    static #getOverlayElement()
    {
        return document.querySelector(".block_ui--overlay");
    }
    
    static #existsOverlayElement()
    {
        return !!(blockUI.#getOverlayElement());
    }
    
    static #removeOverlayElementFromBodyIfExists()
    {
        if(!blockUI.#existsOverlayElement())
        {
            return;
        }
        
        const overlay_element = blockUI.#getOverlayElement();
        overlay_element.remove();
    }
    
    static #addOverlayElementToBodyIfNotExists()
    {
        if(blockUI.#existsOverlayElement())
        {
            return;
        }

        const overlay_wrapper = document.createElement("div");
        overlay_wrapper.innerHTML = `
        <div class="block_ui--overlay">
            <div class="block_ui--spinner_wrapper">
                <span class="block_ui--spinner"></span>
            </div>
        </div>`.replace(/\n */g, "");
        
        document.getElementsByTagName("body")[0].appendChild(overlay_wrapper.firstChild);
    }
    
    static #handler = (e) =>
    {
        e.preventDefault();
    }

    //キー操作による入力を無効化する
    static #blockKeyInteractive(is_block = true)
    {
        const event_key= ["mousedown","mouseup","keydown","keypress","keyup","touchstart","touchend","touchmove"];
        for(const one_event_name of event_key)
        {
            if(is_block)
            {
                document.addEventListener(one_event_name, blockUI.#handler);
            }
            else
            {
                document.removeEventListener(one_event_name, blockUI.#handler);
            }
        }
    }

    static showOverlay(on_finish_func)
    {
        blockUI.#addOverlayElementToBodyIfNotExists();
        blockUI.#blockKeyInteractive(true);
        const overlay_element = blockUI.#getOverlayElement();
        const animation = overlay_element.animate([{opacity:0}, {opacity:1}], 300);
        if(typeof(on_finish_func) === "function")
        {
            animation.onfinish = on_finish_func;
        }
    }

    static closeOverlay(on_finish_funcs)
    {
        if(!blockUI.#existsOverlayElement())
        {
            return;
        }
        
        const overlay_element = blockUI.#getOverlayElement();
        blockUI.#blockKeyInteractive(false);
        const animation = overlay_element.animate([{opacity:1}, {opacity:0}], 300);
        animation.onfinish = () =>
        {
            this.#removeOverlayElementFromBodyIfExists();
            if(typeof(on_finish_funcs) === "function")
            {
                on_finish_funcs();
            }
        };
    }
    
    static showOverlayAsync()
    {
        return new Promise(async function(resolve, _reject)
        {
            blockUI.showOverlay(function()
            {
                resolve();
            })
        });
    }

    static closeOverlayAsync()
    {
        return new Promise(async function(resolve, _reject)
        {
            blockUI.closeOverlay(function()
            {
                resolve();
            })
        });
    }
}

使用例

「BlockUI実装コード」のcssファイルとjsファイルと同じフォルダに以下のコードを「index.html」ファイルとして保存します。

<!DOCTYPE html>
<html>
<head>
    <link href="./blockUI.css" rel="stylesheet" />
    <script src="./blockUI.js"></script>
    
    <script src="./index.js"></script>
</head>
<body>
    <button  type="button" id="btn_blockUI">please wait</button>
</body>
</html>

次に、「index.html」と同じフォルダに以下のコードを「index.js」ファイルとして保存します。

window.addEventListener("load", () =>
{
    document.getElementById("btn_blockUI").addEventListener("click", btnBlockUI_onclick);
});

async function btnBlockUI_onclick()
{
    try
    {
        await blockUI.showOverlayAsync();

        //ここでFetchなどをする
        
        await blockUI.closeOverlayAsync();
    }
    finally
    {
        await blockUI.closeOverlayAsync();
    }
}

「index.html」ファイルをブラウザで開くと、「please wait」のボタンがあるのでクリックすると、ローディング画面が表示されてすぐ消えることが確認できると思います。

原理

ボタンをクリックすると、すぐに横幅100vw、高さ100vhの画面全体を覆う様なdiv要素を、JavaScript側で生成て、bodyに追加します。

このとき、このdiv要素のpositionをfixed、配置位置を上から0、左から0の位置とし、z-indexを9999で常に最前面に表示する様にしています。

このdiv要素が手前にあることで、クリックイベントについては全てこのdiv要素がイベントを受け取り、画面上の要素全てへのクリックを受け付けなくなります。

マウスのクリックはこれで回避できるのですが、これだけではキーボード操作によるボタンやInputの入力への操作を受け付けてしまいます。

そこで、documentオブジェクトに対して直接、キーボードイベントやマウス操作のイベントを受け取る様にし、このイベント内でpreventDefaultで無効化することで、イベントを受け付けなくしています。

こうすることで、F5ボタンやCtrl+Rのキーボード操作での画面更新すらもブラウザは受け付けなくなります。(ブラウザ上のリロードボタンでの画面更新は可能です。)

ローディング画面のカスタマイズ

上記、BlockUI.jsの32行目のHTMLが画面中央に表示される要素を構築しています。

この部分を自由に変更することでカスタマイズ可能です。

現在はクルクル回るローディング画面ですが、Gifアニメーションを表示させたり、メッセージを表示させたりなどを表示することも可能です。

まとめ

今回は、画面操作をブロックしながらローディング画面を簡単に表示することができるJavaScriptのクラスを実装してみました。

Webページでユーザーのアクションに対して、サーバーにデータをフェッチする時にローディング画面を表示させることでユーザーのストレスを緩和させることと、システムが動いていると安心させることができます。

みなさんのローディング処理を作るときの参考となれば幸いです。

コメント

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