iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
自我挑戰組

JS30 學習日記系列 第 29

Day 29 - Vanilla JS Countdown Timer

前言

JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No FrameworksNo CompilersNo LibrariesNo Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。

另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。


本日目標

製作網頁版的倒數計時器,透過點按網頁上的按鈕快速設定倒數計時器或是在輸入框內輸入要設定倒數的分鐘數。

在這裡順便說明什麼是Vanilla JSVanilla JS是一個快速、輕量化、跨平台的 JavaScript 框架。


解析程式碼

HTML 部分

.timer是倒數計時器的本體,以下又可分成兩大部分,.timer__controls是其下設置倒數計時器的控制列,.display是其下用來展示剩餘時間和倒數結束時間的容器。

此外,可以發現在每一個button元素上,都有設定data-time屬性,這個屬性用來幫我們快速地設置倒數計時器且單位是以秒計算。

#custom內部放置一個文字輸入框,在使用者輸入數字按下 enter 後,這個數字會被拿來設定倒數計時器並以分鐘作為單位。

.display__time-left用來放置正在倒數的時間(分:秒)。
display__end-time用來放置倒數結束的時間。

<div class="timer">
    <div class="timer__controls">
      <button data-time="20" class="timer__button">20 Secs</button>
      <button data-time="300" class="timer__button">Work 5</button>
      <button data-time="900" class="timer__button">Quick 15</button>
      <button data-time="1200" class="timer__button">Snack 20</button>
      <button data-time="3600" class="timer__button">Lunch Break</button>
      <form name="customForm" id="custom">
        <input type="text" name="minutes" placeholder="Enter Minutes">
      </form>
    </div>
    <div class="display">
      <h1 class="display__time-left"></h1>
      <p class="display__end-time"></p>
    </div>
</div>

JS 部分

宣告變數countdown,待會用來指定成後面setInterval()的間隔代碼(Interval ID)。

宣告常數timeDisplay取得用來放置倒數時間的元素。

let countdown;
const timeDisplay = document.querySelector('.display__time-left');

timer()裡首先要取得現在的時間(Date.now()),要注意這邊回傳的是以毫秒為單位的timestamp

接著宣告then作為倒數計時結束的時間點,把要倒數的秒數乘上1000(換成毫秒)再加上現在的時間(毫秒)就完成了。

再接下來可以注意到在第五行,呼叫了displayTimeLeft()這個方法,這個方法用來幫我們把倒數中的時間放到.display__time-left還有網頁的標題裡面(方法的詳細內容在更下面)。

那為什麼在開始倒數之前,要先呼叫displayTimeLeft()呢? 因為用setInterval()倒數的話,它會有1秒的延遲時間,也就是說最一開始的那1秒是什麼都沒有的狀態,所以要呼叫displayTimeLeft()補上那1秒的空窗期。

然後就是用setInterval()開始倒數的部分啦~ 在這邊宣告常數secondsLeft放入經計算後的剩餘秒數,記得要除上1000,因為每一秒 then - Date.now() 的結果單位都是毫秒,最後用Math.round()四捨五入取最近的整數。

secondLeft小於0的時候,我們應該要立即停止倒數,這時候前面指定的 Interval ID 就派上用場啦~ 用clearInterval(countdown)就可以把倒數輕鬆移除。

function timer(seconds){
    const now = Date.now();
    const then = now +seconds * 1000;
    //run immediately not run after 1 sec
    displayTimeLeft(seconds);

    countdown = setInterval(()=>{
        const secondsLeft = Math.round((then - Date.now())/1000);
        //display it
        if(secondsLeft < 0){
            clearInterval(countdown);
            return;
        }
        displayTimeLeft(secondsLeft);
    },1000)
}

displayTimeLeft(),它會把拿到的剩餘秒數換算成分和秒,再放回到'.display__time-left'裡並同時修改網頁標題。

