Hidden scroll bar - > El scroll bar

What is El scrollbar?

Element UI, as a set of well-known UI component library of Vue, is almost known by people playing Vue. Recently, when looking at the source code of element, I found an interesting phenomenon. How can I add an EL scrollbar component to autocomplete recommendations?
After some understanding, it turned out to be a scroll bar component written by Element itself (but not published publicly). It shielded the original scroll bar and used a unified style to replace it, which solved the compatibility problem of the scroll bar.

How to use it?

For the usage of El scrollbar, you can Look at the issues on Github , here is a simple demonstration: fill in a list in the default slot of El scrollbar, and set the height of the outermost package element, so that the scroll bar can be generated smoothly.

    // The tag attribute here can be ignored first. It is used to control the specific type of the generated view element
  <el-scrollbar style="width: 150px; height: 50px" tag="ul">

The effect is as follows:

How to achieve it?

First, let's look at the DOM rendered by the code just now:

As you can see, our li is wrapped in. El scrollbar - >&__ wrap -> .&__ In view, there are two DOM's:. Is horizontal and. Is vertical. Each element has its own function:

<div class="el-scrollbar"> //Root element, wrap all elements
  <div class="el-scrollbar__wrap"> // wrap element, a visual viewport element, represents the final window size displayed by the element
    <ul class="el-scrollbar__view"> // Layout viewport elements, which represent the entire list (and their width and height), display different view contents by adjusting scroll top / left of wrap
      // The contents of the default slot will be put here
  <div class="el-scrollbar__bar is-horizontal">...</div> //Horizontal scroll bar
  <div class="el-scrollbar__bar is-vertical">...</div> // Vertical scroll bar

Hide old scroll bar

After understanding the concepts of wrap/view/bar, let's go straight to the source code: element/packages/scrollbar/src/main.js This file is the entry file of the scrollbar component, which defines some props accepted by / components/data /, and the most important: render function. When the render function is called, the scrollbarWidth function is called first:

let gutter = scrollbarWidth();

This gutter means the width of the current browser's scroll bar. element obtains this width through the scrollbarWidth method. Click this method to see that it has done three things:

  1. Create an outer element, set the width, and get the offset width at this time
  2. Set the outer element overflow to visible, create an inner element, append it to the outer (a scroll bar will be generated at this time), and get the offset width of the inner.
  3. The subtraction of the two is the width of the scroll bar
/* eslint-disable no-debugger */
import Vue from 'vue';

let scrollBarWidth;

