Pure Javascript controls multiple scroll bars to scroll synchronously

created at 09-28-2021 views: 2

Introduction

Some websites that support writing articles in markdown generally support markdown instant preview, that is, the entire page is divided into two parts, the left half is the markdown text you input, and the right half is the corresponding preview page in real time.

This article does not explain how to achieve this effect from 0. Apart from others, just look at the left and right container elements in the main body of the page, namely the markdown input box element and the preview display box element.

This article will discuss how to make the other element scroll when one of the container elements scrolls when the content of the two container elements exceeds the height of the container, that is, when a scroll box appears.

DOM structure

Since it is related to the scroll bar, first think of an attribute in js that controls the height of the scroll bar: scrollTop, as long as you can control the value of this attribute, you can naturally control the scrolling of the scroll bar.

For the following DOM structure:

<div id="container">
  <div class="left"></div>
  <div class="right"></div>
</div>

Among them, the .left element is the left half of the input box container element, the .right element is the right half of the display box container element, and .container is their parent element.

Due to the need for overflow scrolling, you also need to set the corresponding style:

#container {
  display: flex;
  border: 1px solid #bbb;
}
.left, .right {
  flex: 1;
  height: 100%;
  word-wrap: break-word;
  overflow-y: scroll;
}

Then stuff enough content into the .left and .right elements to make scroll bars appear on both, which is the following effect:

content

The style is a rough idea, and then you can perform a series of operations on these DOMs.

First try

The general idea is to listen to the scroll events of two container elements. When one element is scrolling, get the value of the scrollTop property of this element, and set this value to the scrollTop value of the other scroll element.

E.g:

var l=document.querySelector('.left')
var r=document.querySelector('.right')
l.addEventListener('scroll',function(){
  r.scrollTop = l.scrollTop
})

result:

First try

It seems very good, but now I not only want the right to follow the left scroll, but also the left to follow the right scroll, so add the following code:

l.addEventListener('scroll',function(){
  r.scrollTop = l.scrollTop
})

It looks very good, however, there is no such simple thing.

At this time, when you use the mouse wheel to scroll, you find that the scrolling is a bit difficult. The scrolling of the two container elements seems to be blocked by something, making it difficult to scroll.

After careful analysis, the reason is very simple. When you scroll on the left, the scroll event on the left is triggered, so the right follows the scroll, but at the same time, the follow scroll on the right is also scroll, so the right scroll is also triggered, so the left must also Follow the scroll to the right... and then you enter a situation similar to mutual triggering, so you will find that scrolling is very difficult.

Solve the problem that the scroll event is triggered at the same time

To solve the above-mentioned problems, there are the following two solutions for the time being.

Replace the scroll event with a mousewheel event

Since the scroll event is not only triggered by the mouse's active scrolling, but also by changing the scrollTop of the container element. The active scrolling of the element is actually triggered by the mouse wheel, so you can replace the scroll event with one that is sensitive to mouse scrolling instead of element scrolling. Event: mousewheel, so the above monitoring code becomes:

l.addEventListener('mousewheel',function(){
    r.scrollTop = l.scrollTop
})
r.addEventListener('mousewheel',function(){
    l.scrollTop = r.scrollTop
})

The result is as follows:

mousewheel event

It seems to be useful, but there are actually two problems.

1. When scrolling one of the container elements, although the other container element is also scrolling, the scrolling is not smooth, and the height has obvious instant bounces.
I googled for a while, but did not find any content related to the scroll frequency of the wheel event. I speculate that this may be a feature of this event.

Every time the mouse scrolls, it is basically not in 1px unit. Its smallest unit is much smaller than the scroll event. I use my mouse to scroll on the chrome browser, and the distance it passes each time is exactly 100px, which is different. The value of your mouse or browser should be different.

If your mouse is of better quality and the gears are finer, it should be less than 100px, and the beating will not be that big. My mouse is a computer provided by the company, and its function is limited to its use, so the gear scale is more Big.

The wheel event actually monitors the event that the mouse wheel rolls over a gear stuck point, which can explain why the bounce occurs.

scrollbar

Generally speaking, every time the mouse wheel rolls over a gear stuck point, a wheel event can be monitored. From the beginning to the end, the element actively scrolled by the mouse has been scrolled by 100px, so another container element that follows the scroll also jumps instantly. Up to 100px

The reason why the above scroll event does not make the following scroll element bounce instantaneously is because each time the scrollTop changes the following scroll element, its value will not have a span as large as 100px, and may not be as small as 1px, but because of its The trigger frequency is high, the scroll span is small, and at least visually, it scrolls smoothly.

If you want the scroll box on the right to scroll smoothly, it is also possible. Whenever a wheel event is monitored, regardless of whether it is 100px or 50px worse than the last time, always let the right side Follow the scroll box to scroll according to 10px (or a slightly larger or smaller span, as long as the visual feeling is smooth scrolling and the delay is not too large) to scroll, scroll 10 times continuously, that is 100px, the same Can reach the exact location, such as the following code:

function scrollToY(rightELe, toY, step = 10) {
    let diff = rightELe.scrollTop - toY
    let realStep = diff > 0 ? -step : step
    if(Math.abs(diff) > step) {
        rightELe.scrollTop = rightELe.scrollTop + realStep
        requestAnimationFrame(()=>{
            scrollToY(rightELe, toY, step)
        })
    } else {
        rightELe.scrollTop = toY
    }
}

2. The wheel only monitors the mouse wheel event, but if the scroll bar is dragged with the mouse, this event will not be triggered, and other container elements will not follow the scrolling.
This is actually very easy to solve. Dragging the scroll bar with the mouse will definitely trigger the scroll event, and in this case, you must be able to easily determine which container element the dragged scroll bar belongs to. The scroll event of this container needs to be processed, and the other scroll event that follows the scroll container does not need to be processed.

