[TOC] ## D11 不要打錯字 `video.pause()` vs `video.paused` ![image](https://hackmd.io/_uploads/B1QqIAOKC.png) ```javascript // 轉成數字 video.currentTime += Number(this.dataset.skip); // 影片用的是parseFloat video.currentTime += parseFloat(this.dataset.skip); ``` Number()可接受科學記號 parseFloat()不行 > parseFloat() does not support non-decimal literals with 0x, 0b, or 0o prefixes but supports everything else. However, parseFloat() is more lenient than Number() because it ignores trailing invalid characters, which would cause Number() to return NaN. [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat#description) 他把name都設計成property的名字了 ![image](https://hackmd.io/_uploads/HJ--W1Yt0.png) ![image](https://hackmd.io/_uploads/SkA--JttR.png) ### 更新進度條 ```javascript // 原本寫的 // 每一秒去更新 setInterval(handleVideoProgress, 1000); // 而且按skip鍵的時候還要註冊這個function skipButtons.forEach((skipButton) => { skipButton.addEventListener("click", skip); skipButton.addEventListener("click", handleVideoProgress); // 這行 }); // 影片教學 // 改成timeupdate event // 因為影片暫停的時候不需要更新 video.addEventListener("timeupdate", handleVideoProgress); // 這樣skip鍵就不需要註冊了 ``` ### 偵測按住滑鼠左鍵的移動 影片教的是使用flag去轉換目前是否處於按住的狀態 ```javascript let mousedown = false; progress.addEventListener("mousemove", (event) => { if (mousedown === true) { scrub(event); } }); // 轉換flag progress.addEventListener("mousedown", () => { mousedown = true; }); progress.addEventListener("mouseup", () => { mousedown = false; }); ``` 我找到另一個可以使用的property叫做which 但可惜他說目前這個屬性已經被棄用,叫大家改用.button > Which mouse button was pressed when the mouse event was triggered > The which property is deprecated. Use the button property instead. > [w3school](https://www.w3schools.com/jsref/event_which.asp) ```javascript progress.addEventListener("mousemove", (event) => { console.log(event); if (event.which === 1) { scrub(event); } }); ``` 改用.button? 回傳值 0 : Left button 1 : Wheel or middle button (if present) 2 : Right button > The button property returns which mouse button is pressed when a mouse event occurs. > [w3school](https://www.w3schools.com/jsref/event_button.asp) ```javascript // 但是改成這樣是失敗的 progress.addEventListener("mousemove", (event) => { console.log(event); if (event.button === 0) { scrub(event); } }); ``` 改用.buttons就可以了 回傳值 1 : Left mouse button 2 : Right mouse button 4 : Wheel or middle button 8 : Fourth button (Browser Back) 16 : Fifth button (Browser Forward) > The buttons property returns which mouse buttons are pressed when a mouse event occurs. > [w3school](https://www.w3schools.com/jsref/event_buttons.asp) ```javascript progress.addEventListener("mousemove", (event) => { console.log(event); if (event.buttons === 1) { scrub(event); } }); ``` .button和.buttons的差異 我的理解是:.buttons是回傳mouse event發生時有哪些按鍵被按著,但.button是回傳是哪些按鍵造成了這個mouse event ![image](https://hackmd.io/_uploads/ry5JPg0KC.png) > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons) ### &&的另一個用法 ```javascript progress.addEventListener("mousemove", (event) => mousedown && scrub(event)); // &&會先確認左邊的是不是true,是的話就會執行右邊 ``` ### 全螢幕 應該是直接觸發瀏覽器提供的全螢幕 要記得設在player上!!不是video!! :heavy_check_mark:回傳的是promise為什麼可以進到if內部 確認是否有requestFullscreen屬性而已 > [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/requestFullscreen) ```javascript function openFullscreen() { if (video.requestFullscreen) { player.requestFullscreen(); } } // 乾脆直接寫這樣XD function openFullscreen() { player.requestFullscreen(); } ``` :heavy_check_mark:找不到把全螢幕關掉的methods耶QQ 乾,結果exitFullscreen是在document上不是element上 >[stackoverflow](https://stackoverflow.com/a/36672683) ```javascript let fullscreen = false; function changeFullscreen() { if (fullscreen === false) { player.requestFullscreen(); // 這裡是下在player this.textContent = "﹥﹤"; } else { document.exitFullscreen(); // 這裡是下在document this.textContent = "﹤﹥"; } fullscreen = !fullscreen; } ``` :question: 有一堆event的時候,要怎麼編排程式碼 ## D12 讓list的元素保持在某個數量 :question: ```javascript const keyPressedList = []; const secretCode = "root"; // 要留的是:從後面數secretCode.length個 // 從頭開始減掉 // 影片教的 keyPressedList.splice(-secretCode.length - 1, keyPressedList.length - secretCode.length); // 我寫的 // 總覺得就是從第一個開始刪掉?? keyPressedList.splice(0, keyPressedList.length - secretCode.length); // 跑起來也沒問題 ``` ## D13 ### Event: scroll 捲動的時候就會觸發 `window.scrollY + window.innerHeight` ![image](https://hackmd.io/_uploads/HypNSVwcA.png) ```javascript // 原本寫的 function checkSlider() { sliderImages.forEach((sliderImage) => { const nowScrollY = window.scrollY + innerHeight; if (nowScrollY > sliderImage.offsetTop + sliderImage.height / 2) { sliderImage.classList.add("active"); } else { sliderImage.classList.remove("active"); } if (window.scrollY > sliderImage.offsetTop + sliderImage.height) { sliderImage.classList.remove("active"); } }); } // 命名問題 // 讓判斷式更有語意 function checkSlider() { sliderImages.forEach((sliderImage) => { const nowScrollY = window.scrollY + innerHeight; const isHalfShown = nowScrollY > sliderImage.offsetTop + sliderImage.height / 2; if (isHalfShown) { sliderImage.classList.add("active"); } else { sliderImage.classList.remove("active"); } const bottomOfImage = sliderImage.offsetTop + sliderImage.height; // 這邊多想了一段,但可以更簡化 if (window.scrollY > bottomOfImage) { sliderImage.classList.remove("active"); } }); } // 更簡化 // 只有在圖片一半到圖片底之間加active,其他位置全部remove掉 function checkSlider() { sliderImages.forEach((sliderImage) => { const nowScrollY = window.scrollY + innerHeight; const bottomOfImage = sliderImage.offsetTop + sliderImage.height; const isHalfShown = nowScrollY > sliderImage.offsetTop + sliderImage.height / 2; const isNotScrollPast = window.scrollY < bottomOfImage; if (isHalfShown && isNotScrollPast) { sliderImage.classList.add("active"); } else { sliderImage.classList.remove("active"); } }); } ``` :question:使用那個debounce()是必要的嗎?? 直接用checkSlider()感覺也沒什麼問題 ## D14 深度複製Object可以利用`JSON.stringify()`轉成string後再用`JSON.parse()`轉回Object ## D15 ### Event: submit 使用者按enter也可以觸發 ### Event.preventDefault() > 如果事件可以被取消,就取消事件(即取消事件的預設行為)。但不會影響事件的傳遞,事件仍會繼續傳遞。[MDN](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/preventDefault) ### 清除輸入框 ```javascript // 原本的寫法 inputField.value = ""; // 可用 this.reset(); ``` 原本把加入記憶體和新增list放在同一個function 但可以拆兩個 利用記憶體內的資料來新增list ```javascript // 隨便寫的 const newLi = document.createElement("li"); newLi.innerHTML = `<input type="checkbox" /> <label>${item.name}</label>`; itemsList.appendChild(newLi); // 教學 function populateList(plates = [], platesList) { // 預設值讓忘記輸入引數的時候程式不會當掉 platesList.innerHTML = plates .map((plate, i) => { return ` <li> <label>${plate.name}</label> </li> `; }) .join(""); } // 缺點是每次都要更新一整個list而不是只增加一個element ``` ### window.localStorage ```javascript const myStorage = window.localStorage; // 儲存 // 鍵值都要是字串 myStorage.setItem("key", "value"); // 所以要存物件或陣列的話就要用JSON.stringify() myStorage.setItem("items", JSON.stringify(items)); // 取值 // 用JSON.parse()轉回物件或陣列 let items = JSON.parse(myStorage.getItem("items")); ``` 存在這裡 ![image](https://hackmd.io/_uploads/SkPGFpt5A.png) #### 影片留言補充 可以直接對localStorage設定property ```javascript myStorage.items = JSON.stringify(items); ``` ### eventlistener的位置!!! 後來新增的element沒有設定監聽器怎摸辦 那就設在原本的父層! ```javascript // 原本設在每個input上 // 但這樣後來新增的input就沒有eventlistener const checkboxes = itemsList.querySelectorAll("input"); checkboxes.forEach((checkbox) => { checkbox.addEventListener("change", toggleDone); }); // 所以設在父層 // 再用event.target去取得這個input itemsList.addEventListener("click", toggleDone); function toggleDone(event) { if (!event.target.matches("input")) { return; } const checkbox = event.target; // 這裡取得input const index = checkbox.dataset.index; items[index].done = !items[index].done; myStorage.setItem("items", JSON.stringify(items)); renderList(items, itemsList); // 再渲染一次 } ``` 回家作業 1. 全選/全取消按鈕 2. 清除暫存 ![image](https://hackmd.io/_uploads/ryJBqJq90.png) ## D16 計算滑鼠的x, y位置,計算陰影偏移量 ```javascript // 解構賦值 const { innerHeight: wholeY, innerWidth: wholeX } = this; let { offsetY: currentY, offsetX: currentX } = event; // ... // event window.addEventListener("mousemove", changeShadow); // 當滑鼠滑到h1的時候event target會變 // 造成event.offsetX/Y會變成用h1去計算 // 所以滑到h1的時候要加上h1的offsetTop/Left才會是整個視窗的x, y位置 if (this !== event.target) { currentY += event.target.offsetTop; currentX += event.target.offsetLeft; } ``` > [offsetTop/Left](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop) > The HTMLElement.offsetTop read-only property returns the distance from the outer border of the current element (including its margin) to the top padding edge of the offsetParent, the closest positioned ancestor element. #### 影片留言補充 直接使用clientX/Y就可以避免掉offset對象不同的麻煩了!! ```javascript const { clientY: currentY, clientX: currentX } = event; // 不需要再有if statement ``` > [clientX/Y](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/clientX) > The clientX read-only property of the MouseEvent interface provides the horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). > > For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. title.style.textShadow style到底有哪些東西嗄 ## D17 就是不要用正規表達式(逃避仔 ```javascript function getNoArticles(bandName) { if (bandName.includes("a ")) { return bandName.replace("a ", ""); } else if (bandName.includes("A ")) { return bandName.replace("A ", ""); } else if (bandName.includes("an ")) { return bandName.replace("an ", ""); } else if (bandName.includes("An ")) { return bandName.replace("An ", ""); } else if (bandName.includes("the ")) { return bandName.replace("the ", ""); } else if (bandName.includes("The ")) { return bandName.replace("The ", ""); } return bandName; // 記得沒做事情的也要return } bands.sort((bandA, bandB) => { if (getNoArticles(bandA) > getNoArticles(bandB)) { return 1; } else { return -1; } }); ``` ## D18 NodeList只能用forEach ```javascript // 原本寫的,用push推 const videos = document.querySelectorAll("[data-time]"); const videoTime = []; videos.forEach((video) => { videoTime.push(video.dataset.time); }); // 影片寫的 const videos = [...document.querySelectorAll("[data-time]")]; const videoTime = videos.map((video) => video.dataset.time); ``` 好像比較短 哈哈 split後可以用map做處理 ```javascript // 原本寫的要把字串轉成number const [minutes, seconds] = currentTime.split(":"); totalTime.minutes += Number(minutes); totalTime.minutes += Math.floor(Number(seconds) / 60); totalTime.seconds += Number(seconds) % 60; // 影片寫的 const [minutes, seconds] = currentTime.split(":").map(parseFloat); ``` 原本分鐘跟秒數分開來處理,影片是先全部換成秒數,再換算成小時分鐘 ## D19 要先npm install? 好像不用 ### navigator > Navigator: mediaDevices > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices) > MediaDevices: getUserMedia() > It returns a Promise that resolves to a MediaStream object. If the user denies permission, or matching media is not available, then the promise is rejected with NotAllowedError or NotFoundError DOMException respectively. > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) ### URL.createObjectURL() 會暫存在瀏覽器內,等到頁面被關掉的時候才會釋放 > [MDN](https://developer.mozilla.org/zh-TW/docs/Web/API/URL/createObjectURL_static) 留言補充這個不能用了 ```javascript // 不能用 video.src = window.URL.createObjectURL(localMediaStream); ``` [stackflow](https://stackoverflow.com/questions/53626318/chrome-update-failed-to-execute-createobjecturl-on-url/53734174) ![image](https://hackmd.io/_uploads/rkgG4C5iA.png) 改用以下 ```javascript video.srcObject = localMediaStream; ``` 希望大家成功的時候都可以拍一張這個照片 ![image](https://hackmd.io/_uploads/Bk-vqCqi0.png) ### 使用 download 屬性將 canvas 另存為 PNG [MDN](https://developer.mozilla.org/zh-TW/docs/Web/HTML/Element/a#%E4%BD%BF%E7%94%A8_download_%E5%B1%AC%E6%80%A7%E5%B0%87_canvas_%E5%8F%A6%E5%AD%98%E7%82%BA_png) 把`<a>`設定屬性download,讓這個連結變得可以下載 ```javascript const data = canvas.toDataURL(); const link = document.createElement("a"); link.href = data; link.setAttribute("download", "photo"); link.innerHTML = `<img src=${data} alt="snapshot" />`; strip.insertBefore(link, strip.firstChild); ``` ### Event: canplay 專屬於media element的事件 ~~救命~~ ```javascript video.addEventListener("canplay", paintToCanvas); ``` ### debugger 讓程式暫停在這個位置 ```javascript const pixels = ctx.getImageData(0, 0, width, height); console.log(pixels); debugger; // 讓程式停在這裡 ``` ![image](https://hackmd.io/_uploads/ryFHJfjoR.png) Uint8ClampedArray裡面存的是每一個pixel的rgba數值 以上面的圖為例,第一個pixel的顏色就是rgb(58, 58, 58, 255),第二個pixel的顏色就是rgb(54, 54, 54, 255) Uint8ClampedArray不是Array! 它是TypedArray ![image](https://hackmd.io/_uploads/B12ryQsiA.png) 喔但TypedArray好像蛇膜都可以用 [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) ### 遇到了不能用Array methods的情況!? :heavy_check_mark:為啥會這樣?改不動pixels.data ```javascript function redEffect(pixels) { const newPixels = pixels.data.map((pixel, index) => { if (index % 4 === 0) { pixel += 100; return pixel; }; return pixel; }) console.log(newPixels); pixels.data = newPixels; console.log(pixels.data); return pixels; } ``` ![image](https://hackmd.io/_uploads/BJl3THG3s0.png) 跟他writable是false有關嗎 ![image](https://hackmd.io/_uploads/H1eTLMnoR.png) 那為什麼影片的寫法可以呢? 我猜是因為他是針對裡面的陣列內容更改而不是針對整個陣列(沒有更改data的指標) ```javascript function redEffect2(pixels) { for(let i = 0; i < pixels.data.length; i += 4){ pixels.data[i + 0] += 100; } return pixels; } ``` 用forEach也不行 ```javascript function redEffect(pixels) { pixels.data.forEach((pixel, index) => { if (index % 4 === 0) { pixel += 100; }; }) console.log(pixels.data); return pixels; } ``` ![image](https://hackmd.io/_uploads/H1cmoMhiR.png) 問問gemini,他說: ![image](https://hackmd.io/_uploads/B1AUsGniC.png) ~~這樣也不行~~ 這樣可以!!! ```javascript function redEffect(pixels) { pixels.data.forEach((pixel, index, array) => { if (index % 4 === 0) { array[index] += 100; }; }) console.log(pixels); return pixels; } ``` ### 綠幕效果 在特定數值內的顏色的透明度變為0 為什麼不用宣告變數 ```javascript const colorInputs = document.querySelectorAll(".rgb input"); function greenScreen(pixels) { // 存放input數值 const levels = {}; // 取得input數值 colorInputs.forEach((input) => { levels[input.name] = input.value; }); for(let i = 0; i < pixels.data.length; i += 4){ // 為什麼不用宣告變數!? red = pixels.data[i + 0]; green = pixels.data[i + 1]; blue = pixels.data[i + 2]; if (levels.rmin <= red && red <= levels.rmax && levels.gmin <= green && green <= levels.gmax && levels.bmin <= blue && blue <= levels.bmax) { pixels.data[i + 3] = 0; } } return pixels; } ``` 問問gemini,他說: ![image](https://hackmd.io/_uploads/By1VsSnjA.png) 所以如果開啟嚴格模式,確實就會報錯 ![image](https://hackmd.io/_uploads/H1shDNniR.png) 最好改成 ```javascript function greenScreen(pixels) { // 存放input數值 const levels = {}; // 取得input數值 colorInputs.forEach((input) => { levels[input.name] = input.value; }); for(let i = 0; i < pixels.data.length; i += 4){ // 用const宣告 const red = pixels.data[i + 0]; const green = pixels.data[i + 1]; const blue = pixels.data[i + 2]; if (levels.rmin <= red && red <= levels.rmax && levels.gmin <= green && green <= levels.gmax && levels.bmin <= blue && blue <= levels.bmax) { pixels.data[i + 3] = 0; } } return pixels; } ``` ## D20 ### SpeechRecognition > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition) interim n.間歇;過渡期間[U] a.[B 間歇的,過渡期間的;臨時的,暫時的 :heavy_check_mark:剛開始會可以錄音,但一陣子就會被關掉 要重新開啟 ```javascript recognition.addEventListener("end", recognition.start); // 但不是speechend這個event recognition.addEventListener("speechend", recognition.start); ``` > **end**: when the speech recognition service has disconnected. 整個斷掉連結時 > **speechend**: when speech recognized by the speech recognition service has stopped being detected. 停止偵測時 找到錄出來的東西存放在哪裡了 ![image](https://hackmd.io/_uploads/ByzV_pAsA.png) 影片說要把results裡面的陣列的transcript收集起來,但我看起來只使用第一個的結果 ```javascript const transcript = event.results[0][0].transcript; const transcript2 = [...event.results] .map((result) => result[0].transcript) .join(""); console.log(transcript); console.log(transcript2); ``` ![image](https://hackmd.io/_uploads/BkateRAoA.png) 新增一個新的`<p>` ```javascript // 我寫的 const p = document.createElement("p"); words.appendChild(p); function addRecognitionResults(event) { const transcript = event.results[0][0].transcript; words.lastChild.textContent = transcript; // 用lastChild去取得p // 最後新增一個 if (event.results[0].isFinal === true) { const p = document.createElement("p"); words.appendChild(p); } } // 影片作法 let p = document.createElement("p"); // 這裡用let words.appendChild(p); function addRecognitionResults(event) { const transcript = event.results[0][0].transcript; p.textContent = transcript; // 最後新增一個 if (event.results[0].isFinal === true) { p = document.createElement("p"); // 直接代換成一個新的p words.appendChild(p); } } ``` ### 隨著內容增加而滾動視窗 自己隨便嘗試 #### WebAPI: MutationObserver > [MDN](https://developer.mozilla.org/zh-TW/docs/Web/API/MutationObserver) childList 改變node的child element時觸發 ```javascript // 建立 MutationObserver const observer = new MutationObserver((mutations) => { // console.log(mutations); mutations.forEach((mutation) => { if (mutation.type === "childList") { // 內容有變更(height變大) const divHeight = words.scrollHeight; window.scrollTo(0, divHeight); } }); }); // 開始監聽 observer.observe(words, { childList: true }); ```