基于 vue2 的响应式基础组件(滚动条)

原创 2017年05月27日 11:20:30

基于 vue2 的响应式基础组件(滚动条)

本文章是个人开源项目 vue2do,主页地址是: https://zen0822.github.io,可以的话请帮我 star 啊,因为听说腾讯有github 项目 star 50 以上有加分,所以纯粹是为了找工作才搞个人开源项目的 -_-||

scroller.js

/**
 *
 * scroller 组件 滚动条
 *
 * @props height - 滚动区域的高度(auto | { Number }px | 100% | <{ Number })
 * @props width - 滚动内容最大高度(auto | {Number}px | 100%)
 * @props autoHide - 自动隐藏滚动条
 *
 * @events scrollY - 滚动事件
 *                  return isBottom - 滚动条是否到低
 *                         isTop - 滚动条是否到顶
 *                         top - 滚动条到滚动区域的顶部的当前距离
 *                         offset - 滚动条离滚动区域的顶部的距离
 * @events scrollX - 滚动事件
 *                  return isRight - 滚动条是否到结束的地方
 *                         isLeft - 滚动条是否到开始的地方
 *                         left - 滚动条到滚动区域的最左边的当前距离
 *                         offset - 滚动条离滚动区域的顶部的距离
 * @events changeYBar - y-bar 滚动条改变
 *                  return isBottom - 滚动条是否到低
 *                         isTop - 滚动条是否到顶
 *                         top - 滚动条到滚动区域的顶部的当前距离
 *                         offset - 滚动条离滚动区域的顶部的距离
 * @events changeXBar - x-bar 滚动条改变
 *                  return isRight - 滚动条是否到结束的地方
 *                         isLeft - 滚动条是否到开始的地方
 *                         left - 滚动条到滚动区域的最左边的当前距离
 *                         offset - 滚动条离滚动区域的顶部的距离
 * @events changeHeight - 滚动内容的高度变化
 *
 */

import './scroller.scss'

import baseMixin from '../../../mixin/base'
import render from './scroller.render.js'

// 滚动一次的滚动区域走的像素大小
const SCROLL_PIXEL = 10

