Vue.jsangular2与vue的那些事IT@程序员猿媛

vue基于elementui的无限滚动组件

2019-01-03  本文已影响4人  阿踏

组件代码如下

<template>
  <div :class="wrapClasses" style="touch-action: none;">
    <div
      :class="scrollContainerClasses"
      :style="{height: height + 'px'}"
      @scroll="handleScroll"
      @wheel="onWheel"
      @touchstart="onPointerDown"
      ref="scrollContainer"
    >
      <div :class="loaderClasses" :style="{paddingTop: wrapperPadding.paddingTop}" ref="toploader"
           v-loading.body="showTopLoader"
           :element-loading-text="loadingText"
           :element-loading-spinner="loadingSpinner">
      </div>
      <div :class="slotContainerClasses" ref="scrollContent">
        <slot></slot>
      </div>
      <div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader"
           v-loading.body="showBottomLoader"
           :element-loading-text="loadingText"
           :element-loading-spinner="loadingSpinner">
      </div>
    </div>
  </div>
</template>

<script>
  import throttle from 'lodash.throttle'
  import { on, off } from 'element-ui/lib/utils/dom'

  const prefixCls = 'xdh-scroll'
  const dragConfig = {
    sensitivity: 10,
    minimumStartDragOffset: 5 // minimum start drag offset
  }
  const noop = () => Promise.resolve()

  export default {
    name: 'XdhScroll',
    props: {
      height: {
        type: [Number, String],
        default: 300
      },
      onReachTop: {
        type: Function
      },
      onReachBottom: {
        type: Function
      },
      onReachEdge: {
        type: Function
      },
      loadingText: {
        type: String,
        default: '加载中...'
      },
      loadingSpinner: {
        type: String,
        default: 'el-icon-loading'
      },
      distanceToEdge: [Number, Array]
    },
    data () {
      const distanceToEdge = this.calculateProximityThreshold()
      return {
        showTopLoader: false,
        showBottomLoader: false,
        showBodyLoader: false,
        lastScroll: 0,
        reachedTopScrollLimit: true,
        reachedBottomScrollLimit: false,
        topRubberPadding: 0,
        bottomRubberPadding: 0,
        rubberRollBackTimeout: false,
        isLoading: false,
        pointerTouchDown: null,
        touchScroll: false,
        handleScroll: () => {},
        pointerUpHandler: () => {},
        pointerMoveHandler: () => {},

        // near to edge detectors
        topProximityThreshold: distanceToEdge[0],
        bottomProximityThreshold: distanceToEdge[1]
      }
    },
    computed: {
      wrapClasses () {
        return `${prefixCls}-wrapper`
      },
      scrollContainerClasses () {
        return `${prefixCls}-container`
      },
      slotContainerClasses () {
        return [
          `${prefixCls}-content`,
          {
            [`${prefixCls}-content-loading`]: this.showBodyLoader
          }
        ]
      },
      loaderClasses () {
        return `${prefixCls}-loader`
      },
      wrapperPadding () {
        return {
          paddingTop: this.topRubberPadding + 'px',
          paddingBottom: this.bottomRubberPadding + 'px'
        }
      }
    },
    methods: {
      // just to improve feeling of loading and avoid scroll trailing events fired by the browser
      waitOneSecond () {
        return new Promise(resolve => {
          setTimeout(resolve, 1000)
        })
      },

      calculateProximityThreshold () {
        const dte = this.distanceToEdge
        if (typeof dte === 'undefined') return [20, 20]
        return Array.isArray(dte) ? dte : [dte, dte]
      },

      onCallback (dir) {
        this.isLoading = true
        this.showBodyLoader = true
        if (dir > 0) {
          this.showTopLoader = true
          this.topRubberPadding = 20
        } else {
          this.showBottomLoader = true
          this.bottomRubberPadding = 20

          // to force the scroll to the bottom while height is animating
          let bottomLoaderHeight = 0
          const container = this.$refs.scrollContainer
          const initialScrollTop = container.scrollTop
          for (let i = 0; i < 20; i++) {
            setTimeout(() => {
              bottomLoaderHeight = Math.max(
                bottomLoaderHeight,
                this.$refs.bottomLoader.getBoundingClientRect().height
              )
              container.scrollTop = initialScrollTop + bottomLoaderHeight
            }, i * 50)
          }
        }

        const callbacks = [this.waitOneSecond(), this.onReachEdge ? this.onReachEdge(dir) : noop()]
        callbacks.push(dir > 0 ? this.onReachTop ? this.onReachTop() : noop() : this.onReachBottom ? this.onReachBottom() : noop())

        let tooSlow = setTimeout(() => {
          this.reset()
        }, 5000)

        Promise.all(callbacks).then(() => {
          clearTimeout(tooSlow)
          this.reset()
        })
      },

      reset () {
        [
          'showTopLoader',
          'showBottomLoader',
          'showBodyLoader',
          'isLoading',
          'reachedTopScrollLimit',
          'reachedBottomScrollLimit'
        ].forEach(prop => (this[prop] = false))

        this.lastScroll = 0
        this.topRubberPadding = 0
        this.bottomRubberPadding = 0
        clearInterval(this.rubberRollBackTimeout)

        // if we remove the handler too soon the screen will bump
        if (this.touchScroll) {
          setTimeout(() => {
            off(window, 'touchend', this.pointerUpHandler)
            this.$refs.scrollContainer.removeEventListener('touchmove', this.pointerMoveHandler)
            this.touchScroll = false
          }, 500)
        }
      },

      onWheel (event) {
        if (this.isLoading) return

        // get the wheel direction
        const wheelDelta = event.wheelDelta ? event.wheelDelta : -(event.detail || event.deltaY)
        this.stretchEdge(wheelDelta)
      },

      stretchEdge (direction) {
        clearTimeout(this.rubberRollBackTimeout)

        // check if set these props
        if (!this.onReachEdge) {
          if (direction > 0) {
            if (!this.onReachTop) return
          } else {
            if (!this.onReachBottom) return
          }
        }

        // if the scroll is not strong enough, lets reset it
        this.rubberRollBackTimeout = setTimeout(() => {
          if (!this.isLoading) this.reset()
        }, 250)

        // to give the feeling its ruberish and can be puled more to start loading
        if (direction > 0 && this.reachedTopScrollLimit) {
          this.topRubberPadding += 5 - this.topRubberPadding / 5
          if (this.topRubberPadding > this.topProximityThreshold) this.onCallback(1)
        } else if (direction < 0 && this.reachedBottomScrollLimit) {
          this.bottomRubberPadding += 6 - this.bottomRubberPadding / 4
          if (this.bottomRubberPadding > this.bottomProximityThreshold) this.onCallback(-1)
        } else {
          this.onScroll()
        }
      },

      onScroll () {
        if (this.isLoading) return
        const el = this.$refs.scrollContainer
        const scrollDirection = Math.sign(this.lastScroll - el.scrollTop) // IE has no Math.sign, check that webpack polyfills this
        const displacement = el.scrollHeight - el.clientHeight - el.scrollTop

        const topNegativeProximity = this.topProximityThreshold < 0 ? this.topProximityThreshold : 0
        const bottomNegativeProximity = this.bottomProximityThreshold < 0 ? this.bottomProximityThreshold : 0
        if (scrollDirection === -1 && displacement + bottomNegativeProximity <= dragConfig.sensitivity) {
          this.reachedBottomScrollLimit = true
        } else if (scrollDirection >= 0 && el.scrollTop + topNegativeProximity <= 0) {
          this.reachedTopScrollLimit = true
        } else {
          this.reachedTopScrollLimit = false
          this.reachedBottomScrollLimit = false
          this.lastScroll = el.scrollTop
        }
      },

      getTouchCoordinates (e) {
        return {
          x: e.touches[0].pageX,
          y: e.touches[0].pageY
        }
      },

      onPointerDown (e) {
        // we just use scroll and wheel in desktop, no mousedown
        if (this.isLoading) return
        if (e.type === 'touchstart') {
          // if we start do touchmove on the scroll edger the browser will scroll the body
          // by adding 5px margin on pointer down we avoid this behaviour and the scroll/touchmove
          // in the component will not be exported outside of the component
          const container = this.$refs.scrollContainer
          if (this.reachedTopScrollLimit) container.scrollTop = 5
          else if (this.reachedBottomScrollLimit) container.scrollTop -= 5
        }
        if (e.type === 'touchstart' && this.$refs.scrollContainer.scrollTop === 0) {
          this.$refs.scrollContainer.scrollTop = 5
        }

        this.pointerTouchDown = this.getTouchCoordinates(e)
        on(window, 'touchend', this.pointerUpHandler)
        this.$refs.scrollContainer.parentElement.addEventListener('touchmove', e => {
          e.stopPropagation()
          this.pointerMoveHandler(e)
        }, {passive: false, useCapture: true})
      },

      onPointerMove (e) {
        if (!this.pointerTouchDown) return
        if (this.isLoading) return

        const pointerPosition = this.getTouchCoordinates(e)
        const yDiff = pointerPosition.y - this.pointerTouchDown.y

        this.stretchEdge(yDiff)

        if (!this.touchScroll) {
          const wasDragged = Math.abs(yDiff) > dragConfig.minimumStartDragOffset
          if (wasDragged) this.touchScroll = true
        }
      },

      onPointerUp () {
        this.pointerTouchDown = null
      }
    },
    created () {
      this.handleScroll = throttle(this.onScroll, 150, {leading: false})
      this.pointerUpHandler = this.onPointerUp.bind(this) // because we need the same function to add and remove event handlers
      this.pointerMoveHandler = throttle(this.onPointerMove, 50, {leading: false})
    }
  }
