# 盆栽盒專案 Part 3 - DOM 元素控制與閉包

![DOM 元素與閉包](/sketchnotes/webdev101-js.png)
> 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製

## 課前測驗

[課前測驗](https://ashy-river-0debb7803.1.azurestaticapps.net/quiz/19?loc=zh_tw)

### 大綱

操作 DOM (Document Object Model) 是網頁開發的一項關鍵。根據 [MDN 文件](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction)， 「Document Object Model (DOM) 元素能根據網頁文件的結構與內容來呈現物件」。藉由使用 JavaScript 框架而非原始的 JavaScript 程式碼來管理 DOM，在網頁上操作 DOM 的挑戰已經不比以前困難了，但這裡我們要自己來管理它們！

此外，這堂課也會介紹有關[JavaScript 閉包(Closure)](https://developer.mozilla.org/docs/Web/JavaScript/Closures)的概念，你可以想像成一個函式被包在另一個函式中，以訪問外面函式範圍中的變數。

> JavaScript 閉包是個廣闊且複雜的主題。本堂課只觸及建立盆栽盒需要的最基礎概念。你能得知一個閉包為：內部函式和外部函式建立一項關係，允許內部函式存取外部函式的變數等作用域。要得知更多關於閉包的原理，請造訪觀看[額外的文件](https://developer.mozilla.org/docs/Web/JavaScript/Closures)。

我們會使用閉包來操控 DOM。

想像 DOM 就像一棵樹，表現出所有操作網頁的方式。多樣的 APIs (Application Program Interfaces) 提供程式開發者，依照自己使用的程式語言，以存取、編輯、編排等方式管理 DOM 元素。

![DOM 樹的表達](../images/dom-tree.png)

> HTML 語法會參考 DOM 的呈現方式。出自 [Olfa Nasraoui](https://www.researchgate.net/publication/221417012_Profile-Based_Focused_Crawler_for_Social_Media-Sharing_Websites)。

在這堂課中，我們會完成我們的盆栽盒專案，建立 JavaScript 來對網頁中的植物進行互動式操作。

### 開始之前

確保盆栽盒的 HTML 與 CSS 已經編輯完成。這堂課會新增拖曳植物進出盆栽罐的功能。

### 課題

在專案資料夾中，新增檔案 `script.js`。 匯入該檔案在 HTML 檔 `<head>` 的部分：

```html
	<script src="./script.js" defer></script>
```

> 筆記：匯入外部 JavaScript 檔案到 HTML 檔案須使用 `defer`，讓 JavaScript 檔案只有在 HTML 被完全載入時才被執行。你也可以使用 `async` 的屬性，允許 JavaScript 在解析 HTML 檔時就被執行。這項專案中，我們必須確保 HTML 的元件被完整建立後才允許使用拖曳功能。
---

## DOM 元素

我們要做的第一件事是建立 DOM 下，要被操控的物件的連結。在專案例子中，我們有罐子外的十四株植物等著被拖曳。

### 課題

```html
dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));
```

發生了什麼事？你正以 DOM 搜尋網頁檔內的物件，藉由 Id 作為依據來搜尋。回想第一堂 HTML 課中，我們可每一株植物一個專屬的 Id (`id="plant1"`)，現在你就可以使用它。在辨別完每一株植物物件後，傳遞給待編輯的函式 `dragElement`，讓 HTML 物件可以被拖曳。

✅ 為什麼我們要以 Id 作為物件的參考？為什麼不以 CSS 的 class 作為參考？請參考以前的 CSS 課程回答此問題。

---

## 閉包(Closure)

現在，你已經準備好要建立 dragElement 閉包，建立包在外部函式內的內部函式組，在我們的例子中，會用上三個函式。 

閉包在一或多個以上函式要存取外部函式時非常好用。看看下面的例子：

```javascript
function displayCandy(){
	let candy = ['jellybeans'];
	function addCandy(candyType) {
		candy.push(candyType)
	}
	addCandy('gumdrops');
}
displayCandy();
console.log(candy)
```

這項例子中，函式 displayCandy 包住另一個函式 addCandy，新增新的糖果樣式到已存在的矩陣當中。當執行這段程式時，矩陣 `candy` 會被認作是未定義，因為它是函式的本地變數。 

✅ 你能讓矩陣 `candy` 被存取嗎？試著將它移到閉包外面。這時，矩陣會變成全域變數，取消閉包內的存取限制。

### 課題

在檔案 `script.js` 的元素宣告下方，新增函式：

```javascript
function dragElement(terrariumElement) {
	//set 4 positions for positioning on the screen
	let pos1 = 0,
		pos2 = 0,
		pos3 = 0,
		pos4 = 0;
	terrariumElement.onpointerdown = pointerDrag;
}
```

`dragElement` 藉由程式定義的參數取得 `terrariumElement` 物件。之後，設定一些位置 `0` 的變數給函式內的物件使用。它們是本地變數，給每一個進到拖曳函式內的物件操控。盆栽盒會被這些拖曳物件填充，我們的網頁應用必須要持續追蹤這些物件的位置。

此外，進到函式的 terrariumElement 也被新增了 `pointerdown` 事件，它是管理 DOM 的其中一項[網頁 APIs](https://developer.mozilla.org/docs/Web/API)。當按鈕按下時，或是在我們案例中，一個拖曳物件被點擊時，`onpointerdown` 事件就會被觸發。這個事件處理器(event handler)皆運作在[網頁與行動瀏覽器](https://caniuse.com/?search=onpointerdown)上，只有少部分的例外。

✅ [事件處理器 `onclick`](https://developer.mozilla.org/docs/Web/API/GlobalEventHandlers/onclick)支援更多的瀏覽器。為什麼我們不在這邊使用它？ 想想看我們在這此建立的視窗互動類型。

---

## 函式 pointerDrag

terrariumElement 已經準備好被拖曳了。當觸發 `onpointerdown` 事件時，函式 pointerDrag 會參與其中。新增這項函式在程式碼 `terrariumElement.onpointerdown = pointerDrag;` 下方：

### 課題

```javascript
function pointerDrag(e) {
	e.preventDefault();
	console.log(e);
	pos3 = e.clientX;
	pos4 = e.clientY;
}
```

許多事情會發生。首先，你使用 `e.preventDefault();` 取消掉 pointerdown 原先的預設事件。這樣你可以操作更多的介面行為。

> 回到你建立的程式碼中，試著刪除 `e.preventDefault()` 並執行看看，發生了什麼事？

第二，用瀏覽器打開 `index.html` 並調查我們的介面。當你點擊植物時，你可以發現 'e' 事件被觸發了。專研一下，一個 pointerdown 事件會產生多少資訊！  

接下來，紀錄本地變數 `pos3` 和 `pos4` 被設定為 e.clientX 和 e.clientY。你可以在觀察面板中，會發現 `e` 的數值。這項數值取得按下植物瞬間的 x 與 y 座標資訊。為了全面的控制植物行為，在拖曳植物時，我們會持續更新座標資訊。

✅ 將整個網頁應用建立在一個大閉包下，會讓程式碼變得比較清楚嗎？如果沒有，你有其他方法管理這十四株可拖曳的植物嗎？

增加初始化函式，在程式碼 `pos4 = e.clientY` 下方加上下列兩行事件處理：

```html
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
```

現在，在游標拖曳時，你的植物能跟著你的游標走，而在你取消點擊時停下來。`onpointermove` 和 `onpointerup` 也是 `onpointerdown` 類型相同的 API。然而，現在介面會出現錯誤訊息，因為我們還沒建立函式 `elementDrag` 與 `stopElementDrag`。

## 函式 elementDrag 與 stopElementDrag

新增兩條內部函式在閉包中，它們會處理拖曳植物與停止拖曳的事件。你希望你可以拖曳任何一株植物且放在螢幕上的任一地方。介面並沒有強制你盆栽盒的配置格式，你可以自由地增加、移除與移動盆栽罐內的植物。

### 課題

新增函式 `elementDrag` 在函式閉包 `pointerDrag` 宣告列的正下方：

```javascript
function elementDrag(e) {
	pos1 = pos3 - e.clientX;
	pos2 = pos4 - e.clientY;
	pos3 = e.clientX;
	pos4 = e.clientY;
	console.log(pos1, pos2, pos3, pos4);
	terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
	terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
```

在這條函式之前，你編輯了四個本地變數位置的初始值在外部函式中。這邊又做了什麼事？

當你拖曳物件時，你更新數值 `pos1` 為 `pos3` 減去現在的 `e.clientX`，而 `pos3` 在之前被初始化為為 `e.clientX`。同樣的行為套用在 `pos2`上。之後，你更新 `pos3` 與 `pos4` 到新的 XY 座標點位置。你能在 console 下看到數值在拖曳下更新的情況。我們也更新植物的 CSS 造型中的定位點為 `pos1` 與 `pos2`，比較植物左上方座標點與新座標點的關係。

> `offsetTop` 和 `offsetLeft` 是 CSS 的屬性，決定物件與它父關係物件的定位關係。父關係物件可以是任何元素，只要它的定位屬性不為 `static`。

這些座標點的計算式讓你成功校整了植物與盆栽盒之間的行為。

### 課題

最後的課題是在介面上新增 `stopElementDrag` 函式，我們將它加在函式閉包 `elementDrag` 的正下方：

```javascript
function stopElementDrag() {
	document.onpointerup = null;
	document.onpointermove = null;
}
```

這條小函式重制 `onpointerup` 與 `onpointermove` 事件，這樣你可以重新開始該植物的拖曳事件，或是拖曳新的植物。

✅ 如果不將這些事件設為空值時，會發生什麼事？

我們終於完成了這項專案！

🥇 恭喜你！你建立了一個漂亮的盆栽盒。![盆栽盒成果圖](../images/terrarium-final.png)

---

## 🚀 挑戰

新增新的事件處理器到你的閉包中，讓你能對植物做更多的事情。舉例來說，雙擊植物讓它排列到最上層。發揮你的創意吧！

## 課後測驗

[課後測驗](https://ashy-river-0debb7803.1.azurestaticapps.net/quiz/20?loc=zh_tw)

## 複習與自學

在螢幕上拖曳物件看似簡單，但依照不同的目的與實現方法會遭遇到不同的問題。事實上，這邊有一份關於你可以嘗試的[拖曳 API](https://developer.mozilla.org/docs/Web/API/HTML_Drag_and_Drop_API)。我們沒在專案中使用是為了建立不一樣的實現方法，試著使用這些 API 到專案中，看看你能完成什麼。

在 [W3C 文件](https://www.w3.org/TR/pointerevents1/) 和 [MDN 網頁文件](https://developer.mozilla.org/docs/Web/API/Pointer_events)上取得更多關於 pointer 的事件。

記得習慣性用 [CanIUse.com](https://caniuse.com/) 檢查網頁的瀏覽器兼容性。

## 作業

[用 DOM 做更多事](assignment.zh-tw.md)

