JavaScript 練習 - 番茄鐘

Demo

JavaScript 練習 - 番茄鐘 - Demo

Introduction 介紹

注意力愈來愈稀缺,而這時候最適合的方法就是「番茄鐘工作法」。番茄鐘工作法是一種能有效大幅提升工作效率的時間管理方法,透過時間的分段和階段性的任務,讓你清楚知道自己的時間花費在哪裡。

  • 一個番茄鐘包含工作 25 分鐘、休息 5 分鐘
  • 可開始或暫停倒數,但同時只顯示其中一個按鈕
  • 每次重置就是從工作時段開始,因為重新開始休息時間不合番茄工作法的邏輯
  • 可客製化時間長度
  • 完成或終止都會顯示該時段任務所達時間長度在下方列表
  • 進度條

功能

開始及暫停倒數

番茄鐘的基本結構包括:顯示倒數時間、開始及暫停鈕、停止按鈕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="pomodoro-clock">
<div id="pomodoro-main">
<div class="pomodoro-timer"></div>
</div>
</div>

<div id="pomodoro-info">
<div id="pomodoro-clock-controller">
<button id="pomodoro-start">
<i class="fas fa-play" id="play-icon"></i>
<i class="fas fa-pause hidden" id="pause-icon"></i>
</button>
<button id="pomodoro-stop" class="hidden">
<i class="fas fa-stop"></i>
</button>
</div>
</div>

首先設置事件監聽器,在點擊的時候就會呼叫 toggleClock 函式,以便在播放及暫停之間切換。還需要一個 isClockRunning變數,以便知道是否需要播放或暫停計時器,它的初始布林值是 false;設定時間的初始值,分別是每個番茄鐘的時段 25 分鐘,要用來儲存現在剩餘時間的變數也是 25 分鐘,休息時段 5 分鐘。我們對點擊 stop 的時候呼叫的toggleClock 函式傳入一個 reset 參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const pomodoroTimer = document.querySelector("#pomodoro-timer");
const startButton = document.querySelector("#pomodoro-start");
const pauseButton = document.querySelector("#pomodoro-pause");
const stopButton = document.querySelector("#pomodoro-stop");
let isClockRunning = false;
let workSessionDuration = 1500; // = 25 mins
let currentTimeLeftInSession = 1500; //目前時段剩餘時間
let breakSessionDuration = 300; // = 5mins

startButton.addEventListener("click", () => {
toggleClock();
});

pauseButton.addEventListener("click", () => {
toggleClock();
});

stopButton.addEventListener("click", () => {
toggleClock(true);
});

裡我們使用 ES6 的箭頭函式寫法,所有的函式會變成匿名的函式。。例如:

const func = (x) => x + 1
相當於
const func = function (x) { return x + 1 }

toggleClock 只會在我們按下 stop 的時候接收 reset 參數;如果不是按下 stope 按鈕,這個函式就會查看 isClockRunning 變數的值,看我們按下的是播放還是暫停計時器

1
2
3
4
5
6
7
8
9
10
11
12
13
const toggleClock = reset => {
if (reset) {
// STOP THE TIMER
} else {
if (isClockRunning === true) {
// PAUSE THE TIMER
isClockRunning = false;
} else {
// START THE TIMER
isClockRunning = true;
}
}
};

這裡使用內建方法setInterval追蹤我計時器,可按照指定的週期(以毫秒)調用函式。讓番茄鐘的剩餘時段每秒鐘都減 1,在 else 語句內部,isClockRunning = true 我們可以這樣寫:

1
2
3
4
clockTimer = setInterval(() => {
// decrease time left / increase time spent
currentTimeLeftInSession--;
}, 1000);

在單擊“暫停”按鈕時暫停此計時器,因此如果時鐘還繼續在倒數的話,我們就暫停這個計時器,這將清除點擊播放按鈕時設置的計時器。

1
2
3
4
if (isClockRunning == true) {
clearInterval(clockTimer);
isClockRunning = false;
}

格式化和顯示時間

現在首先要在頁面顯示計時器,設定一個函式 displayCurrentTimeLeftInSession,這個函式將在每秒被計時器調用,並且我們要讓剩餘的時間每一秒都減少

