如何用 Codemirror6 和 MarkedJS 实现双屏同步滚动

目录

本文收录在《如何开发一个自己的笔记软件》系列中,该系列源码均可在 Blossom 笔记软件 仓库中查看。仓库地址:

如何用 Codemirror6 和 MarkedJS 实现双屏同步滚动

在使用 Markdown 的过程中,经常遇到编辑区域和预览区域在滚动时不同步的问题,本文介绍了一种解决方法,可以一定程度解决该问题。

一般来说,双屏滚动不同步是因为滚动的距离是通过滚动条距离页面顶部的百分比来计算的。但是两个屏幕的高度不是统一的,例如,一张图片的 Markdown 内容只有简单的一行,但如果这张图片高度十分高,那么势必会影响到双屏的同步滚动。

less 复制代码
// 这是一张高度很高的图片,会占用很大的预览空间
![图片](https://w.wallhaven.cc/full/9m/wallhaven-9m2w3x.png)

所以一种方法是,知道当前编辑部分最顶部显示的内容,然后在预览框中也显示相同的内容。比方说当前编辑框顶部是整个文本的第 10 行,那么预览框中就显示这一行 markdown 渲染后的内容。

如果是自己从头实现编辑器,那么进行这样的拓展可能很简单。但是如果是基于 codemirror 需要如何做呢?毕竟我们很难将编辑的每一行和预览的每一行进行一一对应。

但在翻看 codemirror 的 API 时,发现 codemirror 提供了获取当前文档距离浏览器顶部的距离的属性

typescript 复制代码
editor.documentTop

当编辑器滚动条在顶部时,如下图,获取的值即为0

此时我们将滚动条向下滚动到下图所示位置时,将会获取到一个负值,这个负值是已经超出屏幕的文档顶部,距离浏览器顶部的距离。

通过这个值,我们可以获取文档顶部到当前浏览器顶部的文档内容。

typescript 复制代码
// 不可见那部分 markdown
const invisibleMarkdown = editor.state.sliceDoc(0, editor.documentTop)

既然这部分 markdown 不可见了,那么相应的,这部分渲染后的 html 肯定也不能在预览窗口中看到,如下图

那么如何将对应的 html 内容隐藏起来呢?既然我们已经获取到了隐藏部分的 markdown,那么我们可以先把这部分转换成 html

typescript 复制代码
// 不可见那部分 markdown
const invisibleMarkdown = editor.state.sliceDoc(0, editor.documentTop)
// 不可见部分的 html
const invisibleHtml = marked.parse(invisibleMarkdown)

接着把这部分转换成 dom 元素

typescript 复制代码
// 匹配的元素
const matchHtmlTags = 'p, h1, h2, h3, h4, h5, h6, ul, ol, li, pre, blockquote, hr, table, tr, iframe, span'

// 不可见那部分 markdown
const invisibleMarkdown = editor.state.sliceDoc(0, editor.documentTop)
// 不可见部分的 html,如果你使用其他 markdown 解析工具也是同样的
const invisibleHtml = marked.parse(invisibleMarkdown)
// 将不可见的的 html 转换为 dom 对象, 是一个从 <html> 标签开始的 dom 对象
const invisibleDomAll = new DOMParser().parseFromString(invisibleHtml, 'text/html')
// 从这个 dom 对象下, 寻找一系列标签, 获取这些标签集合
const editorDoms = invisibleDomAll.body.querySelectorAll(matchHtmlTags)

通过上面的代码,已经获取到隐藏起来的 html 的 dom 元素有多少个editorDoms.length,那么这个长度,也就是需要隐藏起来的 dom 元素的长度了。那接下来我们只要定位到预览窗口中,这个位置下的 dom 元素,就可以轻松的将滚动条移动到该位置。

typescript 复制代码
// 匹配的元素
const matchHtmlTags = 'p, h1, h2, h3, h4, h5, h6, ul, ol, li, pre, blockquote, hr, table, tr, iframe, span'

// 不可见那部分 markdown
const invisibleMarkdown = editor.state.sliceDoc(0, editor.documentTop)
// 不可见部分的 html,如果你使用其他 markdown 解析工具也是同样的
const invisibleHtml = marked.parse(invisibleMarkdown)
// 将不可见的的 html 转换为 dom 对象, 是一个从 <html> 标签开始的 dom 对象
const invisibleDomAll = new DOMParser().parseFromString(invisibleHtml, 'text/html')
// 从这个 dom 对象下, 寻找一系列标签, 获取这些标签集合
const editorDoms = invisibleDomAll.body.querySelectorAll(matchHtmlTags)

// 从预览页面下同样寻找相同的元素
const previewDoms = this._preview.querySelectorAll(matchHtmlTags)
// dom 集合中,下标位置 与 不可见的 dom 元素的总数相同的,就是应该在预览窗口中显示的那个内容
const tagetDom = previewDoms[editorDoms.length]
tagetDom.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" })

最终效果如下

到此,一个双屏预览功能就完成啦,更完善的代码你都可以在源码地址中看到。

相关推荐
奇舞精选2 分钟前
Vercel AI SDK:构建现代 Web AI 应用指南
前端·aigc
神仙别闹1 小时前
基于C语言实现B树存储的图书管理系统
c语言·前端·b树
玄魂1 小时前
如何查看、生成 github 开源项目star 图表
前端·开源·echarts
前端一小卒2 小时前
一个看似“送分”的需求为何翻车?——前端状态机实战指南
前端·javascript·面试
syt_10132 小时前
Object.defineProperty和Proxy实现拦截的区别
开发语言·前端·javascript
遝靑2 小时前
Flutter 跨端开发进阶:可复用自定义组件封装与多端适配实战(移动端 + Web + 桌面端)
前端·flutter
cypking2 小时前
Web前端移动端开发常见问题及解决方案(完整版)
前端
老前端的功夫2 小时前
Vue 3 vs Vue 2 深度解析:从架构革新到开发体验全面升级
前端·vue.js·架构
栀秋6662 小时前
深入浅出链表操作:从Dummy节点到快慢指针的实战精要
前端·javascript·算法
狗哥哥3 小时前
Vue 3 动态菜单渲染优化实战:从白屏到“零延迟”体验
前端·vue.js