Compatibility issues with wheel events

The wheel event is a standard event of DOM Level3, but in addition to this event, there are many non-standard events. Different browser kernels use different standards, so it may need to be compatible according to the situation. For details, see MDN MouseWheelEvent.

eventobjectstandard or not compatibility
mousewheelMouseWheelEventnonot FirFox
DOMMouseScrollMouseScrollEventnonot FirFox
wheelWheelEventDOM level3FirFox 17+; IE9+

Real-time determination

If you can't stand the bouncing of the wheel and all kinds of compatibility, then there is actually another way to go, it is still the scroll event, but some extra work is required.

The problem with the scroll event is that there is no judgment about which container element is currently actively scrolling. As long as the container element that is actively scrolling is determined, this is easy to do. For example, in the above-mentioned wheel event, the reason why the scroll bar can be dragged with the mouse The scroll event is used because it is easy to determine which one of the currently actively scrolling container elements is.

Therefore, the key to the problem is how to determine the current actively scrolling container element. As long as this problem is solved, the rest will be easy to handle.

Whether it is scrolling by the mouse wheel or dragging the scroll bar on the scroll bar by the mouse, the scroll event will be triggered, and at this time, on the Z axis of the coordinate system, the coordinates of the mouse must be located within the area occupied by the scroll container element , That is to say, on the Z axis, the mouse must be hovering or above the scrolling container element.

When the mouse moves on the screen, the current coordinates of the mouse can be obtained.

Real-time determination

In this way, clientX and clientY are the coordinates of the current mouse relative to the viewport. It can be considered that as long as this coordinate is within the range of a scroll container, the container element is considered to be an active scrolling container element, and the coordinate range of the element can be performed using getBoundingClientRect Obtain.

The following is the sample code when the mouse is moved to the .left element:

if (e.clientX>l.left && e.clientX<l.right && e.clientY>l.top) {
     // Enter the .left element
}

This is indeed possible, but considering that the two scrolling container elements occupy almost the entire screen area, the area that mousemove needs to monitor is a bit large, and may require higher performance, so it can actually be replaced with mouseover events, and you only need to monitor the mouse. It does not matter if you have entered a certain scroll container element, and the above coordinate judgment is also omitted.

l.addEventListener('mouseover',function(){
  // Enter the .left scroll container element
})

When the container element that the mouse actively scrolls is determined, only the scroll event of this container needs to be processed, and the other scroll event that follows the scroll container does not need to be processed.

final result

Well, the effect is very good, the performance is also very good, perfect, you can call it a day~

No! It's not that simple!

Scroll proportionally

The above examples are all effects when the content heights of the two scrolling container elements are exactly the same. What if the content heights of the two scrolling container elements are different?

That is the following effect:

Scroll proportionally

It can be seen that due to the different content heights of the two scroll container elements, the largest scrollTop is also different. When one element with a smaller scrollTop value is rolled to the bottom, the other element still stays in half, or when one of the scrollTop elements is rolled to the bottom. When the element with the larger value is only half-rolled, the other element is already rolled to the bottom.

This situation is very common. For example, when you write in markdown, the height occupied by a first-level title tag # in the edit mode is generally smaller than the height occupied by the preview mode, so that the scroll heights on the left and right sides are inconsistent. .

Therefore, if this situation is also taken into consideration, then it is not as simple as setting the scrollTop value for the two scroll container elements to each other.

Although the height of the content of the scroll container cannot be fixed, one thing can be determined. The maximum scroll height of the scroll bar, or the value of scrollTop, must have a certain relationship with the height of the scroll container content and the height of the scroll container itself.

Since you need to know the height of the content of the scrolling container, there is also a scroll bar, so you need to add a child element to this container element. The height of the child element is not limited, that is, the height of the scrolling container content. The container height is fixed, and the overflow scroll is enough.

<div id="container">
  <div class="left">
     <div class="child"></div>
  </div>
  <div class="right">
      <div class="child"></div>
  </div>
</div>

The structure example is as follows:

Through my observations, deductions and practical verification, the relationship between them has been determined. It is very simple, that is, the most basic addition and subtraction operations:

The maximum scroll height of the scroll bar (scrollTopMax) = the height of the scroll container content (that is, the height of the child element ch)-the height of the scroll container itself (that is, the height of the container element ph)

equation

In other words, if the height of the content of the scroll container (that is, the height of the child element ch) and the height of the scroll container itself (that is, the height of the container element ph) have been determined, then the maximum scroll height of the scroll bar (scrollTop) must be determined, and These two height values are basically available, so you can get scrollTop

Therefore, if you want to make the two scroll element containers scroll up and down in equal proportion, that is, one element rolls to the head or rolls to the bottom, and the other element can also roll to the head and roll to the bottom. Then you only need to get the scrollTop between the two scrolling container elements. The scale of the maximum will do.

equation

After determining the scale, when scrolling in real time, you only need to get the scrollTop1 of the actively scrolling container element, and you can get the scrollTop2 corresponding to another container element that follows the scroll:

equation

The idea is clear, writing code is very easy, and the effect is as follows:

final result

summary

The above requirements have basically been fulfilled. In practice, certain modifications may need to be made according to the actual situation. For example, if you write an online editing and preview page of markdown, you need to update the scale value in real time according to the height of the input content, but The main body has been settled, and minor revisions are not difficult.

In addition, the article described in this article is not only for the following scrolling of the two scrolling container elements, but also can be expanded. More follow scrolling between elements can be implemented according to the ideas of this article. This article is only for the convenience of explanation and specific Two elements on.

Please log in to leave a comment.