infinite-scroll/infinite-scroll.vue

<template>
  <div class="ion-infinite-scroll" :threshold="threshold" :state="state">
    <slot>
      <div class="ion-infinite-scroll-content">
        <div class="infinite-loading">
          <div class="infinite-loading-spinner" v-if="loadingSpinner">
            <vm-spinner :name="loadingSpinner"></vm-spinner>
          </div>
          <div class="infinite-loading-text" v-html="loadingText" v-if="loadingText"></div>
        </div>
      </div>
    </slot>
  </div>
</template>

<script type="text/javascript">
/**
 * @component InfiniteScroll
 * @description
 *
 * ## 数据加载 / InfiniteScroll无限滚动组件
 *
 * ### 说明
 *
 * 当页面滚动到页面底部一定距离时, InfiniteScroll组件会触发`onInfinite`事件, 通过回调传入的参数`infiniteScroll`来处理对应的状态.
 *
 * 比如: 当需要异步请求AJAX数据时, 数据请求后, 需要执行`infiniteScroll.complete()` 来变更InfiniteScroll组件的状态,当继续向下滚动时, 还能继续出发`onInfinite`事件, 如此往复.
 *
 * 当通过AJAX请求的数据已经全部请求完毕(没有更多的数据时), 则执行`infiniteScroll.enable(false)`, 表明InfiniteScroll任务全部结束. 此时, 将解除对Content组件的`onScroll`事件的监听.
 *
 * ### 其他
 *
 * InfiniteScroll组件会附带InfiniteScrollContent内容,它只是起到显示状态的作用, 你也可以自己定义显示状态.
 *
 * 其中, 标示状态的`state`有三种值: enabled/disabled/loading, 且这三种状态会写在父组件上, 因此可以用这个特性定义子组件的样式. 比如像下面的demo
 *
 * ### 建议
 *
 * 首屏的数据至少占满两个以上屏幕高度, 通过 InfiniteScroll 加载的数据也至少能占满两个以上的屏幕高度, 这样的体验效果会好点
 *
 * ### 注意
 *
 * - 组件中的方法是在事件的回调参数中定义的.
 * - 组件支持js滚动监听, 如何设置参考Content组件
 *
 * ### 如何引入
 * ```
 * // 引入
 * import { InfiniteScroll } from 'vimon'
 * // 安装
 * Vue.component(InfiniteScroll.name, InfiniteScroll)
 * // 或者
 * export default{
 *   components: {
 *     InfiniteScroll
 *  }
 * }
 *```
  *
  *
  * @props {Boolean} [enabled=true] - 设置当前组件的可用状态, 如果为false, 则移除当前组件绑定的全部事件处理函数, 隐藏组件并且将状态设置为disabled
  * @props {String} [threshold='15%'] - 激活`onInfinite`事件的阈值. 阈值可以使百分比也可以是具体的px值. 如果为百分比(10%), 则距离底部10%的位置将为激活点; 如果为具体px值(100px), 则页面底部向下100px出为激活点.
  *
  * @fires component:InfiniteScroll#onInfinite
  *
  * @demo #/infinite-scroll
  * @see component:Base/Content
  *
  * @usage
  *
  * <InfiniteScroll threshold="20%" @onInfinite="onInfinite" loadingSpinner="ios" loadingText="正在加载..."></InfiniteScroll>
  *
  * // ....
  *  .ion-infinite-scroll{
  *      .loadedAll{
  *          display: none;
  *      }
  *   }
  *  .ion-infinite-scroll[state=disabled]{
  *      .loadedAll{
  *          display: block;
  *      }
  *  }
  *
  * // ....
  *
  * methods: {
  *    onInfinite(infiniteScroll){
  *      let _start = this.i;
  *      if(_start < 40){
  *        setTimeout(() => {
  *          for (; (10 + _start) > this.i; this.i++) {
  *           this.list.push(`item - ${this.i}`)
  *         }
  *         // 当前异步完成
  *         infiniteScroll.complete();
  *       }, 100)
  *     }else{
  *       // 当前异步结束, 没有新数据了
  *       infiniteScroll.enable(false);
  *     }
  *  }
  *  // ....
  */
import { isTrueProperty } from '../../util/util'
import ModeMixins from '../../themes/theme.mixins'
import VmSpinner from '../spinner'

const STATE_ENABLED = 'enabled'
const STATE_DISABLED = 'disabled'
const STATE_LOADING = 'loading'

