11

Since I have been using the markdown editor to write technical articles, I am very sensitive to the writing experience. I found that markdown editors in major communities basically have synchronous scrolling functions. It's just that some do it well, and some do it so-so. Out of curiosity, I decided to implement this feature myself.

After thinking about it for a while, I finally came up with three solutions:

  1. percent scroll
  2. Simultaneous rendering of elements that occupy a large area on two screens
  3. The elements of each row are assigned an index, and the scroll height of each row is precisely synchronized according to the index

percent scroll

Assuming that screen a is being scrolled, the calculation method of the scroll percentage of screen a is: a 屏的滚动高度 / a 屏的内容总高度 , which is represented by code a.scrollTop / a.scrollHeight . When scrolling screen a, you need to manually synchronize the scroll height of screen b, that is, calculate the scroll height of screen b according to the scroll percentage of screen a:

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

The principle is so simple, but unfortunately the effect is not very good.

在这里插入图片描述

As can be seen from the above animation, when I stop at the second title, the content of the left and right dual screens is synchronized. But when I scroll to the third large title, the content height difference between the left and right dual screens is almost 300 pixels. Therefore, this program is barely usable, and it is better than nothing.

Simultaneous rendering of elements that occupy a large area on two screens

The content height of the dual-screen content is inconsistent because the height of the same element in markdown after rendering is different from that before rendering. For example, for a picture, a line of code is written in markdown, but the rendered pictures are large or small, with heights of tens or hundreds of pixels. If the image code of markdown is rendered on two screens at the same time, this problem can be solved.

在这里插入图片描述

However, in addition to the pictures, there are still many elements with different heights before and after rendering, although they are not as exaggerated as the pictures. For example, h1 h2, when the content of the article is longer, the problems caused by this small difference will become larger and larger, resulting in a larger and larger gap in the height of the dual-screen content. So this solution is not very reliable.

The elements of each row are assigned an index, and the scroll height of each row can be accurately synchronized according to the index

The previous two programs are barely usable and not good enough. Now this third solution is much stronger than the first two, and can almost precisely synchronize the content of each line. How to do it?

The first step is to monitor the content changes of the markdown edit box, and assign an index to each element, except for empty lines and empty texts.

在这里插入图片描述

When passing the HTML of the edit box to the right box for rendering, you need to assign data-index to the rendered element. This allows you to pinpoint the same element before and after rendering with data-index .

在这里插入图片描述

The second step is to calculate the scroll height of the element with the same index on screen b according to the scroll height of the element on screen a

When the a screen is scrolled, it is necessary to traverse all the elements of the a screen from top to bottom, and find the first element in the screen. 找到第一个在屏幕内的元素 This sentence means that during the scrolling process, some elements will run out of the screen due to scrolling (originally in the screen, scrolling to the outside of the screen), we do not need to calculate these elements.

Check if an element is on screen:

 // dom 是否在屏幕内
function isInScreen(dom) {
    const { top, bottom } = dom.getBoundingClientRect()
    return bottom >= 0 && top < window.innerHeight
}

In addition to judging whether the element is on the screen, it is also necessary to judge the percentage of the height of the element in the screen . For example, the markdown string of an image, due to scrolling, causes half of it to be inside the screen and half to be outside the screen. For accurate synchronization, the rendered image must also be half on-screen and half off-screen.

在这里插入图片描述
Calculate the percentage of the element on the screen code:

 // dom 在当前屏幕展示内容的百分比
function percentOfdomInScreen(dom) {
    // 已经通过另一个函数 isInScreen() 确定了这个 dom 在屏幕内,所以只需要计算它在屏幕内的百分比,而不需要考虑它是否在屏幕外
    const { height, bottom } = dom.getBoundingClientRect()
    if (bottom <= 0) return 0 // 不在屏幕内
    if (bottom >= height) return 1 // 完全在屏幕内
    return bottom / height // 部分在屏幕内
}

Now we can traverse all elements of screen a from top to bottom and find the first element in the screen:

 // 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
    }
}

在这里插入图片描述

From the animation point of view, the precise synchronization of the row content has been achieved.

Step on the pit

Some elements will become nested elements after rendering, such as table table, the content level after rendering is:

 <table>
    <tbody>
        <tr>
            <td></td>
        </tr>
    </tbody>
</table>

According to the current rendering logic, if I write a table:

 |1|b|
...

Then |1|b| on data-index will correspond to table .

在这里插入图片描述

在这里插入图片描述

Then there will be a bug, when |1|b| rolls to 50%, the whole table also rolls to 50%. This phenomenon is shown in the following figure:

在这里插入图片描述

This is not the same effect we want. The content of a line has not been scrolled on screen a, and the entire content of screen b has been scrolled to half.

So for nested elements like this, when you hit the data-index tag, hit it on the real content. Using the table as an example, you have to put the mark of data-index tr .

在这里插入图片描述

This way, synchronous scrolling works fine. The same goes for other nested elements (such as ul ol).

在这里插入图片描述

Summarize

I have put the complete code on github:

There is also an online DEMO:

If the online DEMO is relatively slow, you can directly open the html file for access after cloning the project.


谭光志
6.9k 声望13.1k 粉丝