# Day1:JavaScript Drum Kit [竹白記事本](https://chupainotebook.blogspot.com/),Javascript 30,紀錄。 ###### tags: `Javascript 30` ## 實現效果 模擬一個打鼓的頁面。用戶在鍵盤上按下 ASDFGHJKL 這幾個鍵時,頁面上與字母對應的按鈕變大變亮,對應的鼓點聲音響起來。 - [原始碼](https://github.com/chupai/JS30/tree/master/source_code/Day01) - [原始狀態](https://chupai.github.io/JS30/source_code/Day01/index-START.html) - [範例效果](https://chupai.github.io/JS30/source_code/Day01/index-FINISHED.html) ## 重點 1. 鍵盤事件 2. 播放聲音 3. 改變樣式 ## 基礎語法 ### HTML - [`data-*`](https://developer.mozilla.org/zh-TW/docs/Web/HTML/Global_attributes/data-*) 自訂 `data` 屬性的屬性 - 這邊用於存放對應的 `keyCode` 值 - [`<kbd>`](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/kbd) HTML 鍵盤輸入元素 ### JavaScript - ES6 的 [Template literals](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Template_literals) ### DOM - [`classList`](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/classList) 屬性 - `add` 方法 - `remove` 方法 ### Event - [`addEventListener()`](https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget/addEventListener)事件監聽 - [`keydown`](https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event) 事件 - [`transitionend`](https://developer.mozilla.org/zh-CN/docs/Web/Events/transitionend) 事件 - [`KeyboardEvent`](https://developer.mozilla.org/zh-TW/docs/Web/API/KeyboardEvent) 介面 - [`keyCode`](https://developer.mozilla.org/zh-TW/docs/Web/API/KeyboardEvent/keyCode) 屬性 - [`TransitionEvent`](https://developer.mozilla.org/zh-CN/docs/Web/API/TransitionEvent) 介面 - [`propertyName`](https://developer.mozilla.org/zh-CN/docs/Web/API/TransitionEventt#属性) 屬性 - [`Event`](https://developer.mozilla.org/zh-TW/docs/Web/API/Event) 介面 - [`currentTarget`](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/currentTarget) 屬性 - [`target`](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/target) 屬性 ## 說明 ### 1. 鍵盤按鍵與畫面連接 在 HTML 中,已經使用 `data-key`,定義了對應的 `keyCode` 屬性。 當使用鍵盤相關事件時,`event` 物件具備一個 `keyCode` 屬性,可以得知當下所按下的按鍵為何按鍵按鈕。它回傳的值是 ASCII 而鍵盤事件的差異: - `kepdown` - 按下按鍵時觸發,並取得對應的 `keyCode` - 當按下鍵盤不放時,則會不斷地連續觸發該事件 - `keypress` - 只會針對可以輸出文字符號的按鍵有效(方向鍵、ESC 等等無法觸發事件) - 會因為大小寫取得不同的 `keyCode` 值 - 一樣會因為按下不放時,連續觸發事件 - `keyup` - 釋放按鍵時觸發,取得 `keyCode` 的部分與 `kepdown` 相同 觸發優先順序由上到下,`keydown` → `keypress` → `keyup`。 >[JavaScript Event KeyCodes](https://keycode.info/) 查看鍵盤對應的 `keyCode`。 ### 2. 保證按下按鍵,聲音重頭撥放 使用 [`currentTime`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/currentTime) 將聲音歸 `0`。 ### 3. 將按鍵樣式恢復 當按下按鈕的動畫結束時,移除樣式的 `classname`。 所以要使用 `transitionend` 事件的 `event` 物件所繼承的 `propertyName` 屬性來判斷,特定 CSS 屬性已完成動畫顯示。 這裡要注意,CSS 屬性不只一個在樣式變化,所以一定要指定特定 CSS 屬性,否則會一直重複觸發,你所要執行的動作。 ## 實作 ### 1. 步驟 #### Step 1 鍵盤事件監聽 關於鍵盤、卷軸、螢幕旋轉等,基本上都會在 `window` 下進行事件監聽。 因此這邊在 `window` 下新增一個監聽 `keydown` 事件,當事件發時觸發一個函式。 ```javascript window.addEventListener('keydown', playHandler); ``` #### Step 2 建立函式 `playHandler` 宣告函式: ```javascript function playHandler(e) {} ``` 通常用參數 `e` 代表 `event`,`e` 會接收 `keydown` 事件。 這個函式需要做兩件事: 1. 播放聲音 2. 控制 DOM 樣式 HTML 已經用 `data-key` 寫好對應的 `keyCode` 元件,因此只要透過 `querySelector` 來綁定就好了。 ```javascript const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`); const dom = document.querySelector(`div[data-key="${e.keyCode}"]`); ``` 這裡使用 ES6 的樣板字面值(Template literals)來選取,如果不使用就是: ```javascript 'audio[data-key="' + e.keyCode + '"]' 'div[data-key="' + e.keyCode + '"]' ``` 再來為了防止按下其他按鍵而找不到對應的元件時出錯,因此要加入判斷式。 使用肯定語句,將內容寫在裡面,或是否定語句,找不到直接離開,將內容寫在下方。 ```javascript if (audio) { ... } if (dom) { ... } // or if (!audio) {return;} if (!dom) {return;} ... ... ``` 撥放音樂與新增 CSS 樣式: ```javascript audio.currentTime = 0; // 每次撥放音樂都重頭開始 audio.play(); // 播放 dom.classList.add('playing'); // 新增 .playing 類別 ``` 最終 `playHandler()` 函: ```javascript 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` 類別是一個效果,每當觸發完後,必須移除畫面效果,因此必監聽所有的畫面元件,查看動畫是否結束了。 ```javascript document.querySelectorAll('.key').forEach(function(key) { key.addEventListener('transitionend', removeTransition); }); ``` `querySelectorAll` 得到的是一個 [`NodeList`](https://developer.mozilla.org/zh-TW/docs/Web/API/NodeList) 不是陣列,但 `NodeList` 還是有 `forEach()` 方法可使用。為每個元件加入監聽 ` transitionend` 事件。 **注意**,這裡是因為剛好 `NodeList` 有 `forEach()`,如果是 `map()` 還是其他陣列方法,`NodeList` 物件沒有的方法,一定要將它轉成陣列成能使用陣列的方法。 #### Step 4 建立函式 `removeTransition` `transitionend` 事件如果有很多屬性,會重複觸發,因此需要挑一個屬性來判斷是否完成,這裡選擇 `transform` 屬性來判斷,如果完成了就移除 `.playing` 類別。 ```javascript function removeTransition(e) { if (e.propertyName === 'transform') { e.currentTarget.classList.remove('playing'); } } ``` 這裡使用 `target` 或 `currentTarget` 都可以,但兩者也些微差異。 `currentTarget` 是擁有監聽事件的 DOM 物件,而 `target` 是觸發事件的 DOM 物件。 因此如果使用事件委派就要注意,關於事件委派將在下方試著改寫此範例。 #### Step END ```javascript (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 改成事件委派,可以減少監聽事件。 ```javascript 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 事件來處發音樂 新增使用滑鼠點選畫面也能觸發音樂的監聽器。 ```javascript 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` 底下的子、子孫元素都虛擬化,避免點選到。 ```css .key * { pointer-events: none; } ``` ### 3. 實作連結 - [初版](https://chupai.github.io/JS30/source_code/Day01/index1.html) - [使用事件委派並加上 Click 事件](https://chupai.github.io/JS30/source_code/Day01/index2.html) - [自訂 CSS 樣式](https://chupai.github.io/JS30/source_code/Day01/index.html)