• markdown 编辑器实现双屏同步滚动


    由于一直在使用 markdown 编辑器写技术文章,所以对于编写体验很敏感。我发现各大社区的 markdown 编辑器基本都有同步滚动功能。只不过有些做得好,有些做得马马虎虎。出于好奇,我就打算自己亲自实现一下这个功能。

    思考了一段时间,最后想出来了三种方案:

    1. 百分比滚动
    2. 双屏同时渲染占用面积大的元素
    3. 每一行的元素都赋上一个索引,根据索引来精确同步每一行的滚动高度

    百分比滚动

    假设现在正在滚动 a 屏,那 a 屏的滚动百分比计算方式为:a 屏的滚动高度 / a 屏的内容总高度,用代码表示 a.scrollTop / a.scrollHeight。当滚动 a 屏时,需要手动同步 b 屏的滚动高度,也就是根据 a 屏的滚动百分比算出 b 屏的滚动高度:

    a.onscroll = () => {
    	b.scrollTo({ top: a.scrollTop / a.scrollHeight * b.scrollHeight })
    }
    
    • 1
    • 2
    • 3

    原理就是这么简单,可惜实现效果不太好。

    在这里插入图片描述

    从上面的动图可以看出,当我在第二个大标题处停留的时候,左右双屏的内容是同步的。但当我滚动到第三个大标题时,左右双屏的内容高度已经差了将近 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    除了判断元素是否在屏幕内,还需要判断这个元素在屏幕内的部分占整个元素高度的百分比。譬如说一个图片的 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 // 部分在屏幕内
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    现在我们就可以从上到下遍历 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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    从动图来看,目前已经做到行内容的精确同步了。

    踩坑

    有一些元素渲染后会变成嵌套元素,例如表格 table,渲染后的内容层级为:

    <table>
    	<tbody>
    		<tr>
    			<td>td>
    		tr>
    	tbody>
    table>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    按照目前的渲染逻辑,假如我写了个表格:

    |1|b|
    ...
    
    • 1
    • 2

    那么 |1|b| 上的 data-index 会对应到 table 上。

    在这里插入图片描述

    在这里插入图片描述

    那这就会有个 bug,当 |1|b| 滚动到 50% 的时候,整个 table 也会滚动到 50%。这个现象如下图所示:

    在这里插入图片描述

    这和我们相要的效果不一样。a 屏连一行的内容都没滚完,b 屏整个内容已经滚动到一半了。

    所以像这种嵌套的元素,在打 data-index 标记时,要把它打到真正的内容上。用表格 table 来做示例,就得把 data-index 的标记打在 tr 上。

    在这里插入图片描述

    这样一来,同步滚动就正常了。同理,其他的嵌套元素也一样(譬如 ul ol)。

    在这里插入图片描述

    总结

    完整的代码我已经放在 github 上了:

    还有在线 DEMO:

    如果在线 DEMO 比较慢,可以克隆项目后直接打开 html 文件访问。

  • 相关阅读:
    Ubuntu22.04.01Desktop桌面版 允许root用户远程登陆 笔记221110
    Python之线程Thread(一)
    UML类图简单认识
    南京邮电大学电工电子(数电)实验报告——数字电路与模拟电路的综合应用
    wordpress主题开发教程
    【Python&语义分割】Segment Anything(SAM)模型交互式分割+掩膜保存(三)
    【C++ 程序设计入门基础】- Chapter One
    分片上传方案
    【LeetCode】经典的环形链表
    【C++基础】函数指针
  • 原文地址:https://blog.csdn.net/q411020382/article/details/126239001