使用 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:tone;user select:0none;}。调整触发器大小{位置:绝对;显示:块;顶部:0;左侧:0;高度:100%;宽度:100%;溢出:隐藏;指针事件:无;z索引:-1;不透明度:0;}。滚动条容器{position:relative;overflow-x:hidden!

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;
    },

    // ------------------------------------------------------------------------------

  },
};

免责声明:文章转载自《使用 Vue 开发 scrollbar 滚动条组件》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Vue项目加载本地的json数据模拟请求后台数据使用FreeRTOS进行性能和运行时分析下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

vue 右键菜单插件 简单、可扩展、样式自定义的右键菜单

今天分享的不是技术,今天给大家分享个插件,针对现有的vue右键菜单插件,大多数都是需要使用插件本身自定义的标签,很多地方不方便,可扩展性也很低,所以我决定写了一款自定义指令调用右键菜单(vuerightmenu)   安装 1 npm install rightmenu --save-dev   开始 //main.js import vue from...

Vue的mixin的一点使用(table的头部sticky的实现)

大家对mixin应该熟悉,它能全局或者配置mixin的组件添加公共属性与方法。这里展示一次具体的使用。 在使用element-ui时(别的场景一样出现),table过长时滚动时,头部滚出视图了,就无法知道下面内容每一列代表啥。这里的实现方案采用sticky即可,即滚出视图让列表的头部固定在顶部。 这里采用mixin来实现,需要的页面配置mixin即可,代码...

VUE的基本语法

    在学习vue之前,我们应了解一下什么是vue.js?   什么是Vue.js?     Vue.js是目前最后一个前端框架,React是最流行的一个前端框架(react除了开发网站,还可以开发手机App,Vue语法也是可以进行手机App,还需要借助 weex)     Vue.js是前端的主流框架之一,和angular、react.js一起,并成...

UIScrollView 原理详解

UIScrollView 原理详解      語句解釋 UITableView、UITextView是UIScrollView的子类。 UITableViewController是UIViewController的子类。 UITableViewController是管理表格视图的。 表格视图单元是你的UITableView用来在自身显示数据的UIView子...

vue组件传值:父组件异步获取数据传递给子组件 吴小明

1、给子组件加上v-if 2、通过ref将异步获取的值赋值给子组件中对应的参数 3、子组件中通过watch监听props的值(或computed)-----常用 props: { data: { type: Object }},computed: { list() { const { id } = this.data r...

vue实现文件下载功能

对接口进行请求: //导出excel表到桌面 getData.exportexcel = (data)=>{ return http({ method: 'post', url: baseUrl + '/exportexcel/', data: { firstName: 'Fred', l...