vue基于elementui的无限滚动组件

作者: 阿踏 | 来源:发表于2019-01-03 16:09 被阅读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]

    相关文章

      网友评论

        本文标题:vue基于elementui的无限滚动组件

        本文链接:https://www.haomeiwen.com/subject/vrtqrqtx.html