const scrollerComp = {
  name: 'scroller',

  mixins: [baseMixin],

  render,

  props: {
    height: {
      type: [Number, String],
      default: '100%'
    },

    maxHeight: {
      type: [Number, String],
      default: 'none'
    },

    width: {
      type: [Number, String],
      default: '100%'
    },

    autoHide: {
      type: Boolean,
      default: false
    }
  },

  data() {
    // 组件名字
    this.compName = 'scroller'

    return {
      // y-scroller detail
      yData: {
        // 滚动条和滚动区域的偏移值
        barAndScrollerOffset: 0,
        // 滚动条的高度
        barLength: 0,
        // bar 的高度
        barTop: 0,
        // 滚动容器 / 滚动条区域
        boxBarRate: 0,
        // 滚动内容和滚动区域的偏移值
        boxAndScrollerOffset: 0,
        // 滚动条的 mousedown 事件
        isMousedown: false,
        // 记录上一次滚动条的高度
        oldBarTop: 0,
        // 滚动一次的滚动条走的像素大小
        scrollBarPixel: 0,
        // 滚动条的高度是否大于滚动容器
        scrollerContainBox: false
      },
      // x-scroller detail
      xData: {
        barLength: 0,
        barLeft: 0,
        barAndScrollerOffset: 0,
        boxBarRate: 0,
        boxAndScrollerOffset: 0,
        isMousedown: false,
        oldBarLeft: 0,
        scrollBarPixel: 0,
        scrollerContainBox: false
      },
      // box 离最顶端的偏移值
      boxTop: 0,
      // box 离最开始的偏移值
      boxLeft: 0,
      // 滚动区域的高度
      boxHeight: 0,
      // 滚动区域的宽度
      boxWidth: 0,
      // 滚动区域的样式宽度
      boxStyleWidth: '',
      // 滚动容器的高度
      scrollerHeight: 0,
      // 滚动容器的宽度
      scrollerWidth: 0,
      // 滚动条自动隐藏的状态
      showBar: false,
      // 滚动区域的 touchend 事件
      isTouchStart: false,
      // 记录连续滚动的标注
      scrolling: false,
      // 记录是否还在触摸移动中
      moving: false,
      // 是否有 scroller 组件的祖先
      hasScrollerGrandpa: false,
      // 记录开始触摸滚动区域的坐标
      touchStart: {
        x: 0,
        y: 0
      },
      // 记录开始点击滚动条的坐标
      pointStart: {
        x: 0,
        y: 0
      }
    }
  },

  computed: {
    boxStyle() {
      return {
        'top': this.boxTop + 'px',
        'left': this.boxLeft + 'px',
        'width': this.boxStyleWidth
      }
    },

    scrollerStyle() {
      return this.height === '100%'
        ? {}
        : { 'height': this.scrollerHeight + 'px' }
    },

    // x 方向的计算属性
    xComputed() {
      return {
        barDisplay: !this.xData.scrollerContainBox && (!this.autoHide || this.showBar),
        isLeft: this.xData.barLeft === 0,
        isRight: this.xData.barLeft === this.xData.barAndScrollerOffset,
        barStyle: {
          'width': this.xData.barLength + 'px',
          'left': this.xData.barLeft + 'px'
        }
      }
    },

    // y 方向的计算属性
    yComputed() {
      return {
        // 是否显示滚动条
        barDisplay: !this.yData.scrollerContainBox && (!this.autoHide || this.showBar),
        // 滚动条是否在顶部
        isTop: this.yData.scrollerContainBox || this.yData.barTop === 0,
        // 滚动条是否在底部
        isBottom: this.yData.scrollerContainBox || this.yData.barTop === this.yData.barAndScrollerOffset,
        barStyle: {
          'height': this.yData.barLength + 'px',
          'top': this.yData.barTop + 'px'
        }
      }
    },

    // 组件类名的前缀
    cPrefix() {
      return `${this.compPrefix}-scroller`
    }
  },

  watch: {
    barTop(val) {
      this.triggerScroll('y')
    },

    barLeft(val) {
      this.triggerScroll('x')
    },

    boxHeight(boxHeight) {
      this._initScrollerData({
        length: this.height,
        scrollerLength: this.scrollerHeight,
        boxLength: boxHeight,
        type: 'y'
      })
    },

    boxWidth(boxWidth) {
      this._initScrollerData({
        length: this.width,
        scrollerLength: this.scrollerWidth,
        boxLength: boxWidth,
        type: 'x'
      })
    },

    scrollerHeight(scrollerHeight) {
      this._initScrollerData({
        length: this.height,
        scrollerLength: scrollerHeight,
        boxLength: this.boxHeight,
        type: 'y'
      })
    },

    scrollerWidth(scrollerWidth) {
      this._initScrollerData({
        length: this.width,
        scrollerLength: scrollerWidth,
        boxLength: this.boxWidth,
        type: 'x'
      })
    }
  },

  methods: {
    _init() {
      this.$box = this.$refs.box
      this._initScroller()

      setInterval(() => {
        this._initScroller()
      }, 10)
    },

    _binder() {
      document.addEventListener('mousemove', this.scrollerMouseMove)
      document.addEventListener('mouseup', this.scrollerMouseUp)
    },

    // 初始化滚动条
    _initScroller() {
      this.scrollerWidth = this.$el.offsetWidth
      this.scrollerHeight = this.$el.offsetHeight

      this.boxHeight = this.$box.offsetHeight
      this.boxWidth = this.$box.offsetWidth

      let firstChildWidth = this.$box.firstChild ? this.$box.firstChild.offsetWidth : 0

      if (firstChildWidth > this.boxWidth) {
        this.boxStyleWidth = firstChildWidth + 'px'
      } else if (this.boxWidth <= this.scrollerWidth) {
        this.boxStyleWidth = this.scrollerWidth + 'px'
      } else {
        this.boxStyleWidth = 'auto'
      }
    },

    /**
     * 初始化滚动的数据
     * @param { Object } - 选项数据
     *                   type - 滚动条类型
     *                   scrollerLength - 滚动区域的高度/宽度
     *                   boxLength - 滚动内容的高度/宽度
     *                   length - 指定的滚动区域的高度/宽度
     */
    _initScrollerData({ type, scrollerLength, boxLength, length }) {
      // 滚动条数据的名字
      let barName = type + 'Data'
      // 滚动区域是否大过滚动内容
      let scrollerContainBox = false
      // 滚动内容和滚动条的比
      let boxBarRate = 0
      // 滚动条的长度
      let barLength = 0
      // 滚动内容和滚动区域的偏移值
      let boxAndScrollerOffset = 0
      // 滚动条和滚动区域的偏移值
      let barAndScrollerOffset = 0
      // 滚动条位置名字
      let barPositionName = `bar${type === 'y' ? 'Top' : 'Left'}`
      // 滚动内容位置名字
      let boxPositionName = `box${type === 'y' ? 'Top' : 'Left'}`

      if (type === 'y') {
        if (length === '100%') {
          scrollerContainBox = scrollerLength > boxLength
        } else if (length === 'auto') {
          scrollerContainBox = true
          scrollerLength = scrollerContainBox ? boxLength : length
          this.scrollerHeight = scrollerLength
        } else {
          scrollerContainBox = length >= boxLength
          scrollerLength = scrollerContainBox ? boxLength : length
          this.scrollerHeight = scrollerLength
        }

        boxBarRate = boxLength / scrollerLength
        barLength = scrollerLength / boxBarRate

        if (scrollerContainBox) {
          this.boxTop = 0
          this.barTop = 0
        }
      } else {
        if (length === '100%') {
          scrollerContainBox = scrollerLength >= boxLength
        } else {
          scrollerContainBox = length >= boxLength
        }

        boxBarRate = boxLength / scrollerLength
        barLength = scrollerLength / boxBarRate

        if (scrollerContainBox) {
          this.boxLeft = 0
          this.barLeft = 0
        }
      }

      boxAndScrollerOffset = boxLength - scrollerLength
      barAndScrollerOffset = scrollerLength - barLength

      this[barName].scrollerContainBox = scrollerContainBox
      this[barName].boxBarRate = boxBarRate
      this[barName].barLength = barLength
      this[barName].scrollBarPixel = SCROLL_PIXEL / boxBarRate
      this[barName].boxAndScrollerOffset = boxAndScrollerOffset
      this[barName].barAndScrollerOffset = barAndScrollerOffset

      this[barName][barPositionName] = scrollerContainBox ? 0
        : -this[boxPositionName] * barAndScrollerOffset / boxAndScrollerOffset

      this._boxAndBarScroll({
        type: 'y',
        boxDistance: 0,
        barDistance: 0
      })
      this._boxAndBarScroll({
        type: 'x',
        boxDistance: 0,
        barDistance: 0
      })

      this.triggerChangeBar(type)
    },

    /**
     * 滚动条和滚动区域的滚动操作的相关数据
     * @param { Object } - 选项数据
     *                   type - 滚动条类型
     *                   barDistance - 滚动条的位移
     *                   boxDistance - 滚动内容的位移
     */
    _boxAndBarScroll({ type, boxDistance, barDistance }) {
      let barName = type + 'Data'
      let barPositionName = `bar${type === 'y' ? 'Top' : 'Left'}`
      let boxPositionName = `box${type === 'y' ? 'Top' : 'Left'}`
      let boxPosition = this[boxPositionName] + boxDistance
      let barPosition = this[barName][barPositionName] + barDistance
      let barAndScrollerOffset = this[barName].barAndScrollerOffset
      let boxAndScrollerOffset = this[barName].boxAndScrollerOffset

      if (boxDistance >= 0) {
        if (type === 'y') {
          this[barName][barPositionName] = barPosition < 0 ? 0 : barPosition
          this[boxPositionName] = boxPosition > 0 ? 0 : boxPosition
        } else {
          this[barName][barPositionName] = barPosition < 0 ? 0 : barPosition
          this[boxPositionName] = boxPosition > 0 ? 0 : boxPosition
        }
      } else {
        if (type === 'y') {
          this[barName][barPositionName] = barPosition > barAndScrollerOffset ? barAndScrollerOffset : barPosition
          this[boxPositionName] = boxPosition < -boxAndScrollerOffset ? -boxAndScrollerOffset : boxPosition
        } else {
          this[barName][barPositionName] = barPosition > barAndScrollerOffset ? barAndScrollerOffset : barPosition
          this[boxPositionName] = boxPosition < -boxAndScrollerOffset ? -boxAndScrollerOffset : boxPosition
        }
      }
    },

    barClick(evt) {
      evt.preventDefault()
      evt.stopPropagation()
    },

    yBarMouseDown(evt) {
      this.yData.isMousedown = true

      this.pointStart = {
        x: event.clientX,
        y: event.clientY
      }
    },

    xBarMouseDown(evt) {
      this.xData.isMousedown = true

      this.pointStart = {
        x: event.clientX,
        y: event.clientY
      }
    },

    scrollerMouseMove(evt) {
      if (!this.yData.isMousedown && !this.xData.isMousedown) {
        return false
      }

      evt.preventDefault()

      let type = this.yData.isMousedown ? 'y' : 'x'
      let distance = evt[`client${type.toUpperCase()}`] - this.pointStart[type]

      this._boxAndBarScroll({
        type,
        boxDistance: -distance * this[`${type}Data`].boxBarRate,
        barDistance: distance
      })

      this.pointStart = {
        x: evt.clientX,
        y: evt.clientY
      }

      return this.triggerScroll(type)
    },

    scrollerMouseUp(evt) {
      evt.preventDefault()

      this.yData.isMousedown = false
      this.xData.isMousedown = false
    },

    scrollerMouseover(evt) {
      this.showBar = true
    },

    scrollerMouseout(evt) {
      this.showBar = false
    },

    mouseWheel(evt) {
      let barTop = 0
      let boxTop = 0

      this.yData.oldBarTop = this.yData.barTop

      this._boxAndBarScroll({
        type: 'y',
        boxDistance: evt.deltaY > 0 ? -SCROLL_PIXEL : SCROLL_PIXEL,
        barDistance: evt.deltaY > 0 ? this.yData.scrollBarPixel : -this.yData.scrollBarPixel
      })

      this.triggerScroll('y')

      if (this.yComputed.isBottom || this.yComputed.isTop) {
        if (this.scrolling) {
          evt.preventDefault()

          return false
        }

        this.scrolling = true

        setTimeout(() => {
          this.scrolling = false
        }, 200)
      }

      if (!(this.yComputed.isBottom || this.yComputed.isTop) || this.yData.oldBarTop !== this.yData.barTop) {
        evt.preventDefault()
      }
    },

    scrollerTouchStart(evt) {
      this.isTouchStart = true
      this.showBar = true

      this.touchStart = {
        x: evt.touches[0].clientX,
        y: evt.touches[0].clientY
      }
    },

    scrollerTouchMove(evt) {
      if (this.yData.scrollerContainBox && this.xData.scrollerContainBox) {
        this.triggerScroll('y')

        return false
      }

      this.showBar = true

      if (!this.isTouchStart) {
        return false
      }

      let yDistance = this.touchStart.y - evt.touches[0].clientY
      let xDistance = this.touchStart.x - evt.touches[0].clientX

      if (!this.yData.scrollerContainBox) {
        this._boxAndBarScroll({
          type: 'y',
          boxDistance: -yDistance,
          barDistance: yDistance / this.yData.boxBarRate
        })

        this.triggerScroll('y')
      }

      if (!this.xData.scrollerContainBox) {
        this._boxAndBarScroll({
          type: 'x',
          boxDistance: -xDistance,
          barDistance: xDistance / this.xData.boxBarRate
        })


        this.triggerScroll('x')
      }

      this.touchStart = {
        x: evt.touches[0].clientX,
        y: evt.touches[0].clientY
      }

      // 滚动区域正方向移动
      // TODO: 优化,可以在滚动到底部得时候触发父容器得滚动事件
      if (yDistance > 0) {
        if (this.yComputed.isBottom && !this.hasScrollerGrandpa) {
        } else {
          evt.preventDefault()
        }
      } else {
        if (this.yComputed.isTop && !this.hasScrollerGrandpa) {
        } else {
          evt.preventDefault()
        }
      }
    },

    scrollerTouchEnd(evt) {
      this.showBar = false
      this.isTouchStart = false
      this.moving = false
    },

    /**
     * 触发滚动条滚动事件
     */
    triggerScroll(type) {
      let data = {}
      let eventName = ''

      if (type === 'y') {
        eventName = 'scrollY'
        data = {
          top: this.yData.barTop,
          offset: this.yData.barAndScrollerOffset,
          isBottom: this.yComputed.isBottom,
          isTop: this.yComputed.isTop
        }
      } else {
        eventName = 'scrollX'
        data = {
          left: this.xData.barLeft,
          offset: this.xData.barAndScrollerOffset,
          isRight: this.xComputed.isRight,
          isLeft: this.xComputed.isLeft
        }
      }

      return this.$emit(eventName, data)
    },

    triggerChangeBar(type) {
      let data = {}
      let eventName = ''

      if (type === 'y') {
        eventName = 'changeYBar'
        data = {
          isBottom: this.yComputed.isBottom,
          isTop: this.yComputed.isTop,
          boxWidth: this.boxWidth,
          boxHeight: this.boxHeight
        }
      } else {
        eventName = 'changeXBar'
        data = {
          isLeft: this.xComputed.isLeft,
          isRight: this.xComputed.isRight,
          boxWidth: this.boxWidth,
          boxHeight: this.boxHeight
        }
      }

      return this.$emit(eventName, data)
    }
  },

  created() {
    function checkScrollerParent(parent = {}) {
      if (parent.compName === 'scroller') {
        return true
      } else if (parent.constructor.name === 'VueComponent') {
        return checkScrollerParent(parent.$parent)
      } else {
        return false
      }
    }

    this.hasScrollerGrandpa = checkScrollerParent(this.$parent)
  }
}