export default {
  name: 'vm-infinite-scroll',
  mixins: [ModeMixins],
  components: {
    VmSpinner
  },
  inject: {
    contentComponent: {
      from: 'contentComponent',
      default: null
    }
  },
  data () {
    return {
      lastCheck: 0,
      isInited: false,
      isEnabled: isTrueProperty(this.enabled),

      state: STATE_ENABLED
    }
  },
  props: {
    /**
     * @input {boolean} If true, Whether or not the infinite scroll should be
     * enabled or not. Setting to `false` will remove scroll event listeners
     * and hide the display.
     */
    enabled: {
      type: [Boolean, String],
      default: true
    },
    /**
     * @input {string} The threshold distance from the bottom
     * of the content to call the `infinite` output event when scrolled.
     * The threshold value can be either a percent, or
     * in pixels. For example, use the value of `10%` for the `infinite`
     * output event to get called when the user has scrolled 10%
     * from the bottom of the page. Use the value `100px` when the
     * scroll is within 100 pixels from the bottom of the page.
     * Default is `15%`.
     */
    threshold: {
      type: String,
      default: '15%'
    },
    /**
     * @input {string} An animated SVG spinner that shows while loading.
     */
    loadingSpinner: {
      type: String,
      default () {
        return this.$config && this.$config.get('infiniteLoadingSpinner', this.$config.get('spinner', 'ios')) || 'ios'
      }
    },
    /**
     * @input {string} Optional text to display while loading.
     */
    loadingText: {
      type: String,
      default: 'Loading more data...'
    }
  },
  computed: {
    thr () {
      return this.threshold
    },
    thrPx () {
      if (this.threshold.indexOf('%') > -1) {
        return 0
      } else {
        return parseFloat(this.threshold)
      }
    },
    thrPc () {
      if (this.threshold.indexOf('%') > -1) {
        return parseFloat(this.threshold) / 100
      } else {
        return 0
      }
    }
  },
  watch: {
    enabled (val) {
      this.isEnabled = isTrueProperty(val)
      this.setListeners(this.isEnabled)
    }
  },
  mounted () {
    console.assert(this.contentComponent, 'InfiniteScroll组件必须要在Content组件下使用')
    this.contentComponent &&
      this.contentComponent.setElementClass('has-infinite-scroll', true)

    this.isInited = true
    this.setListeners(this.isEnabled)
  },
  destroy () {
    this.setListeners(false)
  },
  methods: {
    onScroll (ev) {
      if (this.state === STATE_LOADING || this.state === STATE_DISABLED) {
        return 1
      }

      if (this.lastCheck + 32 > ev.timeStamp) {
        // no need to check less than every XXms
        return 2
      }
      this.lastCheck = ev.timeStamp

      // ******** DOM READ ****************
      const infiniteHeight = this.getNativeElement().scrollHeight
      if (!infiniteHeight) {
        // if there is no height of this element then do nothing
        return 3
      }

      // ******** DOM READ ****************
      const d = this.contentComponent.getContentDimensions()
      const height = d.contentHeight

      const threshold = this.thrPc ? height * this.thrPc : this.thrPx

      // ******** DOM READS ABOVE / DOM WRITES BELOW ****************

      let distanceFromInfinite =
        d.scrollHeight - infiniteHeight - d.scrollTop - height - threshold
      if (distanceFromInfinite < 0) {
        if (this.state !== STATE_LOADING && this.state !== STATE_DISABLED) {
          this.state = STATE_LOADING
          this.$emit('onInfinite', this)
        }
        return 5
      }

      return 6
    },

    /**
     * Call `complete()` within the `infinite` output event handler when
     * your async operation has completed. For example, the `loading`
     * state is while the app is performing an asynchronous operation,
     * such as receiving more data from an AJAX request to add more items
     * to a data list. Once the data has been received and UI updated, you
     * then call this method to signify that the loading has completed.
     * This method will change the infinite scroll's state from `loading`
     * to `enabled`.
     */
    complete () {
      if (this.state !== STATE_LOADING) {
        return
      }

      this.state = STATE_ENABLED
      this.contentComponent.resize()
    },

    /**
     * Pass a promise inside `waitFor()` within the `infinite` output event handler in order to
     * change state of infiniteScroll to "complete"
     */
    waitFor (action) {
      const enable = this.complete.bind(this)
      action.then(enable, enable)
    },

    /**
     * Call `enable(false)` to disable the infinite scroll from actively
     * trying to receive new data while scrolling. This method is useful
     * when it is known that there is no more data that can be added, and
     * the infinite scroll is no longer needed.
     * @param {boolean} shouldEnable  If the infinite scroll should be
     * enabled or not. Setting to `false` will remove scroll event listeners
     * and hide the display.
     */
    enable (shouldEnable) {
      this.state = shouldEnable ? STATE_ENABLED : STATE_DISABLED
      this.setListeners(shouldEnable)
    },

    /**
     * @hidden
     */
    setListeners (shouldListen) {
      if (this.isInited) {
        if (shouldListen) {
          this.contentComponent.$on('onScroll', this.onScroll)
        } else {
          this.contentComponent.$off('onScroll', this.onScroll)
        }
      }
    }
  }
}
</script>

<style lang="scss">
@import "infinite-scroll";
</style>