JS30 Day11

透過事件的觸發,可以改造 video 播放器,雖然使用機率不高,但也藉此更熟悉 JS 的語法了。


Custom HTML5 Video Player

完成目標

做一個 HTML5 video 播放器,試著改變 video 的預設 style。

  • 功能
    • 和預設介面相同的功能
      • play、pause、跳到指定時間、音量調整、播放速度、往前10秒、往後25秒
  • 畫面
    • 縮起來剩下進度條
    • hover 後全部出現



index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML Video Player</title>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div class="player">
<video class="player__video viewer" src="652333414.mp4"></video>

<div class="player__controls">
<div class="progress">
<div class="progress__filled"></div>
</div>
<button class="player__button toggle" title="Toggle Play"></button>
<input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1">
<input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1">
<button data-skip="-10" class="player__button">« 10s</button>
<button data-skip="25" class="player__button">25s »</button>
</div>
</div>

<script src="scripts.js"></script>
</body>
</html>

style.css

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
html {
box-sizing: border-box;
}

*, *:before, *:after {
box-sizing: inherit;
}

body {
margin: 0;
padding: 0;
display: flex;
background: #7A419B;
min-height: 100vh;
background: linear-gradient(135deg, #7c1599 0%,#921099 48%,#7e4ae8 100%);
background-size: cover;
align-items: center;
justify-content: center;
}

.player {
max-width: 750px;
border: 5px solid rgba(0,0,0,0.2);
box-shadow: 0 0 20px rgba(0,0,0,0.2);
position: relative;
font-size: 0;
overflow: hidden;
}

/* This css is only applied when fullscreen is active. */
.player:fullscreen {
max-width: none;
width: 100%;
}

.player:-webkit-full-screen {
max-width: none;
width: 100%;
}

.player__video {
width: 100%;
}

.player__button {
background: none;
border: 0;
line-height: 1;
color: white;
text-align: center;
outline: 0;
padding: 0;
cursor: pointer;
max-width: 50px;
}

.player__button:focus {
border-color: #ffc600;
}

.player__slider {
width: 10px;
height: 30px;
}

.player__controls {
display: flex;
position: absolute;
bottom: 0;
width: 100%;
transform: translateY(100%) translateY(-5px);
transition: all .3s;
flex-wrap: wrap;
background: rgba(0,0,0,0.1);
}

.player:hover .player__controls {
transform: translateY(0);
}

.player:hover .progress {
height: 15px;
}

.player__controls > * {
flex: 1;
}

.progress {
flex: 10;
position: relative;
display: flex;
flex-basis: 100%;
height: 5px;
transition: height 0.3s;
background: rgba(0,0,0,0.5);
cursor: ew-resize;
}

.progress__filled {
width: 50%;
background: #ffc600;
flex: 0;
flex-basis: 50%;
}

/* unholy css to style input type="range" */

input[type=range] {
-webkit-appearance: none;
background: transparent;
width: 100%;
margin: 0 5px;
}

input[type=range]:focus {
outline: none;
}

input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
background: rgba(255,255,255,0.8);
border-radius: 1.3px;
border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type=range]::-webkit-slider-thumb {
height: 15px;
width: 15px;
border-radius: 50px;
background: #ffc600;
cursor: pointer;
-webkit-appearance: none;
margin-top: -3.5px;
box-shadow:0 0 2px rgba(0,0,0,0.2);
}

input[type=range]:focus::-webkit-slider-runnable-track {
background: #bada55;
}

input[type=range]::-moz-range-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
background: #ffffff;
border-radius: 1.3px;
border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type=range]::-moz-range-thumb {
box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(13, 13, 13, 0);
height: 15px;
width: 15px;
border-radius: 50px;
background: #ffc600;
cursor: pointer;
}

scripts-FINISHED.js

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
47
48
49
50
51
52
53
54
55
/* Get Our Elements */
const player = document.querySelector('.player');
const video = player.querySelector('.viewer');
const progress = player.querySelector('.progress');
const progressBar = player.querySelector('.progress__filled');
const toggle = player.querySelector('.toggle');
const skipButtons = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player__slider');

/* Build out functions */
function togglePlay() {
const method = video.paused ? 'play' : 'pause';
video[method]();
}

function updateButton() {
const icon = this.paused ? '►' : '❚ ❚';
console.log(icon);
toggle.textContent = icon;
}

function skip() {
video.currentTime += parseFloat(this.dataset.skip);
}

function handleRangeUpdate() {
video[this.name] = this.value;
}

function handleProgress() {
const percent = (video.currentTime / video.duration) * 100;
progressBar.style.flexBasis = `${percent}%`;
}

function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}

/* Hook up the event listeners */
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
video.addEventListener('timeupdate', handleProgress);

toggle.addEventListener('click', togglePlay);
skipButtons.forEach(button => button.addEventListener('click', skip));
ranges.forEach(range => range.addEventListener('change', handleRangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate));

let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);



學習筆記


★ object[method]

在這堂課中,看到作者用以下的方法簡化程式碼,值得參考。

  • 原本的 if 判斷式
1
2
3
4
5
if (video.paused){
video.play();
}else{
video.pause();
}
  • 改成三元運算式

    object.method() 寫成 object[method](),可以將 method 以變數表示,此處可替換成 play 或 pause 的字串

1
2
const method = video.paused ? 'play' : 'pause';
video[method]();
  • 直接把三元運算子寫在 [] 中,很酷但不太容易閱讀XD
1
video[video.paused ? 'play' : 'pause']();

★ textcontent

textContent 屬性設置或返回指定節點的文本內容,以及它的所有後代。

*注意:如果設置了 textContent 屬性,會刪除所有子節點,並被替換為包含指定字串的一個單獨的文本節點。

範例:

1
2
3
4
5
<p id="test">文字</p>
<script>
document.getElementById('test').textContent = "修改後的文字";
// 會輸出 <p id="test">修改後的文字</p>
</script>

關於 innerText 與 textContent 的區別可以參考這篇文章:https://juejin.im/post/5b755bcbf265da27de7e7db7


★ parseFloat()

parseFloat 是全域函式,不屬於任何物件。它將字串參數解析成為浮點數並返回。

如果在解析過程中遇到了正負號(+-)、數字(0-9)、小數點,或者科學記數法中的指數(eE)以外的字符,則它會忽略該字符以及之後的所有字符,返回當前已經解析到的浮點數。同時參數字串首位的空白會被忽略。如果參數字串的第一個字符不能被解析成為數字,則返回 NaN

*如果想解析整數,可以使用 parseInt()

範例:

1
2
3
4
5
6
document.write(parseFloat("10.00"))      // 10
document.write(parseFloat("10.33")) // 10
document.write(parseFloat("34 45 66")) // 34
document.write(parseFloat(" 60 ")) // 60
document.write(parseFloat("40 years")) // 40
document.write(parseFloat("He was 40")) // NaN

更多全域物件:https://www.w3school.com.cn/jsref/jsref_obj_global.asp