[TOC] ## D21 ### 無法動彈 我遇到的狀況:用作者寫好的檔案有機會跑出亂碼和方向,用自己寫的還沒有動靜過 有人說heading指向的是你移動時朝向的方向,所以如果你沒有水平移動就不會有heading 另外這個人推薦使用deviceorientation > The heading attribute in the geolocation is not the compas orientation but a direction calculated based on the movement of the phone. So if you are standing still heading is null. > [stackoverflow](https://stackoverflow.com/a/77178204) > If GeolocationCoordinates.speed is 0 or the device is not able to provide heading information, heading is null. > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates/heading) ~~只好把手機丟出去測試看看ㄌ~~ 測試了一下感覺是沒有偵測到經緯度的移動所以速度為0,heading就是null ## D22 :heavy_check_mark:自己做hover效果的用意是什麼 為了之後要做下拉式選單 自己試做起來大概就是抓到a的offset然後把highlight的offset設定成a的 遇到的問題:如果offsetParent不是body,利用offsetTop/Left去移動位置的時候就會怪怪的 我ㄉ解決方法:再加上offsetParent的offsetTop/Left ```javascript let top; let left; if (this.offsetParent !== document.body) { top = this.offsetParent.offsetTop + this.offsetTop; left = this.offsetParent.offsetLeft + this.offsetLeft; } else { top = this.offsetTop; left = this.offsetLeft; } ``` 還是有問題:如果parent的parent仍然不是body就會爆炸 ### 怎麼樣會變成offsetParent ![image](https://hackmd.io/_uploads/Hy-DrVVnC.png) > The offset parent of an element is the nearest ancestor with a position other than static, or the body if none of the ancestor have positioning. [ref](https://polypane.app/blog/offset-parent-and-stacking-context-positioning-elements-in-all-three-dimensions/) ### getBoundingClientRect() 影片用這個來解決 > The Element.getBoundingClientRect() method returns a DOMRect object providing information about the size of an element and its position relative to the viewport. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) ```javascript const linkCoords = this.getBoundingClientRect(); highlight.style.width = `${linkCoords.width}px`; highlight.style.height = `${linkCoords.height}px`; ``` ```javascript // 比起直接用top/left highlight.style.top = `${linkCoords.top}px`; highlight.style.left = `${linkCoords.left}px`; // 影片覺得用transform比較滑順 ``` 記得加上window.scrollX/Y ```javascript const linkCoords = this.getBoundingClientRect(); highlight.style.width = `${linkCoords.width}px`; highlight.style.height = `${linkCoords.height}px`; highlight.style.transform = `translate(${ window.scrollX + linkCoords.left }px, ${window.scrollY + linkCoords.top}px)`; // 重構整理 const coords = { width: linkCoords.width, height: linkCoords.height, top: window.scrollY + linkCoords.top, left: window.scrollX + linkCoords.left, }; ``` ## D23 ### SpeechSynthesis > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) 他的event只有voicechanged!!! ![image](https://hackmd.io/_uploads/HJRzKLE3C.png) > [MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/voiceschanged_event) SpeechSynthesis.getVoices() ```javascript speechSynthesis.getVoices(); ``` 得到目前有哪些聲音可以用 ![image](https://hackmd.io/_uploads/rkERcUEh0.png) 然後把聲音們放進選項裡 ```javascript function populateVoices() { // console.log(this); // SpeechSynthesis{} voices = this.getVoices(); // 把這些聲音放進選項裡 const voiceOptions = voices .map( (voice) => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>` ) .join(""); // 我改用concat,這樣預設的選項Select A Voice才不會消失 voicesDropdown.innerHTML = voicesDropdown.innerHTML.concat("", voiceOptions); } ``` 基本上就是取得input的value然後去改`const msg = new SpeechSynthesisUtterance();`這個msg的資料 例如voice, rate, pitch, text等等的 ![image](https://hackmd.io/_uploads/S1FWgPH2A.png) ```javascript function setVoice() { msg.voice = voices.find((voice) => voice.name === this.value); } function setOptions() { const name = this.name; msg[name] = this.value; } ``` 播放、取消則是使用speechSynthesis的methods ```javascript // window可省略 window.speechSynthesis.speak(msg); window.speechSynthesis.cancel(); ``` ## D24 自己實作一個`position: sticky`![image](https://hackmd.io/_uploads/H1iDqtr2A.png) 原先的想法是滑到nav的頭時, 把nav的position改成fixed ```javascript if (window.scrollY >= nav.offsetTop) { nav.style.position = "fixed"; } ``` 結果nav變成fixed之後offsetTop就變成0了,然後window.scrollY無論如何都無法小於0了哈哈 所以要在一開始把這個數值固定下來 ```javascript const topOfNav = nav.offsetTop; function fixNav() { if (window.scrollY >= topOfNav) { nav.style.position = "fixed"; } // 影片是直接在body新增一個class去控制這個狀態下的其他element document.body.classList.add("fixed-nav"); ``` 另一個問題是因為position變成fixed造成nav這塊脫離normal flow,然後下面的東西往上移的問題 影片作法是增加body的padding ```javascript document.body.style.paddingTop = `${nav.offsetHeight}px`; ``` logo顯示/隱藏 ```css li.logo { max-width: 0; overflow: hidden; transition: all 0.5s; } .fixed-nav li.logo { max-width: 500px; /* 設一個很大的數這樣transition才有作用 */ } ``` ### IntersectionObserver 留言補充了一個酷酷的東西 可以看[這篇](https://jim1105.coderbridge.io/2022/07/30/intersection-observer/)的介紹 ```javascript const callback = function (entries) { console.log(entries); }; const observer = new IntersectionObserver(callback, { root: null, rootMargin: "0px 0px 0px 0px", threshold: 0.0, }); ``` options.root預設是null, 表示以 Viewport 作為判斷依據 用IntersectionObserver做做看 ~~我是抄留言的~~ ```javascript const header = document.querySelector("header"); function fixNav(entries) { const isIntersecting = entries[0].isIntersecting; if (isIntersecting) { document.body.classList.remove("fixed-nav"); document.body.style.paddingTop = 0; } else { document.body.classList.add("fixed-nav"); document.body.style.paddingTop = `${nav.offsetHeight}px`; } } const obs = new IntersectionObserver(fixNav); obs.observe(header); ``` ## D25 Capture, Propagation and Bubbling 設置如下 ```javascript const divs = document.querySelectorAll("div"); const body = document.querySelector(".bod"); function logText() { console.log(this); } divs.forEach((div) => { div.addEventListener("click", logText); }); ``` ![image](https://hackmd.io/_uploads/BJkUasInC.png) 點擊three -> Capture (瀏覽器認為你點擊了one -> 瀏覽器認為你點擊了two -> 瀏覽器認為你點擊了three) -> 把這些事件儲存起來 -> Bubbling 觸發這些事件 (觸發three -> 觸發two -> 觸發one) ![image](https://hackmd.io/_uploads/rJ-mlp83C.png) ### addEventListener的第三個參數 #### 設置`capture: true` ```javascript divs.forEach((div) => { div.addEventListener("click", logText, { capture: true }); }); ``` 觸發變成由上到下 瀏覽器認為你點擊了one -> 觸發one -> 瀏覽器認為你點擊了two -> 觸發two -> 瀏覽器認為你點擊了three -> 觸發three ![image](https://hackmd.io/_uploads/rk5VlpL2A.png) 如果不想要發生這種事件傳遞的問題 `event.stopPropagation();` ```javascript function logText(event) { console.log(this); event.stopPropagation(); } // 然後capture一樣是true的話 divs.forEach((div) => { div.addEventListener("click", logText, { capture: true }); }); ``` 原本是one -> two -> three,變成停在one ![image](https://hackmd.io/_uploads/HJRiNpL20.png) #### 設置`once: true` 點擊一次之後就沒有了 ```javascript divs.forEach((div) => { div.addEventListener("click", logText, { capture: false, once: true, }); }); ``` 效果等同於事件發生一次後,把EventListener刪掉 ```javascript div.removeEventListener("click", logText); ``` ## D26 延續D22的東西,把下拉式選單做出來 ```javascript // 一起加的話,因為background會延遲,所以會造成內容比背景先出現的狀況 function openDropdown(event) { this.classList.add("trigger-enter"); this.classList.add("trigger-enter-active"); // ... } // 所以影片改用setTimeout function openDropdown(event) { this.classList.add("trigger-enter"); setTimeout(() => { this.classList.add("trigger-enter-active"); }, 150); // ... } ``` classList.remove可以一次移除很多個 `this.classList.remove("trigger-enter", "trigger-enter-active");` > add也可以 [name=Jeremy] 如果直接取getBoundingClientRect()的數值去移動背景,會發現不太對,因為背景的初始位置不在頁面最頂端 ![image](https://hackmd.io/_uploads/Hymc_BOnA.png) 所以影片再取得nav的座標 ```javascript const nav = document.querySelector(".top"); const navCoords = nav.getBoundingClientRect(); ``` 但我是直接取背景的座標 ```javascript const dropdownBackground = document.querySelector(".dropdownBackground"); const backgroundCoords = dropdownBackground.getBoundingClientRect(); ``` :heavy_check_mark:結果是一樣的,不知道為什麼不直接取背景的座標 因為背景會被我改掉位置!!! 所以除非在初始化的時候在global取得位置,否則就會改變數值 因為影片是在function裡面重新取得,所以他取固定的nav的座標 另一個問題是游標在上面快速滑動的時候會出錯 發現有兩個li的class都有active ![image](https://hackmd.io/_uploads/SJ-PGLO3A.png) 改成這樣 ```javascript setTimeout(() => { if (this.classList.contains("trigger-enter")) { this.classList.add("trigger-enter-active"); } }, 150); // 或是用&& setTimeout(() => this.classList.contains("trigger-enter") && this.classList.add("trigger-enter-active"), 150); ``` ## D27 首先先看為啥還沒有js的時候手動移就有透視效果 ### perspective 在.items上設了`perspective: 500px` > [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/perspective) 如果是`perspective: 0`會長這樣 ![image](https://hackmd.io/_uploads/ByxkxYO2R.png) 呀哈這是什麼鬼東西 尖尖哇咖乃 如果設一個超大的數`perspective: 1000000px`會變得跟設none差不多 ![image](https://hackmd.io/_uploads/Bkb_etO30.png) 再搭配上rotateY(40deg)/rotateY(-40deg)就有凹凸感 --- 回到正題,概念是滑鼠往左20px,這個div就要往左scroll 20px ### Element.scrollLeft > Get or **Set** the number of pixels by which an element's content is scrolled from its left edge. [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollLeft) 終於不是唯讀屬性了QQ 我的初始解法: ```javascript const items = document.querySelector(".items"); let startX; function setStart(event) { startX = event.clientX; // 紀錄滑鼠點下去的位置 } function moveItems(event) { if (event.buttons === 1) { // 滑鼠移動時需要同時有左鍵點擊才啟動 items.classList.add("active"); const endX = event.clientX; // 移動後的位置 const movementX = endX - startX; // 計算移動距離 this.scrollLeft += -movementX; // 設定要讓這個元素滑多少距離 // 且因為滑鼠往左(movementX為負)時其實是要往右滑(增加數值) // 所以要加上-movementX startX = endX; // 讓這次的移動結束點成為下一次的移動起始點 } } function leaveItems() { items.classList.remove("active"); } items.addEventListener("mousedown", setStart); items.addEventListener("mousemove", moveItems); items.addEventListener("mouseup", leaveItems); ``` 影片解法: 偵測滑鼠是否有點擊是用一個flag去偵測,並用mousedown跟mouseup來控制 [見D8筆記](https://hackmd.io/HUgrtEbVR1u81rTCWMTwxw?view#D8) 多用一個mouseleave來控制當滑鼠移出目標時的情況(滑出去之後還可不可以繼續移動) :question:如果用我自己的方式要怎麼做到mouseleave時取消?感覺應該是不行XD因為點擊框框外後按住不放移進框框內也可以移動目標 使用pageX來定位座標且要扣除目標的offsetLeft `startX = event.pageX - items.offsetLeft;` 但我感覺沒啥差?? 加一個preventDefault防止滑動時選取到裡面的字 但沒加感覺也不會選到就是了0.0 ```javascript function moveItems(event) { event.preventDefault(); } ``` 增加滑動速度 `const movementX = (endX - startX) * 3;` 留言補充 直接用event.movementX也是可以直接取得移動距離 ~~那我們在忙什麼~~ ```javascript function moveItems(event) { event.preventDefault(); if (event.buttons === 1) { items.classList.add("active"); // 只要這一行啊 this.scrollLeft += -event.movementX; } } ``` 不過有警告可能不同瀏覽器的單位會不同 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX) ![image](https://hackmd.io/_uploads/rykicBYn0.png) ## D28 題目是希望可以拖拉旁邊的控制條去調整影片的播放速率 我的解法: ```javascript const speedBar = document.querySelector(".speed"); const currentSpeed = document.querySelector(".speed-bar"); const video = document.querySelector(".flex"); const oneTimeSpeed = currentSpeed.offsetHeight; // 取得1倍速的高度 function adjustSpeed(event) { event.preventDefault(); if (event.buttons === 1) { const speedRate = (event.offsetY / oneTimeSpeed).toFixed(1); // 計算移動的那個位置相對於1倍速是幾倍,並且留小數點第一位 currentSpeed.style.height = `${oneTimeSpeed * speedRate}px`; // 設定控制條的高度 currentSpeed.textContent = `${speedRate}x`; // 設定控制條的文字 video.playbackRate = Number(speedRate); // 調整影片速率 } } speedBar.addEventListener("mousemove", adjustSpeed); ``` 缺點:有顏色的進度條在拉滿的時候有機率超過容器然後變得怪怪的 所以影片的作法是計算百分比,然後去調整有顏色的進度條的height是XX%,並且有設定最大值和最小值 ```javascript // 影片作法 const min = 0.4; const max = 4; const percent = event.offsetY / speedBar.offsetHeight; const height = Math.round(percent * 100) + "%"; const playbackRate = (max - min) * percent + min; currentSpeed.style.height = height; currentSpeed.textContent = `${playbackRate.toFixed(1)}x`; video.playbackRate = playbackRate; ``` ## D29 目前解法 非常之亂,而且還沒處理自行輸入的部份 但大概可以運作XDD ```javascript const timeLeftDisplay = document.querySelector(".display__time-left"); const endTimeDisplay = document.querySelector(".display__end-time"); const timerButtons = document.querySelectorAll(".timer__button"); let intervalID; let totalSeconds; function timer(seconds) { intervalID = setInterval(() => { // 如果秒數為零則清除interval且停止本次的運作 if (seconds === 0) { clearInterval(intervalID); return; } seconds -= 1; const minutes = Math.floor(seconds / 60); // 秒數個位數時補0 const secondsLeft = seconds % 60 < 10 ? `0${seconds % 60}` : `${seconds % 60}`; timeLeftDisplay.textContent = `${minutes}:${secondsLeft}`; }, 1000); } function setTimer() { // 要把前一個setInterval取消 if (intervalID !== undefined) { clearInterval(intervalID); } totalSeconds = this.dataset.time; const minutes = Math.floor(totalSeconds / 60); // 秒數個位數時補0 const seconds = totalSeconds % 60 < 10 ? `0${totalSeconds % 60}` : `${totalSeconds % 60}`; timeLeftDisplay.textContent = `${minutes}:${seconds}`; timer(totalSeconds); } function showEndTime() { const now = new Date(); const currentHour = now.getHours(); const currentMinute = now.getMinutes(); let minutes = Math.floor(totalSeconds / 60); const hours = Math.floor(minutes / 60); minutes = minutes % 60; let endHour = currentHour + hours; // 如果超過12 // 目前只能處理超過一次12 if (endHour > 12) { endHour -= 12; } let endMinute = currentMinute + minutes; // 如果超過60 // 應該只會超過一次60? if (endMinute > 60) { endMinute -= 60; endHour++; } // 分鐘個位數時補0 endMinute = endMinute < 10 ? `0${endMinute}` : `${endMinute}`; endTimeDisplay.textContent = `Be back at ${endHour}:${endMinute}`; } timerButtons.forEach((button) => { button.addEventListener("click", setTimer); button.addEventListener("click", showEndTime); }); ``` 影片教學: 可以把顯示的部份拆成function ```javascript function displayTimeLeft(seconds) { const minutes = Math.floor(seconds / 60); const secondsLeft = seconds % 60 < 10 ? `0${seconds % 60}` : `${seconds % 60}`; timeLeftDisplay.textContent = `${minutes}:${secondsLeft}`; // 設定頁面標題 document.title = `${minutes}:${secondsLeft}`; } ``` ![image](https://hackmd.io/_uploads/rkenw-hh0.png) 計算結束時間的方式是得到一串數字後用Date換算成時間,不是自己手算的!! ```javascript const now = Date.now(); // 單位是milliseconds const then = now + seconds * 1000; // 所以秒數要乘以1000 showEndTime(then); function showEndTime(timestamp) { const end = new Date(timestamp); const hour = end.getHours(); const adjustedHour = hour > 12 ? hour - 12 : hour; const minutes = end.getMinutes(); endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes}`; } ``` 注意型別 `totalSeconds = parseInt(this.dataset.time);` clearInterval傳入undefined沒關係 ### 表單輸入 如果一個element有name屬性,可以直接用document.[name]取得 ![image](https://hackmd.io/_uploads/HJPiA-23R.png) ```javascript function setCustomTimer(event) { // 不要重新整理頁面 event.preventDefault(); totalSeconds = parseInt(this.minutes.value) * 60; timer(totalSeconds); this.reset(); // 送出後清除前面的輸入 } function timer(seconds) { clearInterval(intervalID); displayTimeLeft(seconds); showEndTime(seconds); intervalID = setInterval(() => { if (seconds === 0) { clearInterval(intervalID); return; } seconds -= 1; displayTimeLeft(seconds); }, 1000); } ``` ### 留言出的回家作業 新增暫停和重新啟動按鈕 ```javascript let secondsLeft; function timer(seconds) { clearInterval(intervalID); displayTimeLeft(seconds); showEndTime(seconds); secondsLeft = totalSeconds; // 加這行 intervalID = setInterval(() => { if (seconds === 0) { clearInterval(intervalID); return; } seconds -= 1; secondsLeft = seconds; // 加這行 displayTimeLeft(seconds); }, 1000); } let isPaused = false; function pauseTimer() { clearInterval(intervalID); isPaused = true; } function resumeTimer() { if (isPaused) { timer(secondsLeft); isPaused = false; } } pauseButton.addEventListener("click", pauseTimer); resumeButton.addEventListener("click", resumeTimer); ``` ## D30 初步解法: ```javascript const holes = document.querySelectorAll(".hole"); const scoreBoard = document.querySelector(".score"); const moles = document.querySelectorAll(".mole"); let score = 0; let randomNumber; function startGame() { setInterval(() => { randomNumber = Math.floor(Math.random() * holes.length); //出現地鼠 const mole = holes[randomNumber].querySelector(".mole"); mole.style.top = 0; setTimeout(() => { mole.style.top = "100%"; }, 300); // 地鼠消失 }, 100); } function addScore() { score++; scoreBoard.textContent = `${score}`; } moles.forEach((mole) => { mole.addEventListener("click", addScore); }); ``` 影片教學: 連地鼠出現的時間都要隨機 ```javascript function randomTime(min, max) { return Math.round(Math.random() * (max - min) + min); } //... setTimeout(() => { hole.classList.remove("up"); }, randomTime(200, 1000)); ``` 不要重複控制同一個洞 並且一次只出現一隻 ```javascript let lastHole; function popMole(holes) { randomNumber = Math.floor(Math.random() * holes.length); const hole = holes[randomNumber]; if (hole === lastHole) { // 如果是前一個洞就再重來 return popMole(holes); } hole.classList.add("up"); setTimeout(() => { hole.classList.remove("up"); popMole(holes); // 繼續下一個洞 }, randomTime(200, 1000)); lastHole = hole; } ``` 設定遊戲結束時間 ```javascript let timeUp = false; setTimeout(() => { hole.classList.remove("up"); if (timeUp === false) { // 這裡控制要不要繼續跑下一個 popMole(holes); } }, randomTime(200, 1000)); ``` 設定遊戲排程 ```javascript function startGame() { scoreBoard.textContent = `0`; // 歸零 timeUp = false; score = 0; // 歸零 popMole(holes); // 開始遊戲 // 時間到,遊戲結束 setTimeout(() => { timeUp = true; }, 10000); } ``` 打擊行為 bonk![image](https://hackmd.io/_uploads/rkO9TVh20.png) ### isTrusted 要確定這個點擊不是模擬出來的 > The isTrusted read-only property of the Event interface is a boolean value that is true when the event was generated by the **user agent** (including via user actions and programmatic methods such as HTMLElement.focus()), and false when the event was dispatched via EventTarget.dispatchEvent(). > > The only exception is the **click** event, which initializes the isTrusted property to false in user agents. [MDN](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/isTrusted) 但好像沒觀察到這個現象? ```javascript function bonk() { if (!event.isTrusted) { return; } score++; // 這邊影片寫錯 this.classList.remove("up"); // mole沒有up這個class // 應該是父層的class this.parentNode.classList.remove('up'); scoreBoard.textContent = `${score}`; } moles.forEach((mole) => { mole.addEventListener("click", bonk); }); ```