export default scrollerComp

scroller.render.js

export default function (h) {
  return h(
    'div',
    {
      class: [this.cPrefix],
      style: this.scrollerStyle,
      on: {
        mouseover: this.scrollerMouseover,
        mouseout: this.scrollerMouseout,
        wheel: this.mouseWheel,
        touchstart: this.scrollerTouchStart,
        touchmove: this.scrollerTouchMove,
        touchend: this.scrollerTouchEnd
      }
    },
    [
      h('div', {
        class: [this.xclass('box')],
        style: this.boxStyle,
        ref: 'box'
      }, this.$slots.default),

      h('div', {
        class: [this.xclass(['bar', 'y-bar'])],
        on: {
          click: this.barClick,
          mousedown: this.yBarMouseDown
        },
        style: this.yComputed.barStyle,
        ref: 'bar',
        directives: [{
          name: 'show',
          value: this.yComputed.barDisplay
        }]
      }),

      h('div', {
        class: [this.xclass(['bar', 'x-bar'])],
        on: {
          click: this.barClick,
          mousedown: this.xBarMouseDown
        },
        style: this.xComputed.barStyle,
        ref: 'xBar',
        directives: [{
          name: 'show',
          value: this.xComputed.barDisplay
        }]
      })
    ]
  )
}

scroller.scss

