zoukankan      html  css  js  c++  java
  • 使用 Vue 开发 scrollbar 滚动条组件

    Vue 应该说是很火的一款前端库了,和 React 一样的高热度,今天就来用它写一个轻量的滚动条组件;

    知识储备:要开发滚动条组件,需要知道知识点是如何计算滚动条的大小和位置,还有一个问题是如何监听容器大小的改变,然后更新滚动条的位置;

    先把样式贴出来:

    .disable-selection {
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
    }
    
    .resize-trigger {
      position: absolute;
      display: block;
      top: 0;
      left: 0;
      height: 100%;
      width: 100%;
      overflow: hidden;
      pointer-events: none;
      z-index: -1;
      opacity: 0;
    }
    
    .scrollbar-container {
      position: relative;
      overflow-x: hidden!important;
      overflow-y: hidden!important;
      width: 100%;
      height: 100%;
    }
    
    .scrollbar-container--auto {
      overflow-x: visible!important;
      overflow-y: visible!important;
    }
    
    .scrollbar-container .scrollbar-view {
      width: 100%;
      height: 100%;
      -webkit-overflow-scrolling: touch;
    }
    
    .scrollbar-container .scrollbar-view-x {
      overflow-x: scroll!important;
    }
    
    .scrollbar-container .scrollbar-view-y {
      overflow-y: scroll!important;
    }
    
    .scrollbar-container .scrollbar-vertical,
    .scrollbar-container .scrollbar-horizontal {
      position: absolute;
      opacity: 0;
      cursor: pointer;
      transition: opacity 0.25s linear;
      background: rgba(0, 0, 0, 0.2);
    }
    
    .scrollbar-container .scrollbar-vertical {
      top: 0;
      left: auto;
      right: 0;
      width: 12px;
    }
    
    .scrollbar-container .scrollbar-horizontal {
      top: auto;
      left: 0;
      bottom: 0;
      height: 12px;
    }
    
    .scrollbar-container:hover .scrollbar-vertical,
    .scrollbar-container:hover .scrollbar-horizontal,
    .scrollbar-container .scrollbar-vertical.scrollbar-show,
    .scrollbar-container .scrollbar-horizontal.scrollbar-show {
      opacity: 1;
    }
    
    .scrollbar-container.cssui-scrollbar--s .scrollbar-vertical {
      width: 6px;
    }
    
    .scrollbar-container.cssui-scrollbar--s .scrollbar-horizontal {
      height: 6px;
    }

     然后,把模板贴出来:

    <template>
      <div
        :style="containerStyle"
        :class="containerClass"
        @mouseenter="quietUpdate"
        @mouseleave="quietOff"
      >
        <div
          ref="scroll"
          :style="scrollStyle"
          :class="scrollClass"
          @scroll.stop.prevent="realUpdate"
        >
          <div
            ref="content"
            v-resize="resizeHandle"
          >
            <slot />
          </div>
        </div>
        <div
          v-if="yBarShow"
          :style="yBarStyle"
          :class="yBarClass"
          @mousedown="downVertical"
        />
        <div
          v-if="xBarShow"
          :style="xBarStyle"
          :class="xBarClass"
          @mousedown="downHorizontal"
        />
      </div>
    </template>

    上面的代码中,我用到了 v-resize 这个指令,这个指令就是封装容器大小改变时,向外触发事件的,看到网上有通过 MutationObserver 来监听的,这个问题是监听所有的属性变化,好像还有兼容问题,还有一种方案是用 GitHub 的这个库:resize-observer-polyfill,上面的这些方法都可以,我也是尝试了一下,但我觉得始终是有点小题大做了,不如下面这个方法好,就是创建一个看不见的 object 对象,然后使它的绝对定位,相对于滚动父容器,和滚动条容器的大小保持一致,监听 object 里面 window 对象的 resize 事件,这样就可以做到实时响应高度变化了,贴上代码:

    import Vue from 'vue';
    import { throttle, isFunction } from 'lodash';
    
    Vue.directive('resize', {
      inserted(el, { value: handle }) {
        if (!isFunction(handle)) { return; }
    
        const aimEl = el;
        const resizer = document.createElement('object');
    
        resizer.type = 'text/html';
        resizer.data = 'about:blank';
        resizer.setAttribute('tabindex', '-1');
        resizer.setAttribute('class', 'resize-trigger');
        resizer.onload = () => {
          const win = resizer.contentDocument.defaultView;
          win.addEventListener('resize', throttle(() => {
             const rect = el.getBoundingClientRect();
              handle(rect);
          }, 500));
        };
    
        aimEl.style.position = 'relative';
        aimEl.appendChild(resizer);
        aimEl.resizer = resizer;
      },
    
      unbind(el) {
        const aimEl = el;
    
        if (aimEl.resizer) {
          aimEl.style.position = '';
          aimEl.removeChild(aimEl.resizer);
          delete aimEl.resizer;
        }
      },
    });

    还有用到 tools js中的工具方法:

    if (!Date.now) { Date.now = function () { return new Date().getTime(); }; }
    
    const vendors = ['webkit', 'moz'];
    
    if (!window.requestAnimationFrame) {
      for (let i = 0; i < vendors.length; ++i) {
        const vp = vendors[i];
        window.requestAnimationFrame = window[`${vp}RequestAnimationFrame`];
        window.cancelAnimationFrame = (window[`${vp}CancelAnimationFrame`] || window[`${vp}CancelRequestAnimationFrame`]);
      }
    }
    
    if (!window.requestAnimationFrame || !window.cancelAnimationFrame) {
      let lastTime = 0;
    
      window.requestAnimationFrame = callback => {
        const now = Date.now();
        const nextTime = Math.max(lastTime + 16, now);
        return setTimeout(() => { callback(lastTime = nextTime); }, nextTime - now);
      };
    
      window.cancelAnimationFrame = clearTimeout;
    }
    
    let scrollWidth = 0;
    
    // requestAnimationFrame 封装
    export const ref = (fn) => { window.requestAnimationFrame(fn); };
    
    // 检测 class
    export const hasClass = (el = null, cls = '') => {
      if (!el || !cls) { return false; }
      if (cls.indexOf(' ') !== -1) { throw new Error('className should not contain space.'); }
      if (el.classList) { return el.classList.contains(cls); }
      return ` ${el.className} `.indexOf(` ${cls} `) > -1;
    };
    
    // 添加 class
    export const addClass = (element = null, cls = '') => {
      const el = element;
      if (!el) { return; }
      let curClass = el.className;
      const classes = cls.split(' ');
    
      for (let i = 0, j = classes.length; i < j; i += 1) {
        const clsName = classes[i];
        if (!clsName) { continue; }
    
        if (el.classList) {
          el.classList.add(clsName);
        } else if (!hasClass(el, clsName)) {
          curClass += ' ' + clsName;
        }
      }
      if (!el.classList) {
        el.className = curClass;
      }
    };
    
    // 获取滚动条宽度
    export const getScrollWidth = () => {
      if (scrollWidth > 0) { return scrollWidth; }
    
      const block = docu.createElement('div');
      block.style.cssText = 'position:absolute;top:-1000px;100px;height:100px;overflow-y:scroll;';
      body.appendChild(block);
      const { clientWidth, offsetWidth } = block;
      body.removeChild(block);
      scrollWidth = offsetWidth - clientWidth;
    
      return scrollWidth;
    };

    下面是 js 功能的部分,代码还是不少,有一些方法做了节流处理,用了一些 lodash 的方法,主要还是上面提到的滚动条计算的原理,大小的计算,具体看  toUpdate  这个方法,位置的计算,主要是 horizontalHandler,verticalHandler,实际滚动距离的计算,看mouseMoveHandler 这个方法:

    import { raf, addClass, removeClass, getScrollWidth } from 'src/tools';
    
    const SCROLLBARSIZE = getScrollWidth();
    
    /**
     * ----------------------------------------------------------------------------------
     * UiScrollBar Component
     * ----------------------------------------------------------------------------------
     *
     * @author zhangmao
     * @change 2019/4/15
     */
    export default {
      name: 'UiScrollBar',
    
      props: {
        size: { type: String, default: 'normal' }, // small
        // 主要是为了解决在 dropdown 隐藏的情况下无法获取当前容器的真实 width height 的问题
        show: { type: Boolean, default: false },
         { type: Number, default: 0 },
        height: { type: Number, default: 0 },
        maxWidth: { type: Number, default: 0 },
        maxHeight: { type: Number, default: 0 },
      },
    
      data() {
        return {
          enter: false,
          yRatio: 0,
          xRatio: 0,
          lastPageY: 0,
          lastPageX: 0,
          realWidth: 0,
          realHeight: 0,
          yBarTop: 0,
          yBarHeight: 0,
          xBarLeft: 0,
          xBarWidth: 0,
          scrollWidth: 0,
          scrollHeight: 0,
          containerWidth: 0,
          containerHeight: 0,
          cursorDown: false,
        };
      },
    
      computed: {
        xLimit() { return this.width > 0 || this.maxWidth > 0; },
        yLimit() { return this.height > 0 || this.maxHeight > 0; },
        yBarShow() { return this.getYBarShow(); },
        xBarShow() { return this.getXBarShow(); },
        yBarStyle() { return { top: `${this.yBarTop}%`, height: `${this.yBarHeight}%` }; },
        yBarClass() { return ['scrollbar-vertical', { 'scrollbar-show': this.cursorDown }]; },
        xBarStyle() { return { left: `${this.xBarLeft}%`,  `${this.xBarWidth}%` }; },
        xBarClass() { return ['scrollbar-horizontal', { 'scrollbar-show': this.cursorDown }]; },
        scrollClass() {
          return ['scrollbar-view', {
            'scrollbar-view-x': this.xBarShow,
            'scrollbar-view-y': this.yBarShow,
          }];
        },
        scrollStyle() {
          const hasWidth = this.yBarShow && this.scrollWidth > 0;
          const hasHeight = this.xBarShow && this.scrollHeight > 0;
          return {
             hasWidth ? `${this.scrollWidth}px` : '',
            height: hasHeight ? `${this.scrollHeight}px` : '',
          };
        },
        containerClass() {
          return ['scrollbar-container', {
            'cssui-scrollbar--s': this.size === 'small',
            'scrollbar-container--auto': !this.xBarShow && !this.yBarShow,
          }];
        },
        containerStyle() {
          const showSize = this.xBarShow || this.yBarShow;
          const styleObj = {};
    
          if (showSize) {
            if (this.containerWidth > 0) { styleObj.width = `${this.containerWidth}px`; }
            if (this.containerHeight > 0) { styleObj.height = `${this.containerHeight}px`; }
          }
    
          return styleObj;
        },
      },
    
      watch: {
        show: 'showChange',
         'initail',
        height: 'initail',
        maxWidth: 'initail',
        maxHeight: 'initail',
      },
    
      created() {
        this.dftData();
        this.initEmiter();
      },
    
      mounted() { this.$nextTick(this.initail); },
    
      methods: {
    
        // ------------------------------------------------------------------------------
    
        // 外部调用方法
        refresh() { this.initail(); }, // 手动更新滚动条
        scrollX(x) { this.$refs.scroll.scrollLeft = x; },
        scrollY(y) { this.$refs.scroll.scrollTop = y; },
        scrollTop() { this.$refs.scroll.scrollTop = 0; },
        getScrollEl() { return this.$refs.scroll; },
        scrollBottom() { this.$refs.scroll.scrollTop = this.$refs.content.offsetHeight; },
    
        // --------------------------------------------------------------------------
    
        quietOff() { this.enter = false; },
    
        // ------------------------------------------------------------------------------
    
        quietUpdate() {
          this.enter = true;
          this.scrollUpdate();
        },
    
        // ------------------------------------------------------------------------------
    
        realUpdate() {
          this.quietOff();
          this.scrollUpdate();
        },
    
        // ------------------------------------------------------------------------------
    
        resizeHandle() { this.initail(); },
    
        // ------------------------------------------------------------------------------
    
        // 默认隐藏 异步展示的情况
        showChange(val) { if (val) { this.initail(); } },
    
        // ------------------------------------------------------------------------------
    
        // 组件渲染成功后的入口
        initail() {
          this.setContainerSize();
          this.setScrollSize();
          this.setContentSize();
          this.realUpdate();
        },
    
        // ------------------------------------------------------------------------------
    
        // 设置整个容器的大小
        setContainerSize() {
          this.setContainerXSize();
          this.setContainerYSize();
        },
    
        // ------------------------------------------------------------------------------
    
        // 设置滚动容器的大小
        setScrollSize() {
          this.scrollWidth = this.containerWidth + SCROLLBARSIZE;
          this.scrollHeight = this.containerHeight + SCROLLBARSIZE;
        },
    
        // ------------------------------------------------------------------------------
    
        // 设置内容区域的大小
        setContentSize() {
          const realElement = this.$refs.content.firstChild;
    
          if (realElement) {
            const { offsetWidth = 0, offsetHeight = 0 } = realElement;
    
            this.realWidth = this.lodash.round(offsetWidth);
            this.realHeight = this.lodash.round(offsetHeight);
          }
        },
    
        // ------------------------------------------------------------------------------
    
        setContainerXSize() {
          if (this.xLimit) {
            this.containerWidth = this.width || this.maxWidth;
            return;
          }
          if (this.yLimit) { this.containerWidth = this.lodash.round(this.$el.offsetWidth); }
        },
    
        // ------------------------------------------------------------------------------
    
        setContainerYSize() {
          if (this.yLimit) {
            this.containerHeight = this.height || this.maxHeight;
            return;
          }
          if (this.xLimit) { this.containerHeight = this.lodash.round(this.$el.offsetHeight); }
        },
    
        // ------------------------------------------------------------------------------
    
        downVertical(e) {
          this.lastPageY = e.pageY;
          this.cursorDown = true;
          addClass(document.body, 'disable-selection');
          document.addEventListener('mousemove', this.moveVertical, false);
          document.addEventListener('mouseup', this.upVertical, false);
          document.onselectstart = () => false;
          return false;
        },
    
        // ------------------------------------------------------------------------------
    
        downHorizontal(e) {
          this.lastPageX = e.pageX;
          this.cursorDown = true;
          addClass(document.body, 'disable-selection');
          document.addEventListener('mousemove', this.moveHorizontal, false);
          document.addEventListener('mouseup', this.upHorizontal, false);
          document.onselectstart = () => false;
          return false;
        },
    
        // ------------------------------------------------------------------------------
    
        moveVertical(e) {
          const delta = e.pageY - this.lastPageY;
          this.lastPageY = e.pageY;
    
          raf(() => { this.$refs.scroll.scrollTop += delta / this.yRatio; });
        },
    
        // ------------------------------------------------------------------------------
    
        moveHorizontal(e) {
          const delta = e.pageX - this.lastPageX;
          this.lastPageX = e.pageX;
    
          raf(() => { this.$refs.scroll.scrollLeft += delta / this.xRatio; });
        },
    
        // ------------------------------------------------------------------------------
    
        upVertical() {
          this.cursorDown = false;
          removeClass(document.body, 'disable-selection');
          document.removeEventListener('mousemove', this.moveVertical);
          document.removeEventListener('mouseup', this.upVertical);
          document.onselectstart = null;
        },
    
        // ------------------------------------------------------------------------------
    
        upHorizontal() {
          this.cursorDown = false;
          removeClass(document.body, 'disable-selection');
          document.removeEventListener('mousemove', this.moveHorizontal);
          document.removeEventListener('mouseup', this.upHorizontal);
          document.onselectstart = null;
        },
    
        // ------------------------------------------------------------------------------
    
        scrollUpdate() {
          const {
            clientWidth = 0,
            scrollWidth = 0,
            clientHeight = 0,
            scrollHeight = 0,
          } = this.$refs.scroll;
    
          this.yRatio = clientHeight / scrollHeight;
          this.xRatio = clientWidth / scrollWidth;
    
          raf(() => {
            if (this.yBarShow) {
              this.yBarHeight = Math.max(this.yRatio * 100, 1);
              this.yBarTop = this.lodash.round((this.$refs.scroll.scrollTop / scrollHeight) * 100, 2);
    
              // 只更新不触发事件
              if (this.enter) { return; }
    
              const top = this.$refs.scroll.scrollTop;
              const left = this.$refs.scroll.scrollLeft;
              const cHeight = this.$refs.scroll.clientHeight;
              const sHeight = this.$refs.scroll.scrollHeight;
    
              // trigger event
              this.debounceScroll({ top, left });
              if (top === 0) {
                this.debounceTop();
              } else if (top + cHeight === sHeight) {
                this.debounceBottom();
              }
            }
    
            if (this.xBarShow) {
              this.xBarWidth = Math.max(this.xRatio * 100, 1);
              this.xBarLeft = this.lodash.round((this.$refs.scroll.scrollLeft / scrollWidth) * 100, 2);
    
              // 只更新不触发事件
              if (this.enter) { return; }
    
              const top = this.$refs.scroll.scrollTop;
              const left = this.$refs.scroll.scrollLeft;
              const cWidth = this.$refs.scroll.clientWidth;
              const sWidth = this.$refs.scroll.scrollWidth;
    
              // trigger event
              this.debounceScroll({ top, left });
              if (left === 0) {
                this.debounceLeft();
              } else if (left + cWidth === sWidth) {
                this.debounceRight();
              }
            }
          });
        },
    
        // ------------------------------------------------------------------------------
    
        dftData() {
          this.debounceLeft = null;
          this.debounceRight = null;
          this.debounceTop = null;
          this.debounceBottom = null;
          this.debounceScroll = null;
        },
    
        // ------------------------------------------------------------------------------
    
        // 初始化触发事件
        initEmiter() {
          this.turnOn('winResize', this.initail);
          this.debounceTop = this.lodash.debounce(() => this.$emit('top'), 500);
          this.debounceLeft = this.lodash.debounce(() => this.$emit('left'), 500);
          this.debounceRight = this.lodash.debounce(() => this.$emit('right'), 500);
          this.debounceBottom = this.lodash.debounce(() => this.$emit('bottom'), 500);
          this.debounceScroll = this.lodash.debounce(obj => this.$emit('scroll', obj), 250);
        },
    
        // ------------------------------------------------------------------------------
    
        // 是否展示垂直的滚动条
        getYBarShow() {
          if (this.yLimit) {
            if (this.height > 0) { return this.realHeight > this.height; }
            if (this.maxHeight > 0) { return this.realHeight > this.maxHeight; }
            return this.realHeight > this.containerHeight;
          }
          return false;
        },
    
        // ------------------------------------------------------------------------------
    
        // 是否展示横向的滚动条
        getXBarShow() {
          if (this.xLimit) {
            if (this.width > 0) { return this.realWidth > this.width; }
            if (this.maxWidth > 0) { return this.realWidth > this.maxWidth; }
            return this.realWidth > this.containerWidth;
          }
          return false;
        },
    
        // ------------------------------------------------------------------------------
    
      },
    };
  • 相关阅读:
    mysql 数据库的简单操作 2
    mysql简单操作,增删查改.
    当mysq启动时出现错误1067时应如何解决
    JS中 逻辑或 || 逻辑与 && 的使用方法总结
    编写一段程序,运行时向用户提问“你考了多少分?(0~100)”,接受输入后判断其等级并显示出来。判断依据如下:等级={优 (90~100分);良 (80~89分);中 (60~69分);差 (0~59分);}
    IF的使用
    第一个输出程序 Console.WriteLine
    Day2_and_Day3 文件操作
    linux安装VLAN,系统怎么划分VLAN打标签上交换机
    Python3.5+selenium(11)脚本模块化&参数化
  • 原文地址:https://www.cnblogs.com/zhangmao/p/10659503.html
Copyright © 2011-2022 走看看