# 盆栽盒專案 Part 3 - DOM 元素控制與閉包 ![DOM 元素與閉包](/sketchnotes/webdev101-js.png) > 由 [Tomomi Imura](https://twitter.com/girlie_mac) 繪製 ## 課前測驗 [課前測驗](https://calm-wave-0d1a32b03.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 檔 `` 的部分: ```html ``` > 筆記:匯入外部 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://calm-wave-0d1a32b03.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)