iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 29
0
Modern Web

一起挑戰 JavaScript 30 吧!系列 第 29

JS30 Day 29 - Countdown Timer

成品連結:Countdown TimerHTML 程式碼完成後 JS 程式碼
(今天沒有完成前 JS 程式碼,自己創一個吧!但記得 script 的連結要改)

今天要做的是時間倒數器,有預設幾種選項,或是讓使用者指定時間。話不多說,我們開始吧!

操作步驟

首先列出待辦事項,等等再一一解決:

  • 做出時間倒數功能
  • 渲染倒數時間至畫面
  • 按下按鈕時渲染畫面
  • input 輸入時間 + 畫面渲染

做出時間倒數功能

要做出倒數功能,首先要先取得目前時間戳(timestamp),然後將指定的時間長度+取得的時間戳,最後再使用 setInterval(..) 每秒更新。

看起來或許有點負責複雜,我們一個步驟一個來吧!

取得目前時間戳(timestamp)

其實也不一定要用時間戳,你也可以取得時、分、秒再全部轉成秒數,但使用時間戳會相對不麻煩,所以這裡會使用此方法。

我們宣吿一個 function timer 來處理倒數的功能,並會帶入秒數作為參數

function timer(seconds) {
    // code here
}

接著取得目前時間戳

function timer(seconds) {
    const now = Date.now();
}

接著將設定的秒數加上 now 作為結束的時間,這裡要注意時間戳是使用毫秒,所以要記得將引數 seconds 乘以 1000

function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
}

使用 setInterval(..) 每秒更新

使用 setInterval 設定每 1000 毫秒更新,但要如何更新?我們可以透過每秒將 then(有就是結束時間的時間戳)扣掉目前的時間戳並轉成秒,直到倒數到 0 為止

function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
    setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        
        // 將時間渲染至畫面
        
    }, 1000);  
}

要注意的是當倒數秒數為 0 時要停止 setInterval,如果是寫下方的 code:

if (secondsLeft < 0) {
    return;
}

這樣的確會讓計時停止,但其實 setInterval function 本身並沒有停止,如果要停止需要執行另一個 function clearInterval 來清除 setInterval

要使用 clearInterval 我們樣先將 setInterval 存入一變數 countdown,等等才能使用 clearInterval 停止

let countdown;
function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
    countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        if (secondsLeft < 0) {
            clearInterval(countdown);
            return;  // return 會跳出 function 
        }
        // 將時間渲染至畫面
        
    }, 1000);  
}

將時間渲染至畫面

最後要將時間渲染至畫面,可以看到 HTML 上已有兩個元素 h1.display__time-leftp.display__end-time 分別要顯示倒數時間以及結束的時間

我們另外宣告一個 function 來處理時間2倒數,這個 function 會帶入一個秒數的引數

由於我希望能以分、秒的方式顯示在畫面上,所以要先設定分、秒的值

const timeLeft = document.querySelector('.display__time-left');
const endTIme = document.querySelector('.display__end-time');

function displayTimeLeft(seconds) {
    let minutes = Math.floor(seconds / 60);
    let remainderSeconds = seconds % 60;
    
    const display = `${minutes}:${remainderSeconds}`;
    
}

並把它加到 timer 當中執行

function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
    countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        if (secondsLeft < 0) {
            clearInterval(countdown);
            return;  // return 會跳出 function 
        }
        // 將時間渲染至畫面
        displayTimeLeft(secondsLeft);
    }, 1000);  
}

這裡會發現當印出時比如是 20 秒,一開始並不會顯示 00:20 而是 00:19,這是因為 setInterval 過了一秒才執行,而顯示 00:20 的時機已經過了,因此要在 setInterval 開始前先執行一次 displayTimeLeft(..)

function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
     displayTimeLeft(seconds);
    
    countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        if (secondsLeft < 0) {
            clearInterval(countdown);
            return;  // return 會跳出 function 
        }
        // 將時間渲染至畫面
        displayTimeLeft(secondsLeft);
    }, 1000);  
}

如果你試著印出結果,會發現有用,但如果分或秒小於 10 的時候並不會如長的的時鐘顯示如 09:08 的格式而是 9:8。要解決這問題我們要多做一些判斷

function displayTimeLeft(seconds) {
    let minutes = Math.floor(seconds / 60);
    let remainderSeconds = seconds % 60;
    
    if (minutes < 10) {
        minutes = '0' + minutes;
    }
    if (remainderSeconds < 10) {
        remainderSeconds = '0' + remainderSeconds;
    }
    
    const display = `${minutes}:${remainderSeconds}`;
}

