<!-- Author: ferrinweb -->
<!-- Create Time: 2018/10/9 11:32 -->
<!-- Description: scroll-box -->
<template>
  <div class="scroll-box">
    <div class="before"
        v-if="enableDragDown"
        :style="'height:' + this.dragDistance + 'px'"
        :class="{'release-and-trigger': dragDownDistance > triggerDistance, 'holding before-holding': reloading}"
    >
      <div ref="before">
        <slot name="dragDownArea"><span class="drag-down-text">{{ beforeText }}</span></slot>
      </div>
    </div>
    <div class="scroll-content-wrapper" :id="'scroll-wrapper-' + _uid" ref="scrollBox"
      :class="{'scroll-lock': scrollLockByUser || scrollLock}"
      @scroll="_boxScrolling"
      @touchmove="_startTouchDrag($event)"
      @touchstart="_markDragStart($event)"
      @touchend="_markDragEnd"
      @mousemove="_startTouchDrag($event)"
      @mousedown="_markDragStart($event)"
      @mouseup="_markDragEnd"
      @mouseleave="_markDragEnd"
    >
      <div class="scroll-content"
          ref="scrollContent"
          :class="{'holding': reloading || loading, 'draging': draging && (dragDownDistance || dragUpDistance)}"
      >
        <slot></slot>
      </div>
    </div>
    <div class="after"
        v-if="enableDragUp"
        :style="'height:' + this.dragDistance + 'px'"
        :class="{'release-and-trigger': dragUpDistance > triggerDistance, 'holding after-holding': loading}"
    >
      <div ref="after">
        <slot name="dragUpArea"><span class="drag-up-text">{{ afterText }}</span></slot>
      </div>
    </div>
    <return-top-button :target="'#scroll-wrapper-' + _uid" v-if="enableTopButton"></return-top-button>
  </div>
</template>

