[TOC] [JS30 影片連結](https://youtube.com/playlist?list=PLu8EoSxDXHP6CGK4YVJhL_VWetA865GOH&si=01oY9gemI7BCC9Ex) [JS30 github教材下載](https://github.com/wesbos/JavaScript30) ## 01 Make a JavaScript Drum Kit 做什麼事: 1. 當按下每個鍵時,就會找到對應的元素並播放音檔 2. 按下需要有transition動畫 ### keycode 按下鍵盤的鍵,會有對應的數字 [網站](https://www.toptal.com/developers/keycode) ### html 上的 data-* attribute 可以自己設定? 資料屬性可以額外儲存在 html 的標籤裡,不會影響樣式,以 `data-` 為前綴,並透過 JS 方便存取。 範例: html有一個 button , data attribute 自定義為 `data-user-id="12345"` 和 `data-user-role="admin"`。須注意 data- 後面不能大寫字母,一律小寫 ```html! <button id="loginButton" data-user-id="12345" data-user-role="admin"> Login </button> ``` 用JS存取: ```javascript! // 1. 用dataset // 須將自定義名稱改成駝峰命名 const button = document.getElementById("loginButton"); console.log(button.dataset.userId); // "12345" console.log(button.dataset.userRole); // "admin" // 2. 用getAttribute讀取 console.log(button.getAttribute("data-user-id")); // "12345" // 3. 如要設置,用setAttribute button.setAttribute("data-user-role", "editor"); console.log(button.getAttribute("data-user-role")); // "editor" ``` [MDN - Using data attributes](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Solve_HTML_problems/Use_data_attributes) [什麼是 HTML 5 中的資料屬性(data-* attribute)](https://pjchender.dev/html/html-data-attribute/) ### addEventListener #### keydown 事件 ```javascript! window.addEventListener("keydown", (keyboardInput) => { console.log(keyboardInput); }); ``` 按下 g 鍵,會印出 keyboardEvent 物件,裡面有此事件的資料,找到`keyCode:71`,就是 g 鍵的對應數字,屬性`key:"G"`代表按大寫 G ![image](https://hackmd.io/_uploads/Sye2hFJ8yl.png) 所以利用此屬性,來抓取 keyCode 值: ```javascript! window.addEventListener("keydown", (keyboardInput) => { console.log(keyboardInput.keyCode); }); ``` ![image](https://hackmd.io/_uploads/BJ6TnKy8kg.png) JS有幾種事件種類,參考資料如下連結: [MDN - Event reference](https://developer.mozilla.org/en-US/docs/Web/Events) [重新認識 JavaScript: Day 16 那些你知道與不知道的事件們](https://ithelp.ithome.com.tw/articles/10192175) ### querySelector ```javascript! window.addEventListener("keydown", (keyboardInput) => { // 取得 `<audio>`且屬性為[data-key="按下的鍵的keyCode"] const audio = document.querySelector( `audio[data-key="${keyboardInput.keyCode}"]` ); console.log(audio); }); ``` 印出結果: 取得 audio 標籤且屬性有符合的 ![image](https://hackmd.io/_uploads/BkJkatJU1l.png) 如果沒有對應元素,就會跳出 `null` ### currentTime 為什麼連點很多下按鍵才播放一次,因為音檔還在播放中,此時又再按,他就不會再播放,所以在`audio.play()`之前,先加上`audio.currentTime = 0`,讓音檔從頭開始播放。 ```javascript! window.addEventListener("keydown", (keyboardInput) => { const audio = document.querySelector( `audio[data-key="${keyboardInput.keyCode}"]` ); if (!audio) return; // 如果為null就結束 audio.currentTime = 0; // 讓音檔從頭開始 audio.play(); }); ``` ### classList 為了按下後會有動畫,取得 class 為"key"的標籤 ```javascript! const key = document.querySelector( `.key[data-key="${keyboardInput.keyCode}"]` ); ``` 在這個標籤上,新增 class 屬性 `"playing"` `.playing`是原先就寫好的css ```javascript! key.classList.add("playing"); ``` 就會在此標籤的 class 上新增 `playing` ![image](https://hackmd.io/_uploads/BJfeptkUJx.png) ### querySelectorAll 按完後要讓黃框消失,監聽動畫結束,讓每個按鍵都會被監聽,使用 `querySelectorAll` ```javascript! const keys = document.querySelectorAll(".key"); console.log(keys); ``` `querySelectorAll` 拿到是一個 NodeList 節點,是==array-like==,展開`[[Prototype]]`查看,可以知道 NodeList 無法使用所有的陣列方法。 ![image](https://hackmd.io/_uploads/HyBb6tyIke.png) 按下按鍵會跳出6個TransitionEvent,作者找最後一個`propertyName:"transform"` ![image](https://hackmd.io/_uploads/ryxzatJIJl.png) ```javascript! function removeTransition(e) { if (e.propertyName !== "transform") return; // this就是forEach的key // 把playing移除 this.classList.remove("playing"); } // 監聽動畫結束後,執行移除transition const keys = document.querySelectorAll(".key"); keys.forEach((key) => { key.addEventListener("transitionend", removeTransition); }); ``` 完整程式碼: ```javascript! function playSound(keyboardInput) { const audio = document.querySelector( `audio[data-key="${keyboardInput.keyCode}"]` ); const key = document.querySelector( `.key[data-key="${keyboardInput.keyCode}"]` ); if (!audio) return; audio.currentTime = 0; audio.play(); key.classList.add("playing"); } function removeTransition(e) { if (e.propertyName !== "transform") return; this.classList.remove("playing"); // this就是forEach的key } // 監聽動畫結束後,執行移除transtion const keys = document.querySelectorAll(".key"); keys.forEach((key) => { key.addEventListener("transitionend", removeTransition); }); window.addEventListener("keydown", playSound); ``` ## 02 We build a CSS + JS Clock ### transform-origin 透過 `transform-origin` 設定中心點位置,一般起始點為物件的中心。 ### transition-timing-function 漸變函式(transition timing function)漸變函式可用來定義轉場發生的時間曲線。 其規範方式是以四個參數的貝茲曲線代表。 ### Date Date 作為建構函式呼叫後,會回傳一個新的 Date 物件,會回傳當前日期和時間的**字串**。 [MDN - Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) 在 `Date.prototype` 可以查到有關日期和時間共用的實例方法: ![image](https://hackmd.io/_uploads/S1XRiCJIyg.png) - getSeconds(): 根據當地時間傳回指定日期的對應秒數 - getMinutes(): 根據當地時間傳回指定日期的對應分鐘 - getHours(): 根據當地時間傳回指定日期的對應小時 ```javascript! const now = new Date(); const hour = now.getHours(); const minute = now.getMinutes(); const second = now.getSeconds(); console.log(hour); // 16 console.log(minute); // 54 console.log(second); // 30 ``` 所以作者要做出時鐘秒針的動作,設置以下: ```javascript! function setTime() { const now = new Date(); // 現在時間 const seconds = now.getSeconds(); // now是一個實例,就可以調用getSeconds方法來取得現在秒數 } setInterval(setTime, 1000); ``` 設置秒數佔的角度的百分比 因為原本指向9,所以要把角度指回12,所以再加上90度 ```javascript! const secondsDegrees = (seconds / 60) * 360 + 90; ``` 最後選取元素,設置此元素的 `style.transform` ```javascript! secondHand.style.transform = `rotate(${secondsDegrees}deg)`; ``` 分針、時針也都跟秒針一樣的寫法 完整程式碼: ```javascript! const secondHand = document.querySelector(".second-hand"); const minuteHand = document.querySelector(".min-hand"); const hourHand = document.querySelector(".hour-hand"); function setTime() { const now = new Date(); // 現在時間 const seconds = now.getSeconds(); // now是一個實例,就可以調用getSeconds方法 const secondsDegrees = (seconds / 60) * 360 + 90; secondHand.style.transform = `rotate(${secondsDegrees}deg)`; console.log(seconds); const minutes = now.getMinutes(); const minutesDegrees = (minutes / 60) * 360 + 90; minuteHand.style.transform = `rotate(${minutesDegrees}deg)`; const hours = now.getHours(); const hourDegrees = (hours / 12) * 360 + 90; hourHand.style.transform = `rotate(${hourDegrees}deg)`; } setInterval(setTime, 1000); ``` 但會發現秒針到60秒時候會倒轉歸零,所以計算的時間要累加上去,而不是60秒又歸零重跑。(參考大家的筆記,還不是很懂,6 30是什麼?) ```javascript! const secondHand = document.querySelector(".second-hand"); const minuteHand = document.querySelector(".min-hand"); const hourHand = document.querySelector(".hour-hand"); function setTime() { const now = new Date(); // 現在時間 const seconds = now.getSeconds(); const secondsDegrees = (seconds / 60) * 360 + 90; secondHand.style.transform = `rotate(${secondsDegrees}deg)`; console.log(seconds); const minutes = now.getMinutes(); const minutesDegrees = (minutes / 60) * 360 + (seconds / 60) * 6 + 90; minuteHand.style.transform = `rotate(${minutesDegrees}deg)`; const hours = now.getHours(); const hourDegrees = (hours / 12) * 360 + (minutes / 60) * 30 + 90; hourHand.style.transform = `rotate(${hourDegrees}deg)`; } setInterval(setTime, 1000); ``` ## 03 Woah! CSS Variables?! 透過 JS 來調整網頁上的元素參數 ### 設定css 變數 作者先在 style 裡面設定 `:root` 變數,並套用到 `.hl` 和 `img` 上 ```css! :root { --base: #ffc600; --spacing: 30px; --blur: 5px; } .hl { color: var(--base); } img { background: var(--base); padding: var(--spacing); filter: blur(var(--blur)); } ``` ### dataset 是一個物件,會取得元素上有 `data-*`屬性的所有內容 範例: ```html! <div class="controls"> <input type="text" value="Test" data-sizing = "px" data-type = "spacing"> </div> ``` input 自定義了 `data-sizing = "px"` ` data-type = "spacing"` ```javascript! const data = document.querySelector(".controls input"); const input = data.dataset; console.log(input); // { "sizing": "px", "type": "spacing"} ``` 選取到 input 元素後,使用 dataset 把此 input 元素上的 data-* 屬性回傳成一個物件。 如需要某 data-* 屬性上的資料: ```javascript! const data = document.querySelector(".controls input"); const sizing = data.dataset.sizing; // 用物件方法取值 console.log(sizing); // "px" ``` [MDN - HTMLElement: dataset property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) 作者為了要取得 suffix 後綴,也就是 px ,所以在 html 的 input 標籤上自定義了 `data-sizing="px"` ```javascript! const inputs = document.querySelectorAll(".controls input"); function handleUpdate() { const suffix = this.dataset.sizing || ""; } inputs.forEach((input) => input.addEventListener("change", handleUpdate)); inputs.forEach((input) => input.addEventListener("mousemove", handleUpdate)); ``` `this.dataset.sizing || ""` 為什麼是 或為空? 因為顏色他不需要取得 px,所以設定為空 那 `this.dataset` 的 this 是指向誰? this 指向哪個對象取決於 handleUpdate 函式是如何被調用的。 當 change 事件被觸發時,handleUpdate 函式會被執行,且 this 會指向觸發該事件的 `<input> 元素`。 ### document.documentElement & style.setProperty 最後設定 `<html>` 的 style 屬性 ```javascript! document.documentElement.style.setProperty( `--${this.name}`, this.value + suffix ); ``` - `document.documentElement` 是專門用來操作 `根元素 <html>`,如果要操作其他元素就用`document.querySelector()`或 `document.querySelectorAll()`之類的。 - `style.setProperty` 用來設定元素的 inline style 的方法 ```javascript! element.style.setProperty(propertyName, value); ``` 也就是當滑鼠移動 input 就會把當下的值加在`<html>` 的 style 屬性,如下圖劃線處 ![image](https://hackmd.io/_uploads/rJyFvXZUyx.png) ## 04 Array Cardio Practice - Day 1 練習陣列的方法~ ### Array.prototype.filter() 1. Filter the list of inventors for those who were born in the 1500's ```javascript! const fiften = inventors.filter( (inventor) => inventor.year >= 1500 && inventor.year < 1600 ); console.table(fiften); ``` ![image](https://hackmd.io/_uploads/SJxBhEb8Jg.png) ### Array.prototype.map() 2. Give us an array of the inventors first and last names ```javascript! const fullName = inventors.map( (inventor) => `${inventor.first} ${inventor.last}` ); console.log(fullName); ``` ![image](https://hackmd.io/_uploads/r1WxyBZ8yl.png) ### Array.prototype.sort() 3. Sort the inventors by birthdate, oldest to youngest [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) ```javascript! const ordered = inventors.sort((a, b) => (a.year > b.year ? 1 : -1)); console.log(ordered); ``` ![image](https://hackmd.io/_uploads/S13ekrbI1l.png) ### Array.prototype.reduce() 4. How many years did all the inventors live all together? ```javascript! const totalYears = inventors.reduce((accumulator, currentValue) => { return accumulator + (currentValue.passed - currentValue.year); }, 0); console.log(totalYears); // 861 ``` totalYears 跟影片算出來不一樣,看了大家筆記,原來是 inventors 數量不同 5. Sort the inventors by years lived ```javascript! const oldest = inventors.sort((a, b) => { const lastLive = a.passed - a.year; const nextLive = b.passed - b.year; return lastLive - nextLive; }); console.table(oldest); ``` ![image](https://hackmd.io/_uploads/HkDW1SWUyg.png) 6. create a list of Boulevards in Paris that contain 'de' anywhere in the name. [操作網站](https://en.wikipedia.org/wiki/Category:Boulevards_in_Paris) 這題需要在此網站用devTools操作,所有項目都放在 `class="mw-category"`的位置 ![image](https://hackmd.io/_uploads/S1riaam8Je.png) 先選取 `class="mw-category"`,再選取category裡面所有的 `<a>` ```javascript! const category = document.querySelector(".mw-category"); const links = category.querySelectorAll("a"); ``` 因為 `querySelectorAll`結果為 arry-like ,所以透過 `Array.from()` 或是 `展開運算子` 將arry-like轉成陣列。 ```javascript! // 使用Array.from() const links = Array.from(category.querySelectorAll("a")); // 使用展開運算子 const links = [...category.querySelectorAll("a")]; ``` ![image](https://hackmd.io/_uploads/BJN26TmU1e.png) 最後把`<a>`裡面文字取出,並用字串方法 `includes()` 篩選文字中有包含 `de` 的 ```javascript! const linkTexts = links .map((link) => link.textContent) .filter((streetName) => streetName.includes("de")); ``` ![image](https://hackmd.io/_uploads/B1qa6aQU1l.png) 7. sort Exercise Sort the people alphabetically by last name (用people陣列把 last name 排序) ```javascript! const alpha = people.sort((lastOne, nextOne) => { const [aLast, aFirst] = lastOne.split(", "); const [bLast, bFirst] = nextOne.split(", "); return aLast > bLast ? 1 : -1; }); console.log(alpha); ``` 比較排序會從第一個字母開始,如相同就比第二個 ![image](https://hackmd.io/_uploads/BJO1Rp781x.png) 8. Reduce Exercise Sum up the instances of each of these 有一個data陣列,要計算每個項目的數量 ```javascript! const data = [ "car", "car", "truck", "truck", "bike", "walk", "car", "van", "bike", "walk", "car", "van", "car", "truck", ]; ``` ```javascript const totalItemList = data.reduce((obj, item) => { if (!obj[item]) { // 如果沒有此item,就新增item,且初始值設置為0 obj[item] = 0; } obj[item]++; return obj; }, {}); console.log(totalItemList); // {"car": 5,"truck": 3,"bike": 2,"walk": 2,"van": 2} ``` 好實用的方法!! ## 05 Flexbox + JavaScript Image Gallery 先將排版排好,使用flex ```css! .panels { min-height: 100vh; overflow: hidden; display: flex; /* 手動新增 */ } .panel { background: #6b0f9c; box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.1); color: white; text-align: center; align-items: center; /* Safari transitionend event.propertyName === flex */ /* Chrome + FF transitionend event.propertyName === flex-grow */ transition: font-size 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), flex 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), background 0.2s; font-size: 20px; background-size: cover; background-position: center; /* 以下手動新增 */ display: flex; flex-direction: column; flex: 1; justify-content: center; } .panel > * { margin: 0; width: 100%; transition: transform 0.5s; /* 以下手動新增 */ display: flex; justify-content: center; align-items: center; flex: 1 0 auto; } ``` 設定完會長這樣~ ![image](https://hackmd.io/_uploads/HJMRMJ481x.png) 再來設定 上下的文字預設在框外,加上 `open-active` 樣式再進來框內 ```css! .panel > *:first-child { transform: translateY(-100%); } .panel > *:last-child { transform: translateY(100%); } .panel.open-active > *:first-child { transform: translateY(0); } .panel.open-active > *:last-child { transform: translateY(0); } ``` 上面的字在Y軸 -100% 位置,下面的字Y軸 100% 位置,所以看不到他們 ![image](https://hackmd.io/_uploads/Byo5t_IUkx.png) 因為要點擊時候讓那圖片框和字體變大,在 `.panel.open` 的css設置: ```css! .panel.open { flex: 5; font-size: 40px; } ``` ![image](https://hackmd.io/_uploads/HklRYuILkl.png) 再來進入到設置JS,設計點擊時候,自動新增或刪除class 完整程式碼: ```javascript! const panels = document.querySelectorAll(".panel"); function toggleOpen() { this.classList.toggle("open"); } function toggleActive(e) { console.log(e); if (e.propertyName.includes("flex")) { this.classList.toggle("open-active"); } } panels.forEach((panel) => panel.addEventListener("click", toggleOpen)); panels.forEach((panel) => panel.addEventListener("transitionend", toggleActive) ); ``` 當click時,觸發toggleOpen這個函式,裡面做的事情toggle open 這個class 再來等動畫結束後,需要把原先放在外面的字進入到框內。 為什麼直接寫 `this.classList.toggle("open-active");`就不能動? 還要加上if判斷? 在 toggleActive 函式印出 `e.propertyName`,可以看到 `flex-grow` 和 `font-size`會有transition變化,在這裡只要監聽 `flex-grow`變化就好。 (其他筆記是說 觸發transition雙數個就會當機,效果被抵銷掉) ![image](https://hackmd.io/_uploads/rJ0CY_ILkl.png) ```javascript! e.propertyName.includes("flex") ``` 加上if判斷,是因為作者說到,在其他瀏覽器像是 safari 他就不叫做flex-grow ,而是flex的關係。 [classList property](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) ## 06 Ajax Type Ahead with fetch() ### fetch ```javascript! const endpoint = "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json"; const result = fetch(endpoint); console.log(result); // 會回傳Promise ``` fetch回傳是一個Promise,長的如下圖 ![image](https://hackmd.io/_uploads/SyXpv_5Iye.png) ```javascript! fetch(endpoint) .then((blob) => console.log(blob); ``` 用 `.then` 來看看回傳的blob是什麼 可以看到是一個 `Response` ,但還無法使用 所以使用 `[[Prototype]]` 裡面的 `json()` 方法轉換資料,拿到資料後再把資料印出 ![image](https://hackmd.io/_uploads/SkDRw_qLke.png) ```javascript! fetch(endpoint) .then((blob) => blob.json()) // 用json方法轉換資料 .then((data) => console.log(data)); // 取得資料 ``` 這樣才能拿到資料 ![image](https://hackmd.io/_uploads/H1PkddqIyl.png) 展開看看 ![image](https://hackmd.io/_uploads/r1MeOdcLJe.png) [MDN - fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) ```javascript! function findMatches(wordToMatch, cities) { return cities.filter((place) => { const regex = new RegExp(wordToMatch, "gi"); return place.city.match(regex) || place.state.match(regex); }); } ``` 做一個 findMatches 函式,主要來篩選輸入的內容(wordToMatch)。 作者這邊使用 `正則表達式` 篩選每個物件中的 city 屬性值和 state 屬性值,如有符合則會回傳。 ### Regex `i` 是「忽略大小寫」(case-insensitive)的標誌,讓正則表達式忽略大小寫差異。 `g` 是「全域匹配」(global match)的標誌。它的作用是讓正則表達式在字串中查找所有匹配項目,而不是在找到第一個匹配後就停止。 ```javascript! // const regex = new RegExp("apple", "gi"); // 也可以這樣寫 const regex3 = /apple/gi; const sentence = "apple banana orange APPle"; console.log(sentence.match(regex3)); // ["apple","APPle"] ``` ```javascript! const regex5 = /orange/; const sentence5 = "apple banana orange APPle"; console.log(sentence5.match(regex5)); // ["orange",index:13....]如下截圖 ``` match得到的資料是回傳一個陣列 ![image](https://hackmd.io/_uploads/r1p-dOcIJl.png) 在 String 的方法:search、match、replace、split 等,也有支援正規表達式。 [十五分鐘認識正規表達式,解決所有文字難題](https://5xcampus.com/posts/15min-regular-expression.html?srsltid=AfmBOoro37cghD6rK8R59rIFPZd8b28I8MHFGFBCBcutjzAB6y10tJa8) --- 再來要監聽 <input>,當有改變 (change) 或是輸入內容 (keyup) 會觸發 displayMatches 函式,讓篩選後的資料可以呈現在頁面。 ![image](https://hackmd.io/_uploads/Hk3Qdd9IJg.png) ```javascript! // input標籤 const searchInput = document.querySelector(".search"); // ul標籤 const suggestions = document.querySelector(".suggestions"); searchInput.addEventListener("change", displayMatches); searchInput.addEventListener("keyup", displayMatches); ``` ```javascript! function displayMatches() { const matchArray = findMatches(this.value, cities); // value是input的屬性,讀取輸入的內容 const html = matchArray .map( (place) => ` <li> <span class = name>${place.city},${place.state}</span> <span class = population>${place.population}</span> </li> ` ) .join(""); suggestions.innerHTML = html; ``` matchArray.map 出來是 `["<li><span>XXX</span></li>", "<li><span>XXX</span></li>", "<li><span>XXX</span></li>"]`,加上join("")改成字串。 這邊主要是為了將篩選完的資料,用innerHTML 把 `<li>` 塞入到 `<ul>` 內,才能更新頁面內容 ![image](https://hackmd.io/_uploads/B1JSdd5Lyl.png) ### innerHTML - 用來動態更新頁面內容 - 會完全替換目標元素==內部的所有內容== --- 最後要把輸入的字突顯,要做 highlight 作者有引入style.css,裡面已有 `.hl` 的 class 設定。 在跑map時,先把匹配到的字抓出來,再用 `replace` 把匹配到的字(regex),替換成 `<span class="hl">${this.value}</span>` ,那些字就會加上顏色 [String.prototype.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) ```javascript! const regex = new RegExp(this.value, "gi"); const cityName = place.city.replace( regex, `<span class="hl">${this.value}</span>` ); const stateName = place.state.replace( regex, `<span class="hl">${this.value}</span>` ); ``` ![image](https://hackmd.io/_uploads/BJJUudcLkl.png) ![image](https://hackmd.io/_uploads/HyVL_d5Ukg.png) displayMatches 調整後的程式碼: ```javascript! function displayMatches() { const matchArray = findMatches(this.value, cities); const html = matchArray .map((place) => { // 將輸入的字有highlight const regex = new RegExp(this.value, "gi"); const cityName = place.city.replace( regex, `<span class="hl">${this.value}</span>` ); const stateName = place.state.replace( regex, `<span class="hl">${this.value}</span>` ); return ` <li> <span class = name>${cityName},${stateName}</span> <span class = population>${place.population}</span> </li> `; }) .join(""); suggestions.innerHTML = html; } ``` --- population 人口數需要加上數字逗號 作者說直接上網找 ```javascript! // 數字加上逗號 function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } ``` `<li class = population>`內容要改成`${numberWithCommas( place.population )}` ```javascript! ` <li> <span class = name>${cityName},${stateName}</span> <span class = population>${numberWithCommas( place.population )}</span> </li> ` ``` 逗號出現了 ![image](https://hackmd.io/_uploads/H1dPOu5L1l.png) ## 07 .some(), .every(), .find() and [...SPREADS] — Array Cardio Day 2 ```javascript! const people = [ { name: "Wes", year: 1988 }, { name: "Kait", year: 1986 }, { name: "Irv", year: 1970 }, { name: "Lux", year: 2015 }, ]; ``` ### Array.prototype.some() 至少一個元素符合條件,就回傳true [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) Q: is at least one person 19 or older? ```javascript! const isAudlt = people.some((person) => { const currentYear = new Date().getFullYear(); if (currentYear - person.year >= 19) return true; }); console.log(isAudlt); // true // 更簡化寫法 const isAudlt = people.some( (person) => new Date().getFullYear() - person.year >= 19 ); console.log(isAudlt); ``` ### Array.prototype.every() 全部元素符合條件,就回傳true [MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/every) Q: is everyone 19 or older? ```javascript! const allAudlt = people.every( (person) => new Date().getFullYear() - person.year >= 19 ); console.log(allAudlt); // false ``` ### Array.prototype.find() 回傳第一個找到的元素值,非回傳陣列! [MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/find) Q: Find the comment with the ID of 823423 ```javascript! const comments = [ { text: "Love this!", id: 523423 }, { text: "Super good", id: 823423 }, { text: "You are the best", id: 2039842 }, { text: "Ramen is my fav food ever", id: 123523 }, { text: "Nice Nice Nice!", id: 542328 }, ]; ``` ```javascript! const findValue = comments.find((item) => item.id === 823423); console.log(findValue); ``` ![image](https://hackmd.io/_uploads/SyRV0vjIyx.png) ### Array.prototype.findIndex() 回傳第一個找到的元素值的索引值,非回傳陣列! [MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) Q: delete the comment with the ID of 823423 ```javascript! const index = comments.findIndex((item) => item.id === 823423); // 刪除元素方法1 comments.splice(index, 1); console.log(comments); // 刪除元素方法2 const newComments = [ ...comments.slice(0, index), ...comments.slice(index + 1), ]; console.table(newComments); ``` ![image](https://hackmd.io/_uploads/BJqrCviU1e.png) ## 08 Let's build something fun with HTML5 Canvas [MDN - canvas](https://developer.mozilla.org/zh-TW/docs/Web/API/Canvas_API/Tutorial/Basic_usage) 先渲染環境(rendering context) 2d 繪圖就輸入 `2d` ```javascript! const canvas = document.querySelector("#draw"); const ctx = canvas.getContext("2d"); ``` 進行任何繪圖前,調整畫布大小 ```javascript! canvas.width = window.innerWidth; canvas.height = window.innerHeight; ``` 再對一些樣式基本設定 ```javascript! // 樣式設定 ctx.strokeStyle = "#BADASS"; ctx.lineJoin = "round"; ctx.lineCap = "round"; ``` 找出滑鼠座標 ```javascript! let isDrawing = false; let lastX = 0; // 設定X軸 let lastY = 0; // 設定Y軸 function draw(e) { console.log(e); } canvas.addEventListener("mousemove", draw); ``` 只要在畫布移動,就會抓取到mousemove事件的資料 ![image](https://hackmd.io/_uploads/B15yL238yl.png) 再判斷滑鼠點擊並移動時才繪圖,如滑鼠放開只是移動不能畫 ```javascript! function draw(e) { // 意思是isDrawing為true,表示滑鼠未按下 if (!isDrawing) return; console.log(e); } canvas.addEventListener("mousemove", draw); canvas.addEventListener("mousedown", () => (isDrawing = true)); canvas.addEventListener("mouseup", () => (isDrawing = false)); canvas.addEventListener("mouseout", () => (isDrawing = false)); ``` 會有兩個問題, 一個是都會從座標 (0,0) 開始畫 一個是畫的時候會從剛才結束的位置開始,變成線都會連續 調整程式碼: ```javascript! function draw(e) { if (!isDrawing) return; ctx.beginPath(); // 開始 ctx.moveTo(lastX, lastY); // go to ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; } canvas.addEventListener("mousemove", draw); canvas.addEventListener("mousedown", (e) => { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; // 滑鼠從當前位置 }); canvas.addEventListener("mouseup", () => (isDrawing = false)); canvas.addEventListener("mouseout", () => (isDrawing = false)); ``` ![image](https://hackmd.io/_uploads/BykW8h2Uke.png) 調整畫筆粗細&製作顏色 ```javascript! ctx.lineWidth = 15; // 調整畫筆的粗細 let hue = 0; // 色相預設為0 function draw(e) { if (!isDrawing) return; console.log(e); ctx.strokeStyle = `hsl(${hue},100%,50%)`; // 加hsl ctx.beginPath(); // 開始 ctx.moveTo(lastX, lastY); // go to ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; hue++; // 色相增加 } ``` 有彩色了! ![image](https://hackmd.io/_uploads/rkXz8hhLJe.png) 再來要讓畫筆顏色都在360內跑,和畫筆顏色在一定寬度內跑 ```javascript! let direction = true; // 用來判斷畫筆要變寬還是變細 // 在 draw 函式內加上 if (hue >= 360) { hue = 0; // 重置為0,讓顏色都在360以內跑 } // 畫筆粗細動態調整 if (ctx.lineWidth >= 100 || ctx.lineWidth <= 1) { direction = !direction; } if (direction) { ctx.lineWidth++; } else { ctx.lineWidth--; } ``` 如想要有photoshop的混合模式 ```javascript! ctx.globalCompositeOperation = "multiply"; ``` 其他的模式:[globalCompositeOperation property](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation) ## 09 Must Know Chrome Dev Tools Tricks - Regular ```javascript! console.log("hello"); ``` - Interpolated 插入 在第一個值加上 `%s` ,第二個值插入到第一個值 ```javascript! console.log("have a %s day!", "nice"); // 印出 have a nice day! ``` - Styled 加上樣式 在第一個值加上 `%c` ,第二個值設定的樣式會套用到第一個值 ```javascript! console.log("%cwarning", "font-size:24px;background:red"); ``` ![image](https://hackmd.io/_uploads/S1388p3U1g.png) - warning! ```javascript! console.warn("警告!"); ``` ![image](https://hackmd.io/_uploads/ryYvL6nI1x.png) - error! ```javascript! console.error("錯誤!"); ``` ![image](https://hackmd.io/_uploads/H1KuLp3I1l.png) - Info ```javascript! console.info("資訊呈現"); ``` - testing Assertion 斷言只會出現在出現問題時 ```javascript! console.assert(1 === 2, "its' wrong!"); ``` ![image](https://hackmd.io/_uploads/ByYKUahL1l.png) - clearing 清空 ```javascript! console.clear(); ``` ![image](https://hackmd.io/_uploads/Sk85LphI1x.png) - Viewing DOM Elements ```javascript! console.log(p); console.dir(p); ``` ![image](https://hackmd.io/_uploads/ryIoUT3Lkx.png) - Grouping together 組群組 ```javascript! dogs.forEach((dog) => { console.group(`${dog.name}`); console.log(`${dog.name}`); console.log(`${dog.name} is ${dog.age} years old`); console.groupEnd(`${dog.name}`); }); ``` ![image](https://hackmd.io/_uploads/SkB3IT38yg.png) - counting 會自動計算數量 ```javascript! console.count("dog"); console.count("cat"); console.count("bird"); console.count("cat"); console.count("bird"); console.count("dog"); console.count("dog"); console.count("dog"); console.count("cat"); console.count("bird"); ``` ![image](https://hackmd.io/_uploads/SkP68ThUJg.png) - timing 計時器 例如:可以計算拿到資料花了多少時間 ```javascript! console.time("fetching data"); fetch("https://api.github.com/users/wesbos") .then((res) => res.json()) .then((data) => { console.timeEnd("fetching data"); console.log(data); }); ``` ![image](https://hackmd.io/_uploads/BymCIp2Uye.png) - table ```javascript! console.table(dogs); ``` ![image](https://hackmd.io/_uploads/SkekPp2IJl.png) ## 10 JS Checkbox Challenge! 要製作 checkbox 可以按`shift鍵`部份勾選,且反向選取也可以,應用像是在gmail勾選信件等設計。 先選取到所有的checkbox,並且監聽click事件 ```javascript! const checkboxes = document.querySelectorAll(".inbox input[type='checkbox']"); let lastChecked; // 用來紀錄現在勾選的是哪個input function handleCheck(e) { lastChecked = this; } checkboxes.forEach((checkbox) => checkbox.addEventListener("click", handleCheck) ); ``` 再來設計按下shift鍵的邏輯 ```javascript! let inBetween = false; // 是否按住 Shift 並點擊勾選框 if (e.shiftKey && this.checked) { checkboxes.forEach((checkbox) => { // 遍歷勾選框,當遇到 this 當前勾選框,就變為true,遇到lastChecked,切換為false if (checkbox === this || checkbox === lastChecked) { inBetween = !inBetween; } if (inBetween) { checkbox.checked = true; } }); } lastChecked = this; // 紀錄當前點擊的勾選框 ``` 這裡有點難 1. 先判斷當使用者按住 Shift 並勾選當前的勾選框時,進行裡面的程式碼 2. 遍歷到 `當前勾選框 (this)` 或 `上次勾選框 (lastChecked)` 時,切換 inBetween 狀態。 因為inBetween 初始為 false,當遇到 this 時變為 true,表示進入範圍,遇到 lastChecked 時再切換為 false,表示範圍結束。 3. 最後把 `lastChecked = this` ; 更新並紀錄上一次點擊的勾選框 這樣設計就能任意方向勾選(例如:先點擊後面的勾選框,再點擊前面的)。 完整程式碼: ```javascript! const checkboxes = document.querySelectorAll(".inbox input[type='checkbox']"); let lastChecked; function handleCheck(e) { let inBetween = false; // 是否按住 Shift 並點擊勾選框 if (e.shiftKey && this.checked) { checkboxes.forEach((checkbox) => { // 遍歷勾選框,當遇到 this 當前勾選框,就變為true,遇到lastChecked,切換為false if (checkbox === this || checkbox === lastChecked) { inBetween = !inBetween; } if (inBetween) { checkbox.checked = true; } }); } lastChecked = this; // 紀錄上一次被點擊的勾選框 } checkboxes.forEach((checkbox) => checkbox.addEventListener("click", handleCheck) ); ``` ![image](https://hackmd.io/_uploads/rJnrFzT81l.png)