Javascript

カウントダウンタイマーをタブを閉じたら終了(リロードは継続)処理させる方法【JavaScript】

上記記事より拝借

記事内に商品プロモーションを含む場合があります

生徒
生徒
カウントダウンタイマーをリロードしても、継続してカウントさせておきたい。でもタブを閉じたら終了にしたいんだけど、そんな処理できるのかなぁ

以上のような、お悩みの解決記事です。

カウントダウンタイマー実装の記事は多いのですが、タブを閉じたらカウント終了させる機能を持ったものの解説記事がなく、時間をかけながらようやく実装できたので、コードと実装ポイントを要約します。

やりたいこととしては、以下のような感じです。

実装方法

手っ取り早く、コピペしたいという方向けにコードは以下の通りです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap" rel="stylesheet">
    <title>Document</title>
    <style>
        .timer {
            font-weight: bold;
            text-align: center;
            line-height: 2;
            display: -ms-flexbox;
            display: flex;
            -ms-flex-pack: center;
            justify-content: center;
            -ms-flex-align: center;
            align-items: center;
        }
        .countdown_timer_area {
            display: flex;
            align-items: center;
        }
        .text {
            line-height: 1;
        }
        .num {
            font-size: 24px;
        }
        .num .hour {
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            width: 40px;
            display: inline-block;
            padding: 0 .2em;
        }
        .num .min {
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            width: 40px;
            display: inline-block;
            padding: 0 .2em;
        }
        .num .sec {
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            width: 40px;
            display: inline-block;
            padding: 0 .2em;
        }
        .num .milisec {
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            width: 40px;
            display: inline-block;
            padding: 0 .2em;
        }
        .btn {
            width: 480px;
            margin: 0 auto;
            text-align: center;
            background-image: linear-gradient(45deg, #709dff 0%, #91fdb7 100%);
            border-radius: 10px;
        }
        .btn.disabled {
            background-image: none;
            background-color: #d8d8d8;
        }
        .btn a {
            font-size: 20px;
            text-decoration: none;
            display: block;
            color: #fff;
            padding: 14px 0;
        }
        .btn.disabled a {
            color: #8f8f8f;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div class="timer">
        <div class="countdown_timer_area">
            <div class="text">終了<br>まで</div>
            <div class="num"><span class="hour js_time_reset">00</span>時間<span class="min js_time_reset">00</span>分<span class="sec js_time_reset">00</span>秒<span class="milisec js_time_reset">00</span></div>
        </div>
    </div>
    <div id="btn" class="btn">
        <a href="#">お申し込みはこちら</a>
    </div>
    <script>
        // カウントダウンタイマー
    let timer_id = ''
    let goal;

    // 2回目以降の訪問
    if(localStorage.getItem('timer') === 'start') {
        // 初回訪問時間の継続カウント
        goal = sessionStorage.getItem('goalTime')
    }
    // 初回訪問
    else if(localStorage.getItem('timer') !== 'start') {
        // 初回訪問時間
        let standardTime = new Date();
        goal = new Date(standardTime.getTime() + 10000 * 1000);
        // 初回訪問 or 2回目以降訪問か判別用
        localStorage.setItem('timer', 'start')
        // 24時間カウント用
        sessionStorage.setItem('goalTime', goal)
    }

    // タイマー計算関数
    function countdown(due) {
        const now = new Date();
        let rest;
        if(localStorage.getItem('timer') === 'start') {
            // sessionStorageより取得した値をString → Date型に変換
            rest = new Date(due).getTime() - now.getTime();
        } else if(localStorage.getItem('timer') !== 'start') {
            rest = due.getTime() - now.getTime();
        }
        const milisec = Math.floor(rest) % 60
        const sec = Math.floor(rest/1000) % 60;
        const min = Math.floor(rest/1000/60) % 60;
        const hours = Math.floor(rest/1000/60/60) % 24
        const count = [hours, min, sec, milisec];
        return count;
    }

    // タイマー表示関数
    function recalc() {
        const counter = countdown(goal);
        
        let hour = document.querySelector('.hour');
        hour.textContent = String(counter[0]).padStart(2, '0')

        let min = document.querySelector('.min');
        min.textContent = String(counter).padStart(2, '0')
        
        let sec = document.querySelector('.sec');
        sec.textContent = String(counter).padStart(2, '0')
        
        let milisec = document.querySelector('.milisec');
        milisec.textContent = String(counter).padStart(2, '0')

        refresh()

        // タイマー終了後の処理
        if(counter[0] <= 0 && counter <= 0 && counter <= 0 && counter <= 0) {
            clearTimeout(timer_id)
            
            const js_time_reset = document.querySelectorAll('.js_time_reset');
            for(let i = 0; i < js_time_reset.length; i++ ) {
                js_time_reset[i].textContent = '00'
            }

            const btn = document.getElementById('btn');
            btn.classList.add('disabled');
        }
    }
    function refresh() {
        timer_id = setTimeout(recalc, 10)
    }
    recalc()
    </script>
</body>
</html>

 

※時間は10000秒( ≠2時間47分)になっていますが、107行目の10000を変更すると、最大86399.9秒(≠23時間59分59分)まで変更可能です。

 

実装のポイント

 

sessionStorageの挙動が要件にマッチする

今回の実装のポイントとなるのは、リロードした時はカウントダウンを継続し、タブを閉じたら終了させる処理です。

最初は「リロード」と「タブを閉じる」のイベントで切り分ける予想をしていました。

しかし、調べてみた結果、どうやらsessionStorageがその要件を満たしていることが分かりました。

上記記事より拝借

 

そこで、以下のように記述しました。

// 2回目以降の訪問
if(localStorage.getItem('timer') === 'start') {
    // 初回訪問時間の継続カウント
    goal = sessionStorage.getItem('goalTime')
    console.log(goal)
}
// 初回訪問
else if(localStorage.getItem('timer') !== 'start') {
    // 初回訪問時間
    let standardTime = new Date();
    goal = new Date(standardTime.getTime() + 10 * 1000);
    // 初回訪問 or 2回目以降訪問か判別用
    localStorage.setItem('timer', 'start')
    // 24時間カウント用
    sessionStorage.setItem('goalTime', goal)
}

localStorageにtimer(キー), start(バリュー)が存在しない場合、sessionStorageにgoalTime(キー)、goal(バリュー・タイマー時間の変数)をセットします。

その性質上これで2回目以降の訪問、つまりリロード時には続きからカウントし、新しいタブで訪問した場合はgoalTimeが無効(nullが代入)となり、結果カウントダウンが終了となります。

 

sessionStorageを経由すると、String型に変換される

また、注意点として、sessionStorageは一度setしたものをgetする(経由する)と、String型に変換されてしまいます。

つまり、1回目と2回目のdue.getTIme()は各々異なるdue変数が関数にかけられます。
そのため7, 9行目にあるように条件分岐によって変数を同じ型になるように記述を分けているのです。

getTimeメソッドはString型でなく、Date型(のオブジェクト)にしないといけません。したがってnew Date(due)としています。

// タイマー計算関数
function countdown(due) {
    const now = new Date();
    let rest;
    if(localStorage.getItem('timer') === 'start') {
        // sessionStorageより取得した値をString → Date型に変換
        rest = new Date(due).getTime() - now.getTime();
    } else if(localStorage.getItem('timer') !== 'start') {
        rest = due.getTime() - now.getTime();
    }
    const milisec = Math.floor(rest) % 60
    const sec = Math.floor(rest/1000) % 60;
    const min = Math.floor(rest/1000/60) % 60;
    const hours = Math.floor(rest/1000/60/60) % 24
    const count = [hours, min, sec, milisec];
    return count;
}


// タイマー実装関数
function recalc() {
    const counter = countdown(goal);
    
    let hour = document.querySelector('.hour');
    hour.textContent = String(counter[0]).padStart(2, '0')

    let min = document.querySelector('.min');
    min.textContent = String(counter).padStart(2, '0')
    
    let sec = document.querySelector('.sec');
    sec.textContent = String(counter).padStart(2, '0')
    
    let milisec = document.querySelector('.milisec');
    milisec.textContent = String(counter).padStart(2, '0')

    refresh()

    // タイマー終了後の処理
    if(counter[0] <= 0 && counter <= 0 && counter <= 0 && counter <= 0) {
        clearTimeout(timer_id)
        
        const js_time_reset = document.querySelectorAll('.js_time_reset');
        for(let i = 0; i < js_time_reset.length; i++ ) {
            js_time_reset[i].textContent = '00'
        }

        const btn = document.getElementById('btn');
        btn.classList.add('disabled');
    }
}

 

1回目と2回目で、typeof 演算子でconsole出力すると、型が異なるのが分かると思います。

出力される値は全く同じなのですが、typeof で見てみるとString型とObject型で異なります。これを発見するのに時間を要しました・・・。

 

今回のポイントとなるのは、以上の2点となります。

 

まとめ

本記事では、「カウントダウンタイマーをタブを閉じたら終了(リロードは継続)処理させる方法【JavaScript】」について、解説しました。

  • 実装方法
  • 実装のポイント

一から実装するのは、とても労力がかかりますが、JavaScriptの挙動を理解するのに大変役立ちます。よかったら参考にしてみてください。

 

参考記事

今回は以下の記事を参考にさせていただきました。
ありがとうございました!

JavaScript カウントダウンタイマー

【JavaScript】Cookie , localStorage, sessionStorageの違い

JavaScriptで【日付(Date)型⇔文字列(String)型】に変換する方法

ABOUT ME
ゆう
エンジニア歴7年。 大学卒業後、フィールドエンジニア職に就くが「手に職をつけたい」と思い、未経験からWEBデザイナーに転職。 その後、「WEB制作会社」「上場企業のECサイト運用」を経て、現在は「自社開発企業」のフロントエンドエンジニアとして仕事をしています。