JavaScript markdown 編輯器實(shí)現(xiàn)雙屏同步滾動(dòng)
前言
由于一直在使用 markdown 編輯器寫技術(shù)文章,所以對于編寫體驗(yàn)很敏感。我發(fā)現(xiàn)各大社區(qū)的 markdown 編輯器基本都有同步滾動(dòng)功能。只不過有些做得好,有些做得馬馬虎虎。出于好奇,我就打算自己親自實(shí)現(xiàn)一下這個(gè)功能。
思考了一段時(shí)間,最后想出來了三種方案:
- 百分比滾動(dòng)
- 雙屏同時(shí)渲染占用面積大的元素
- 每一行的元素都賦上一個(gè)索引,根據(jù)索引來精確同步每一行的滾動(dòng)高度
百分比滾動(dòng)
假設(shè)現(xiàn)在正在滾動(dòng) a 屏,那 a 屏的滾動(dòng)百分比計(jì)算方式為:a 屏的滾動(dòng)高度 / a 屏的內(nèi)容總高度,用代碼表示 a.scrollTop / a.scrollHeight。當(dāng)滾動(dòng) a 屏?xí)r,需要手動(dòng)同步 b 屏的滾動(dòng)高度,也就是根據(jù) a 屏的滾動(dòng)百分比算出 b 屏的滾動(dòng)高度:
a.onscroll = () => {
b.scrollTo({ top: a.scrollTop / a.scrollHeight * b.scrollHeight })
}原理就是這么簡單,可惜實(shí)現(xiàn)效果不太好。

從上面的動(dòng)圖可以看出,當(dāng)我在第二個(gè)大標(biāo)題處停留的時(shí)候,左右雙屏的內(nèi)容是同步的。但當(dāng)我滾動(dòng)到第三個(gè)大標(biāo)題時(shí),左右雙屏的內(nèi)容高度已經(jīng)差了將近 300 像素了。所以說這個(gè)方案勉勉強(qiáng)強(qiáng)能用吧,聊勝于無。
雙屏同時(shí)渲染占用面積大的元素
雙屏內(nèi)容高度不一致,是因?yàn)?markdown 同一個(gè)元素渲染后的高度和渲染前會(huì)有差別。例如一個(gè)圖片,用 markdown 寫就一行代碼的事,但渲染出來的圖片有大有小,高度幾十、幾百像素的都有。如果 markdown 的圖片代碼雙屏同時(shí)渲染,倒是能解決這個(gè)問題。

但是除了圖片仍然有不少元素渲染前后的高度是有差距的,雖然沒有圖片這么夸張。譬如 h1 h2 這種,當(dāng)文章內(nèi)容越長,這種小差異帶來的問題會(huì)越來越大,導(dǎo)致雙屏內(nèi)容高度的差距也會(huì)越來越大。所以說這種方案也不是很靠譜。
賦上一個(gè)索引
每一行的元素都賦上一個(gè)索引,根據(jù)索引來精確精確同步每一行的滾動(dòng)高度
之前兩個(gè)方案都屬于勉強(qiáng)能用,不夠好?,F(xiàn)在這個(gè)第三方案就比前面兩個(gè)強(qiáng)多了,幾乎能做到精確同步每一行的內(nèi)容。具體怎么做呢?
第一步,監(jiān)聽 markdown 編輯框的內(nèi)容變化,為每一個(gè)元素賦上一個(gè)索引,空行空文本除外。

當(dāng)把編輯框的 HTML 傳給右邊的框渲染時(shí),需要把 data-index 賦值給渲染后的元素。這樣就能通過 data-index 精確定位渲染前后的同一元素了。

第二步,根據(jù) a 屏的元素滾動(dòng)高度計(jì)算 b 屏上同一索引的元素滾動(dòng)高度
在 a 屏進(jìn)行滾動(dòng)時(shí),需要從上到下遍歷 a 屏的所有元素,并且找到第一個(gè)在屏幕內(nèi)的元素。找到第一個(gè)在屏幕內(nèi)的元素 這句話的意思是因?yàn)樵跐L動(dòng)過程中,有些元素會(huì)因?yàn)闈L動(dòng)跑到屏幕外面(原來在屏幕內(nèi),滾動(dòng)到屏幕外),這些元素我們是不需要計(jì)算的。
判斷一個(gè)元素是否在屏幕內(nèi):
// dom 是否在屏幕內(nèi)
function isInScreen(dom) {
const { top, bottom } = dom.getBoundingClientRect()
return bottom >= 0 && top < window.innerHeight
}除了判斷元素是否在屏幕內(nèi),還需要判斷這個(gè)元素在屏幕內(nèi)的部分占整個(gè)元素高度的百分比。譬如說一個(gè)圖片的 markdown 字符串,由于滾動(dòng)的原因,導(dǎo)致一半在屏幕內(nèi),一半在屏幕外。為了精確同步,那么渲染后的圖片也必須有一半在屏幕內(nèi)一半在屏幕外。

