[TOC] # F_JS30 11-20 ## 11 Custom HTML5 Video Player 一開始長這樣,什麼也沒有 ![image](https://hackmd.io/_uploads/HkD8GakPkx.png) 先取得元素 ```javascript! const player = document.querySelector("video"); const video = player.querySelector(".viewer"); const progress = player.querySelector(".progress"); const progressBar = player.querySelector(".progress__filled"); const toggle = player.querySelector(".toggle"); const ranges = player.querySelectorAll(".player__slider"); const skipButtons = player.querySelectorAll("[data-skip]"); ``` [play() method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) `video.paused` 是一個屬性,是判斷影片是否正在暫停狀態 影片暫停, `video.paused` 會顯示 true ,反之顯示 false。作者透過這個來做一些判斷。 `console.dir(video)` 可以查看到此屬性: ![image](https://hackmd.io/_uploads/rJOvMp1w1e.png) 先監聽`video`有click事件時,觸發影片切換播放或暫停 ```javascript! // 切換播放或暫停 function togglePlayer() { const method = video.paused ? "play" : "pause"; video[method](); } video.addEventListener("click", togglePlayer); ``` 再來監聽`video`是play事件、pause事件,都觸發按鈕的圖示,以及監聽按鈕是click事件,觸發切換影片播放或暫停 ```javascript! // 設定播放鈕圖示 function updateButton() { const icon = this.paused ? "►" : "❚❚"; toggle.textContent = icon; } video.addEventListener("play", updateButton); video.addEventListener("pause", updateButton); toggle.addEventListener("click", togglePlayer); ``` 再來製作skip button,一個是退後10秒,一個是往前25秒 `this.dataset.skip` 是字串,所以要轉成數字讓 `video.currentTime` 可以加。 ```javascript! // 設定skip按鈕 function skip() { console.log(this.dataset.skip); video.currentTime += parseFloat(this.dataset.skip); } skipButtons.forEach((button) => button.addEventListener("click", skip)); ``` Q:為什麼用parseFloat轉成數字?是因為影片秒數是小數點關係嗎? ![image](https://hackmd.io/_uploads/SJ2uMakwkg.png) 設定音量和播放速率 ```html! <input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1" /> <input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1" /> ``` 作者特別將這兩個 range 的 name 設定跟 video 裡面的屬性一樣名字:`name="volume"`、`name="playbackRate"` ![image](https://hackmd.io/_uploads/rJ2tz6kP1l.png) ![image](https://hackmd.io/_uploads/SkVcM6kv1g.png) ```javascript! // 設定音量和播放速率 function handleRange() { video[this.name] = this.value; } ranges.forEach((range) => range.addEventListener("change", handleRange)); ranges.forEach((range) => range.addEventListener("mousemove", handleRange)); ``` 最後是進度條了! 因為是用 flex-basis 來顯示播放進度位置,所以設計 flex-basis 的百分比對應影片時間 ```css! .progress__filled { width: 50%; background: #ffc600; flex: 0; flex-basis: 50%; } ``` ```javascript! // 設定進度條顏色位置 function handleProgress() { const percent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = `${percent}%`; } video.addEventListener("timeupdate", handleProgress); ``` 作者說用 timeupdate 事件來觸發 根據影片播放不斷更新百分比 ![image](https://hackmd.io/_uploads/H14lNLLvkl.png) --- 進度條也需要有點擊的方式 所以去找整個進度條的 offset 可以看到progress的是640px,如點擊一半就是 offetX 屬性為320的位置 ![image](https://hackmd.io/_uploads/H13nGTkDJx.png) 透過console.log(e)來查看我click progress中的offsetX為多少 ![image](https://hackmd.io/_uploads/H1Opfayw1x.png) ```javascript! // 設定進度條可以點擊要的位置 function scrub(e) { const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration; video.currentTime = scrubTime; } progress.addEventListener("click", scrub); progress.addEventListener("mousemove", scrub); ``` 但影片跟不上mousemove位置,所以需要再寫條件判斷 ```javascript! let mousedown = false; progress.addEventListener("click", scrub); progress.addEventListener("mousemove", (e) => mousedown && scrub(e)); progress.addEventListener("mousedown", () => (mousedown = true)); progress.addEventListener("mouseup", () => (mousedown = false)); ``` `(e) => mousedown && scrub(e)` 這寫法特別?? Q:offsetWidth是蝦咪?元素的完整可見寬度 Q:progress.offsetWidth為什麼不是this.offsetWidth? 完整程式碼: ```javascript! // 取得元素 const player = document.querySelector(".player"); const video = player.querySelector(".viewer"); const progress = player.querySelector(".progress"); const progressBar = player.querySelector(".progress__filled"); const toggle = player.querySelector(".toggle"); const ranges = player.querySelectorAll(".player__slider"); const skipButtons = player.querySelectorAll("[data-skip]"); // 切換播放或暫停 function togglePlayer() { const method = video.paused ? "play" : "pause"; video[method](); } // 設定播放鈕圖示 function updateButton() { const icon = this.paused ? "►" : "❚❚"; toggle.textContent = icon; } // 設定skip按鈕 function skip() { video.currentTime += parseFloat(this.dataset.skip); } // 設定音量和播放速率 function handleRange() { video[this.name] = this.value; } // 設定進度條顏色位置 function handleProgress() { const percent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = `${percent}%`; } // 設定進度條可以點擊要的位置 function scrub(e) { const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration; video.currentTime = scrubTime; } // 連接監聽 video.addEventListener("click", togglePlayer); video.addEventListener("play", updateButton); video.addEventListener("pause", updateButton); video.addEventListener("timeupdate", handleProgress); toggle.addEventListener("click", togglePlayer); skipButtons.forEach((button) => button.addEventListener("click", skip)); ranges.forEach((range) => range.addEventListener("change", handleRange)); ranges.forEach((range) => range.addEventListener("mousemove", handleRange)); let mousedown = false; progress.addEventListener("click", scrub); progress.addEventListener("mousemove", (e) => mousedown && scrub(e)); progress.addEventListener("mousedown", () => (mousedown = true)); progress.addEventListener("mouseup", () => (mousedown = false)); ``` ## 12 JavaScript KONAMI CODE! 輸入一段特定字串之後出現特定的畫面,稱為 key senquence。 把輸入的key,push到 pressed 陣列 ```javascript! window.addEventListener("keyup", (e) => { pressed.push(e.key); // 用來設計pressed的陣列長度都不會超過secretCode的長度 console.log(pressed); ``` ![image](https://hackmd.io/_uploads/Bk4UYPzDyl.png) ### [Array.prototype.splice()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) 作者為了要讓 pressed 陣列長度都不會超過secretCode的長度 ```javascript! pressed.splice( -secretCode.length - 1, pressed.length - secretCode.length ); ``` - `-secretCode.length - 1` :意思是從 pressed 陣列最後面往前數到-5的位置,因為`secretCode.length`長度為4,只要長度超過4,就刪除 - `pressed.length - secretCode.length`:這個位置是要刪除元素數量。所以長度超過 `secretCode.length` 的元素都是多餘元素,讓 pressed 的長度都保持不超過 `secretCode.length` - 結果會是`splice(-5,1)`,表示從陣列的倒數第5個元素開始,刪除1個元素 那既然都是刪除最前面一個字,為什麼不直接寫0就好? 測試這樣寫也沒問題XD ```javascript! pressed.splice(0, pressed.length - secretCode.length); ``` 可以看到第5個開始,就開始刪除最前面的元素,維持陣列內是4個元素,直到符合secretCode字串就執行之後的程式碼。 ![image](https://hackmd.io/_uploads/HkkPtDzPkx.png) 完整程式碼: ```javascript! const pressed = []; const secretCode = "fang"; window.addEventListener("keyup", (e) => { pressed.push(e.key); // 用來設計pressed的陣列長度都不會超過secretCode的長度 pressed.splice( -secretCode.length - 1, pressed.length - secretCode.length ); if (pressed.join("").includes(secretCode)) cornify_add(); }); // cornify.js檔案是作者另外引入的,會執行裡面的事情 ``` ![image](https://hackmd.io/_uploads/SyYvFwzwJe.png) ## 13 Vanilla JavaScript Slide In on Scroll 使用 scroll 事件,會發現他不斷觸發 例如將此網頁從頭滾動到底部,就觸發了 scroll 事件179次 ![image](https://hackmd.io/_uploads/H1CFmUUwJe.png) 透過 debounce 函式,讓整個事件觸發幾次就好 作者也是上網找 debounce 函式套用而已 ```javascript! function debounce(func, wait = 20, immediate = true) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function checkSlide(e) { console.count(e); } const slideImages = document.querySelectorAll(".slide-in"); window.addEventListener("scroll", debounce(checkSlide)); // 透過debounce減少checkSlide觸發次數 ``` 可以看到同樣都是從頭滾動到底部,觸發次數只有13次 ![image](https://hackmd.io/_uploads/B1pzN88Dkx.png) [debounce 防抖函式](https://www.explainthis.io/zh-hant/swe/debounce) 再來設計當滾動到此圖片的高度50%,就增加圖片的動畫滑入 [window.innerHeight](https://developer.mozilla.org/en-US/docs/Web/API/Window/innerHeight):是視窗高度 [window.scrollY](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY):是瀏覽器頂部向下滾動多少 好難懂作者的計算方式。。。。先照抄 完整程式碼: ```javascript! function debounce(func, wait = 20, immediate = true) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } const slideImages = document.querySelectorAll(".slide-in"); function checkSlide() { slideImages.forEach((slideImage) => { // 判斷視窗底部到達圖片中線 const slideInAt = window.scrollY + window.innerHeight - slideImage.height / 2; // 用來判斷視窗是否滾動過圖片的底部 const imageBottom = slideImage.offsetTop + slideImage.height; // 判斷圖片的中線是否到視窗 const isHalfShown = slideInAt > slideImage.offsetTop; // 判斷圖片是否還未滾出視窗 const isNotScrolledPast = window.scrollY < imageBottom; if (isHalfShown && isNotScrolledPast) { slideImage.classList.add("active"); } else { slideImage.classList.remove("active"); } }); } window.addEventListener("scroll", debounce(checkSlide)); ``` ## 14 JavaScript Fundamentals: Reference VS Copy 這邊講「傳值」概念 ```javascript! let age = 100; let age2 = age; console.log(age, age2); // 100 100 age = 200; console.log(age, age2); // 200 100 let name = "fang"; let name2 = name; console.log(name, name2); // fang fang name = "Vicky"; console.log(name, name2); // Vicky fang ``` 這邊講「傳址」概念 ```javascript! const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const team = players; team[3] = "Peter"; console.log(team); // ["Wes","Sarah","Ryan","Peter"] console.log(players); // ["Wes","Sarah","Ryan","Peter"] ``` 複製array的方法(不會有傳參考) 1. slice方法,shallow copy一個陣列,這樣就不會改到原陣列 ```javascript! const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const result = players.slice(); console.log(result); // ["Wes", "Sarah", "Ryan", "Poppy"] result[3] = "Vicky"; console.log(result); // ["Wes", "Sarah", "Ryan", "Vicky"] 索引值3的值改成"Vicky" console.log(players); // ["Wes", "Sarah", "Ryan", "Poppy"] 值沒有因為result變動 ``` 2. concat() ```javascript! const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const copyArray = [].concat(players); console.log(copyArray); // ["Wes", "Sarah", "Ryan", "Poppy"] ``` 3. ...展開運算子 ```javascript! const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const result = [...players]; console.log(result); // ["Wes", "Sarah", "Ryan", "Poppy"] ``` 4. Array.from() ```javascript! const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const result3 = Array.from(players); console.log(result); // ["Wes", "Sarah", "Ryan", "Poppy"] ``` 物件也有傳址 ```javascript! const person = { name: "Wes Bos", age: 80, }; const answer = Object.assign({}, person, { address: "Tainan" }); console.log(answer); // {name: 'Wes Bos', age: 80, address: 'Tainan'} console.log(person); // {name: 'Wes Bos', age: 80} ``` Object.assign 只能對第一層屬性shallow copy ,第二層還是會被修改 ```javascript! const family = { parents: { dad: "David", mom: "Esther", }, country: "Israel", familyNumber: 2, }; // shallow copy const dev = Object.assign({}, family); dev.parents.mom = "Maria"; console.log(family); console.log(dev); ``` ![image](https://hackmd.io/_uploads/HkdNEUUPkl.png) 如需要deep copy,須使用JSON.stringify先轉成字串,再用JSON.parse轉回物件,這樣修改第二層之後的內容都不會動到原物件 ```javascript! const dev2 = JSON.parse(JSON.stringify(family)); dev2.parents.dad = "Peter"; console.log(family); console.log(dev2); ``` - 使用JSON.stringify(family),回傳物件的字串 ``` '{"parents":{"dad":"David","mom":"Maria"},"country":"Israel","familyNumber":2}' ``` - 再使用JSON.parse(),把物件的字串解析,轉回成物件 ![image](https://hackmd.io/_uploads/HyFH4IIvJe.png) 修改物件第二層資料: ```javascript! dev2.parents.dad = "Peter"; console.log(family); console.log(dev2); ``` family的資料不會被改動 ![image](https://hackmd.io/_uploads/rJsIEUIwyg.png) ## 15 How LocalStorage and Event Delegation work ```html! <div class="wrapper"> <h2>LOCAL TAPAS</h2> <p></p> <ul class="plates"> <li>Loading Tapas...</li> </ul> <form class="add-items"> <input type="text" name="item" placeholder="Item Name" required /> <input type="submit" value="+ Add Item" /> </form> </div> ``` ```javascript! const addItems = document.querySelector(".add-items"); const plates = document.querySelector(".plates"); const itemList = JSON.parse(localStorage.getItem("food")) || []; populateList(itemList, plates); // 新增品項 function addItem(event) { event.preventDefault(); const inputText = this.querySelector("[name=item]").value; const item = { inputText, done: false, }; itemList.push(item); populateList(itemList, plates); localStorage.setItem("food", JSON.stringify(itemList)); this.reset(); // 表單回初始狀態 } ``` Q1: `document.querySelector` 的 `document` 是什麼? `document` 是window物件之一,代表整個HTML文件(DOM)。 ```javascript! const addItems = document.querySelector(".add-items"); ``` 所以這段程式碼意思是,JS會從HTML文件中找尋class為 `add-items` 的元素並存入變數 `addItems` 。如有符合就回傳第一個找到的元素,如沒有則回傳`null`。 當縮小到特定範圍了,就可以直接在該元素使用`querySelector`,不需要再用`document.querySelector()`,因為JS只會在該元素內尋找,而不是整個`document`,會更有效率。 例如: ```javascript! const addItems = document.querySelector(".add-items"); const text = addItems.querySelector("input[name=item]"); // 確保抓取到<input>,可加上標籤名稱 // 寫法等同 document.querySelector(".add-items").querySelector("input[name=item]") ``` Q2: 為什麼不是監聽 `input[type="text"]` 按鈕就好?而是監聽整個 `<form>` 表單? - 監聽 `<form>` 的 `submit`事件,確保觸發表單提交的方式都被攔截並處理,例如:輸入後可以直接按enter鍵提交。 - 監聽 `<form>` 會比針對單一 `input[type="submit"]` 更靈活,例如:表單新增其他元素提交,就不需修改程式碼。 ### event delegation 事件委派 因為事件傳遞機制是先捕捉後冒泡,所以不管點擊任何li都會回到ul身上,所以把listener放在ul,透過父節點統一處理子節點的事件就是事件委派。 [Huli - DOM 的事件傳遞機制:捕獲與冒泡](https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/) Q3: 這裡的this指向誰? ```javascript! addItems.addEventListener("submit", function(e){ console.log(this); // ? const text = this.querySelector("[name=item]").value; }); ``` - 當使用普通函式function時,this指向「觸發事件的元素」,就是`addItems`,也就是`<form>`,當提交表單時,addItem函式才會被執行。 > 測試輸入abc,並提交,this印出`<form>`內容 ![image](https://hackmd.io/_uploads/ryV_VcxKJl.png) - 使用箭頭函式,因為父層this是什麼,箭頭函式的this就是什麼 ```javascript! addItems.addEventListener("submit",(e)=>{ console.log(this); // 這裡的`this`是指向`window` }); ``` Q4: 事件處理的參數(e / event)? 是==event物件(事件物件)==,當事件發生時,瀏覽器自動把event物件傳進事件處理函式,如不需使用到event物件,則可以省略不寫。 ### event.preventDefault() 為了阻止瀏覽器的預設行為。 瀏覽器默認情況下,當表單提交時會刷新頁面,或將數據發送到server端 ; 不是所有事件都有預設行為。 [Event: preventDefault() method](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) ```javascript! function addItem(e){ console.log(e); } ``` 查找 event 物件中的 preventDefault 方法,從`SubmitEvent物件`沿著原型在`Event物件`找到了~ [SubmitEvent](https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent) [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) ![image](https://hackmd.io/_uploads/Skl2V9xt1e.png) ![image](https://hackmd.io/_uploads/rktnVqgFyg.png) ### EventTarget > Element, and its children, as well as Document and Window, are the most common event targets, but other objects can be event targets, too. > 最常見的eventTarget,例如:element和其子元素、document、window EventTarget 的實例方法: - `EventTarget.addEventListener()` - `EventTarget.removeEventListener()` - `EventTarget.dispatchEvent()` ```javascript! const addItems = document.querySelector(".add-items"); addItems.addEventListener("submit", function(e){ console.log(this); }); ``` 因為 `document.querySelector(".add-items")` 會回傳一個 HTMLElement,HTMLElement 繼承自 Element,Element 又繼承自 Node,Node 最終繼承自 EventTarget。所以平常使用`document.querySelector(".add-items")` 才可以使用 EventTarget的方法! `(addEventListener/removeEventListener/dispatchEvent)` ### reset() 將表單欄位初始狀態 ### 物件縮寫 - 屬性縮寫:屬性與變數名稱相同,可直接寫變數名稱 ```javascript! let name = "David"; let age = 18; // 一般寫法 const person = { name:name, age:age } // 屬性縮寫 const person = {name,age}; // {name: 'David', age: 18} ``` 試著輸入noodles和pizza並按新增,印出資料如下: ![image](https://hackmd.io/_uploads/HyJCV9xK1g.png) ```javascript! function addItem(){ // ... populateList(itemList, plates); } // 渲染HTML function populateList(itemList, plates) { plates.innerHTML = itemList .map((plate, index) => { return ` <li> <input type="checkbox" id="item${index}" data-index=${index} ${ plate.done ? "checked" : "" }/> <label for="item${index}">${plate.inputText}</label> </li> `; }) .join(""); } ``` 物件`done:false` 是什麼作用?A:之後用來判斷checkbox是否checked 在`<input>`裡面寫是否checked的判斷 ```javascript! ${plate.done ? "checked" : ""} ``` Q5: 為什麼要寫三元判斷?A:用來判斷每個品項的狀態是否打勾,因為當重新渲染頁面時,有打勾的就維持。 渲染HTML出來後長這樣: ![image](https://hackmd.io/_uploads/BJ1yScgYJe.png) 因為刷新頁面後,剛剛輸入的內容都不會保留,所以需要出動 `localStorage` 本地儲存。 ### localStorage 可將文本儲存在瀏覽器的儲存空間 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 1. 是`window`裡面的物件 ![image](https://hackmd.io/_uploads/HkGeH5eFye.png) 2. 在devTools的 `application` 可以查看 ![image](https://hackmd.io/_uploads/ryceBqgtJl.png) 3. 只能儲存`String`,如果不是字串,會自己轉型,例如Object轉成`"[Object Object]"`。使用`JSON.stringify`把物件轉字串 > The keys and the values stored with localStorage are always in the UTF-16 string format, which uses two bytes per character. As with objects, integer keys are automatically converted to strings. 4. 語法 ```javascript! // key和value分別對應local Storage的key和value,可自訂 localStorage.setItem("key","value"); // 新增資料 localStorage.getItem("key"); // 讀取資料 localStorage.removeItem("key"); // 移除資料 ``` --- ```javascript! const items = JSON.parse(localStorage.getItem("items")) || []; function addItem(){ // ... localStorage.setItem("items", JSON.stringify(items)); } populateList(items, itemsList); ``` ![image](https://hackmd.io/_uploads/SypZSqxK1e.png) 刷新頁面,資料都不會消失(但會發現原本打勾的又沒打勾了) ![image](https://hackmd.io/_uploads/ByUMr9gYkx.png) Q6:`JSON.parse(localStorage.getItem("food"))`拿到不是物件嗎?這樣怎麼運作array methods? A: `JSON.parse(localStorage.getItem("food"))` 解析出來的值會是一個 `array` !! (自己看錯XD) 截圖確認 ![image](https://hackmd.io/_uploads/rye7B9gtyl.png) ![image](https://hackmd.io/_uploads/BkDQBqgt1e.png) 頁面加載的時候,檢查`const items = JSON.parse(localStorage.getItem("items")) || [];`是否有東西,沒東西就給空陣列 --- 最後因為頁面每次加載後,有 checked 的會變成沒打勾狀態,所以新增 toggleDone 函式處理。 ### event.target 因為每次點擊勾選時,都會同時兩個 pointerEvent 出現,印出來看是 `<label>` 和 `<input type="checkbox">` ![image](https://hackmd.io/_uploads/rklBS5xtye.png) ![image](https://hackmd.io/_uploads/SJIrBcet1x.png) 所以用 `event.target` 方式,假如不是 `"input"` 的就跳過。所以確保選到`<input>`標籤。 運用加在 `<input type="checkbox">` 的 `data-index=${index}`,將itemList陣列的每一個元素的done做反向(true->false或 false->true) 完成後把結果儲存在localStorage,並將結果渲染至頁面。 ```javascript! // 切換勾選狀態 function toggleDone(event) { if (!event.target.matches("input")) return; const el = event.target; const index = el.dataset.index; itemList[index].done = !itemList[index].done; localStorage.setItem("food", JSON.stringify(itemList)); populateList(itemList, plates); } ``` [target property](https://developer.mozilla.org/en-US/docs/Web/API/Event/target) 完整程式碼: ```javascript! const addItems = document.querySelector(".add-items"); const plates = document.querySelector(".plates"); const itemList = JSON.parse(localStorage.getItem("food")) || []; populateList(itemList, plates); // 新增品項 function addItem(event) { event.preventDefault(); const inputText = this.querySelector("[name=item]").value; const item = { inputText, done: false, }; itemList.push(item); populateList(itemList, plates); localStorage.setItem("food", JSON.stringify(itemList)); this.reset(); } // 渲染畫面 function populateList(itemList, plates) { plates.innerHTML = itemList .map((plate, index) => { return ` <li> <input type="checkbox" id="item${index}" data-index=${index} ${ plate.done ? "checked" : "" }/> <label for="item${index}">${plate.inputText}</label> </li> `; }) .join(""); } // 切換勾選狀態 function toggleDone(event) { if (!event.target.matches("input")) return; const el = event.target; const index = el.dataset.index; itemList[index].done = !itemList[index].done; localStorage.setItem("food", JSON.stringify(itemList)); populateList(itemList, plates); } addItems.addEventListener("submit", addItem); plates.addEventListener("click", toggleDone); ``` ## 16 CSS Text Shadow on Mouse Move Effect offsetWidth / offsetHeight是元素實際顯示的寬度/高度 包含:width、padding、border,但不包含margin ```javascript! const hero = document.querySelector(".hero"); const text = hero.querySelector("h1"); function addShadow(e) { // div的實際顯示寬高 const { offsetWidth: width, offsetHeight: height } = hero; // mousemove事件的offset let { offsetX: x, offsetY: y } = e; // 計算滑鼠在div的相對位置 const xWalk = x / width; const yWalk = y / height; console.log(xWalk, yWalk); } hero.addEventListener("mousemove", addShadow); ``` 橘色框是div,紅色框是h1 ![image](https://hackmd.io/_uploads/Hy5Gu17KJe.png) 可以看到,當滑鼠滑到h1時,因為h1是div裡面的元素,offset會變成h1去計算,為了讓滑鼠移動不會因為裡面有元素而重新計算,判斷如下: ```javascript! if (this !== e.target) { x += e.target.offsetLeft; y += e.target.offsetTop; } ``` offsetLeft與offsetTop的理解圖: ![image](https://hackmd.io/_uploads/SJNX_y7Yyx.png) 補充: 用`clientX / clientY`就可以避免元素不同的offset問題!不用再寫if判斷(灑花~~~) ```javascript! function addShadow(e) { // div的實際顯示寬高 const { offsetWidth: width, offsetHeight: height } = hero; const { clientX: currentX, clientY: currentY } = e; } ``` 設定陰影的偏移量,作者設定100px,表示偏移量為 -50px~50px ,左上角為 `(-50,-50)`,右下角為 `(50,50)` 用Math.round取四捨五入 ```javascript! const xWalk = Math.round((currentX / width) * walk - walk / 2); const yWalk = Math.round((currentY / height) * walk - walk / 2); ``` 最後加上text也就是h1的陰影樣式,可以加上多個,設定不同方向的陰影 ```javascript! text.style.textShadow = ` ${xWalk}px ${yWalk}px 0 pink, ${xWalk * -1}px ${yWalk}px 0 aqua, ${xWalk}px ${yWalk * -1}px 0 gray `; ``` ![image](https://hackmd.io/_uploads/H1x4_kXtyl.png) ## 17 Sorting Band Names without articles