JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks
、No Compilers
、No Libraries
、No Boilerplate
在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。
另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。
今天我們要實作的內容是當使用者hover in 或 out 導覽列時,展開顯示下方的連結、文字內容並搭配上 CSS 的動畫效果。
<h2></h2>
用來模擬<nav></nav>
不是 HTML 最上面元素的情況。
<nav></nav>(.top)
是我們的導覽列,其中包含作項目內容背景用途的<div></dvi>(.dropDownBackground)
,以及項目列表<ul></ul>(.cool)
和項目內容<li></li>
,其中每一個項目的內容除了<a></a>
標題連結有顯示外,剩餘資訊.dropdown
的部分都被預設隱藏。
<h2>Cool</h2>
<nav class="top">
<div class="dropdownBackground">
<span class="arrow"></span>
</div>
<ul class="cool">
<li>
<a href="#">About Me</a>
<div class="dropdown dropdown1">
<!--省略-->
</div>
</li>
<li>
<a href="#">Courses</a>
<ul class="dropdown courses">
<!--省略-->
</ul>
</li>
<li>
<a href="#">Other Links</a>
<ul class="dropdown dropdown3">
<!--省略-->
</ul>
</li>
</ul>
</nav>
.dropdown
: 分別設定opacity: 0;
、display: none;
預設隱藏.dropdown
,然後用transition
監控 CSS 屬性值的變化,達成 CSS 動畫的效果。
.trigger-enter .dropdown
: .trigger-enter
這個 class 會在 hover in 任意一個<li></li>
時,被加到.dropdown
上。設定display:block;
把隱藏的項目資訊 show 出來,但現在還是透明的。
.trigger-enter-active .dropdown
: .trigger-enter-active
這個 class 會在 hover in 任意一個<li></li>
時,被加到.dropdown
上。設定opacity:1;
把隱藏的項目資訊 show 出來,現在是不透明的。
為什麼要把opacity
、display
分開寫而不寫在同一個 class 選擇器呢? 因為寫在一起的話,transition
的動畫效果會失效。
.dropdown {
/*省略...*/
opacity: 0;
/*省略...*/
transition: all 0.5s;
/*省略...*/
display: none;
}
.trigger-enter .dropdown {
display: block;
}
.trigger-enter-active .dropdown {
opacity: 1; /*display和opacity分開寫是為了CSS動畫效果*/
}
.dropdownBackground
: 初始項目內容的背景被隱藏(透明)。
.dropdownBackground.open
: 讓項目內容的背景被顯示出來(不透明)。而.open
這個 class 會在 hover in 任意一個<li></li>
時,被加到.dropdownBackground
上。
.dropdownBackground {
/*省略...*/
opacity:0;
}
.dropdownBackground.open { /*show the background*/
opacity: 1;
}
首先取得所有必要的網頁元素,包括項目列表下的每個<li>
、作為列表項目內容背景的.dropdownBackground
以及導覽列.top
。
const triggers = document.querySelectorAll('.cool > li');
const background = document.querySelector('.dropdownBackground');
const nav = document.querySelector('.top');
為項目列表中的每個<li>
都分別註冊mouseenter event listener
(hover in 觸發)和mouseleave event listener
(hover out 觸發),然後各自以handleEnter()
和handleLeave
作事件處理。
//listen for hovering in and out
function handleEnter(){
}
function handleLeave(){
}
triggers.forEach(trigger => trigger.addEventListener('mouseenter',handleEnter));
triggers.forEach(trigger => trigger.addEventListener('mouseleave',handleLeave));
hover in <li>
時,將trigger-enter
加到<li>
上。
然後利用setTimeout()
讓在<li>
加上trigger-enter-active
這件事被延遲150毫秒,利用條件判斷只有當<li>
上已經有.trigger-enter
這個 class,才執行後方的在<li>
加上.trigger-enter-active
,會需要做到這樣是為避免使用者 hover in and out 的時間過快,讓一個項目的內容還沒來得及消失,另一個卻馬上出現,造成殘影的效果。
最後,在項目內容背景加上.open
這個 class 讓它被 show 出來。
function handleEnter(){
this.classList.add('trigger-enter');
//use the arrow function or 'this' will be the window
//不要讓內容太早被show出來,不然可能會出現殘影
setTimeout(()=> this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'),150);
//set background
background.classList.add('open');
}
handleLeave()
要做的事情很簡單,就是當使用者 hover out 時,移除所有被新增的 class。
function handleLeave(){
this.classList.remove('trigger-enter','trigger-enter-active');
//set background
background.classList.remove('open');
}
最後的最後,我們要把項目內容的背景移到它該待的地方。
首先取得項目內容的元素然後再取得項目內容相對視窗右上角的座標(這個座標不隨著 scroll down 而有所改變)。
複習一下getBoundingClientRect()
取得的元素座標圖 :
那為什麼我們還需要取得導覽列nav
的座標呢? 因為在 nav 不是 HTML 文件裡的第一個元素時,如果只單單用.dropdown
的left
、top
進行項目內容背景的位置設定,會發現到背景被稍微往下擠了一點,被往下擠的大小剛好可以用導覽列nav
的位置來作修正,分別將指定的left
、top
減去nav
的left
、top
就好。
我們將大小、位置等等資訊整合在物件coords
裡,之後再個別利用coords
裡的指定值去修改項目內容背景的 CSS 屬性就完成了。
function handleEnter(){
// 上略...
// set position of the background
const dropDown = this.querySelector('.dropdown');
const dropDownCoords = dropDown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();
console.log(dropDownCoords);
const coords = {
height: dropDownCoords.height,
width: dropDownCoords.width,
top: dropDownCoords.top - navCoords.top,
left: dropDownCoords.left - navCoords.left
}
background.style.setProperty('width',`${coords.width}px`);
background.style.setProperty('height',`${coords.height}px`);
background.style.setProperty('transform',`translate(${coords.left}px,${coords.top}px)`);
}
mouseenter
mouseleave
Element.getBoundingClientRect()
CSSStyleDeclaration.setProperty()