JS實(shí)現(xiàn)頁(yè)面導(dǎo)航與內(nèi)容相互錨定實(shí)例詳解
引文
在日常的學(xué)習(xí)和工作中,經(jīng)常會(huì)瀏覽的這樣一種網(wǎng)頁(yè),它的結(jié)構(gòu)為左側(cè)是側(cè)邊欄,右側(cè)是內(nèi)容區(qū)域,當(dāng)點(diǎn)擊左側(cè)的側(cè)邊欄上的目錄時(shí),右側(cè)的內(nèi)容區(qū)域會(huì)自動(dòng)滾動(dòng)到該目錄所對(duì)應(yīng)的內(nèi)容區(qū)域;當(dāng)滾動(dòng)內(nèi)容區(qū)域時(shí),側(cè)邊欄上對(duì)應(yīng)的目錄也會(huì)高亮。
恰巧最近需要寫(xiě)個(gè)類(lèi)似的小玩意,簡(jiǎn)單的做下筆記,為了避免有人只熟悉Vue或React框架中的一個(gè)框架,還是使用原生JS來(lái)進(jìn)行實(shí)現(xiàn)。
思路
點(diǎn)擊側(cè)邊欄上的目錄時(shí),通過(guò)獲取點(diǎn)擊的目錄的類(lèi)名、或id、或index,用這些信息作為標(biāo)記,然后在內(nèi)容區(qū)域查找對(duì)應(yīng)的內(nèi)容。
滾動(dòng)內(nèi)容區(qū)域時(shí),根據(jù)內(nèi)容區(qū)域的內(nèi)容的dom節(jié)點(diǎn)獲取標(biāo)記,根據(jù)標(biāo)記來(lái)查找目錄。
實(shí)現(xiàn)
頁(yè)面初始化
首先把html和css寫(xiě)成左邊為目錄,右邊為內(nèi)容的頁(yè)面結(jié)構(gòu),為測(cè)試提供ui界面。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>目錄與內(nèi)容相互錨定</title> <style> .container { display: flex; flex-direction: row; } #nav { width: 150px; height: 400px; background-color: #eee; } #nav .nav-item { cursor: pointer; } #nav .nav-item.active { font-weight: bold; background-color: #f60; } #content { flex: 1; margin-left: 10px; position: relative; width: 300px; height: 400px; overflow-y: scroll; } .content-block { margin-top: 25px; height: 200px; background-color: #eee; } .content-block:first-child { margin-top: 0; } </style> </head> <body> <div class="container"> <div id="nav"> <div class="nav-item">目錄 1</div> <div class="nav-item">目錄 2</div> <div class="nav-item">目錄 3</div> <div class="nav-item">目錄 4</div> <div class="nav-item">目錄 5</div> <div class="nav-item">目錄 6</div> </div> <div id="content"> <div class="content-block">內(nèi)容 1</div> <div class="content-block">內(nèi)容 2</div> <div class="content-block">內(nèi)容 3</div> <div class="content-block">內(nèi)容 4</div> <div class="content-block">內(nèi)容 5</div> <div class="content-block">內(nèi)容 6</div> </div> </div> </body> </html>
通過(guò)點(diǎn)擊實(shí)現(xiàn)內(nèi)容的滾動(dòng)
const nav = document.querySelector("#nav"); const navItems = document.querySelectorAll(".nav-item"); navItems[0].classList.add("active"); nav.addEventListener('click', e => { navItems.forEach((item, index) => { navItems[index].classList.remove("active"); if (e.target === item) { navItems[index].classList.add("active"); content.scrollTo({ top: contentBlocks[index].offsetTop, }); } }); })
通過(guò)滾動(dòng)內(nèi)容實(shí)現(xiàn)導(dǎo)航的高亮
const content = document.querySelector("#content"); const contentBlocks = document.querySelectorAll(".content-block"); let currentBlockIndex = 0; const handleScroll = function () { for (let i = 0; i < contentBlocks.length; i++) { const block = contentBlocks[i]; if ( block.offsetTop <= content.scrollTop && block.offsetTop + block.offsetHeight > content.scrollTop ) { currentBlockIndex = i; break; } } for (let i = 0; i < navItems.length; i++) { const item = navItems[i]; item.classList.remove("active"); } navItems[currentBlockIndex].classList.add("active"); }; content.addEventListener("scroll", handleScroll);
最后實(shí)際效果如下
現(xiàn)在能基本實(shí)現(xiàn)點(diǎn)擊左側(cè)的導(dǎo)航來(lái)使右側(cè)內(nèi)容滾動(dòng)到指定區(qū)域,這樣完全可行,但是如果需要平滑滾動(dòng)的話(huà),該怎么來(lái)實(shí)現(xiàn)?
scrollTo
這個(gè)函數(shù)提供了滾動(dòng)方式的選項(xiàng)設(shè)置,指定滾動(dòng)方式為平滑滾動(dòng)方式,就可以實(shí)現(xiàn)。
content.scrollTo({ top: contentBlocks[index].offsetTop, behavior: 'smooth });
來(lái)看下效果
發(fā)現(xiàn)頁(yè)面的滾動(dòng)確實(shí)變得平滑了,但是在點(diǎn)擊左側(cè)的目錄后會(huì)發(fā)生抖動(dòng)的情況,那么為什么會(huì)發(fā)生這樣的情況?
首先觀(guān)察下現(xiàn)象,在點(diǎn)擊目錄5后,目錄5會(huì)在短暫高亮后,然后目錄1開(kāi)始高亮直到目錄5。能夠改變高亮目錄的出了我們點(diǎn)擊的時(shí)候會(huì)讓目錄高亮,另外一個(gè)會(huì)使目錄高亮的地方就是在滾動(dòng)事件函數(shù)里會(huì)根據(jù)內(nèi)容所在位置來(lái)讓目錄高亮。
// content.addEventListener("scroll", handleScroll);
那么我們把對(duì)滾動(dòng)事件的監(jiān)聽(tīng)給去掉后,我們可以看看測(cè)試結(jié)果。
那么現(xiàn)在問(wèn)題確定了,就是在滾動(dòng)過(guò)程中會(huì)影響目錄導(dǎo)航的高亮,所以在剛開(kāi)始滾動(dòng)的時(shí)候會(huì)首先高亮目錄1,那么怎么解決?
比較直接的想法就是我在點(diǎn)擊目錄后,內(nèi)容區(qū)域在滾動(dòng)到對(duì)應(yīng)內(nèi)容區(qū)域時(shí)這段時(shí)間不觸發(fā)滾動(dòng)事件,自然也不會(huì)反過(guò)來(lái)錨定目錄了,但是scrollTo
引起內(nèi)容區(qū)域的滾動(dòng)是平滑滾動(dòng),需要一段時(shí)間滾動(dòng)才能結(jié)束,但怎么判斷滾動(dòng)已經(jīng)結(jié)束了呢?
這里我給出自己的思路,就是判斷內(nèi)容區(qū)域的scrollTop是否還在變化,如果沒(méi)有變化了,那么就認(rèn)為滾動(dòng)過(guò)程已經(jīng)結(jié)束了。
let timerId = null; nav.addEventListener("click", (e) => { if (timerId) { window.clearInterval(timerId); } content.removeEventListener("scroll", handleScroll); let lastScrollPosition = content.scrollTop; timerId = window.setInterval(() => { const currentScrollPosition = content.scrollTop; console.log(currentScrollPosition, lastScrollPosition); if (lastScrollPosition === currentScrollPosition) { content.addEventListener("scroll", handleScroll); // 滾動(dòng)結(jié)束后,記得把滾動(dòng)事件函數(shù)重新綁定到scroll事件上去 window.clearInterval(timerId); } lastScrollPosition = currentScrollPosition; }, 150); navItems.forEach((item, index) => { navItems[index].classList.remove("active"); if (e.target === item) { navItems[index].classList.add("active"); content.scrollTo({ top: contentBlocks[index].offsetTop, behavior: "smooth", }); } }); });
看看效果
總結(jié)
目前功能已經(jīng)實(shí)現(xiàn),下面把完整的代碼貼出來(lái)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>目錄與內(nèi)容相互錨定</title> <style> .container { display: flex; flex-direction: row; } #nav { width: 150px; height: 400px; background-color: #eee; } #nav .nav-item { cursor: pointer; } #nav .nav-item.active { font-weight: bold; background-color: #f60; } #content { flex: 1; margin-left: 10px; position: relative; width: 300px; height: 400px; overflow-y: scroll; } .content-block { margin-top: 25px; height: 200px; background-color: #eee; } .content-block:first-child { margin-top: 0; } </style> </head> <body> <div class="container"> <div id="nav"> <div class="nav-item">目錄 1</div> <div class="nav-item">目錄 2</div> <div class="nav-item">目錄 3</div> <div class="nav-item">目錄 4</div> <div class="nav-item">目錄 5</div> <div class="nav-item">目錄 6</div> </div> <div id="content"> <div class="content-block">內(nèi)容 1</div> <div class="content-block">內(nèi)容 2</div> <div class="content-block">內(nèi)容 3</div> <div class="content-block">內(nèi)容 4</div> <div class="content-block">內(nèi)容 5</div> <div class="content-block">內(nèi)容 6</div> </div> </div> <script> const content = document.querySelector("#content"); const contentBlocks = document.querySelectorAll(".content-block"); const navItems = document.querySelectorAll(".nav-item"); const nav = document.querySelector("#nav"); let timerId = null; let currentBlockIndex = 0; navItems[currentBlockIndex].classList.add("active"); const handleScroll = function () { for (let i = 0; i < contentBlocks.length; i++) { const block = contentBlocks[i]; if ( block.offsetTop <= content.scrollTop && block.offsetTop + block.offsetHeight > content.scrollTop ) { currentBlockIndex = i; break; } } for (let i = 0; i < navItems.length; i++) { const item = navItems[i]; item.classList.remove("active"); } navItems[currentBlockIndex].classList.add("active"); }; nav.addEventListener("click", (e) => { if (timerId) { window.clearInterval(timerId); } content.removeEventListener("scroll", handleScroll); let lastScrollPosition = content.scrollTop; timerId = window.setInterval(() => { const currentScrollPosition = content.scrollTop; console.log(currentScrollPosition, lastScrollPosition); if (lastScrollPosition === currentScrollPosition) { content.addEventListener("scroll", handleScroll); window.clearInterval(timerId); } lastScrollPosition = currentScrollPosition; }, 150); navItems.forEach((item, index) => { navItems[index].classList.remove("active"); if (e.target === item) { navItems[index].classList.add("active"); content.scrollTo({ top: contentBlocks[index].offsetTop, behavior: "smooth", }); } }); }); content.addEventListener("scroll", handleScroll); </script> </body> </html>
以上就是JS實(shí)現(xiàn)頁(yè)面導(dǎo)航與內(nèi)容相互錨定的詳細(xì)內(nèi)容,更多關(guān)于JS實(shí)現(xiàn)頁(yè)面導(dǎo)航與內(nèi)容相互錨定的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript開(kāi)發(fā)Chrome瀏覽器擴(kuò)展程序UI的教程
Chrome擴(kuò)展開(kāi)發(fā)API中提供了一些關(guān)于UI外觀(guān)的操作,如果是剛剛上手的話(huà)首先需要了解Browser Actions、Omnibox、選項(xiàng)頁(yè)等,在這篇JavaScript開(kāi)發(fā)Chrome瀏覽器擴(kuò)展程序UI的教程中,我們先來(lái)回顧一下基本知識(shí):2016-05-05詳解微信小程序開(kāi)發(fā)用戶(hù)授權(quán)登陸
這篇文章主要介紹了微信小程序開(kāi)發(fā)用戶(hù)授權(quán)登陸,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Bootstrap表單簡(jiǎn)單實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Bootstrap表單的簡(jiǎn)單實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03js學(xué)習(xí)總結(jié)_選項(xiàng)卡封裝(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇js學(xué)習(xí)總結(jié)_選項(xiàng)卡封裝(實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07JS+JSP通過(guò)img標(biāo)簽調(diào)用實(shí)現(xiàn)靜態(tài)頁(yè)面訪(fǎng)問(wèn)次數(shù)統(tǒng)計(jì)的方法
這篇文章主要介紹了JS+JSP通過(guò)img標(biāo)簽調(diào)用實(shí)現(xiàn)靜態(tài)頁(yè)面訪(fǎng)問(wèn)次數(shù)統(tǒng)計(jì)的方法,基于JavaScript動(dòng)態(tài)調(diào)用jsp頁(yè)面通過(guò)對(duì)TXT文本文件的讀寫(xiě)實(shí)現(xiàn)統(tǒng)計(jì)訪(fǎng)問(wèn)次數(shù)的功能,需要的朋友可以參考下2015-12-12Javascript中獲取瀏覽器類(lèi)型和操作系統(tǒng)版本等客戶(hù)端信息常用代碼
跟蹤一些最基本的客戶(hù)端訪(fǎng)問(wèn)信息,這里將一些公用的代碼總結(jié)下來(lái),需要的朋友可以參考下2016-06-06