前幾天公司要求寫一個錨點自動高亮功能,頁面滾動到哪裏錨點高亮到哪裏。javascript
因此這裏簡單記錄一下,由於遇到了自動高亮斷定的問題,因此發在這裏,若是有更好的方法,但願能教教我~~css
這是大概的dom結構html
<html>
<body>
<ul>
<li><a href="#a1">a1</a></li>
<li><a href="#a2">a2</a></li>
<li><a href="#a3">a3</a></li>
<li><a href="#a4">a4</a></li>
<li><a href="#a5">a5</a></li>
</ul>
<div id="container">
<div id="a1"></div>
<div id="a2"></div>
<div id="a3"></div>
<div id="a4"></div>
<div id="a5"></div>
</div>
</body>
</html>
複製代碼
a
標籤 即圖中 anchor
id
的元素,如圖中 a1 element
和 a2 element
容器
,若是是整個頁面那就是 document
,或者就是自定義的滾動容器,這裏統一叫container
高亮效果,這裏就是簡單的給對應的 a
標籤添加一個class
便可java
// 獲取a全部元素
function getAllAnchorElement(container) {
const target = container ? container : document
return target.querySelectorAll('a')
}
// 對應id的添加高亮類名,非對應id移除以前添加的高亮類名
function highLightAnchor(id){
getAllAnchorElement().forEach(element => {
element.classList.remove('highLight')
if (element.hash.slice(1) == id) {
element.classList.add('highLight')
}
});
}
複製代碼
原理很簡單,就是監聽容器元素的滾動,在對應條件下去自動高亮a標籤git
// 這裏注意須要用 thorttle 處理一下handleScroll 函數,否則觸發次數太多可能卡頁面哦 🤪🤪
const throttleFn = throttle(handleScroll, 100)
// 若是你沒有阻止滾動事件,能夠加上 passive: true 能夠提升滾動性能
ScrollContrainer.addEventListener('scroll', throttleFn, {passive: true})
複製代碼
可是這個對應條件
有點麻煩,我一共想了3種方法,各有缺點和優勢,若是你們有完美的方法,望不吝賜教github
我這裏方案都是經過 getBoundingClientRect
來判斷元素的位置。web
這裏貼一點MDN上的解釋dom
返回值是一個 DOMRect 對象,這個對象是由該元素的 getClientRects() 方法返回的一組矩形的集合, 即:是與該元素相關的CSS 邊框集合 。函數
DOMRect 對象包含了一組用於描述邊框的只讀屬性——left、top、right和bottom,單位爲像素。除了 width 和 height 外的屬性都是相對於視口的左上角位置而言的。性能
我這裏3種方案,先說第一種
let highligthId;//須要高亮的id
const windowHeight = this.ScrollContrainer.offsetHeight //容器高度
this.anchors.forEach(element => {
const id = element.hash.slice(1)
const target = document.getElementById(id)
if (target) {
const {
top
} = target.getBoundingClientRect()
// 當元素頭部可見時
if (top < windowHeight) {
highligthId = id
}
}
})
if (highligthId) {
// 調用高亮方法
this.highLightAnchor(highligthId)
}
複製代碼
第一個元素
沒有滿屏好比 a1
,屏幕下方漏出一點 a2
元素,那 a1
元素的高亮會裏面跳走,這個時候 a1
再也高亮不到了
let highligthId;
let maxRatio = 0 // 佔據屏幕的比例
const windowHeight = this.ScrollContrainer.offsetHeight
this.anchors.forEach(element => {
const id = element.hash.slice(1)
const target = document.getElementById(id)
if (target) {
let visibleRatio = 0;
let {
top,
height,
bottom
} = target.getBoundingClientRect();
// 當元素所有可見時
if (top >= 0 && bottom <= windowHeight) {
visibleRatio = height / windowHeight
}
// 當元素就頭部可見時
if (top >= 0 && top < windowHeight && bottom > windowHeight) {
visibleRatio = (windowHeight - top) / windowHeight
}
// 當元素佔滿屏幕時
if (top < 0 && bottom > windowHeight) {
visibleRatio = 1
}
// 當元素尾部可見時
if (top < 0 && bottom > 0 && bottom < windowHeight) {
visibleRatio = bottom / windowHeight
}
if (visibleRatio >= maxRatio) {
maxRatio = visibleRatio;
highligthId = id;
}
}
});
if (highligthId) {
this.highLightAnchor(highligthId)
}
複製代碼
a2
的比例比 a1
大,那 a1
就沒法高亮了
a5
比 a4
小,那麼 a5
就沒法高亮,由於 a5
佔據屏幕的比例是大不過 a4
的
代碼判斷條件和 誰佔據屏幕比例大就高亮誰 的方案同樣,就是分母不同
let highligthId;
let maxRatio = 0
const windowHeight = this.ScrollContrainer.offsetHeight
this.anchors.forEach(element => {
const id = element.hash.slice(1)
const target = document.getElementById(id)
if (target) {
let visibleRatio = 0;
let {
top,
height,
bottom
} = target.getBoundingClientRect();
// 當元素所有可見時
if (top >= 0 && bottom <= windowHeight) {
visibleRatio = 1
}
// 當元素就頭部可見時
if (top >= 0 && top < windowHeight && bottom > windowHeight) {
visibleRatio = (windowHeight - top) / height
}
// 當元素佔滿屏幕時
if (top < 0 && bottom > windowHeight) {
visibleRatio = windowHeight / height
}
// 當元素尾部可見時
if (top < 0 && bottom > 0 && bottom < windowHeight) {
visibleRatio = bottom / height
}
if (visibleRatio >= maxRatio) {
maxRatio = visibleRatio;
highligthId = id;
}
}
});
if (highligthId) {
this.highLightAnchor(highligthId)
}
複製代碼
誰佔據屏幕比例大就高亮誰的方案
好一點在頁面頂部a1
和 a2
都所有顯示,那 a1
的優先級就沒有 a2
高,沒法高亮
在頁面中間 a3
很小,那麼 a3
出現後就每次計算比例都是 1
,a4 就必須等 a3
開始消失,纔有可能高亮
經過以上能夠看到我能想到的方案在某些狀況下都有問題。
在個人工做中由於每個元素都至少有半屏都大小,因此誰佔據屏幕比例大就高亮誰
這個方案效果是綜合起來效果最好的。
其實一開始用的是誰顯示的自身百分比大就高亮誰
這個方案,可是這個方案在某一個元素特別特別長
的時候效果就差點。
<html>
<body>
<nav>
<ul>
<li><a href="#a1">a1</a></li>
<li><a href="#a2">a2</a></li>
<li><a href="#a3">a3</a></li>
<li><a href="#a4">a4</a></li>
<li><a href="#a5">a5</a></li>
</ul>
<input type="radio" name="strategy" checked value="type1">冒頭就高亮<br><br>
<input type="radio" name="strategy" value="type2">佔據屏幕比例大就高亮<br><br>
<input type="radio" name="strategy" value="type3">顯示的自身百分比大就高亮<br><br>
<p>顏色右下角能夠拖動改變對應元素的大小</p>
</nav>
<div id="container">
<div id="a1"></div>
<div id="a2"></div>
<div id="a3"></div>
<div id="a4"></div>
<div id="a5"></div>
</div>
</body>
<script> function throttle(fn, interval = 1000) { let timer = null; return function(...args) { if (!timer) { timer = setTimeout(() => { timer = null fn.call(this, ...args) }, interval); } } } class AutoHighLightAnchor { anchors; ScrollContrainer; throttleFn; strategy; constructor(anchorsContainer, ScrollContrainer, strategy = AutoHighLightAnchor.Strategys.type1) { this.anchors = anchorsContainer.querySelectorAll('a') this.ScrollContrainer = ScrollContrainer; this.strategy = strategy; this.init() } init(strategy = this.strategy) { if (this.throttleFn) { this.remove() } this.throttleFn = throttle(this[strategy].bind(this), 100) this.throttleFn() // 初始執行一次更新位置 this.ScrollContrainer.addEventListener('scroll', this.throttleFn, { passive: true }) } remove() { this.ScrollContrainer.removeEventListener('scroll', this.throttleFn, { passive: true }) } highLightAnchor(id) { this.anchors.forEach(element => { element.classList.remove('highLight') if (element.hash.slice(1) == id) { element.classList.add('highLight') } }); } type1(e) { let highligthId; const windowHeight = this.ScrollContrainer.offsetHeight this.anchors.forEach(element => { const id = element.hash.slice(1) const target = document.getElementById(id) if (target) { const { top } = target.getBoundingClientRect() // 當元素頭部可見時 if (top < windowHeight) { highligthId = id } } }) if (highligthId) { this.highLightAnchor(highligthId) } } type2(e) { let highligthId; let maxRatio = 0 const windowHeight = this.ScrollContrainer.offsetHeight this.anchors.forEach(element => { const id = element.hash.slice(1) const target = document.getElementById(id) if (target) { let visibleRatio = 0; let { top, height, bottom } = target.getBoundingClientRect(); // 當元素所有可見時 if (top >= 0 && bottom <= windowHeight) { visibleRatio = height / windowHeight } // 當元素就頭部可見時 if (top >= 0 && top < windowHeight && bottom > windowHeight) { visibleRatio = (windowHeight - top) / windowHeight } // 當元素佔滿屏幕時 if (top < 0 && bottom > windowHeight) { visibleRatio = 1 } // 當元素尾部可見時 if (top < 0 && bottom > 0 && bottom < windowHeight) { visibleRatio = bottom / windowHeight } if (visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; } } }); if (highligthId) { this.highLightAnchor(highligthId) } } type3(e) { let highligthId; let maxRatio = 0 const windowHeight = this.ScrollContrainer.offsetHeight this.anchors.forEach(element => { const id = element.hash.slice(1) const target = document.getElementById(id) if (target) { let visibleRatio = 0; let { top, height, bottom } = target.getBoundingClientRect(); // 當元素所有可見時 if (top >= 0 && bottom <= windowHeight) { visibleRatio = 1 } // 當元素就頭部可見時 if (top >= 0 && top < windowHeight && bottom > windowHeight) { visibleRatio = (windowHeight - top) / height } // 當元素佔滿屏幕時 if (top < 0 && bottom > windowHeight) { visibleRatio = windowHeight / height } // 當元素尾部可見時 if (top < 0 && bottom > 0 && bottom < windowHeight) { visibleRatio = bottom / height } if (visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; } } }); if (highligthId) { this.highLightAnchor(highligthId) } } } AutoHighLightAnchor.Strategys = { type1: 'type1', type2: 'type2', type3: 'type3' } const high = new AutoHighLightAnchor(document.querySelector('ul'), document.querySelector('#container'), AutoHighLightAnchor.Strategys.type1) document.querySelectorAll('input[type=radio]').forEach(element => { element.onchange = e => high.init(e.target.value) }) </script>
<style> body { margin: 0; } a { display: block; width: 100%; height: 100%; color: #898A95; line-height: 30px; border-radius: 0 50px 50px 0; text-decoration: none; text-align: center; } .highLight { color: #ffffff; background: #77b9e1; } nav { width: 120px; float: left; } ul { width: 100px; display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; } #container { height: 100%; overflow: scroll; } #container>div { position: relative; resize: vertical; overflow: scroll; /* color: #ffffff; font-size: 30px; */ } #container>div::after { content: attr(id); display: block; font-size: 100px; color: #fff; text-align: center; width: 100%; position: absolute; top: 50%; transform: translateY(-50%); } #container>div:hover { outline: 1px dashed #09f; } #container>div::-webkit-scrollbar { width: 25px; height: 20px; } #a1 { height: 100vh; background-color: #77b9e1; } #a2 { height: 50vh; background-color: #9fc6e6; } #a3 { height: 33vh; background-color: #73a5d7; } #a4 { height: 33vh; background-color: #1387aa; } #a5 { height: 20vh; background-color: #0c5ea8; } </style>
</html>
複製代碼