# J_JS30 01-10 [TOC] > 這個單元如果要學得扎實,首先透過前面幾章先理解網頁的基本API,熟悉一些寫法,後面的章節看影片時聽到他說今天要做什麼功能就先按暫停,先自己絞盡腦汁想出辦法,也可以問人,寫好以後再看影片的寫法對答案,解法不是唯一,但在自己探索的過程中可以學到更多方法,我是這樣學的。 > [name=Chris] 我只寫我聽懂的,聽不懂的沒講到的麻煩大家幫我補充:cry: [JS30 github](https://github.com/wesbos/JavaScript30) 去將他clone下來就可以看到他的教材了 # 01-JavaScript Drum Kit 打開`01 - JavaScript Drum Kit`的`index-START.html`就可以開始了 一開始網頁中就有寫好的按鈕跟要播放的音效,開始寫`<script>`吧! 首先是對`window`註冊事件`"keydown"`,當我按下鍵盤時執行後面function,也就是將按下那個按鍵的資訊印出來: ```javascript= window.addEventListener("keydown", (keyboardInput) => { console.log(keyboardInput); }); ``` 所以我按下鍵盤他就把對應的資訊印出來: ![image](https://hackmd.io/_uploads/Sk4sASLO0.png) 可以觀察到裡面有個`keyCode`屬性,所以我可以寫: ```javascript= window.addEventListener("keydown", (keyboardInput) => { console.log(keyboardInput.keyCode); }); ``` 所以我按下鍵盤他就會把對應的`keyCode`印出來: ![image](https://hackmd.io/_uploads/HJapoHLdR.png) 再來試著取得某個標籤: ```javascript= window.addEventListener("keydown", (keyboardInput) => { const audio = document.querySelector(`audio[data-key='${keyboardInput.keyCode}']`); console.log(audio); }); ``` 使用`document.querySelector()`去取得DOM裡面的元素`<audio>`,這邊我要取得的是`data-key`這個屬性為我按下的按鍵`keyCode`的元素,結果就可以取道對應的元素: ![image](https://hackmd.io/_uploads/BJiplILd0.png) 但如果按了鍵盤但頁面中沒有對應的元素就會接收到`null` 再來就是設定按下鍵盤播放對應的元素音檔: ```javascript= window.addEventListener("keydown", (keyboardInput) => { const audio = document.querySelector(`audio[data-key='${keyboardInput.keyCode}']`); if (!audio) return; //取到null的時候就在這邊結束,不要往下跑 audio.play(); }); ``` 寫好以後就可以去網頁中試試看,按下指定的按鍵就會有聲音了~ 接下來會遇到的問題是當我連點的時候,我連點好幾次但大部分時候只會播放一次,原因是點擊第一次後播放音檔可能需要兩三秒,但這個過程你繼續點擊叫他播放他會認為他正在播放,但我們想要的應該是每次按都會從頭播放。 解決這個狀況可以在`audio.play()`執行前將他的執行時間歸零,這樣你每次按下按鍵他都會從頭開始: ```javascript= window.addEventListener("keydown", (keyboardInput) => { const audio = document.querySelector(`audio[data-key='${keyboardInput.keyCode}']`); if (!audio) return; audio.currentTime = 0; //設定從頭開始 audio.play(); }); ``` 回到網頁就可以確認每次按按鈕都有聲音了,丟需啊內 再來嘗試取得特定標籤: ```javascript const key = document.querySelector(`.key[data-key='${keyboardInput.keyCode}']`); console.log(key); ``` 這裡取到的就會是`class`為`key`的,且`data-key`為你按下的按鍵對應的`keyCode`: ![image](https://hackmd.io/_uploads/BJxlw9IuA.png) 這邊也可以用別的方式取得,像是`<div>`: ```javascript const key = document.querySelector(`div[data-key='${keyboardInput.keyCode}']`); console.log(key); ``` 將取得的標籤加上新的`class`屬性: ```javascript key.classList.add("playing"); ``` `playing`這個樣式可以去看`style.css`檔: ![image](https://hackmd.io/_uploads/B13z72I_C.png) 大致上就是加上黃色框框跟陰影 所以現在的`<script>`長這樣: ```javascript= window.addEventListener("keydown", (keyboardInput) => { const audio = document.querySelector(`audio[data-key='${keyboardInput.keyCode}']`); const key = document.querySelector(`div[data-key='${keyboardInput.keyCode}']`); if (!audio) return; audio.currentTime = 0; audio.play(); console.log(key); key.classList.add("playing"); }); ``` 當我按下鍵盤,對應的那個鍵會發出音效然後加上`playing`這個屬性: ![image](https://hackmd.io/_uploads/HyyA72IdC.png) 這邊也提到另外幾個API: ```javascript key.classList.remove("playing"); key.classList.toggle("playing"); ``` 第一個就是移除,第二個有看沒有懂,他也沒多做解釋,以後用到再說吧 但我們期望的不是黃框框一直存在,而是亮一下就消失,所以要寫另一個函式去移除`playing`這個屬性,但如果直接移除就會變成==屬性一加上去馬上就被移除,肉眼只會看到沒有任何樣式==,所以最簡單是去看他的動畫有多久然後使用`setTimeout`設定一樣的時間後移除屬性: ```javascript setTimeout(() => { key.classList.remove("playing"); }, 70); ``` 但他就只能固定是這個秒數,萬一其他按鈕動畫不是這個秒數就很奇怪,所以採用另種辨識`transition`時間的方式,讓這個屬性的動畫結束在刪除這個屬性。 較適合做法: ```javascript const keys = document.querySelectorAll(".key"); console.log(keys); ``` 設定一個`keys`來存放所有擁有`.key`屬性的元素,印出來會是這樣,可以發現他是個陣列: ![image](https://hackmd.io/_uploads/rkp15hUdC.png) 所以我們就可以用array method來處理它: ```javascript= function removeTransition(e) { console.log(e); } const keys = document.querySelectorAll(".key"); keys.forEach((key) => key.addEventListener("transitionend", removeTransition)); ``` 在每個按鈕都註冊一個事件處理器,當它發生狀態轉換時就觸發,可以看到按一個按鍵就觸發了好幾個transition(上下左右border等等),但最重要的是`transform`,但也不知道為什麼要特別跳過其他的。 ![image](https://hackmd.io/_uploads/B1KminLd0.png) 再來要理解函式執行時的this: ```javascript= function removeTransition(e) { if (e.propertyName !== "transform") return; console.log(this); } const keys = document.querySelectorAll(".key"); keys.forEach((key) => key.addEventListener("transitionend", removeTransition) ); ``` 當我觸發狀態轉換時就執行`removeTransition`,然後他會將當下的`this`印出來,但這邊的`this`是標籤而不是物件(有待討論): ![image](https://hackmd.io/_uploads/r1eEphLO0.png) 結果是發生狀態轉換的元素本身,然後我們就可以將他的`playing`移除了: ```javascript this.classList.remove("playing"); ``` 這樣就完成了練習 整理一下程式碼: ```javascript= <script> 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"); } const keys = document.querySelectorAll(".key"); keys.forEach((key) => key.addEventListener("transitionend", removeTransition)); window.addEventListener("keydown", playSound); </script> ``` # 02-JS and CSS Clock 開始做時鐘!打開檔案後嘗試旋轉他發現並不是我們預期的樣式,沒有特別設定時他將從中心為基準去旋轉: ![image](https://hackmd.io/_uploads/SkQ01BP_0.png) 但我們想讓他以最右邊為基準旋轉,這時候就用到`transform-origin`(只要是`transform`都是用這個調整基準點),參數分別為`(x, y)`軸,`(0, 0)`為左上角,預設會是`(50%, 50%)`也就是box的中心點,所以將旋轉基準點移動到最右側`transform-origin: 100%`,這樣就有如預期了: ![image](https://hackmd.io/_uploads/Hy4JXSvOA.png) [MDN transform-origin](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin) 接下來設定一些動畫參數: ![image](https://hackmd.io/_uploads/rk-WDrwdC.png) 這邊只是讓時鐘動起來比較厲害,太認真會陷得很深... 設定好就可以開始做時鐘的功能了。 先定義一個`getTime`來抓取當下的秒數,然後設定一個`setInterval`來每一秒執行一次`getTime`: ```javascript= function getTime() { const now = new Date(); const second = now.getSeconds(); console.log(second); } setInterval(getTime, 1000); ``` 每秒印出來一次當下的秒數: ![image](https://hackmd.io/_uploads/SJfraBPd0.png) 設定秒數對應的角度: ```javascript const secondsDegrees = (seconds / 60) * 360 + 90; ``` 因為原本位置是指向數字9的位置(-90度),所以加回90度。 接著抓取屬性是`.second-hand`的元素: ```javascript const secondHand = document.querySelector(".second-hand"); ``` 使用`style.transform`來給他動畫: ```javascript secondHand.style.transform = `rotate(${secondsDegrees}deg)`; ``` 如此一來秒針就會每一秒都抓取一次時間,根據當下時間來決定要旋轉的角度。 時針跟分針一樣步驟,全部整理好就是: ```javascript= const secondHand = document.querySelector(".second-hand"); const minHand = document.querySelector(".min-hand"); const hourHand = document.querySelector(".hour-hand"); function getTime() { const now = new Date(); const seconds = now.getSeconds(); const secondsDegrees = (seconds / 60) * 360 + 90; secondHand.style.transform = `rotate(${secondsDegrees}deg)`; const mins = now.getMinutes(); const minsDegrees = (mins / 60) * 360 + 90; minHand.style.transform = `rotate(${minsDegrees}deg)`; const hours = now.getHours(); const hoursDegrees = (hours / 12) * 360 + 90; hourHand.style.transform = `rotate(${hoursDegrees}deg)`; } setInterval(getTime, 1000); ``` 這樣時鐘就做完啦~ ![image](https://hackmd.io/_uploads/rJzgqd__A.png) 有一些小細節可以調整: * 可以調整三個指針的樣式 * 秒針動的時候分針跟時針有需要動嗎? * 秒針在數完60秒後會跳一次,因為從360度瞬間變0度 如果設計既算秒數時也加到分針上,計算分鐘時也加到時針上: ```javascript= function getTime() { const now = new Date(); const seconds = now.getSeconds(); const secondsDegrees = (seconds / 60) * 360 + 90; secondHand.style.transform = `rotate(${secondsDegrees}deg)`; const mins = now.getMinutes(); const minsDegrees = (mins / 60) * 360 + (seconds / 60) * 6 + 90; minHand.style.transform = `rotate(${minsDegrees}deg)`; const hours = now.getHours(); const hoursDegrees = (hours / 12) * 360 + (mins / 60) * 30 + 90; hourHand.style.transform = `rotate(${hoursDegrees}deg)`; } ``` 角度問題很難解釋,歡迎來討論 下一個問題是秒針在數完60秒會跳一下,這個問題橘子的解法是一開始訂好角度,接下來計算時間就累加上去,所以角度會是無限增加,不會有60秒一個循環。 # 03-Playing with CSS Variables and JS 這個單元要做出網頁中有參數可以調整,依據調整會改變底下圖片的顯示。 這邊圖片連結好像壞了,要自己去找圖片。 首先先將樣式寫好: ![image](https://hackmd.io/_uploads/rJoRad_d0.png) 設了一些變數,之後只要改動`:root`的變數值就好。 設定好以後就來取得`<input>`吧: ```javascript const inputs = document.querySelectorAll(".controls input"); ``` ![image](https://hackmd.io/_uploads/SJYEQFOdA.png) 拿到啦!展開`prototype`可以看到有一些method可以用,再來就在每個input都註冊事件處理器,當input發生變化時提取他的值: ```javascript= function handleUpdate() { console.log(this.value); } inputs.forEach((input) => input.addEventListener("change", handleUpdate) ); ``` ![image](https://hackmd.io/_uploads/rJyLNYdOC.png) 成功在調整刻度時拿到值。 但現在只能調整完才拿到值,我們的目的可能想要移動刻度的時候畫面也跟著變化,所以使用另一個事件:mousemove ```javascript inputs.forEach((input) => input.addEventListener("mousemove", handleUpdate)); ``` ![image](https://hackmd.io/_uploads/S1s_srou0.png) 此時我只要游標在`<input>`上移動他就會取得當下的值。 再來介紹`dataset`這個鬼東西。 因為我們取得值之後也需要加上單位我們才能放到`css`中使用,例如我取到`blur`的值是`10`,但要加上單位變成`10px`才能被使用,這個單位他寫在`<input>`中的`data-sizing`裡面,但我們==沒辦法直接取`data-sizing`==,必須透過`dataset`,屬性中以`data-`為前綴的屬性會被存在`dataset`中,我們印出來看看: ```javascript function handleUpdate() { console.log(this.dataset); } ``` ![image](https://hackmd.io/_uploads/B1uDTHoO0.png) 看到他是一個物件包含`sizing`屬性,而這個`sizing`就是標籤中的`data-sizing`: ![image](https://hackmd.io/_uploads/r1OKhHjuA.png) 所以我嘗試加入其他的data資料進去看看: ![image](https://hackmd.io/_uploads/SykVyLsu0.png) ![image](https://hackmd.io/_uploads/Hy320HjuA.png) 印出來就會有我設定的屬性資料了。 現在我們已經可以取得單位了,將他跟剛剛的`value`結合在一起放到`style`裡面: ```javascript= function handleUpdate() { const suffix = this.dataset.sizing || ""; //這裡的意思是取到sizing就設定sizing,沒取到東西就設定"" document.documentElement.style.setProperty( `--${this.name}`, this.value + suffix ); } inputs.forEach((input) => input.addEventListener("mousemove", handleUpdate)); ``` 用`documentElement.style.setProperty`設定`style`屬性,再檢查一下我們做了什麼: * 在每個`<input>`註冊`mousemove`事件觸發`handleUpdate()` * 觸發`handleUpdate()`會取得當下的`<input>`標籤的`data-sizing`屬性的值,接著將`style`這個屬性根據`<input>`的種類添加到`documentElement`裡面 來看看成果: ![image](https://hackmd.io/_uploads/Hyn0n5sdC.png) 將游標移動到`<input>`上他就會添加當下的值進去`<html>`裡面,這個設定值會影響到整個`<html>`裡面的`--spacing`、`--blur`及`--base`的值,也就可以影響圖片的顯示了! # 04-Array Cardio Day 1 這個單元看起來是在練習一些Array method,他列了8個練習: ## Array.prototype.filter() 1. Filter the list of inventors for those who were born in the 1500's 第一題用`filter`篩選出在15世紀出生的人: ```javascript= const fifthCentury = inventors.filter( (inventor) => inventor.year > 1500 && inventor.year < 1600 ); console.table(fifthCentury); ``` ![image](https://hackmd.io/_uploads/SkcCv5nuR.png) 上面是原來的表格,成功取得15世紀出生的人。 ## Array.prototype.map() 2. Give us an array of the inventors first and last names 第二題用`map`做出inventors的全名陣列: ```javascript= const fullNames = inventors.map( (inventor) => `${inventor.first} ${inventor.last}` ); console.log(fullNames); ``` ![image](https://hackmd.io/_uploads/H1TRd53OC.png) 完成。 ## Array.prototype.sort() 3. Sort the inventors by birthdate, oldest to youngest 第三題用`sort`排序從出生年最大到最小: > 題外話,到這裡我突然發現教學的跟教材的inventors名單不同,影片中的只有7個人,教材裡面有12個人。 簡單來排序吧,就把年份最早的排前面: ```javascript const ordered = inventors.sort((a, b) => a.year - b.year); console.log(ordered); ``` ![image](https://hackmd.io/_uploads/SJRnc5hdR.png) 完成。 ## Array.prototype.reduce() 4. How many years did all the inventors live all together? 第四題用`reduce`計算所有人的得年總和: ```javascript= const totalYears = inventors.reduce((totalYears, inventor) => { return totalYears + (inventor.passed - inventor.year); }, 0); console.log(totalYears); ``` 跟影片不一樣,因為他只有7個人算出來是523 我寫12個人印出來是861,但我不想驗算。 > 我也是算861,這樣算是驗算嗎 > [name=橘子] > 我也算861,覺得詭異,來了發現我不4一個倫。 > [name=Jamixcs] 然後發現影片裡的名單就是到`Max Planck`這個人,所以教材把後面的都拿掉就是影片的名單了。 拿掉後算出來也是523。 接下來我都改成跟影片一樣的名單好了。 ## 5. Sort the inventors by years lived 第五題依照他們的得年排序: ```javascript= const oldest = inventors.sort((a, b) => { const lastGuyLived = a.passed - a.year; const nextGuyLived = b.passed - b.year; return nextGuyLived - lastGuyLived; }); console.table(oldest); ``` ![image](https://hackmd.io/_uploads/SkKipc3dC.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) 進入連結將裡面包含`de`的項目篩選出來,所以接下來就進去連結使用devTools來操作: ![image](https://hackmd.io/_uploads/Skc67j2OC.png) 進到網站中找到所有項目都會放在擁有`class="mw-category"`的標籤裡面: 1. 所以首先取得`<div class="mw-category ..."></div>`這個標籤 2. 將裡面的`<a>`標籤都取出來做成array 3. 在對每個`<a>`做取得裡面的文字 4. 最後搜尋並保留裡面有`de`的項目 ```javascript= const category = document.querySelector('.mw-category'); const links = Array.from(document.querySelectorAll('a')); const de = links .map(link => link.textContent) .filter(streetName => streetName.includes('de')); console.log(de) ``` ![image](https://hackmd.io/_uploads/ryk7XindA.png) ## 7. sort Exercise Sort the people alphabetically by last name 回到教材的檔案名單,第七題是將第二個名單用姓氏的字母順序排列: ```javascript= const alpha = people.sort((a, b) => { const [aLast, aFirst] = a.split(", "); const [bLast, bFirst] = b.split(", "); return aLast > bLast ? 1 : -1; }); console.log(alpha); ``` ![image](https://hackmd.io/_uploads/ryw4wonuC.png) 這裡我不太理解他說用last name去排序,我以為last name是後面的,但看起來他的結果是用前面那個字去排。 這邊值得注意的是兩個字串比大小時,會從第一個字母的code去比,如果一樣就比第二個字母的code... ## 8. Reduce Exercise Sum up the instances of each of these 最後一題,將陣列中的項目數量分別加總: ```javascript= const data = ["car", "car", "truck", "truck", "bike", "walk", "car", "van", "bike", "walk", "car", "van", "car", "truck", ]; const sumList = data.reduce((resultObj, item) => { if (!resultObj[item]) { //檢查物件中有沒有這個項目屬性 resultObj[item] = 0; //沒有就建立 } resultObj[item]++; return resultObj; }, {}); console.log(sumList); ``` ![image](https://hackmd.io/_uploads/Skm1YonO0.png) 完成。 如果我將名單最後加上`"Jeremy"`: ```javascript const data = ["car", "car", "truck", "truck", "bike", "walk", "car", "van", "bike", "walk", "car", "van", "car", "truck", "Jeremy"]; ``` ![image](https://hackmd.io/_uploads/r12fKs2OR.png) 成功計算有一個`Jeremy`。 # 05-Flex Panels Image Gallery 打開05資料夾`index-START.html`檔案吧,發現圖片連結好像壞了好幾個,也是要自己換圖片。 這個章節要做的是點擊圖片會有flex伸縮效果,且上下兩排字會掉進來。 一開始的版面不是我們要的排版,複習一下之前學的`flex`將他調成我們要的樣式。 原排版: ![image](https://hackmd.io/_uploads/H1h9UVktC.png) 調整後排版: ![image](https://hackmd.io/_uploads/S16aLVkKR.png) 具體來講增加了這些樣式: 前面這些都是一般排版: ![image](https://hackmd.io/_uploads/rJ7OS4yt0.png) ![image](https://hackmd.io/_uploads/HJrgB4JY0.png) 上下排的字預設是往外丟,直到加上`open-active`樣式才掉進來: ![image](https://hackmd.io/_uploads/ryMzB4ytR.png) `panel`加上`open`時放大: ![image](https://hackmd.io/_uploads/BJDG_NkFC.png) 註冊事件處理器: ```html= <script> const panels = document.querySelectorAll(".panel"); function toggleOpen() { this.classList.toggle("open"); } function toggleActive(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) ); </script> ``` `toggleActive`裡面要做的判斷是因為事件`transitionend`會捕捉所有的樣式變化,如果不篩選其中一個,他就可能因為辨識到好幾個樣式變化而跑好幾次,所以特別只判斷有沒有`flex`的變化,具體來講`panel`加上`open`後的`transition`變化有`font-size`跟`flex-grow`兩個,所以他瞬間執行兩次感覺上被抵銷了: ![image](https://hackmd.io/_uploads/BycAxqlYA.png) 後來我多加一個`color`的`transition`進去,所以現在`transtiion`變成了三個: ![image](https://hackmd.io/_uploads/ryfb-qgYA.png) ![image](https://hackmd.io/_uploads/ryle-5gKR.png) 就成功有效果了! ![image](https://hackmd.io/_uploads/HyTr-qxt0.png) 觀察後的結論就是一次觸發兩個事件有可能會被抵銷,所以==盡可能讓觸發事件的次數單純==。 教學就到這邊而已。 不管怎麼截圖檔案都太大= =,只能截一小塊: ![image](https://hackmd.io/_uploads/BkhlmrytA.png) # 06-Type Ahead 這章要做搜尋關鍵字的範例,先獲取外部的資料然後在`input`輸入文字,會搜尋所有符合的項目且列在下方。 打開就有一個連結,他是一個`json`形式的檔案,我們先用`fetch`來取得這個檔案的內容然後印出來看看: ```javascript const endpoint = "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json"; console.log(fetch(endpoint)); ``` 他是一個Promise!!但還不知道他傳出來的資料是什麼: ![image](https://hackmd.io/_uploads/r16rdwkt0.png) 所以我們用`.then()`去接出來看看是什麼: ```javascript fetch(endpoint).then(console.log); ``` ![image](https://hackmd.io/_uploads/H1-hOPkFC.png) 結果還是看不懂,但教學有說用裡面的`.json()`方法去處理它: ```javascript fetch(endpoint) .then((blob) => blob.json()) .then(console.log); ``` ![image](https://hackmd.io/_uploads/SyzXKvyt0.png) ![image](https://hackmd.io/_uploads/rkQIYP1FA.png) 就看出來是一個很多資料的陣列,裡面有各個城市的資訊。 拿到資料後來存進一個陣列方便以後只用吧: ```javascript= <script> const endpoint = "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json"; const cities = []; fetch(endpoint) .then((blob) => blob.json()) .then((data) => cities.push(...data)); </script> ``` ![image](https://hackmd.io/_uploads/S1zpYD1FC.png) 成功放進`cities`裡面等等再來操作他~ 接下來寫幾個函式跟取得包含`search`跟`suggestions`的標籤: ```javascript= function findMatches(words, cities) { return cities.filter((place) => { const regex = new RegExp(words, "gi"); return place.city.match(regex) || place.state.match(regex); }); } function displayMatches() { const matchArray = findMatches(this.value, cities); console.log(matchArray); } const searchInput = document.querySelector(".search"); const suggestions = document.querySelector(".suggestions"); searchInput.addEventListener("keyup", displayMatches); ``` **findMatches()** `findMatches`會在`cities`中篩選出有包含傳入的`words`,回傳一個包含結果的陣列。 這邊他用了正規表達式...有機會再研究一下 **displayMatches()** 接著取得包含`.search`的`<input>`標籤,註冊事件,當鍵盤作動時就觸發`displayMatches`,那他就會將`<input>`標籤中的值丟進`findMatches`執行,最後印出過濾的結果陣列! ![image](https://hackmd.io/_uploads/SJSDLtgYC.png) 接下來這段比較麻煩,目的在創建html元素: ```javascript= function addCommasToNumber(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function displayMatches() { const matchArray = findMatches(this.value, cities); const html = matchArray .map((place) => { 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>${cityName}, ${stateName}</span> <span>${addCommasToNumber(place.population)}</span> </li> `; }) .join(""); suggestions.innerHTML = html; } ``` 簡單講就是==將剛剛得到的`matchArray`轉換成想要在網頁中建立的元素==,影篇分成幾個步驟,但我覺得很麻煩就直接寫最終版本: * regex是正規表達式,內容會根據使用者輸入的內容 * 再來想要生成的區塊,先來看一下結果: ![image](https://hackmd.io/_uploads/r1wz_YeFA.png) * 其中我們希望符合的字加上`hl`(highlight)樣式 所以我們先將剛剛得到的`matchArray`做`map`處理,把裡面的每個元素都先換成標籤代碼。用正規表達式`regex`來抓取完全符合的字,將他替換成`<span class="hl">${this.value}</span>`,好像有點難想像:cry: 以上面的例子來講,就是將本來物件中的`Boston` 替換成`<span class="hl">Boston</span>` 然後放到`cityName`裡面,`stateName`也是一樣的步驟 還有人口數也加進去,這邊用了`addCommasToNumber`這個函式,目的是要將一串數字加上逗點,他也用正規表達式,就照抄吧: ```javascript function addCommasToNumber(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } ``` 最後再將它們組起來: ```html <li> <span>${cityName}, ${stateName}</span> <span>${addCommasToNumber(place.population)}</span> </li> ``` 然後在`map`區塊`return`,這樣就做好我們想要的陣列了。 最後這一連傳的陣列用`join("")`組起來,放進`suggestions`這個區塊中 然後就做完了: ![image](https://hackmd.io/_uploads/Hydx6YeKC.png) 完整程式碼: ```html= <script> const endpoint = "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json"; const cities = []; fetch(endpoint) .then((blob) => blob.json()) .then((data) => cities.push(...data)); function findMatches(words, cities) { return cities.filter((place) => { const regex = new RegExp(words, "gi"); return place.city.match(regex) || place.state.match(regex); }); } function addCommasToNumber(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function displayMatches() { const matchArray = findMatches(this.value, cities); const html = matchArray .map((place) => { 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>${cityName}, ${stateName}</span> <span>${addCommasToNumber(place.population)}</span> </li> `; }) .join(""); suggestions.innerHTML = html; } const searchInput = document.querySelector(".search"); const suggestions = document.querySelector(".suggestions"); searchInput.addEventListener("keyup", displayMatches); </script> ``` # 07-Array Cardio Day 2 這個章節也是在練習Array method: 1. Array.prototype.some(), is at least one person 19 or older? 只需要知道陣列中有人有任何人至少19歲,所以使用`Array.prototype.some()`: ```javascript= const isAdult = people.some((person) => { const currentYear = new Date().getFullYear(); return currentYear - person.year >= 19; }); console.table(people); console.log(isAdult); //true ``` 這跟`Array.prototype.filter()`有點像,需要回傳一個布林值讓他判斷,所以只要有一個人年齡至少19歲,整個`some()`就會回傳`true`: ![image](https://hackmd.io/_uploads/SJb00u4YR.png) 2. Array.prototype.every(), is everyone 19 or older? 這次要用`Array.prototype.every()`判斷是不是所有人都至少有19歲: ```javascript= const allAdult = people.every( (person) => new Date().getFullYear() - person.year >= 19 ); console.log(allAdult); //false ``` 這次變成只要有一個人不到19歲就回傳`false`。 3. find the comment with the ID of 823423 取得其中一個符合的資料用`Array.prototype.find()`: ```javascript= const comment = comments.find((comment) => comment.id === 823423); console.table(comments); console.log(comment); ``` ![image](https://hackmd.io/_uploads/rybwgKEtC.png) 5. delete the comment with the ID of 823423 首先要先選中目標元素的索引值: ```javascript= const index = comments.findIndex( (comment) => comment.id === 823423 ); console.table(comments); console.log(index); ``` ![image](https://hackmd.io/_uploads/B16MfKEKR.png) 目標的索引值是`1`。 再來想刪掉選中的元素,影片中先示範用了`Array.prototype.splice()`,但這個方法會改動到原來的`comments`: ```javascript= const index = comments.findIndex( (comment) => comment.id === 823423 ); comments.splice(index, 1); console.table(comments); ``` ![image](https://hackmd.io/_uploads/H1Q9GYNFC.png) 雖然成功刪除目標元素,但從此以後他就消失在名單了,如果這符合需求也是沒問題,但如果我們需求是想要保留原本的名單而另外建立新的目標名單,可以用`Array.prototype.slice()`然後放入新的陣列: ```javascript= const index = comments.findIndex( (comment) => comment.id === 823423 ); const newComments = [ ...comments.slice(0, index), ...comments.slice(index + 1), ]; console.table(newComments); console.table(comments); ``` ![image](https://hackmd.io/_uploads/BydYQK4KR.png) 上面的新陣列就是刪除目標元素的名單,下面則是原本的名單。 # 08-Fun with HTML5 Canvas 這個單元在建立一個可以畫畫的區塊,有些用法好難懂:cry: 首先提取想要畫畫的`<canvas>`區塊: ```javascript const canvas = document.querySelector("#draw"); ``` 接著在這個區塊建立一個畫筆(我完全不懂,只好先這樣理解): ```javascript const ctx = canvas.getContext("2d"); ``` 將畫板大小設成視窗大小(看你自己要多大,保持原本的800*800也行): ```html canvas.width = window.innerWidth; canvas.height = window.innerHeight; ``` 一些畫筆的設定: ```javascript ctx.strokeStyle = "#BADA55"; //畫筆顏色 ctx.lineJoin = "round"; //轉彎處的處理 ctx.lineCap = "round"; //頭尾的處理 ctx.lineWidth = 1; //寬度 ``` 然後是一些控制狀態的設定: ```javascript let isDrawing = false; //控制畫筆作動與否,等之後用到再多做解釋 let lastX = 0; //畫筆起始位置X let lastY = 0; //畫筆起始位置Y let hue = 0; //調整色碼 let direction = true; //這邊是用來調整粗細,等之後用到再多做解釋 ``` ```javascript= function draw(e) { if (!isDrawing) return; //如果畫筆還沒啟動就不做事 ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`; //畫筆顏色根據hue的值決定 //這段就是主要在畫畫的區塊 ctx.beginPath(); //開啟一個新路徑 ctx.moveTo(lastX, lastY); //起點的(x, y) ctx.lineTo(e.offsetX, e.offsetY); //終點的(x, y) ctx.stroke(); //使用目前的筆觸樣式畫畫 [lastX, lastY] = [e.offsetX, e.offsetY]; //更新起點 hue++; //改變色碼 if (hue >= 360) { hue = 0; //色碼360為一個循環,所以到了就歸零 } if (ctx.lineWidth >= 100 || ctx.lineWidth <= 1) { direction = !direction; //畫筆大小在碰到1跟100的時候反轉 } if (direction) { ctx.lineWidth++; } else { ctx.lineWidth--; } } canvas.addEventListener("mousemove", draw); ``` 設置好之後還要開啟畫筆,在按下滑鼠的時候開啟: ```javascript canvas.addEventListener("mousedown", (e) => { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; //這邊如果沒有設定他就會還是上次畫完的終點,並不是我們要的效果 }); ``` 最後設置要關閉畫筆的時機: ```javascript canvas.addEventListener("mouseup", () => (isDrawing = false)); canvas.addEventListener("mouseout", () => (isDrawing = false)); ``` 做完啦~ ![image](https://hackmd.io/_uploads/r1TUsDPtR.png) ```javascript ctx.globalCompositeOperation = "multiply"; ``` 有很多種效果可以去研究: [MDN globalCompositeOperation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation) # 09-14 Must Know Dev Tools Tricks 這個章節介紹一些DevTools的功能。 1. 查看屬性變化的詳情: ![image](https://hackmd.io/_uploads/rJdwEdwFA.png) 這段文字在點擊後會觸發`makeGreen()`,但我們可能很難找到他做了什麼,點右鍵打開『屬性修正』: ![image](https://hackmd.io/_uploads/H1HpNOPFA.png) 再來就觸發事件: ![image](https://hackmd.io/_uploads/rykkBOvtR.png) 就會跳到他觸發的行為了,可以看到他加上了顏色,再來放大了字體。 2. 一般的`console.log` 3. Interpolated加入變數 ```javascript console.log("hello %s", "Jeremy"); //hello Jeremy ``` 4. Styled加入樣式 ```javascript console.log("%c hello", "color: red; font-size: 50px; background: yellow"); ``` ![image](https://hackmd.io/_uploads/S18j-P_FC.png) 5. warning! ```javascript console.warn("NO!"); ``` ![image](https://hackmd.io/_uploads/rJmnfv_YA.png) 6. Error ```javascript console.error("It's an Error!"); ``` ![image](https://hackmd.io/_uploads/HkT3zPOF0.png) 7. info ```javascript console.info("15T has Jeremy, Watson, FangFang, Tangerine and Jami."); ``` ![image](https://hackmd.io/_uploads/H1oemPuKA.png) > 在影片中的年代前面是有一個小小的`i`圖示,現在都拿掉了所以跟`console.log`看起來幾乎沒有區別,跟Chris一起討論的結果是`console.log`可能用來除錯用,印出結果看有沒有符合預期,看完就會拿掉;`console.info`則是真的要展示給使用者看的,有了這個區別後就能更好的分辨哪些是需要留下來的哪些可以不用。 > [name=Chris and Jeremy] 8. Testing ```javascript const p = document.querySelector("p"); console.assert(p.classList.contains("ouch"), "You didn't select the right Element!"); ``` ![image](https://hackmd.io/_uploads/H1J_HPOtC.png) 9. clearing ```javascript console.clear(); ``` ![image](https://hackmd.io/_uploads/HkuFqDOtC.png) 將之前的訊息清空。 10. Viewing DOM Elements ```javascript console.log(p); console.dir(p); ``` ![image](https://hackmd.io/_uploads/B19q2vuYC.png) 查看DOM元素,可以找到可以用的屬性。 10. Grouping together ```javascript= dogs.forEach((dog) => { console.group(`${dog.name}`); console.log(`This is ${dog.name}`); console.log(`${dog.name} is ${dog.age} years old.`); console.log(`${dog.name} is ${dog.age * 7} dog years old.`); console.groupEnd(`${dog.name}`); }); ``` ![image](https://hackmd.io/_uploads/rJk09DuKA.png) 用兩個`group`區塊夾住,將中間內容組成群組。 11. counting ```javascript= console.count("Jeremy"); console.count("Jeremy"); console.count("Jeremy"); console.count("Watson"); console.count("Watson"); console.count("Watson"); ``` ![image](https://hackmd.io/_uploads/r1IWiv_Y0.png) 12. timing ```javascript= console.time("fetch time"); fetch("https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json") .then((data) => data.json()) .then((data) => console.timeEnd("fetch time")); ``` ![image](https://hackmd.io/_uploads/HJ6tsDdF0.png) 用兩個`time`區塊夾住,可以測出中間執行經過的時間。 13. Table ```javascript console.table(dogs); ``` ![image](https://hackmd.io/_uploads/rkALAPOFA.png) 最後講到`console.table`,將陣列或物件這種有多筆資料的類型用表格方式展示。 他說14個?我做下來只記了13個,但我懶得回去找,掰掰~ > 後來發現有網友整理,查看DOM元件那邊`console.log`跟`console.dir`算兩個case,如果這樣算就有14個。 # 10-Hold Shift to Check Multiple Checkboxes 這個章節要講的是勾選一個項目,然後按著`shift`勾選另一個項目,將會勾選兩個項目之間的所有項目。 首先選取所有的`checkbox`: ```javascript const checkboxes = document.querySelectorAll(".inbox input[type=checkbox]"); ``` 註冊點擊事件: ```javascript= let lastChecked; //記錄上次勾選的項目 function handleCheck(e) { let inBetween = false; //用來判斷項目有沒有在兩個勾選的項目之間 if (e.shiftKey && this.checked) { //當使用者按著shift,且勾選checkbox時 checkboxes.forEach((checkbox) => { console.log(checkbox); if (checkbox === this || checkbox === lastChecked) { //當檢查到的checkbox是使用者當下勾選的,或是先前勾選的 inBetween = !inBetween; //將inBetween狀態換成true console.log("what's inBetween?"); //在狀態切換時印出來 } if (inBetween) { checkbox.checked = true; //將inBetween是true的期間經過的checkbox打勾 } }); } lastChecked = this; //每次勾選完都更新成最後勾選的項目 } checkboxes.forEach((checkbox) => checkbox.addEventListener("click", handleCheck) ); ``` `inBetween`狀態為`true`時經過了紅框框的四個項目並將他們勾選起來: ![image](https://hackmd.io/_uploads/SJN7G6OYC.png)