Skip to content

Commit 905afb9

Browse files
committed
Initial implementation with ES2015 modules and web components
1 parent ffb69a3 commit 905afb9

File tree

7 files changed

+458
-0
lines changed

7 files changed

+458
-0
lines changed

components/Stopwatch.css

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
.wrapper {
6+
--spacing: 0.5em;
7+
display: grid;
8+
padding: var(--spacing);
9+
grid-template-columns: 1fr 1fr;
10+
grid-template-rows: auto 1fr auto;
11+
grid-gap: var(--spacing);
12+
width: 12em;
13+
height: 15em;
14+
color: white;
15+
background: black
16+
linear-gradient(
17+
-237deg,
18+
hsla(0, 0%, 100%, 0),
19+
hsla(0, 0%, 100%, 0) 35%,
20+
hsla(0, 0%, 100%, 0.2) 35%,
21+
hsla(0, 0%, 100%, 0)
22+
);
23+
border: calc(var(--spacing) / 2) solid black;
24+
border-radius: var(--spacing);
25+
font-family: Helvetica, sans-serif;
26+
}
27+
28+
.display {
29+
--multiplier: 2.5;
30+
grid-area: 1 / span 2;
31+
padding: calc(var(--spacing) / var(--multiplier)) 0;
32+
font-size: calc(var(--multiplier) * 1em);
33+
text-align: center;
34+
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
35+
font-weight: lighter;
36+
transition: font-size 0.1s ease-out;
37+
letter-spacing: -0.1em;
38+
font-family: "Droid Sans Mono for Powerline", sans-serif;
39+
}
40+
41+
.has-laps .display {
42+
--multiplier: 1.5;
43+
}
44+
45+
button {
46+
font-size: 1em;
47+
padding: var(--spacing);
48+
grid-row: 3;
49+
border: 0;
50+
border-radius: calc(var(--spacing) / 2);
51+
}
52+
53+
.start {
54+
background: lawngreen;
55+
}
56+
57+
.stop {
58+
background-color: crimson;
59+
}
60+
61+
.reset,
62+
.lap {
63+
background-color: hsl(0, 0%, 50%);
64+
color: hsl(0, 0%, 86%);
65+
}
66+
67+
button:disabled {
68+
color: hsl(0, 0%, 60%);
69+
}
70+
71+
button:active {
72+
transform: scale(.95);
73+
}
74+
75+
.laps {
76+
grid-column: span 2;
77+
overflow: auto;
78+
font-weight: lighter;
79+
}
80+
81+
.lap__row {
82+
display: flex;
83+
justify-content: space-between;
84+
align-items: center;
85+
font-size: 0.8em;
86+
margin-bottom: var(--spacing);
87+
border-top: 1px solid hsla(0, 0%, 66%, 0.3);
88+
padding-top: var(--spacing);
89+
animation: slideInAllRows 0.2s ease-out;
90+
}
91+
92+
.lap__row:first-child {
93+
border-top: none;
94+
overflow: hidden;
95+
animation: none;
96+
}
97+
98+
.lap__row:first-child > * {
99+
animation: slideInFirstLapRow 0.2s ease-out;
100+
}
101+
102+
@keyframes slideInFirstLapRow {
103+
from {
104+
transform: translateY(-100%);
105+
opacity: 0;
106+
}
107+
to {
108+
transform: translateY(0);
109+
opacity: 1;
110+
}
111+
}
112+
113+
@keyframes slideInAllRows {
114+
from {
115+
transform: translateY(-100%);
116+
}
117+
to {
118+
transform: translateY(0);
119+
}
120+
}
121+
122+
.lap__row--fastest .lap__time::before,
123+
.lap__row--slowest .lap__time::before {
124+
content: " ";
125+
display: inline-block;
126+
width: 0.5em;
127+
height: 0.5em;
128+
border-radius: 50%;
129+
margin-right: 0.5em;
130+
overflow: hidden;
131+
background-color: lawngreen;
132+
}
133+
134+
.lap__row--slowest .lap__time::before {
135+
background-color: crimson;
136+
}
137+
138+
.lap__number {
139+
color: darkgray;
140+
}
141+
142+
.lap__time {
143+
display: flex;
144+
align-items: center;
145+
}