</script>

使用方法

<template>
    <xdh-scroll :on-reach-bottom="handleReachBottom">
        <el-card v-for="(item, index) in list1" :key="index" style="margin: 32px 0">
            Content {{ item }}
        </el-card>
    </xdh-scroll>
</template>
<script>
    export default {
        data () {
            return {
                list1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
            }
        },
        methods: {
            handleReachBottom () {
                return new Promise(resolve => {
                    setTimeout(() => {
                        const last = this.list1[this.list1.length - 1];
                        for (let i = 1; i < 11; i++) {
                            this.list1.push(last + i);
                        }
                        resolve();
                    }, 2000);
                });
            }
        }
    }
</script>

属性

参数 说明 类型 可选值 默认值
height 滚动区域的高度,单位像素 String/Number - 300
loading-text 加载中的文案 String - 加载中...
loading-spinner 自定义加载图标类名 String - el-icon-loading
on-reach-top 滚动至顶部时触发,需返回 Promise Function - -
on-reach-bottom 滚动至底部时触发,需返回 Promise Function - -
on-reach-edge 滚动至顶部或底部时触发,需返回 Promise Function - -
distance-to-edge 从边缘到触发回调的距离。如果是负的,回调将在到达边缘之前触发。值最好在 24 以下。 Number/Array - [20, 20]
上一篇下一篇

猜你喜欢

热点阅读