JavaScript markdown 編輯器實現雙屏同步滾動
前言
由於一直在使用 markdown 編輯器寫技術文章,所以對於編寫體驗很敏感。我發現各大社區的 markdown 編輯器基本都有同步滾動功能。隻不過有些做得好,有些做得馬馬虎虎。出於好奇,我就打算自己親自實現一下這個功能。
思考瞭一段時間,最後想出來瞭三種方案:
- 百分比滾動
- 雙屏同時渲染占用面積大的元素
- 每一行的元素都賦上一個索引,根據索引來精確同步每一行的滾動高度
百分比滾動
假設現在正在滾動 a 屏,那 a 屏的滾動百分比計算方式為:a 屏的滾動高度 / a 屏的內容總高度
,用代碼表示 a.scrollTop / a.scrollHeight
。當滾動 a 屏時,需要手動同步 b 屏的滾動高度,也就是根據 a 屏的滾動百分比算出 b 屏的滾動高度:
a.onscroll = () => { b.scrollTo({ top: a.scrollTop / a.scrollHeight * b.scrollHeight }) }
原理就是這麼簡單,可惜實現效果不太好。
從上面的動圖可以看出,當我在第二個大標題處停留的時候,左右雙屏的內容是同步的。但當我滾動到第三個大標題時,左右雙屏的內容高度已經差瞭將近 300 像素瞭。所以說這個方案勉勉強強能用吧,聊勝於無。
雙屏同時渲染占用面積大的元素
雙屏內容高度不一致,是因為 markdown 同一個元素渲染後的高度和渲染前會有差別。例如一個圖片,用 markdown 寫就一行代碼的事,但渲染出來的圖片有大有小,高度幾十、幾百像素的都有。如果 markdown 的圖片代碼雙屏同時渲染,倒是能解決這個問題。
但是除瞭圖片仍然有不少元素渲染前後的高度是有差距的,雖然沒有圖片這麼誇張。譬如 h1 h2 這種,當文章內容越長,這種小差異帶來的問題會越來越大,導致雙屏內容高度的差距也會越來越大。所以說這種方案也不是很靠譜。
賦上一個索引
每一行的元素都賦上一個索引,根據索引來精確精確同步每一行的滾動高度
之前兩個方案都屬於勉強能用,不夠好。現在這個第三方案就比前面兩個強多瞭,幾乎能做到精確同步每一行的內容。具體怎麼做呢?
第一步,監聽 markdown 編輯框的內容變化,為每一個元素賦上一個索引,空行空文本除外。
當把編輯框的 HTML 傳給右邊的框渲染時,需要把 data-index
賦值給渲染後的元素。這樣就能通過 data-index
精確定位渲染前後的同一元素瞭。
第二步,根據 a 屏的元素滾動高度計算 b 屏上同一索引的元素滾動高度
在 a 屏進行滾動時,需要從上到下遍歷 a 屏的所有元素,並且找到第一個在屏幕內的元素。找到第一個在屏幕內的元素
這句話的意思是因為在滾動過程中,有些元素會因為滾動跑到屏幕外面(原來在屏幕內,滾動到屏幕外),這些元素我們是不需要計算的。
判斷一個元素是否在屏幕內:
// dom 是否在屏幕內 function isInScreen(dom) { const { top, bottom } = dom.getBoundingClientRect() return bottom >= 0 && top < window.innerHeight }
除瞭判斷元素是否在屏幕內,還需要判斷這個元素在屏幕內的部分占整個元素高度的百分比。譬如說一個圖片的 markdown 字符串,由於滾動的原因,導致一半在屏幕內,一半在屏幕外。為瞭精確同步,那麼渲染後的圖片也必須有一半在屏幕內一半在屏幕外。
計算元素在屏幕內的百分比代碼:
// dom 在當前屏幕展示內容的百分比 function percentOfdomInScreen(dom) { // 已經通過另一個函數 isInScreen() 確定瞭這個 dom 在屏幕內,所以隻需要計算它在屏幕內的百分比,而不需要考慮它是否在屏幕外 const { height, bottom } = dom.getBoundingClientRect() if (bottom <= 0) return 0 // 不在屏幕內 if (bottom >= height) return 1 // 完全在屏幕內 return bottom / height // 部分在屏幕內 }
現在我們就可以從上到下遍歷 a 屏的所有元素,找到第一個在屏幕內的元素瞭:
// scrollContainer 即上面說的 a 屏,ShowContainer 是 b 屏 const nodes = Array.from(scrollContainer.children) for (const node of nodes) { // 從上往下遍歷,找到第一個在屏幕內的元素 if (isInScreen(node) && percentOfdomInScreen(node) >= 0) { const index = node.dataset.index // 根據滾動元素的索引,找到它在渲染框中對應的元素 const dom = ShowContainer.querySelector(`[data-index="${index}"]`) // 獲取滾動元素在 a 屏中展示的內容百分比 const percent = percentOfdomInScreen(node) // 計算這個對等元素在 b 屏中距離容器頂部的高度 const heightToTop = getHeightToTop(dom) // 根據 percent 算出對等元素在 b 屏中需要隱藏的高度 const domNeedHideHeight = dom.offsetHeight * (1 - percent) // scrollTo({ top: heightToTop }) 會把對等元素滾動到在 b 屏中恰好完全展示整個元素的位置 // 然後再滾動它需要隱藏的高度 domNeedHideHeight,組合起來就是 scrollTo({ top: heightToTop + domNeedHideHeight }) ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight }) break } }
從動圖來看,目前已經做到行內容的精確同步瞭。
踩坑
有一些元素渲染後會變成嵌套元素,例如表格 table,渲染後的內容層級為:
<table> <tbody> <tr> <td></td> </tr> </tbody> </table>
按照目前的渲染邏輯,假如我寫瞭個表格:
|1|b| ...
那麼 |1|b|
上的 data-index
會對應到 table
上。
那這就會有個 bug,當 |1|b|
滾動到 50% 的時候,整個 table
也會滾動到 50%。
這個現象如下圖所示:
這和我們相要的效果不一樣。a 屏連一行的內容都沒滾完,b 屏整個內容已經滾動到一半瞭。
所以像這種嵌套的元素,在打 data-index
標記時,要把它打到真正的內容上。用表格 table 來做示例,就得把 data-index
的標記打在 tr
上。
這樣一來,同步滾動就正常瞭。同理,其他的嵌套元素也一樣(譬如 ul ol)。
到此這篇關於JavaScript markdown 編輯器實現雙屏同步滾動的文章就介紹到這瞭,更多相關JS markdown 雙屏同步滾動內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Element Plus實現Affix 固釘
- Vue實現上拉加載下一頁效果的示例代碼
- 關於uniApp editor微信滑動問題
- element 穿梭框性能優化的實現
- vue導入.md文件的步驟(markdown轉HTML)