計(jì)算元素在屏幕內(nèi)的百分比代碼:
// dom 在當(dāng)前屏幕展示內(nèi)容的百分比
function percentOfdomInScreen(dom) {
// 已經(jīng)通過另一個(gè)函數(shù) isInScreen() 確定了這個(gè) dom 在屏幕內(nèi),所以只需要計(jì)算它在屏幕內(nèi)的百分比,而不需要考慮它是否在屏幕外
const { height, bottom } = dom.getBoundingClientRect()
if (bottom <= 0) return 0 // 不在屏幕內(nèi)
if (bottom >= height) return 1 // 完全在屏幕內(nèi)
return bottom / height // 部分在屏幕內(nèi)
}現(xiàn)在我們就可以從上到下遍歷 a 屏的所有元素,找到第一個(gè)在屏幕內(nèi)的元素了:
// scrollContainer 即上面說的 a 屏,ShowContainer 是 b 屏
const nodes = Array.from(scrollContainer.children)
for (const node of nodes) {
// 從上往下遍歷,找到第一個(gè)在屏幕內(nèi)的元素
if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
const index = node.dataset.index
// 根據(jù)滾動(dòng)元素的索引,找到它在渲染框中對應(yīng)的元素
const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
// 獲取滾動(dòng)元素在 a 屏中展示的內(nèi)容百分比
const percent = percentOfdomInScreen(node)
// 計(jì)算這個(gè)對等元素在 b 屏中距離容器頂部的高度
const heightToTop = getHeightToTop(dom)
// 根據(jù) percent 算出對等元素在 b 屏中需要隱藏的高度
const domNeedHideHeight = dom.offsetHeight * (1 - percent)
// scrollTo({ top: heightToTop }) 會(huì)把對等元素滾動(dòng)到在 b 屏中恰好完全展示整個(gè)元素的位置
// 然后再滾動(dòng)它需要隱藏的高度 domNeedHideHeight,組合起來就是 scrollTo({ top: heightToTop + domNeedHideHeight })
ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
break
}
}
從動(dòng)圖來看,目前已經(jīng)做到行內(nèi)容的精確同步了。
踩坑
有一些元素渲染后會(huì)變成嵌套元素,例如表格 table,渲染后的內(nèi)容層級為:
<table> <tbody> <tr> <td></td> </tr> </tbody> </table>
按照目前的渲染邏輯,假如我寫了個(gè)表格:
|1|b| ...
那么 |1|b| 上的 data-index 會(huì)對應(yīng)到 table 上。


那這就會(huì)有個(gè) bug,當(dāng) |1|b| 滾動(dòng)到 50% 的時(shí)候,整個(gè) table 也會(huì)滾動(dòng)到 50%。
這個(gè)現(xiàn)象如下圖所示:

這和我們相要的效果不一樣。a 屏連一行的內(nèi)容都沒滾完,b 屏整個(gè)內(nèi)容已經(jīng)滾動(dòng)到一半了。
所以像這種嵌套的元素,在打 data-index 標(biāo)記時(shí),要把它打到真正的內(nèi)容上。用表格 table 來做示例,就得把 data-index 的標(biāo)記打在 tr 上。

這樣一來,同步滾動(dòng)就正常了。同理,其他的嵌套元素也一樣(譬如 ul ol)。

到此這篇關(guān)于JavaScript markdown 編輯器實(shí)現(xiàn)雙屏同步滾動(dòng)的文章就介紹到這了,更多相關(guān)JS markdown 雙屏同步滾動(dòng)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript代碼實(shí)現(xiàn)春晚劉謙魔術(shù)的模擬程序
昨晚春晚上劉謙的兩個(gè)魔術(shù)表演都非常精彩,尤其是第二個(gè)魔術(shù),他演繹了經(jīng)典的約瑟夫環(huán)問題!約瑟夫環(huán)是一個(gè)經(jīng)典的數(shù)學(xué)問題,本文給出了完整的 JavaScript 代碼實(shí)現(xiàn),感興趣的同學(xué)可以自己動(dòng)手實(shí)現(xiàn)一下2024-02-02
Bootstrap多級導(dǎo)航欄(級聯(lián)導(dǎo)航)的實(shí)現(xiàn)代碼
這篇文章主要介紹了Bootstrap多級導(dǎo)航欄的實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2016-03-03
JavaScript 動(dòng)態(tài)添加腳本,并觸發(fā)回調(diào)函數(shù)的實(shí)現(xiàn)代碼
JavaScript 動(dòng)態(tài)添加腳本,并觸發(fā)回調(diào)函數(shù)的實(shí)現(xiàn)代碼,需要的朋友可以參考下。2011-01-01
js最實(shí)用string(字符串)類型的使用及截取與拼接詳解
這篇文章主要介紹了js string使用截取與拼接,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
JS生態(tài)系統(tǒng)加速npm腳本優(yōu)化及性能分析探索
這篇文章主要為大家介紹了JS生態(tài)系統(tǒng)加速npm腳本優(yōu)化及性能分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
原生javascript實(shí)現(xiàn)自動(dòng)更新的時(shí)間日期
這篇文章主要介紹了原生javascript實(shí)現(xiàn)自動(dòng)更新的時(shí)間日期的相關(guān)資料,對實(shí)現(xiàn)代碼進(jìn)行詳細(xì)分析,感興趣的朋友可以參考一下2016-02-02

