---
# System prepended metadata

title: C_JS30 D1-10
tags: [DOM]

---

[TOC]
## D1
查詢keycode
https://www.toptal.com/developers/keycode
```html
 <div class="keys">
    <!-- data-key後面的數字是keycode -->
    <div data-key="65" class="key">
      <kbd>A</kbd>
      <span class="sound">clap</span>
    </div>
</div>
```
:question:為什麼不把data-key放在`<kbd>`?
> 看起來如果放在`<kbd>`那在取得元素的時候就會取到`<kbd data-key="65">A</kbd>`，這樣在加入屬性`playing`時就只會加到A這個字上而不是整個框框，不知道我有沒有針對到問題討論哈哈哈
> [name=Jeremy]

### addEventListener(type, listener)
type: event type
listener: function

各類型的Events內有不同的小event
[MDN](https://developer.mozilla.org/en-US/docs/Web/Events)


```javascript
window.addEventListener("keydown", (keyBoardInput) => {
    console.log(keyBoardInput);
});
// 這邊的keyBoardInput會是描述每次鍵盤輸入的細節的物件
```
例如說我按了向上鍵
就會在console中顯示這個向上鍵的物件
![image](https://hackmd.io/_uploads/SJgg_VUOC.png)
```javascript
window.addEventListener("keydown", (keyBoardInput) => {
    // 選擇<audio>且其data-key屬性的值等於65
    const audio = document.querySelector('audio[data-key="65"]')
});


window.addEventListener("keydown", (keyBoardInput) => {
    // 選擇<audio>且其data-key屬性的值等於我用鍵盤按的那個鍵
    const audio = document.querySelector(`audio[data-key="${keyBoardInput.keyCode}"]`);
    console.log(audio);
});
```
### DOM提供的屬性
想要用`Object.keys(audio)`去查我擷取到的`<audio>`到底有哪些屬性失敗了QQ
![image](https://hackmd.io/_uploads/r18fqBvO0.png)

:heavy_check_mark:因為他是Element不是Object? 
> 我嘗試使用`console.dir`將他視為物件展開：
![image](https://hackmd.io/_uploads/HyEGX6U_C.png)
但前提是我在全域環境有取得到`<audio>`這個元素，而不是在function裡面：
```javascript
const audio = document.querySelector(`audio[data-key='65']`);
```
> 但我覺得他是DOM的特殊物件所以應該沒辦法當成一般物件為所欲為
> 正常印出來長這樣
![image](https://hackmd.io/_uploads/S1N14pLO0.png)
> [name=Jeremy]

結果是在原型的原型上面:clap:
![image](https://hackmd.io/_uploads/HJf5qSPO0.png)

但可以在devTool去看
![image](https://hackmd.io/_uploads/Sy9umSLOA.png)
看到a播放完之後currentTime變成播放完的時間點0.483991
:question:`Element.play()`為什麼不會從頭開始播放??
> media.play()
Sets the paused attribute to false, loading the media resource and beginning playback **if necessary.** 
**If the playback had ended, will restart it from the start.**
[規範](https://html.spec.whatwg.org/multipage/media.html#dom-media-play-dev)

已經播放完畢的定義：
1. 目前時間點在結束的點，且播放方向是向前播放，且沒有loop屬性
或是
2. 目前時間點在開始的點，且播放方向是倒帶播放

When the play() method on a media element is invoked 
=> 1., 2., 3., 4. Run the internal play steps for the media element.

internal play steps
1. 跳過
2. 如果已經播放完畢，則從頭開始播放
3. 如果 .paused = true
4. 如果 .readyState = 3(HAVE_FUTURE_DATA)或4(HAVE_ENOUGH_DATA)，通知這個element正在播放
> take pending play promises and queue a media element task given the media element to resolve pending play promises with the result. :question::question::question::question:
> The media element is already playing. However, it's possible that promise will be rejected before the queued task is run.
5. 跳過

### 監聽所有事件
原本想說先增加class => 播放媒體 => 刪掉class
```javascript
keyDiv.classList.add("playing");

audio.currentTime = 0;
audio.play();

keyDiv.classList.remove("playing");
```
結果沒有反應XD
所以要去監聽這個key的css變化(transition)是不是結束了

[events about CSS transition](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions#detecting_the_start_and_completion_of_a_transition)

```javascript
const keys = document.querySelectorAll(".key"); // return Array

keys.forEach((key) =>
    key.addEventListener("transitionend", removeTransition)
);

function removeTransition(transitionEvent) {
    console.log(transitionEvent);
}
// 回傳的是transitionEvent
```
transitionEvent
![image](https://hackmd.io/_uploads/H122kDI_R.png)

:question:為什麼要過濾events?
```javascript
function removeTransition(transitionEvent) {
    // 影片中多加的這行，結果連點就會當掉？？？
    if (transitionEvent.propertyName !== "transform"){
        return;
    }
}
```
要怎麼得到這個key element呢？
用this!!!!
看看忍者說什麼
> The event-handling system of the browser defines the context of the invocation to be the target element of the event, which causes the context to be the `<button>` element, not the button object.

也就是說，**瀏覽器**會把`addEventListener`這類型處理事件的東西(?)會把function context定義為這個事件的目標element，在這裡的例子就是指key

### 被棄用的keyCode屬性
我把程式碼移到一個新的.js後VSCode跟我說keyCode這個屬性已經被棄用了
![image](https://hackmd.io/_uploads/HJgYWPvOC.png)
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)也有標註這件事
被棄用的原因：KeyboardEvent.keyCode單指實體的按鍵，無視實際按出來的字到底是什麼，例如小寫a和大寫A的keyCode會是一樣的

那目前要使用什麼比較好？
- KeyboardEvent.code
    直接回傳實際按出來的字，缺點是會出現符號，可能在寫程式碼上會有問題？
- KeyboardEvent.key
    表示實體的按鍵，就算按的是符號，回傳的也是英文字串
> [參考](https://www.reddit.com/r/learnjavascript/comments/zciru6/comment/iyyt3ch/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)

## D2
### 自己調transition曲線
![image](https://hackmd.io/_uploads/r1tE0cPdR.png)

### 分檔發生語法問題
我分了一個tools.js，然後import到main.js
結果發生以下錯誤
> Uncaught SyntaxError: Cannot use import statement outside a module

解決方法：在html引入script的地方加上屬性`type="module"`
![image](https://hackmd.io/_uploads/rk8bsAOOA.png)

### 從59秒跳回0秒的時候會亂跳
因為角度會從354度到0度，所以transition偵測到的是transform中倒轉的變化
目前的解決方法是讓角度一直往上加，而不是從0~360度之間一直重複
```javascript
// 用+=
secondDegree += getSecondDegree(secondDifference);
minuteDegree += getMinuteDegree(minuteDifference, secondDifference);
hourDegree += getHourDegree(hourDifference, minuteDifference);

rotateHand(minuteHand, minuteDegree);
rotateHand(secondHand, secondDegree);
rotateHand(hourHand, hourDegree);

// functions
export function getSecondDegree(second) {
  return (second / 60) * 360;
}

export function getMinuteDegree(minute, second) {
  return (minute / 60) * 360 + (second * 6) / 60;
}

export function getHourDegree(hour, minute) {
  return (hour / 12) * 360 + (minute * 30) / 60;
}

export function rotateHand(hand, degree) {
  hand.style.transform = `rotate(${degree}deg)`;
}
```
考慮到如果單純讓setInterval跑一秒加一次角度可能會有誤差，所以我用當下時間點扣掉上次轉的時間點的時間差去計算角度
不過不曉得這樣到底有沒有解決誤差的問題XD


## D3
```html
<h2>Update CSS Variables with <span class='hl'>JS</span></h2>
```
:heavy_check_mark: 為什麼h2裡面的span的class是h1？？？什麼怪命名
原來是highlight的hl啊🙄

### NodeList
和Array不一樣，也沒有那麼多[].methods可以用
```javascript
const inputs = document.querySelectorAll(".controls input"); 
// 回傳的是NodeList
```
![image](https://hackmd.io/_uploads/H1dsycjuR.png)

### 滑動滑桿(?)要跟著變化
這兩個要一起設定
addEventListener("change", );
addEventListener("mousemove", );
:heavy_check_mark:為什麼測試的時候，滑鼠在上面移動會印出東西，但實際運作的時候滑鼠移動不會更改變數數值，一定要點擊拉動滑桿才會？
> 因為單純mousemove不會改變input的value
> [name=Jeremy]

影片有人留言說可以改用input
試起來只需要input就可以達到change和mousemove的效果
> The **input** event is fired every time the value of the element changes. This is unlike the **change** event, which only fires when the value is committed. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event)

### DOM.dataset是啥
會在elements上加一些前綴為data-的屬性，這些屬性會被存在dataset裡面
```html
<input
        id="spacing"
        type="range"
        name="spacing"
        min="10"
        max="200"
        value="10"
        data-sizing="px"
        data-range="20-100"
        data-happy="yes"
      />
```
![image](https://hackmd.io/_uploads/B1U1UqjuR.png)

### documentElement表示root
```javascript
document.documentElement.style.setProperty(
    `--${this.name}`, this.value + suffix
);
```
![image](https://hackmd.io/_uploads/r1-Dq5iuC.png)

順帶一提，我很無聊加了恢復預設值的按鈕
暴力解決
如果有更好的寫法歡迎跟我說🥹
```javascript
const reset = document.querySelector(".reset input");

function resetValues() {
  document.documentElement.style.setProperty("--spacing", "10px");
  document.documentElement.style.setProperty("--base", "#ffc600");
  document.documentElement.style.setProperty("--blur", "0px");
}

reset.addEventListener("click", resetValues);
```

## D4
### Q6 微爬蟲
題目是從網頁上的清單過濾出'de'的
![image](https://hackmd.io/_uploads/HydvFonOC.png)
不過教學是教兩段式選取
:question: 那我一次選完可以嗎?
```javascript
const category = document.querySelector(".mw-category")
const links = category.querySelectorAll("a")
// querySelectorAll的主詞不一定要是document
```
把NodeList轉成Array
```javascript
// 使用Array.from
const boulevardsName = Array.from(boulevards).map(
    (elememt) => elememt.textContent
);

// 或是使用展開運算子
const boulevardsName = [...boulevards].map(
    (elememt) => elememt.textContent
);
```
:question: 為什麼要取textContent? 取text呢? 其他的呢?
![image](https://hackmd.io/_uploads/HypgyAhuA.png)
![image](https://hackmd.io/_uploads/rkBzkAnO0.png)
![image](https://hackmd.io/_uploads/rkGXJ0ndR.png)

### Q7
字串比大小
```javascript
const peopleSortedByLast = people.sort((personA, personB) => {
    const [personALast, personAFirst] = personA.split(", ");
    const [personBLast, personBFirst] = personB.split(", ");
    return personALast > personBLast ? -1 : 1;
    // 字串不能直接相減，會變成NaN，要用比較的
});
```

## D5
flex忘光光中 余光中
> flex:  <‘flex-grow’> <‘flex-shrink’> <‘flex-basis’> 
> 三個的預設值為1, 1, 0
> 但flex本身的預設值0 1 auto
> [規範](https://www.w3.org/TR/css-flexbox-1/#flex-property)

設了兩個class，`.open`和`.open-active`

### 箭頭函式沒有this!!!
```javascript
// 原本寫這樣
// 結果因為this是window所以報錯
panels.forEach((panel) =>
  panel.addEventListener("click", () => {
    this.classList.add("open");
  })
);

// 後來改成這樣就好了
panels.forEach((panel) =>
  panel.addEventListener("click", function () {
    this.classList.add("open");
  })
);
```
:question:結果我還是不懂QwQ
> [先端上MDN的說明](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#the_value_of_this_within_the_handler)

```javascript
my_element.addEventListener("click", function (e) {
  console.log(this.className); // logs the className of my_element
  console.log(e.currentTarget === this); // logs `true`
});

my_element.addEventListener("click", (e) => {
  console.log(this.className); // WARNING: `this` is not `my_element`
  console.log(e.currentTarget === this); // logs `false`
});
```


### Element.classList.toggle
本來以為要加class就跟以前一樣用.add結果這次給我用.toggle
> 補充
> [classList的methods](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList)

```javascript
// if visible is set remove it, otherwise add it
// 如果有這個class就刪掉，沒有就加入
div.classList.toggle("visible");
```
:question:open-active如果不篩選transition就不能動

> 好像是因為任何`transition`都會觸發事件，只要超過一種`transition`他就會當機，所以才只篩選他有`flex`出現的時候觸發。
> 我有做另外一個實驗，將`.panel.open`的`font-size`樣式拿掉，讓他只剩一個`flex`，就不需要篩選就會看到效果了。
> [name=Jeremy]
```javascript
// 這樣動不了
function toggleOpenActive(event) {
  this.classList.toggle("open-active");
}

// 一定要篩選才可以動
function toggleOpenActive(event) {
  // if (event.propertyName === "flex") { // 因為瀏覽器不一樣
  if (event.propertyName.includes("flex")) {
    this.classList.toggle("open-active");
  }
}
```

## D6
### fetch
是瀏覽器API，回傳一個promise
> 會讓它發生 reject 的只有網路錯誤或其他會中斷 request 的情況。
> [MDN](https://developer.mozilla.org/zh-TW/docs/Web/API/Fetch_API/Using_Fetch)

第一個參數是資料，第二個參數是用來設定request(還不是很確定要怎麼用)

```javascript
// MDN的範例
// 這裡要使用 fetch 透過網路取得 json 然後印出在 console，
// 只需要一個參數: 資料的 URI
// fetch 會回傳一個包含 response 的 promise 
fetch("http://example.com/movies.json")
  .then(function (response) {
    return response.json(); 
  })
  .then(function (myJson) {
    console.log(myJson);
  });
```
回傳的response長什麼樣子
```javascript
const endpoint =
  "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json";

fetch(endpoint).then(console.log);
```
![image](https://hackmd.io/_uploads/Bkfjt_1Y0.png)
利用.json把這個response解析成JSON
![image](https://hackmd.io/_uploads/HJgM9dyF0.png)

```javascript
fetch(endpoint)
  .then((response) => response.json()); // 得到長度為1000的Array
  
const fetchedData = [];

// 白痴作法==
fetch(endpoint)
  .then((response) => response.json())
  .then((data) => {
    data.forEach((cityData) => fetchedData.push(cityData));
  });

// 請善用展開運算子
fetch(endpoint)
  .then((response) => response.json())
  .then((data) => fetchedData.push(...data));
```
![image](https://hackmd.io/_uploads/S1CB0uJFC.png)

### RegExp
該來的還是躲不過嗎QQ
```javascript
// 用RegExp object才能把變數放進去
const regex = new RegExp(wordToMatch, "gi"); // g是global, i是case-insensitive
return place.city.match(regex) || place.state.match(regex);
```

### 邊打字邊觸發
```javascript
// 影片使用組合技
searchInput.addEventListener("change", showValue);
searchInput.addEventListener("keyup", showValue);

// 用之前查到的input可以作到
searchInput.addEventListener("input", showValue);
```

### 直接把陣列作為html丟進去innerHTML發生的事
```javascript
function showMatches() {
  const matchArray = findMatches(this.value, fetchedData);
  const newElement = matchArray.map((place) => {
    return `
      <li>
          <span class="name">${place.city}, ${place.state}</span>
          <span class="name">${place.population}</span>
      </li>
      `;
  });
  // newElement會是一個陣列  
  // ul下的li顯示找到的物件
  suggestions.innerHTML = newElement;
}
```
![image](https://hackmd.io/_uploads/Bk8jLYkYR.png)

所以要先把陣列變成串起來的字串

### highlight效果
使用"".replace()
```javascript
const newElement = 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 class="name">${cityName}, ${stateName}</span>
        <span class="name">${place.population}</span>
    </li>
    `;
})
.join("");
```
:heavy_check_mark:為什麼輸入的是小寫但標注的顯示大寫 
![image](https://hackmd.io/_uploads/HJBdct1F0.png)
因為css = = 太白痴ㄌ
```css
.suggestions li {
  text-transform: capitalize;
}
```
阿那個加千位數逗點的是怎麼回事...
為了逃避正則(?)自己寫了一個奇怪的function哈哈哈
```javascript
function separateThousands(input) {
  // input是字串
  // ["1", "2", "3", "4"]
  // i=4    3    2    1
  return [...input].reduceRight(
    (result, currentFigure, currentIndex, input) => {
      const backwardIndex = input.length - currentIndex;
      if (backwardIndex % 3 === 1) {
        // i = 4, 7,...
        return currentFigure.concat(",", result);
      }
      return currentFigure.concat("", result);
    }
  );
}

// ...
`<span class="name">${separateThousands(place.population)}</span>`
```

## D7
### Q1
何不使用Date物件來取得今年年份
```javascript
const someAdults = people.some((person) => {
    const thisYear = 2024; // 我偷懶的寫法

    const thisYear = (new Date()).getFullYear(); // 影片教學
    
    // BUT!!!!! prettier直接給我把new Date()外面的()拿掉
    // 還是可以跑
    const thisYear = new Date().getFullYear();
    
    return thisYear - person.year >= 19;
});

console.log({someAdults}) 
// 回傳一個物件，屬性是變數名稱，值是變數的值
```
![image](https://hackmd.io/_uploads/B1peT7ZYA.png)

阿他是在叫什麼笑鼠

> 效果做很足
> [name=Jerey]


![image](https://hackmd.io/_uploads/HJYXAm-tC.png)

### Q4
用slice切兩段出來再組合到新的array
```javascript
const newComments = [
    ...comments.slice(0, targetIndex),
    ...comments.slice(targetIndex + 1),
];
```
## D8
:question:為什麼要用js來重新設定大小？
```javascript
const canvas = document.querySelector("#draw");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// <canvas id="draw" width="678" height="798"></canvas>
```

> [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)
> **CanvasRenderingContext2D.strokeStyle**
Color or style to use for the lines around shapes. Default #000 (black).
> **CanvasRenderingContext2D.lineJoin**
Defines the type of corners where two lines meet. Possible values: round, bevel, miter (default).
> **CanvasRenderingContext2D.lineCap**
Type of endings on the end of lines. Possible values: butt (default), round, square.

滑鼠點擊時繪製
```javascript
canvas.addEventListener("mousemove", draw);
// 讓點擊時才可以繪製
canvas.addEventListener("mousedown", () => (isDrawing = true));
canvas.addEventListener("mouseup", () => (isDrawing = false));

// 如果超出範圍就停止，就算再回範圍內也不能畫
canvas.addEventListener("mouseout", () => (isDrawing = false));
```

開始繪製路線
> [CanvasRenderingContext2D: beginPath()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/beginPath) starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path.
> [CanvasRenderingContext2D: stroke()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/stroke) strokes (outlines) the current or given path with the current stroke style. Strokes are aligned to the center of a path; in other words, half of the stroke is drawn on the inner side, and half on the outer side.
```javascript
ctx.beginPath(); // 路線開始
ctx.strokeStyle = "blue";
ctx.moveTo(20, 20); // 起點
ctx.lineTo(200, 20); // 終點
ctx.stroke(); // 繪製！

// offset是啥
// 和canvas原點(0,0)的差
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(event.offsetX, event.offsetY);
ctx.stroke();
```
> The offsetX read-only property of the MouseEvent interface provides the offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX)

### 調整畫筆
[Mother-effing](https://mothereffinghsl.com/)
0到360度代表顏色，第二個是飽和度，第三個是亮度
```javascript
hsl(${hue}, 100%, 50%)
```
混色效果
```javascript
ctx.globalCompositeOperation = "multiply";
```
> [globalCompositeOperation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation) 可以參考PS的混色效果

消除一切灰飛煙滅
```javascript
ctx.clearRect(0, 0, canvas.width, canvas.height)
```
## D9
沒什麼好寫的0.0

## D10
:question:影片中"change"和"click"的差別是什麼
好像跟鍵盤輸入有關？但我小測一下覺得沒什麼差
應該是"change"沒有shiftKey這個屬性可以用來偵測是否有鍵盤輸入shift

### 偵測鍵盤輸入
```javascript
// pointerEvent
event.shiftKey 
```
> [pointer event](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/shiftKey)

![image](https://hackmd.io/_uploads/rJmcGEDF0.png)

input element
![image](https://hackmd.io/_uploads/B1sL-4DtR.png)

不去依賴DOM的結構去尋找元素跟元素之間的關係
-> 從頭開始掃描確認每個input應該要是的狀態是什麼

### 留言提到的bug
1. 同一個選項，按兩次，然後按住shift按第三次，這個選項下方的選項就會全部被選起來
按第一次
![image](https://hackmd.io/_uploads/r1wLDLvtA.png)
按第二次
![image](https://hackmd.io/_uploads/Hy1vvIDFA.png)
按住shift按第三次
![image](https://hackmd.io/_uploads/rJuDPLDYR.png)
解法：lastChecked和現在check的不能是同一個選項
```javascript
if (
    event.shiftKey === true &&
    this.checked === true &&
    this !== lastChecked // 加這行
  )
```
2. lastChecked沒有被選起來的情況下，去按住shift點另一個選項也會把這中間的選項選起來
上一個取消選擇的是Just regular JavaScript, 按住shift
![image](https://hackmd.io/_uploads/S1jgFIDKA.png)
點最後一個選項也有作用
![image](https://hackmd.io/_uploads/BJSHF8DYC.png)
解法：要確認lastChecked是checked的狀態
```javascript
if (
    event.shiftKey === true &&
    this.checked === true &&
    lastChecked.checked === true && // 加這行
    this !== lastChecked
  )
```