接著要印出結束時間,也就是顯示如 Be Back At 09:33 的格式。如上,我們新建一個 function displayEndTime 來處理這個動作,並帶入一個時間戳的參數(也就是完成時間戳 then

function displayEndTime(timestamp) {
    // code here
}

由於只是要顯示完成時間,所以不同於更新剩餘時間,這個 function 並不需要在 setInterval 中執行,只需要執行一次即可。

displayEndTime 當中不需要顯示秒數,所以我們只要算出時、發、分就好

function displayEndTime(timestamp) {
    const time = new Date(timestamp);
    const hours = time.getHours();
    const minutes = time.getMinutes();
    
    
}

new Date(timestamp) 可以將時間戳轉換成我們熟悉的時間顯示方式。例如 1541698934495 就是 Fri Nov 09 2018 01:42:14 GMT+0800 (台北標準時間)

displayTimeLeft 相同,我希望時、分的顯示方式是 08:21 般的格式,所以需要利用 if...else 做判斷。但這裡稍微用不同方式,利用三元運算是判斷。

function displayEndTime(timestamp) {
    const time = new Date(timestamp);
    const hours = time.getHours();
    const minutes = time.getMinutes();
    
    const display = `Be Back At ${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}`;
    endTIme.textContent = display;
}

三元運算式的判斷方式是當時或分小於 10 的時候在數字前面加上 string '0',若大於 10 則加上空 string ''

這裡其實有隱含強制轉型的發生。當目前「時」是 7 的時候,number 7 前面會加上 string '0',這時候 7 會被強制轉型成 string '7' 所以相加後才會是 string '07',但這不是今天主題故不做太多討論。

最後將 displayEndTime 放到 timer 中執行

function timer(seconds) {
    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
     displayTimeLeft(seconds);
     displayEndTime(then);
    
    countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        if (secondsLeft < 0) {
            clearInterval(countdown);
            return;  // return 會跳出 function 
        }
        // 將時間渲染至畫面
        displayTimeLeft(secondsLeft);
    }, 1000);  
}

按下按鈕時渲染畫面

到這裡大概的功能完成了,我們來綁定預設的時間吧!

首先是大家熟悉的事件綁定

const buttons = document.querySelectorAll('button[data-time]');

function startTimer() {
    // code here
}

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

callback 中需要做的其實就是將 dataset 中的秒數取出並執行 timer

function startTimer() {
    const seconds = parseInt(this.dataset.time);  // parseInt 取出數字的整數部分
    timer(seconds);
}

到這裡點擊按鈕會正常顯示倒數以及結束時間了!但是...

再點擊其他按鈕會同時有好幾個 timer?

當已有一個倒數計時器而你再點擊另一個時間,會發現倒數不斷在各倒數器間跳動。這是因為我們實際上也因為 setInterval 同時執行了好幾個倒數計時器,也因此我們需要在 timer 執行時先執行 clearInterval

function timer(seconds) {
    // 清除已有的倒數計時器
    clearInterval(countdown);

    const now = Date.now();
    const then = Date.now() + seconds * 1000;
    
     displayTimeLeft(seconds);
     displayEndTime(then);
    
    countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000)  // Math.round() 可以四捨五入並取整數部分
        
        // 檢查是否需要停止倒數計時(當數字為 0 時)
        if (secondsLeft < 0) {
            clearInterval(countdown);
            return;  // return 會跳出 function 
        }
        // 將時間渲染至畫面
        displayTimeLeft(secondsLeft);
    }, 1000);  
}

input 輸入時間 + 畫面渲染

最後就是 input 輸入時間了。與按鈕一樣,我們先綁定事件。在 HTML 當中 input 是被 form 包著的,所以事件我們使用 form 的 submit

const custom = document.querySelector('#custom');

function startCustomTime(e) {
    // code here
}

custom.addEventListener('submit', startCustomTime);

由於 form submit 之後會提交表單重整頁面,但我們並不需要,因此要先 event.preventDefault() 來停止預設行為

而當使用者輸入分鐘數時我們要先轉換成秒數,並執行 timer,最後使用表單的 method event.reset() 來重置表單

function startCustomTime(e) {
    e.preventDefault();
  const minutes = parseInt(this.minutes.value);  // 使用 this.minutes.value 取得 input 的值
  
  timer(minutes * 60);  // 轉成秒數後再執行 timer
  this.reset();
}

到這裡基本上就完成今天的作品了,但我想再完善一下篩選使用者輸入的分鐘數

  • 輸入分鐘數必需是 number
  • 輸入分鐘數必需小於 1440(也就是 24 小時)
function startCustomTime(e) {
    e.preventDefault();
    const minutes = parseInt(this.minutes.value);  // 使用 this.minutes.value 取得 input 的值
  
    if ((0 / minutes) !== 0 || minutes > 1440) {  // 1440 minutess = 1 day
        clearInterval(countdown);
        timeLeft.textContent = (0 / minutes) !== 0 ? 'Numbers only!' : 'Too long!';
        endTIme.textContent = '';
        return;
    }
  
    timer(minutes * 60);  // 轉成秒數後再執行 timer
    this.reset();
}

當輸入分鐘數不是 number 時畫面顯示「Numbers only!」;當輸入分鐘數大於 1440 時顯示「Too long!」

恭喜完成!明天就是最後一天了!


上一篇
JS30 Day 28 - Video Speed Controller
下一篇
JS30 Day 30 - Whack A Mole
系列文
一起挑戰 JavaScript 30 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言