@import "../../../scss/config.scss";
@import "../../../scss/extend.scss";
@import "../../../scss/name.scss";

/**
 * scroller 组件样式
 */

$C_PREFIX: #{ $COMP_SCROLLER };

.#{ $C_PREFIX } {
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 100%;

  .#{ $C_PREFIX }-box {
    position: absolute;
    top: 0;
    left: 0;
  }

  .#{ $C_PREFIX }-bar {
    position: absolute;
    border-radius: 4px;
    background-color: $grey;
    opacity: .4;

    z-index: 1;

    &:hover{
      background-color: $grey-dark;
      opacity: 1;
    }

    &.#{ $C_PREFIX }-x-bar {
      bottom: 0;
      height: 5px;
    }

    &.#{ $C_PREFIX }-y-bar {
      right: 0;
      width: 5px;
    }
  }
}

相关文章推荐

vue框架下的滚动条优化插件

https://github.com/GarveyZuo/EasyScroll Vue下的滚动条优化插件EasyScroll使用: 运行: npm isntall --save easys...
  • zjw0742
  • zjw0742
  • 2017年09月02日 17:49
  • 2156

vue组件最佳实践

看了老外的一篇关于组件开发的建议(强烈建议阅读英文原版),感觉不错翻译一下加深理解。 这篇文章制定一个统一的规则来开发你的vue程序,以至于达到一下目的。 1.让开发者和开发团队更容易发现一些事情...
  • zk65645
  • zk65645
  • 2017年03月13日 11:51
  • 1447