<script>
import returnTopButton from './return-top-button'
export default {
  name: 'scroll-box',
  components: {
    returnTopButton
  },
  props: {
    // 拖拽阈值，超过该值后释放将触发下拉或上拉事件
    dragDistance: {
      type: Number,
      default: 150
    },
    // 是否允许下拉
    enableDragDown: {
      type: Boolean,
      default: false
    },
    // 是否允许上拉
    enableDragUp: {
      type: Boolean,
      default: false
    },
    // 是否使用返回顶部按钮
    enableTopButton: {
      type: Boolean,
      default: true
    },
    // 下拉刷新提示文本
    beforeText: {
      type: String,
      default: 'Release and reload'
    },
    // 上拉加载更多提示文本
    afterText: {
      type: String,
      default: 'Release and load more'
    }
  },
  data () {
    return {
      scrollBox: null,
      scrollBoxHeight: 0,
      scrollContent: null,
      scrollContentHeight: 0,
      y: 0,
      currentY: null,
      draging: false,
      dragUpDistance: 0,
      dragDownDistance: 0,
      dragDownTrigger: false,
      loading: false,
      reloading: false,
      transforming: false,
      scrollLock: false,
      scrollLockByUser: false,
      scrollBefore: null,
      scrollAfter: null
    }
  },
  methods: {
    _boxScrolling () {
      if (this.scrollLock) return
      let y = this.scrollBox.scrollTop
      if (this.enableDragDown || this.enableDragUp) this.y = y
      // Fire when the scroll box is scrolling
      this.$emit('box-scroll', {y})
    },
    _markDragStart (e) {
      if (!this.enableDragDown && !this.enableDragUp) return
      if (e.which !== 1 && e.which !== 0) return
      if (this.reloading || this.loading) return
      // 仅当在滚动边界处赋值拖动起始点
      if (!this.isBottom && !this.isTop) return
      this.currentY = e.type === 'touchstart' ? e.touches[0].pageY : e.pageY
    },
    _markDragEnd () {
      this.draging = false
      this.scrollLock = false
      if (this.reloading || this.loading) return
      this.currentY = null
      if (!this.dragDownDistance && !this.dragUpDistance) return
      if (this.dragDownDistance >= this.triggerDistance) {
        setTimeout(() => {
          // 触发下拉刷新
          this.$emit('pull-down')
        }, 200)
        this.reloading = true
      }
      if (this.dragUpDistance >= this.triggerDistance) {
        setTimeout(() => {
          // 触发上拉加载更多
          this.$emit('pull-up')
        }, 200)
        this.loading = true
      }
      requestAnimationFrame(() => {
        if (!this.reloading && !this.loading) {
          this.scrollUpdate()
        } else {
          this._reboundDrag()
        }
      })
    },
    _startTouchDrag (e) {
      if (this.currentY !== null) {
        e.type === 'mousemove' && e.preventDefault()
        this.draging = true
        this.scrollContent.style.transition = 'unset'
        // 判断拖动方向
        let pageY = e.type === 'touchmove' ? e.touches[0].pageY : e.pageY
        let direction = (pageY - this.currentY)
        // 减小拖距，增强体验
        direction *= this.dragDistanceRatio
        // 坐标变化为正值，表示为下拉操作
        direction > 0 && this.enableDragDown && this.isTop && this._dragDown(direction)
        // 坐标变化为负值，表示为上拉操作
        direction < 0 && this.enableDragUp && this.isBottom && this._dragUp(direction)
      }
    },
    _doTransform (y) {
      if (this.transforming) return
      this.transforming = true
      requestAnimationFrame(() => {
        this.scrollContent.style.willChange = 'transform'
        this.scrollContent.style.transform = 'translateY(' + y + 'px)'
        this.transforming = false
      })
    },
    _dragDown (distance) {
      this.scrollLock = true
      this.dragDownDistance = distance > this.dragDistance ? this.dragDistance : distance
      this._doTransform(this.dragDownDistance)
    },
    _dragUp (distance) {
      this.scrollLock = true
      this.dragUpDistance = distance < -this.dragDistance ? this.dragDistance : -distance
      this._doTransform(-this.dragUpDistance)
    },
    // 下拉刷新、上拉加载事件最终被触发后的回弹
    _reboundDrag () {
      this.scrollContent.style.transition = 'transform .2s cubic-bezier(.11,.49,.61,.99)'
      let reboundY = this.halfDistance
      if (this.dragDownDistance) {
        this.dragDownDistance = reboundY
        this.scrollContent.style.transform = 'translateY(' + reboundY + 'px)'
      }
      if (this.dragUpDistance) {
        this.dragUpDistance = reboundY
        this.scrollContent.style.transform = 'translateY(' + -reboundY + 'px)'
      }
    },
    // @vuese
    // 禁用滚动
    disableScroll () {
      this.scrollLockByUser = true
    },
    // @vuese
    // 启用滚动
    enableScroll () {
      this.scrollLockByUser = false
    },
    /**
     * @vuese
     * 下拉刷新或上拉加载更多后须执行滚动盒子实例的 scrollUpdate 方法更新状态
     * this.$refs.scrollInstance.scrollUpdate()
     * @arg contentUpdate {boolean} 盒子内容是否有更新
     */
    scrollUpdate (contentUpdate) {
      if (this.loading) {
        contentUpdate && this.$nextTick(() => {
          this.scrollContent.style.transition = 'unset'
          this.scrollContent.style.transform = ''
          this.scrollBox.scroll({top: this.y + this.halfDistance})
        })
        this.loading = false
      }
      this.$nextTick(() => {
        this.scrollContentHeight = this.scrollContent.clientHeight
      })
      this.reloading = false
      this.dragDownDistance = 0
      this.dragUpDistance = 0
      this.scrollContent.style.transition = ''
      this.scrollContent.style.transform = ''
      setTimeout(() => {
        this.scrollContent.style.willChange = ''
      }, 300)
    },
    /**
     * @vuese
     * 滚动至指定位置或元素
     * @arg top 滚动至何处，object为dom对象，string为选择器，number为像素
     * @arg offset 滚动偏移
     */
    scrollTo (top, offset) {
      let type = typeof top
      let y = 0
      switch (type) {
        case 'number':
          y = top
          break
        case 'string':
          const element = this.scrollContent.querySelector(top)
          y = element ? element.offsetTop : this.y
          break
        case 'object':
          y = top instanceof HTMLElement ? top.offsetTop : this.y
          break
        default:
          y = this.y
      }
      this.scrollBox.scroll({top: y - (offset || 0), behavior: 'smooth'})
    },
    updateScrollBoxHeight () {
      this.$nextTick(() => {
        this.scrollBoxHeight = this.scrollBox.clientHeight
      })
    }
  },
  computed: {
    dragDistanceRatio () {
      return this.dragDistance / this.scrollBoxHeight * 1.618
    },
    halfDistance () {
      return this.dragDistance / 2
    },
    isTop () {
      return this.y === 0
    },
    isBottom () {
      return this.y + this.scrollBoxHeight === this.scrollContentHeight
    },
    triggerDistance () {
      return this.dragDistance * 0.618
    }
  },
  watch: {
  },
  mounted () {
    this.scrollBox = this.$refs.scrollBox
    this.scrollBoxHeight = this.scrollBox.clientHeight
    this.scrollContent = this.$refs.scrollContent
    this.scrollContentHeight = this.scrollContent.clientHeight
    if (this.enableDragDown) this.scrollBefore = this.$refs.before
    if (this.enableDragUp) this.scrollAfter = this.$refs.after
    window.addEventListener('resize', this.updateScrollBoxHeight)
  },
  beforeDestroy () {
    window.removeEventListener('resize', this.updateScrollBoxHeight)
  }
}
</script>

<style scoped>
  .scroll-box{
    overflow: hidden;
  }
  .scroll-content-wrapper{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    z-index: 2;
  }
  .scroll-content-wrapper.scroll-lock {
    overflow-y: hidden;
  }
  .scroll-content{
    position: relative;
    width: 100%;
    min-height: 100%;
    height: auto;
    overflow: visible;
    transform: translate3d(0,0,0);
    transition: transform .2s cubic-bezier(.11,.49,.61,.99);
  }
  .scroll-content.draging > * {
    pointer-events: none;
  }
  .after, .before{
    position: absolute;
    z-index: 1;
    width: 100%;
    height: 0;
    left: 0;
    overflow: hidden;
  }
  .after .drag-up-text,
  .before .drag-down-text{
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 50%;
    font-size: 16px;
    opacity: 0;
    transition: opacity .3s;
  }
  .after .drag-up-text{
    bottom: 0;
  }
  .before .drag-down-text{
    top: 0;
  }
  .after.release-and-trigger,
  .before.release-and-trigger{
    color: #4dc247;
  }
  .after.release-and-trigger .drag-up-text,
  .before.release-and-trigger .drag-down-text{
    opacity: 1;
  }
  .after.holding,
  .before.holding{
    background: url(data:image/gif;base64, ) no-repeat center 82.5%;
    background-size: auto 20%;
  }
  .before.holding{
    background-position: center 17.5%;
  }
  .after.holding .drag-up-text,
  .before.holding .drag-down-text{
    opacity: 0;
  }
  .after{
    bottom: 0;
  }
  .before{
    top: 0;
  }
</style>
