[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

> 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!!!

> [MDN](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/voiceschanged_event)
SpeechSynthesis.getVoices()
```javascript
speechSynthesis.getVoices();
```
得到目前有哪些聲音可以用

然後把聲音們放進選項裡
```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等等的

```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`
原先的想法是滑到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);
});
```

點擊three -> Capture (瀏覽器認為你點擊了one -> 瀏覽器認為你點擊了two -> 瀏覽器認為你點擊了three) -> 把這些事件儲存起來 -> Bubbling 觸發這些事件 (觸發three -> 觸發two -> 觸發one)

### addEventListener的第三個參數
#### 設置`capture: true`
```javascript
divs.forEach((div) => {
div.addEventListener("click", logText, {
capture: true
});
});
```
觸發變成由上到下
瀏覽器認為你點擊了one -> 觸發one -> 瀏覽器認為你點擊了two -> 觸發two -> 瀏覽器認為你點擊了three -> 觸發three

如果不想要發生這種事件傳遞的問題
`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

#### 設置`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()的數值去移動背景,會發現不太對,因為背景的初始位置不在頁面最頂端

所以影片再取得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

改成這樣
```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`會長這樣

呀哈這是什麼鬼東西 尖尖哇咖乃
如果設一個超大的數`perspective: 1000000px`會變得跟設none差不多

再搭配上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)

## 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}`;
}
```

計算結束時間的方式是得到一串數字後用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]取得

```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
### 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);
});
```