vue实现动态添加数据滚动条自动滚动到底部

在使用vue实现聊天页面的时候,聊天数据动态加到页面中,需要实现滚动条也自动滚动到底部。这时我找到网上有个插件 vue-chat-scroll https://www.npmjs.com/packag...

修改浏览器默认的滚动条样式

css修改滚动条样式
  • ning0_o
  • ning0_o
  • 2016年08月12日 09:53
  • 6410

Vue常用经典开源项目汇总参考-海量

http://www.jianshu.com/p/58e72750cc62?utm_source=tuicool&utm_medium=referral Vue是什么? Vue.js(...

vue插件

UI组件 http://www.jqsite.com/notes/1704205187.html element ★11612 - 饿了么出品的Vue2的web UI工具套...

CSS3自定义滚动条样式

有的公司或许会要求做个好看的滚动条,这时候,我们就需要来自定义滚动条,但是IE跟别的是不一样的。所以,在下面的文章中,会有两种不同的方式来自定义滚动条样式。 webkit浏览器css设置滚动条:(下面...

Vue 组件间滚动条互相影响 详情页列表页滚动条相互影响的解决方案 (或许就是你的正解)

今天踩了一个坑 也是自己理解不深的原因吧 写Vue列表页到详情页的跳转的时候 详情页怎样布局都会带有列表页的滚动条 后来发现是路由的关系 受组件的影响 以为详情页要和列表页嵌套在一起 后来发现路由拆开...

Vue 测试实例-组件嵌套二种方式

Vue 测试实例-组件嵌套二种方式

vue之滚动轴插件better-scroll

跟做慕课网的vue高仿外卖项目中用到了一个很好用的插件BScroll,用来计算左侧menu栏对应右侧foods栏相应显示的食物区,如果不用插件就比较费事了,因此这里分享一下这个插件的简单使用: 一,...
  • HZKang
  • HZKang
  • 2017年10月16日 18:11
  • 210
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:基于 vue2 的响应式基础组件(滚动条)
举报原因:
原因补充:

(最多只允许输入30个字)