這次的練習是網頁中很常見到的頂部導覽列,當你滑鼠移動進入其中一個導覽項目時,會跑出下拉的選單,再次移動到下一個導覽項目,這個選單會跟著移動,並且寬度會跟隨內容大小改變。
JS操作上不困難,比較困難的應該是html和css的結構規劃。因為我一直被自己的爛css雷到哈哈
個人codepen
先來看一下html結構的部分
<div class="dropdown-background">
<span class="arrow"></span>
</div>
<nav class="top-nav">
<ul class="list">
<li>
<a href="">about me</a>
<div class="dropdown dropdown1">
<div class="intro">
<img src="https://picsum.photos/200/200" alt="">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Aperiam deserunt atque dignissimos illum quia minus doloribus ab, harum magni fugit quam perferendis nulla, aspernatur neque suscipit omnis ratione corporis doloremque.</p>
</div>
</div>
</li>
<li>
<a href="">course</a>
<ul class="dropdown dropdown2">
<li>Vue Tutorial</li>
<li>React Tutorail</li>
<li>Angular Tutorial</li>
<li>Basic ES6</li>
</ul>
</li>
<li>
<a href="">other links</a>
<ul class="dropdown dropdown3">
<li>twitter</li>
<li>facebook</li>
<li>instagram</li>
<li>line</li>
</ul>
</li>
</ul>
</nav>
底下都會用元素的class名稱說明。
上面中的dropdown-background,就是會跟著移動的背景,他的擺放位置很重要!!!,目前是擺放在最外層的位置,或者將他移動進入到.top-nav的內層,這時會定位在top-nav身上。因為這個dropdown-background會做絕對定位,定位在內層或外層,會影響到你移動的問題。
dropdown-background的寬度高度,會動態改變。所以每次當你滑鼠移動到li元素時,就取得dropdown的getBoundingClientRect(),再將dropdown的寬高,和相對於視窗口的left、top距離賦予給dropdown-background。
因為是相對視窗口的位置,假設dropdown-background是定位在top-nav身上,此時外層多了一個h1元素,就會發現dropdown-background的位置沒辦法對準在dropdown身上。都是因為上面多了一個h1元素所佔據的高度。
要解決上面的問題,你要先知道dropdown-background是定位在top-nav身上,但你取得的dropdown的getBoundingClientRect(),是相對於視窗口。所以定位在top-nav時,就多了上面的h1高度。這時候,就要計算top-nav相對於視窗最上緣的高度為多少,再將要移動的top距離,減掉top-nav的offsetTop。
可以參考程式碼如下:
const triggers = document.querySelectorAll(".list > li");
const dropdownBackGround = document.querySelector(".dropdown-background");
const nav = document.querySelector(".top-nav");
function openDropdown() {
this.querySelector(".dropdown").classList.add("dropdown-show");
setTimeout(() => {
this.querySelector(".dropdown").classList.contains("dropdown-show") &&
this.querySelector(".dropdown").classList.add("dropdown-show-active");
}, 100);
const { width, height, top, left } = this.querySelector(
".dropdown"
).getBoundingClientRect();
dropdownBackGround.style.width = `${width}px`;
dropdownBackGround.style.height = `${height}px`;
dropdownBackGround.style.transform = `translate(${left - nav.offsetLeft}px, ${
top - nav.offsetTop
}px)`;
dropdownBackGround.style.opacity = 1;
}
function closeDropdown() {
this.querySelector(".dropdown").classList.remove(
"dropdown-show",
"dropdown-show-active"
);
dropdownBackGround.style.opacity = 0;
}
// 每一個.list底下的li元素都綁定滑鼠移動事件
triggers.forEach((item) => item.addEventListener("mouseenter", openDropdown));
triggers.forEach((item) => item.addEventListener("mouseleave", closeDropdown));
所以dropdown-background元素的擺放位置很重要,如果拉去最外層定位在body身上時,你就不用管top-nav相對於視窗口的top、left距離是不是0。
其實不管放哪裡,只要清楚他目前定位在哪一個元素身上,並且理解getBoundingClientRect的概念,應該隨便你放都不是問題。
另外這邊的位移使用transform,而不是用position:absolute top left的方式。你要用也是可以。只是css的這兩個元素差別在於畫面需不需要重新繪製,對效能上可能有些許的影響。
不論用transform或position:absolute top left的方式,你的元素定位才是影響到你JS計算的問題。
function openDropdown() {
// 利用定時器,當容器已經變成block的時候,再去加上opacity的變化,並且防止背景還沒出現時,dropdown的內容已經出現
this.querySelector(".dropdown").classList.add("dropdown-show");
setTimeout(() => {
this.querySelector(".dropdown").classList.contains("dropdown-show") &&
this.querySelector(".dropdown").classList.add("dropdown-show-active");
}, 100);
// 解構賦值
const { width, height, top, left } = this.querySelector(
".dropdown"
).getBoundingClientRect();
// 動態設定寬高和位置
dropdownBackGround.style.width = `${width}px`;
dropdownBackGround.style.height = `${height}px`;
// dropdownBackGround.style.transform = `translate(${left - nav.offsetLeft}px, ${
// top - nav.offsetTop
// }px)`;
dropdownBackGround.style.transform = `translate(${left}px, ${top}px)`;
// dropdownBackGround.style.left = `${left - nav.offsetLeft}px`;
// dropdownBackGround.style.top = `${top - nav.offsetTop}px`;
// dropdownBackGround.style.left = `${left}px`;
// dropdownBackGround.style.top = `${top}px`;
dropdownBackGround.style.opacity = 1;
}
體會到html和css寫得好,再操作JS會比較輕鬆。不然在做dom元素移動時,根本沒有照我想的方式在移動...QQ