export default function() {
  if (Vue.prototype.$isServer) return 0;
  if (scrollBarWidth !== undefined) return scrollBarWidth;

  // Create the outer div, which is a normal dom
  const outer = document.createElement('div');
  outer.className = 'el-scrollbar__wrap';
  outer.style.visibility = 'hidden';
  outer.style.width = '100px';
  outer.style.position = 'absolute';
  outer.style.top = '-9999px';

  // Get the actual width of this dom
  const widthNoScroll = outer.offsetWidth;
  // Modify the css of the outer dom to overflow: scroll
  outer.style.overflow = 'scroll';
    // Create the inner div and append it to the outer
  const inner = document.createElement('div');
  inner.style.width = '100%';
  // Calculate the actual width of the inner div
  const widthWithScroll = inner.offsetWidth;
  // Calculate the specific width of the scroll bar by subtracting the width with scroll bar from the width without scroll bar
  scrollBarWidth = widthNoScroll - widthWithScroll;

  return scrollBarWidth;

The main purpose of getting the scroll bar is to hide it, which is what the render function does next.

const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;

// Add gutterStyle according to different types of the incoming wrapStyle
if (Array.isArray(this.wrapStyle)) {
  style = toObject(this.wrapStyle);
    style.marginRight = style.marginBottom = gutterWith;
  } else if (typeof this.wrapStyle === 'string') {
    style += gutterStyle;
  } else {
    style = gutterStyle;

Create DOM

The next step is the DOM creation process. The view/wrap (listening for its scrolling events) and the root element of the non native / native version are created successively. If you pass in native: true, it means using the native scrollbar version of scrollbar.

    if (!this.native) {
      nodes = ([
          move={ this.moveX }
          size={ this.sizeWidth }></Bar>,
          move={ this.moveY }
          size={ this.sizeHeight }></Bar>
    } else {
      nodes = ([
          class={ [this.wrapClass, 'el-scrollbar__wrap'] }
          style={ style }>
          { [view] }

As the wrap window scrolls, the handleScroll method is executed, updating the moveY and moveX properties in data. These two will be passed into the scroll Bar component Bar, and its translateY()/translateX(), Bar component will be updated as we will see later.

mount/beforeDestroy hook

One more thing I did when I mounted the view element was to add a listener for the resize event (to cancel listening before destroy):

!this.noresize && addResizeListener(this.$refs.resize, this.update);

It is worth noting that addResizeListener is not simply set window.resize Callback, but a new ship api is used to monitor the resize of DOM elements: ResizeObserver API (see the introduction here for details) . In general, ResizeObserver can directly bind events to DOM, which is specially used to observe whether the size of DOM elements has changed and reduce window.resize Extra monitoring.
In order to listen to multiple resize events for an element, element also uses the observer pattern and binds a DOM element__ resizeListeners__ Array, when a resize event is triggered, execute the entire \_ resizeListeners__ All callbacks to the array.

Once the DOM element is resize d, an update callback is executed. So what did you do during update?

update() {
  let heightPercentage, widthPercentage;
  const wrap = this.wrap;
  if (!wrap) return;
    // Get a new ratio of width to height
  heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);
  widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth);

  this.sizeHeight = (heightPercentage < 100) ? (heightPercentage + '%') : '';
  this.sizeWidth = (widthPercentage < 100) ? (widthPercentage + '%') : '';

The update method is responsible for updating the slider length of Bar (possibly horizontal / vertical scroll Bar). Take the vertical scroll Bar as an example: first, get the ratio of the reset wrap display height to the total height through clientHeight * 100/scrollHeight, which is also the ratio of the scroll slider length, and then pass it to the Bar component representing the scroll Bar to update the scroll Bar height.
At this time, if the scale value is greater than 100, it means that the scroll Bar is no longer needed, then an empty string will be passed to Bar.

Click / drag the scroll bar

At this stage, our scroll bar component has been created, but when we click or drag the scroll bar, how to deal with this component? See also element/packages/scrollbar/src/bar.js This component.
The Bar component is responsible for displaying the scroll Bar. Let's look at its render function directly:

  render(h) {
    // The move property is used to control the scroll position of the scroll bar
    const { size, move, bar } = this;

    return (
        class={ ['el-scrollbar__bar', 'is-' + bar.key] }
        onMousedown={ this.clickTrackHandler } >
          onMousedown={ this.clickThumbHandler }
          style={ renderThumbStyle({ size, move, bar }) }>

We can see that the key points are clickTrackHandler/clickThumbHandler, which are respectively used to control the behavior when the scroll bar container is clicked and when the scroll bar itself is clicked.

clickTrackHandler: quickly jump to a certain interval

clickTrackHandler(e) {
     * 0. Take the vertical scroll bar as an example:
     * this.bar.direction = "top"/this.bar.client = "clientY"/this.bar.offset="offsetHeight"/this.bar.scrollSize="scrollHeight"
     * 1. getBoundingClientRect()[this.bar.direction] Returns the top value of the element (the height value from the browser viewport)
     * 2. Subtract e.clientY from the value of 1, and then Math.abs  Get the relative value, which is the relative offset of the mouse on the scroll bar container.
     * 3. Calculate the thumb half position of the scroll bar slider
     * 4. offset - thumbHalf Get the specific offset and divide by the offset height of the whole bar to get the percentage of the new position of the slider.
     * 5. Next, you can happily update the scrollTop of the wrap element to show the new content
     * 6. wrap After scrolling, the handleScroll method is triggered to update the move value of the Bar component to update the scroll Bar position.
    const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);
    const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
    //  Calculate the proportion in the total height of the scroll bar area based on the offset after clicking, that is, the position of the scroll block
    const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);
    //  Set the new value of scrollHeight or scrollWidth for the shell. Achieve the effect of scrolling content
    this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);

clickThumbHandler: drag the scroll bar slider to update the view

This is mainly to calculate the ratio of the height of the slider and the whole scroll bar when dragging, so as to update the scrollTop value of the wrap element. The specific code is similar to that of clickTrackHandler. Due to the limited space, it will not be described in detail.
There is a small point here. We bind the onMousedown event to the slider element, but mousemove and mouseup are bound to the document. This is because the mouse will move faster than the slider in the process of moving. At this time, the slider element will lose the onMousemove event, so when mousemove is bound, it cannot be bound to the corresponding element.


From the whole life cycle of the scroll bar element, we can see how the element creates a scroll bar, how to monitor the changes of the element, how to control the sliding of the scroll bar, and so on. The reading of the source code is all over here. If there is any mistake or omission, please point it out. If you get something, it's my great honor.

Source code analysis of element UI El scrollbar
ResizeObserver API

Tags: Javascript Vue github Attribute shell

Posted on Wed, 10 Jun 2020 02:13:53 -0400 by raja9911