這邊要特別注意如果經換算後,秒數的部分不足10秒,則要透過條件判斷在前方補上0。(ex. 10:5 >>> 10:05)

function displayTimeLeft(seconds){
    const minutes = Math.floor(seconds/60);
    const reminderSeconds = seconds%60;
    const display = `${minutes}:${reminderSeconds < 10 ? '0' : ''}${reminderSeconds}`;
    document.title = display;
    timeDisplay.textContent = display;
}

宣告常數endTime取得放置倒數結束時間的元素,接下來就要用displayEndTime()來處理倒數結束的時間囉!

displayEndTime(),他把傳入的timestamp(單位 : 毫秒),用new Date(timestamp)建立成一個Date物件再放入常數end裡面。

接下來 Do Re Mi SO,把endTime(結束時間),修改成自Date物件取得的小時、分鐘。特別注意遇到分鐘數不足10分鐘時,要利用條件判斷在數字前方補上0,例如: 21:5 >>> 21:05。

const endTime = document.querySelector('.display__end-time');
/*中略....*/
function displayEndTime(timestamp){
    const end = new Date(timestamp);
    const hour = end.getHours();
    const minutes = end.getMinutes();
    endTime.textContent = `Be Back At ${hour}:${minutes < 10 ? '0' : ''}${minutes}`;
}

new Date(timestamp)的一些操作如下圖 :

(我一開始覺得怪怪的,為什麼getMonth()回傳的是8而不是9? 後來一查才發現0代表的是1月 XD)

下面把timer()倒數結束的時間(then,毫秒),傳入displayEndTime(),修改頁面上的倒數結束時間。

function timer(seconds){
    /*上略...*/
    displayEndTime(then);
    /*下略...*/
}

宣告常數buttons取得所有用來快速設定倒數計時器的按鈕。

然後為每個button都註冊click event listenerstartTimer()作為event handler

startTimer(),宣告常數seconds取得被點擊按鈕上的data-time屬性值,接著把這個seconds丟入timer(),成功建立一個倒數計時器。

const buttons = document.querySelectorAll('[data-time]');
/*中略...*/
function startTimer(){
    const seconds = parseInt(this.dataset.time);
    timer(seconds);
}

buttons.forEach(button => button.addEventListener('click',startTimer));

如果這時候開始瘋狂點擊按鈕建立倒數計時器,你會發現網頁上的倒數時間開始"抽搐",因為之前建立的倒數計時器仍在運作,造成網頁倒數時間的文字被頻繁地修改,然後不自然的"抽搐"就出現了。

所以我們需要在timer()的最上面加上clearInterval(countdown);這一行,在每一次設定倒數計時器的時候,把之前的倒數計時器通通清掉。

function timer(seconds){
    //clear any existing timer
    clearInterval(countdown);
    /*下略...*/
}

最後要來處理文字輸入框的部分,在form元素上註冊submit event listener以後面的function()作為event handler

因為submit表單時會重新整理網頁,所以在第一行寫e.preventDefault()防止網頁重新整理。接著宣告常數mins承接來自<input name="minutes">的分鐘數(value),然後再把分鐘數乘上60換成秒傳入timer(),成功設定倒數計時器。最後一行的this.reset()用來重置表單元素。

document.customForm.addEventListener('submit',function(e){
    e.preventDefault();
    const mins = this.minutes.value;
    console.log(mins);
    timer(mins * 60);
    this.reset();
});
補充資料:

Date.now()
WindowOrWorkerGlobalScope.setInterval()
clearInterval()
Math.floor()
Node.textContent
Date.prototype.getHours()
Date.prototype.getMinutes()
HTMLElement.dataset

範例網頁

完整程式碼請點此


上一篇
Day 28 - Build a Experimental Video Speed Controller UI
下一篇
Day 30 - [打地鼠] Make a Whack A Mole Game with Vanilla JS
系列文
JS30 學習日記30

尚未有邦友留言

立即登入留言