1
2
3
4
clockTimer = setInterval(() => {
currentTimeLeftInSession--;
displayCurrentTimeLeftInSession();
}, 1000);

現在我們要格式化時間,把時間轉換為分及秒。使用 % 取得餘數,

例如

const x = 70 //現在有 70 秒
x % 60 ; // 餘數 10

70 秒就是 1 分 10 秒,所以透過 % 60 我們取得剩餘秒數,並且同時使用 parseInt()把字串轉成數值。然後再把剩餘的秒數除以 3600 (一小時的總秒數),獲得目前剩餘的小時。

並且我們要讓分鐘及秒鐘在小於 10 的時候前面加 0 ,項這樣的格式 “09:05”。如果時間小於 10,則返回前面的零(例如:09 而不是 9),如果時間大於 10,則返回它。

最後組成字串:將小時(如果有),分鐘和秒,組一起成一個字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const displayCurrentTimeLeftInSession = () => {
const secondsLeft = currentTimeLeftInSession;
let result = "";
const seconds = secondsLeft % 60;
const minutes = parseInt(secondsLeft / 60) % 60;
let hours = parseInt(secondsLeft / 3600);
// add leading zeroes if it's less than 10
function addLeadingZeroes(time) {
return time < 10 ? `0${time}` : time;
}
if (hours > 0) result += `${hours}:`;
result += `${addLeadingZeroes(minutes)}:${addLeadingZeroes(seconds)}`;
pomodoroTimer.innerText = result.toString();
};

停止計時器

上面已經把 stop 按鈕連結到事件監聽,而且只有點擊它的時候,才會調用 reset 參數。現在在 toggleClock 的條件句裡調用另一個函式

1
2
3
4
5
 const toggleClock = (reset) => {
if (reset) {
stopClock();
} //....
}

終止計時器的時候,要做以下 2 件事情:

  • 清除目前設置的計時器
  • 把計時器恢復到初始值
1
2
3
4
5
6
7
8
9
10
const stopClock = () => {
// 1) 重置目前設定的計時器
clearInterval(clockTimer);
// 2) 把計時器切換到停止
isClockRunning = false;
// 把目前時段剩下的時間切換回原始設定值
currentTimeLeftInSession = workSessionDuration;
// 更新目前顯示在畫面上的時間
displayCurrentTimeLeftInSession();
};

在工作和休息間切換

將上面在數器裡面每秒都持續減少的方法移除,並且獨立成一個函式。

  • 在「工作」和 「休息」之間切換
  • 顯示已完成/已停止的日誌
  • 能夠編輯任務的標籤

如果 currentTimeLeftInSession > 0 這意味著計時器仍在運行,那麼我們只想減少它;新增一個儲存任務類型的變數 let type="Work"如果現在剩餘的時間等於 0 ,我們需要切換任務類型和接下來時段的持續時間

並且將完成或終止的任務顯示在日誌,在日誌裡面顯示這個時段總計累計時間。所以我們要宣告一個會隨著任務的花費每一秒而增加新變數let timeSpentInCurrentSession = 0;, 同時在函式內部加入 timeSpentInCurrentSession++; 讓花費的時間持續增加。每次任務結束時,我們都會重置此計數器,因為計時器已結束或已停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

let type = "Work";
let timeSpentInCurrentSession = 0;


const toggleClock = reset => {
//.........
else {

clockTimer = setInterval(() => {
stepDown();//讓計數器持續減少


}, 1000);

}

}
}


const stepDown = () => {
if (currentTimeLeftInSession > 0) {
currentTimeLeftInSession--;
timeSpentInCurrentSession++; // 累積花費的時間持續增加
} else if (currentTimeLeftInSession === 0) {
timeSspentInCurrentSession = 0; //重置累積花費的時間
if (type === "Work") {
currentTimeLeftInSession = breakSessionDuration;
displaySessionLog("Work");
type = "Break";
} else {
currentTimeLeftInSession = workSessionDuration;
type = "Work";
displaySessionLog("Break");
}
}
displayCurrentTimeLeftInSession();
};