components/Stopwatch.js

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Stopwatch from "../lib/stopwatch.js";
2+
3+
class StopwatchComponent extends HTMLElement {
4+
constructor() {
5+
super();
6+
7+
this.stopwatch = new Stopwatch();
8+
this.shadow = this.attachShadow({ mode: "open" });
9+
this.wrapper = document.createElement("div");
10+
this.wrapper.className = "wrapper";
11+
this.shadow.appendChild(this.wrapper);
12+
13+
this.initDisplay();
14+
this.initLaps();
15+
16+
this.initResetButton();
17+
this.initLapButton();
18+
this.initStartButton();
19+
this.initStopButton();
20+
21+
this.manageButtons();
22+
this.updateDisplay();
23+
this.updateLaps();
24+
}
25+
26+
initDisplay() {
27+
const styles = document.createElement("link");
28+
styles.href = "./components/Stopwatch.css";
29+
styles.rel = "stylesheet";
30+
31+
this.wrapper.appendChild(styles);
32+
33+
this.display = document.createElement("div");
34+
this.display.className = "display";
35+
this.wrapper.appendChild(this.display);
36+
}
37+
38+
initLapButton() {
39+
const lapButton = document.createElement("button");
40+
lapButton.innerText = "Lap";
41+
lapButton.className = "lap";
42+
43+
lapButton.addEventListener("click", e => {
44+
e.preventDefault();
45+
e.stopPropagation();
46+
47+
this.stopwatch.lap();
48+
this.manageButtons();
49+
this.updateLaps();
50+
});
51+
52+
this.wrapper.appendChild(lapButton);
53+
this.lapButton = lapButton;
54+
}
55+
56+
initResetButton() {
57+
const resetButton = document.createElement("button");
58+
resetButton.innerText = "Reset";
59+
resetButton.className = "reset";
60+
61+
resetButton.addEventListener("click", e => {
62+
e.preventDefault();
63+
e.stopPropagation();
64+
65+
this.stopwatch.reset();
66+
this.updateDisplay();
67+
this.manageButtons();
68+
this.updateLaps();
69+
});
70+
71+
this.wrapper.appendChild(resetButton);
72+
this.resetButton = resetButton;
73+
}
74+
75+
initStopButton() {
76+
const stopButton = document.createElement("button");
77+
stopButton.innerText = "Stop";
78+
stopButton.className = "stop";
79+
80+
stopButton.addEventListener("click", e => {
81+
e.preventDefault();
82+
e.stopPropagation();
83+
84+
this.stopwatch.stop();
85+
this.manageButtons();
86+
});
87+
88+
this.wrapper.appendChild(stopButton);
89+
this.stopButton = stopButton;
90+
}
91+
92+
initStartButton() {
93+
const startButton = document.createElement("button");
94+
startButton.innerText = "Start";
95+
startButton.className = "start";
96+
97+
startButton.addEventListener("click", e => {
98+
e.preventDefault();
99+
e.stopPropagation();
100+
101+
this.stopwatch.start();
102+
this.updateDisplay();
103+
this.manageButtons();
104+
});
105+
106+
this.wrapper.appendChild(startButton);
107+
this.startButton = startButton;
108+
}
109+
110+
updateDisplay() {
111+
this.display.innerText = this.stopwatch.elapsed;
112+
113+
if (this.stopwatch.isCounting) {
114+
requestAnimationFrame(() => this.updateDisplay());
115+
}
116+
}
117+
118+
manageButtons() {
119+
[
120+
this.startButton,
121+
this.stopButton,
122+
this.resetButton,
123+
this.lapButton
124+
].forEach(btn => (btn.style.display = "none"));
125+
126+
if (this.stopwatch.isCounting) {
127+
this.lapButton.style.display = "";
128+
this.lapButton.disabled = false;
129+
this.stopButton.style.display = "";
130+
} else if (this.stopwatch.timeElapsed !== 0) {
131+
this.resetButton.style.display = "";
132+
this.startButton.style.display = "";
133+
} else {
134+
this.lapButton.style.display = "";
135+
this.lapButton.disabled = true;
136+
this.startButton.style.display = "";
137+
}
138+
}
139+
140+
initLaps() {
141+
this.laps = document.createElement("div");
142+
this.laps.className = "laps";
143+
144+
this.wrapper.appendChild(this.laps);
145+
}
146+
147+
updateLaps() {
148+
this.wrapper.classList.toggle("has-laps", !!this.stopwatch.laps.length);
149+
150+
let fastest;
151+
let slowest;
152+
153+
if (this.stopwatch.laps.length > 1) {
154+
const laps = this.stopwatch.laps.slice().sort();
155+
fastest = laps.shift();
156+
slowest = laps.pop();
157+
}
158+
159+
const getSpeed = lapTime => {
160+
switch (lapTime) {
161+
case fastest:
162+
return "lap__row--fastest";
163+
case slowest:
164+
return "lap__row--slowest";
165+
default:
166+
return "";
167+
}
168+
};
169+
170+
this.laps.innerHTML = this.stopwatch.laps
171+
.map((lapTime, lapNumber) => {
172+
return `
173+
<div class="lap__row ${getSpeed(lapTime)}">
174+
<div class="lap__number">Lap ${lapNumber + 1}</div>
175+
<div class="lap__time">${lapTime}</div>
176+
</div>
177+
`;
178+
})
179+
.reverse()
180+
.join("");
181+
}
182+
}
183+
184+
window.customElements.define("stop-watch", StopwatchComponent);

index.html

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Stopwatch</title>
6+
<script type="module" src="components/Stopwatch.js"></script>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
padding: 0;
14+
margin: 0;
15+
min-width: 100vw;
16+
min-height: 100vh;
17+
display: flex;
18+
flex-wrap: wrap;
19+
align-items: center;
20+
justify-content: center;
21+
}
22+
23+
body > * {
24+
margin: 1em;
25+
}
26+
</style>
27+
</head>
28+
<body>
29+
<stop-watch></stop-watch>
30+
<stop-watch></stop-watch>
31+
</body>
32+
</html>

lib/TimerFormatter.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export function extractMinutes(time) {
2+
return Math.floor(time / 100 / 60);
3+
}
4+
5+
export function extractSeconds(time) {
6+
const minutes = extractMinutes(time) * 100 * 60;
7+
8+
return Math.floor((time - minutes) / 100);
9+
}
10+
11+
export function extractMilliseconds(time) {
12+
const minutes = extractMinutes(time) * 100 * 60;
13+
const seconds = extractSeconds(time) * 100;
14+
15+
return time - minutes - seconds;
16+
}
17+
18+
export function format(time) {
19+
const minutes = (extractMinutes(time) + "").padStart(2, "0");
20+
const seconds = (extractSeconds(time) + "").padStart(2, "0");
21+
const milliseconds = (extractMilliseconds(time) + "").padStart(2, "0");
22+
23+
return `${minutes}:${seconds}.${milliseconds}`;
24+
}

0 commit comments

Comments
 (0)