---
# System prepended metadata

title: C_JS30 D21-30
tags: [DOM]

---

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