const stopClock = () => {



}

顯示日誌,並把任務類型重置為工作。重置休息會話並啟動另一個休息會話沒有意義,因此我們希望在用戶停止時始終恢復為工作會話。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const displaySessionLog = type => {
const sessionsList = document.querySelector("#pomodoro-sessions");
// append li to it
const li = document.createElement("li");
let sessionLabel = type;
let elapsedTime = parseInt(timeSpentInCurrentSession / 60);
elapsedTime = elapsedTime > 0 ? elapsedTime : "< 1";

const text = document.createTextNode(`${sessionLabel} : ${elapsedTime} min`);
li.appendChild(text);
sessionsList.appendChild(li);
};

const stopClock = () => {
timeSpentInCurrentSession = 0;
displaySessionLog(type); //顯示到目前為止在此會話中花費的時間
type = "Work"; //重置任務類型
};

自定義任務名稱

在 HTML 新增一個輸入框

1
2
3
4
5
6
7
<div>
<input
type="text"
id="pomodoro-clock-task"
placeholder="Enter your task..."
/>
</div>

接下來,僅可針對工作任務給自定義的標籤,休息時間將始終顯示為休息。把 let sessionLabel = type 如下更新。這裡的意思是首先查看我們當前的任務類型,如果它是「工作」,則它將查看 input 標記的值。如果我們將其保留為空,則會顯示「工作」,否則將採用我們的自定義值。如果會話的類型為「休息」,它將僅使用字串「休息」作為日誌標籤。

1
2
3
4
5
6
7
8
const displaySessionLog = type => {
if (type === "Work") {
sessionLabel = currentTaskLabel.value ? currentTaskLabel.value : "Work";
workSessionLabel = sessionLabel;
} else {
sessionLabel = "Break";
}
};

