Try   HackMD

Day1:JavaScript Drum Kit

竹白記事本,Javascript 30,紀錄。

tags: Javascript 30

實現效果

模擬一個打鼓的頁面。用戶在鍵盤上按下 ASDFGHJKL 這幾個鍵時,頁面上與字母對應的按鈕變大變亮,對應的鼓點聲音響起來。

重點

  1. 鍵盤事件
  2. 播放聲音
  3. 改變樣式

基礎語法

HTML

  • data-* 自訂 data 屬性的屬性
    • 這邊用於存放對應的 keyCode
  • <kbd> HTML 鍵盤輸入元素

JavaScript

DOM

Event

說明

1. 鍵盤按鍵與畫面連接

在 HTML 中,已經使用 data-key,定義了對應的 keyCode 屬性。

當使用鍵盤相關事件時,event 物件具備一個 keyCode 屬性,可以得知當下所按下的按鍵為何按鍵按鈕。它回傳的值是 ASCII

而鍵盤事件的差異:

  • kepdown
    • 按下按鍵時觸發,並取得對應的 keyCode
    • 當按下鍵盤不放時,則會不斷地連續觸發該事件
  • keypress
    • 只會針對可以輸出文字符號的按鍵有效(方向鍵、ESC 等等無法觸發事件)
    • 會因為大小寫取得不同的 keyCode
    • 一樣會因為按下不放時,連續觸發事件
  • keyup
    • 釋放按鍵時觸發,取得 keyCode 的部分與 kepdown 相同

觸發優先順序由上到下,keydownkeypresskeyup

JavaScript Event KeyCodes 查看鍵盤對應的 keyCode

2. 保證按下按鍵,聲音重頭撥放

使用 currentTime 將聲音歸 0

3. 將按鍵樣式恢復

當按下按鈕的動畫結束時,移除樣式的 classname

所以要使用 transitionend 事件的 event 物件所繼承的 propertyName 屬性來判斷,特定 CSS 屬性已完成動畫顯示。

這裡要注意,CSS 屬性不只一個在樣式變化,所以一定要指定特定 CSS 屬性,否則會一直重複觸發,你所要執行的動作。

實作

1. 步驟

Step 1 鍵盤事件監聽

關於鍵盤、卷軸、螢幕旋轉等,基本上都會在 window 下進行事件監聽。

因此這邊在 window 下新增一個監聽 keydown 事件,當事件發時觸發一個函式。

window.addEventListener('keydown', playHandler);

Step 2 建立函式 playHandler

宣告函式:

function playHandler(e) {}

通常用參數 e 代表 evente 會接收 keydown 事件。

這個函式需要做兩件事:

  1. 播放聲音
  2. 控制 DOM 樣式

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');
  }
}

Step 3 transitionend 事件監聽

.playing 類別是一個效果,每當觸發完後,必須移除畫面效果,因此必監聽所有的畫面元件,查看動畫是否結束了。

document.querySelectorAll('.key').forEach(function(key) {
  key.addEventListener('transitionend', removeTransition);
});

querySelectorAll 得到的是一個 NodeList 不是陣列,但 NodeList 還是有 forEach() 方法可使用。為每個元件加入監聽 transitionend 事件。

注意,這裡是因為剛好 NodeListforEach(),如果是 map() 還是其他陣列方法,NodeList 物件沒有的方法,一定要將它轉成陣列成能使用陣列的方法。

Step 4 建立函式 removeTransition

transitionend 事件如果有很多屬性,會重複觸發,因此需要挑一個屬性來判斷是否完成,這裡選擇 transform 屬性來判斷,如果完成了就移除 .playing 類別。

function removeTransition(e) {
  if (e.propertyName === 'transform') {
    e.currentTarget.classList.remove('playing');
  }
}

這裡使用 targetcurrentTarget 都可以,但兩者也些微差異。

currentTarget 是擁有監聽事件的 DOM 物件,而 target 是觸發事件的 DOM 物件。

因此如果使用事件委派就要注意,關於事件委派將在下方試著改寫此範例。

Step END

(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);
  });

})();

2. 進階

2.1 事件委派

將步驟 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);
  1. if (!e.target.classList.contains('key')) { return; } 用來判斷觸發的 DOM 物件是否擁有 .key 類別,如果沒有就跳出。
  2. 由於監聽器只有一個,所以要使用 target 來判斷觸發的 DOM 物件,而不是本來使用的 currentTarget 因為監聽事件改用父元素了。

2.2 加入 Click 事件來處發音樂

新增使用滑鼠點選畫面也能觸發音樂的監聽器。

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');
}
  1. 將監聽器下 .keys,在一樣使用事件委派模式。
  2. click 事件的 event 物件沒有 keyCode 屬性,所以可以利用 dataset 來取得對應的 keyCode 值。
  3. 由於點選的元件一定存在,所以這次不用去判斷 DOM 物件是否存在。

這裡有一個地方要注意,因為使用事件委派,所以只有點選擁有 .key 類別的 DOM 物件會觸發,所以必須將 .key 底下的子、子孫元素都虛擬化,避免點選到。

.key * {
  pointer-events: none;
}

3. 實作連結