竹白記事本,Javascript 30,紀錄。
Javascript 30
模擬一個打鼓的頁面。用戶在鍵盤上按下 ASDFGHJKL 這幾個鍵時,頁面上與字母對應的按鈕變大變亮,對應的鼓點聲音響起來。
classList
屬性
add
方法remove
方法addEventListener()
事件監聽
keydown
事件transitionend
事件KeyboardEvent
介面
keyCode
屬性TransitionEvent
介面
propertyName
屬性Event
介面
currentTarget
屬性target
屬性在 HTML 中,已經使用 data-key
,定義了對應的 keyCode
屬性。
當使用鍵盤相關事件時,event
物件具備一個 keyCode
屬性,可以得知當下所按下的按鍵為何按鍵按鈕。它回傳的值是 ASCII
而鍵盤事件的差異:
kepdown
keyCode
keypress
keyCode
值keyup
keyCode
的部分與 kepdown
相同觸發優先順序由上到下,keydown
→ keypress
→ keyup
。
JavaScript Event KeyCodes 查看鍵盤對應的
keyCode
。
使用 currentTime
將聲音歸 0
。
當按下按鈕的動畫結束時,移除樣式的 classname
。
所以要使用 transitionend
事件的 event
物件所繼承的 propertyName
屬性來判斷,特定 CSS 屬性已完成動畫顯示。
這裡要注意,CSS 屬性不只一個在樣式變化,所以一定要指定特定 CSS 屬性,否則會一直重複觸發,你所要執行的動作。
關於鍵盤、卷軸、螢幕旋轉等,基本上都會在 window
下進行事件監聽。
因此這邊在 window
下新增一個監聽 keydown
事件,當事件發時觸發一個函式。
window.addEventListener('keydown', playHandler);
playHandler
宣告函式:
function playHandler(e) {}
通常用參數 e
代表 event
,e
會接收 keydown
事件。
這個函式需要做兩件事:
HTML 已經用 data-key
寫好對應的 keyCode
元件,因此只要透過 querySelector
來綁定就好了。
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const dom = document.querySelector(`div[data-key="${e.keyCode}"]`);
這裡使用 ES6 的樣板字面值(Template literals)來選取,如果不使用就是:
'audio[data-key="' + e.keyCode + '"]'
'div[data-key="' + e.keyCode + '"]'
再來為了防止按下其他按鍵而找不到對應的元件時出錯,因此要加入判斷式。
使用肯定語句,將內容寫在裡面,或是否定語句,找不到直接離開,將內容寫在下方。
if (audio) { ... }
if (dom) { ... }
// or
if (!audio) {return;}
if (!dom) {return;}
...
...
撥放音樂與新增 CSS 樣式:
audio.currentTime = 0; // 每次撥放音樂都重頭開始
audio.play(); // 播放
dom.classList.add('playing'); // 新增 .playing 類別
最終 playHandler()
函:
function playHandler(e) {
const audio = document.querySelector(
`audio[data-key="${e.keyCode}"]`
);
const dom = document.querySelector(
`div[data-key="${e.keyCode}"]`
);
if (audio) {
audio.currentTime = 0;
audio.play();
}
if (dom) {
dom.classList.add('playing');
}
}
transitionend
事件監聽.playing
類別是一個效果,每當觸發完後,必須移除畫面效果,因此必監聽所有的畫面元件,查看動畫是否結束了。
document.querySelectorAll('.key').forEach(function(key) {
key.addEventListener('transitionend', removeTransition);
});
querySelectorAll
得到的是一個 NodeList
不是陣列,但 NodeList
還是有 forEach()
方法可使用。為每個元件加入監聽 transitionend
事件。
注意,這裡是因為剛好 NodeList
有 forEach()
,如果是 map()
還是其他陣列方法,NodeList
物件沒有的方法,一定要將它轉成陣列成能使用陣列的方法。
removeTransition
transitionend
事件如果有很多屬性,會重複觸發,因此需要挑一個屬性來判斷是否完成,這裡選擇 transform
屬性來判斷,如果完成了就移除 .playing
類別。
function removeTransition(e) {
if (e.propertyName === 'transform') {
e.currentTarget.classList.remove('playing');
}
}
這裡使用 target
或 currentTarget
都可以,但兩者也些微差異。
currentTarget
是擁有監聽事件的 DOM 物件,而 target
是觸發事件的 DOM 物件。
因此如果使用事件委派就要注意,關於事件委派將在下方試著改寫此範例。
(function() {
function playHandler(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const dom = document.querySelector(`div[data-key="${e.keyCode}"]`);
if (audio) {
audio.currentTime = 0;
audio.play();
}
if (dom) {
dom.classList.add('playing');
}
}
function removeTransition(e) {
if (e.propertyName === 'transform') {
e.currentTarget.classList.remove('playing');
}
}
window.addEventListener('keydown', playHandler);
document.querySelectorAll('.key').forEach(function(key) {
key.addEventListener('transitionend', removeTransition);
});
})();
將步驟 3 與 4 改成事件委派,可以減少監聽事件。
function removeTransition(e) {
// 1
if (!e.target.classList.contains('key')) {
return;
}
if (e.propertyName === 'transform') {
// 2
e.target.classList.remove('playing');
}
}
const keys = document.querySelector('.keys');
keys.addEventListener('transitionend', removeTransition);
if (!e.target.classList.contains('key')) { return; }
用來判斷觸發的 DOM 物件是否擁有 .key
類別,如果沒有就跳出。target
來判斷觸發的 DOM 物件,而不是本來使用的 currentTarget
因為監聽事件改用父元素了。新增使用滑鼠點選畫面也能觸發音樂的監聽器。
const keys = document.querySelector('.keys');
// 1
keys.addEventListener('click', clickHandler);
function clickHandler(e) {
if (!e.target.classList.contains('key')) {
return;
}
// 2
const keyCode = e.target.dataset.key;
const audio = document.querySelector(`audio[data-key="${keyCode}"]`);
// 3
audio.currentTime = 0;
audio.play();
e.target.classList.add('is-playing');
}
.keys
,在一樣使用事件委派模式。event
物件沒有 keyCode
屬性,所以可以利用 dataset
來取得對應的 keyCode
值。這裡有一個地方要注意,因為使用事件委派,所以只有點選擁有 .key
類別的 DOM 物件會觸發,所以必須將 .key
底下的子、子孫元素都虛擬化,避免點選到。
.key * {
pointer-events: none;
}