同時也要更新 stepDown 功能,加入 currentTaskLabel.disabled = true,因為我們要在休息時段禁用自定義標籤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else if (currentTimeLeftInSession === 0) {
timeSpentInCurrentSession = 0;
// Timer is over -> if work switch to break, viceversa
if (type === 'Work') {
currentTimeLeftInSession = breakSessionDuration;
displaySessionLog('Work');
type = 'Break';
// new
currentTaskLabel.value = 'Break';
currentTaskLabel.disabled = true;
} else {
currentTimeLeftInSession = workSessionDuration;
type = 'Work';
// new
if (currentTaskLabel.value === 'Break') {
currentTaskLabel.value = workSessionLabel;
}
currentTaskLabel.disabled = false;
displaySessionLog('Break');
}

自定義工作時間與休息時間

新宣告四個變數,並且手動將 25 和 5 設置為兩個輸入欄位的值,這裡的數值代表的是分鐘

1
2
3
4
5
6
7
8
let updatedWorkSessionDuration;
let updatedBreakSessionDuration;

let workDurationInput = document.querySelector("#input-work-duration");
let breakDurationInput = document.querySelector("#input-break-duration");

workDurationInput.value = "25";
breakDurationInput.value = "5";

所以要更新計時器就要這些值從分鐘轉換為秒。新增兩個事件監聽,每當在其中一個 input 輸入新值時,我們就會使用新值。在調用一個函式 minuteToSeconds,它將幫助我們將 input 分鐘字串轉換為秒。要得到所有秒數,就將分鐘數乘 60。

1
2
3
4
5
6
7
8
9
10
11
workDurationInput.addEventListener("input", () => {
updatedWorkSessionDuration = minuteToSeconds(workDurationInput.value);
});

breakDurationInput.addEventListener("input", () => {
updatedBreakSessionDuration = minuteToSeconds(breakDurationInput.value);
});

const minuteToSeconds = mins => {
return mins * 60;
};

最後步驟

由於我們自定義的工作時間與休息時間,只會在下一個時段更新,所以要設定一個新的函式。此函式將做兩件事:

  • 查看 updatedBreakSessionDuration以查看用戶是否更新了會話持續時間
  • 如果是的話,它將根據用戶輸入的值設置新的會話持續時間

如果目前任務類型為工作,將檢查用戶是否更新了 updatedWorkSessionDuration 。如果是,我們將其設置為下一個時段任務的值,否則我們使用中的默認值 workSessionDuration。休息也是如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
const setUpdatedTimers = () => {
if (type === "Work") {
currentTimeLeftInSession = updatedWorkSessionDuration
? updatedWorkSessionDuration
: workSessionDuration;
workSessionDuration = currentTimeLeftInSession;
} else {
currentTimeLeftInSession = updatedBreakSessionDuration
? updatedBreakSessionDuration
: breakSessionDuration;
breakSessionDuration = currentTimeLeftInSession;
}
};

但是我們不要在暫停後更新計時器並且也不要計時器的長度被更新,所以新增一個變數來幫助計時器辨識是否該更新為自定義時段。並且在stepDown裡面使用更新為自定義時段的要求。當時鐘停止時,這意味著我們正在啟動時鐘,而不僅僅是在暫停後重新開始。在這種情況下,我們將設置更新的計時器並將變量切換為 false。當用戶停止計時時,我們將變量設置 isClockStopped 為 true,然後調用 setUpdatedTimers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let isClockStopped = true;

const toggleClock = reset => {
if (reset) {
stopClock();
} else {
// new
if (isClockStopped) {
setUpdatedTimers();
isClockStopped = false;
}
}
};

const stopClock = () => {
setUpdatedTimers();
displaySessionLog(type);
clearInterval(clockTimer);
isClockStopped = true;
isClockRunning = false;
currentTimeLeftInSession = workSessionDuration;
displayCurrentTimeLeftInSession();
type = "Work";
timeSpentInCurrentSession = 0;
};

調整按鈕樣式

暫停的時候顯示播放鈕,反之亦然。我們只要在「播放」、「暫停」的圖示之間切換,所以請刪除以下幾行:

1
2
3
4
5
6
const pauseButton = document.querySelector("#pomodoro-pause");

// PAUSE
pauseButton.addEventListener("click", () => {
toggleClock();
});

並且在 css 裡面添加 hidden 類別的定義,我們在切換的時候會添加到需要的按鈕上

1
2
3
.hidden {
display: none;
}

所以要設定一個新的函式,讓我們在在時鐘不工作時隱藏暫停圖標。

  • 該參數 reset 是一個布林值,它將確定我們是否要重置時鐘。重置時無論時鐘暫停與否,我們總是要顯示播放鈕。
  • 檢查元素裡面是否具有 hidden 類別,有的話移除,沒有的話加上去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const togglePlayPauseIcon = reset => {
const playIcon = document.querySelector("#play-icon");
const pauseIcon = document.querySelector("#pause-icon");
if (reset) {
// 重置番茄鐘的時候,永遠都切換回播放鈕
if (playIcon.classList.contains("hidden")) {
playIcon.classList.remove("hidden");
}
if (!pauseIcon.classList.contains("hidden")) {
pauseIcon.classList.add("hidden");
}
} else {
playIcon.classList.toggle("hidden");
pauseIcon.classList.toggle("hidden");
}
};

同時也要讓這個函式在 toggleClock 裡面調用。

1
2
const toggleClock = reset => {
togglePlayPauseIcon(reset);//......

現在缺少的是 stopIcon。預設情況下停止鈕是隱藏的,它只會在我們第一次啟動時鐘時顯示,

創建一個名為的新函數 showStopIcon,如下所示:

1
2
3
4
const showStopIcon = () => {
const stopButton = document.querySelector("#pomodoro-stop");
stopButton.classList.remove("hidden");
};

這個函式也將在 toggleClock 內部被調用,最後看起來像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const toggleClock = reset => {
togglePlayPauseIcon(reset);
if (reset) {
stopClock();
} else {
console.log(isClockStopped);
if (isClockStopped) {
setUpdatedTimers();
isClockStopped = false;
}

if (isClockRunning === true) {
clearInterval(clockTimer);
isClockRunning = false;
} else {
clockTimer = setInterval(() => {
stepDown();
displayCurrentTimeLeftInSession();
}, 1000);
isClockRunning = true;
}

showStopIcon();
}
};

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :