成品連結:Stripe Follow Along Nav、操作前程式碼、完成後程式碼
今天要做的作品我覺得還蠻有趣的,JS30 作者的靈感是這個網站的導覽列切換的方式,如果只是切換 display: none
、display: block
是做不出這種效果的,今天我們就一起學習做做看吧!
當從一個選項移動到另一個時,白色背景看起來就像是動態地調整大小,這所用到的方法其實與第 22 天的方式相同,就是透過 element.getBoundingClientRect()
取得寬、高以及位置所做到
解釋完做法,我們就開始吧!
我們想要的效果是當滑鼠移入導覽列選項時時顯示 dropdown、當移出時隱藏,故要設定兩種監聽事件分別處理不同狀況
// 會使用到的 DOM 元素
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');
triggers.forEach(li => li.addEventListener('mouseenter', showDropdown));
triggers.forEach(li => li.addEventListener('mouseleave', hideDropdown));
display: none
--> display: block
加入動畫效果?當必需使用 display: none
又希望更改成 display: block
時能加入 transition
的動畫效果,但是 trnasition
並不支援 display 的顯現與隱藏,這時就需要用一點小方法了。
由於不能直接在 display
下 transition
(會沒有效果),所以要利用 CSS 的 opacity
以及 setTimeout
來完成(opacity
可以是其他屬性,像是 max-width
、visibility
等等...)。
依順序理解如下:
display: none
轉成 display: block
setTimeout
,並在 callback 中更改 opacity: 1
與配合設定的 transition
讓元素出現綜合以上解釋,我們來處理我們的 navbar 吧!
function showDropdown(e) {
this.classList.add('trigger-enter');
setTimeout(() => {
this.classList.add('trigger-enter-active');
}, 150);
}
方法如上面所解釋,我們先在導覽列選項新增 class trigger-enter
,更改 dropdown 的 CSS display: block
,並在 150 毫秒後新增另一個 class trigger-enter-active
將透明度改為 1,如此動畫效果就出來了!(當然還是要配合 transition
)
this
?this.classList.add('trigger-enter')
的 this
很明顯是被觸發事件的元素,但是 setTimeout
裡面的 this
呢?
當你不是使用 arrow function 時會發現 this
是 window
而不是如上被觸發事件的元素。這是因為 function 的 this
並不會繼承,即便 showDropdown
的 this
是被觸發事件的元素,當你在裡面寫入新的 function 時,this
會變成 window
而不原本的元素。有個 hack 是在外面先將 this
存入變數,在裡面的 function 使用變數而不是 this
。
在我們狀況下會是這樣:
function showDropdown(e) {
this.classList.add('trigger-enter');
const self = this;
setTimeout(function() {
self.classList.add('trigger-enter-active');
}, 150);
}
可以看到我們將 this
從入變數 self
,這樣使用上就沒有問題了!
另一個方法是使用 ES6 的 arrow function,它的 this
會與包著他的function 相同,所以不用另外存入變數中,但細節這裡就不多說了。
mouseleave
時移除 classshowDropdown
先設定一些東西了,接下來當滑鼠移出時要隱藏 dropdown
內容。這個簡單許多,只要將 class 移除即可
function hideDropdown(e) {
this.classList.remove('trigger-enter', 'trigger-enter-active');
}
製作白色背景大家應該不陌生,就是使用之前使用過的element.getBoundingClientRect()
以取得 dropdown 的寬、高以及位置
照著之前的方式,我們要先取得 dropdown 的資訊並套用到 background
function showDropdown(e) {
this.classList.add('trigger-enter');
const self = this;
setTimeout(function() {
self.classList.add('trigger-enter-active');
}, 150);
const dropdownCoords = dropdown.getBoundingClientRect();
const coords = {
width: dropdownCoords.width,
height: dropdownCoords.height,
top: dropdownCoords.top,
left: dropdownCoords.left
}
background.classList.add('open');
background.style.width = `${coords.width}px`;
background.style.height = `${coords.height}px`;
background.style.transform = `translate(${coords.left}px, ${coords.top}px)`;
}
與之前唯一不同是要新增 class open
,這讓 background
的透明度變為 1,並配合 transition
呈現動畫效果
滑鼠移入設定好了,接著設定 hideDropdown
function hideDropdown(e) {
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}
你沒看錯,當初這個問題也困擾我很久,我一直找不到發生的原因。
這裡先說明一下 element.getBoundingClientRect()
的寬、高沒有問題,而 top
、bottom
、left
、right
是與 viewport(使用者裝置視窗)上下左右的距離。以 top
來說距離是從瀏覽器上緣到導覽列選項頂端的距離。
但是當套用到 background
時,top
的起點是 nav
上緣而不是瀏覽器上緣,這也是為什麼 background
的位置會這麼下面了
可以試試看將
nav
上方的p
以及h2
移除,會發現這個問題消失了,因為現在nav
上緣就是瀏覽器上緣
解法是要扣掉 nav
頂端到瀏覽器上緣的距離,才時 background
應該有的 top
值
function showDropdown(e) {
this.classList.add('trigger-enter');
const self = this;
setTimeout(function() {
self.classList.add('trigger-enter-active');
}, 150);
const dropdownCoords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();
const coords = {
width: dropdownCoords.width,
height: dropdownCoords.height,
top: dropdownCoords.top - navCoords.top, // 扣掉 nav 到頂端的距離
left: dropdownCoords.left - navCoords.left // 扣掉 nav 到左側的距離
}
background.classList.add('open');
background.style.width = `${coords.width}px`;
background.style.height = `${coords.height}px`;
background.style.transform = `translate(${coords.left}px, ${coords.top}px)`;
}
到這裡算是完成了,但如果看仔細點會發現當你快速在三個導覽列選項移動時,trigger-enter-active
這個 class 並不會因滑鼠移出而移除。這是因為移動速度過快,在 setTimeout
150 毫秒加入 class 前就試著刪除 class,也因此會殘留。
解決變法是在 setTimeout
的 callback 中加入判斷式,當有 class trigger-enter
時才執行 setTimeout
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);
當 && 左方為 true
時才會執行右方動作
完成!