Browse Source

feat: 新增下拉刷新&&无限加载

luobinbin 5 years ago
parent
commit
7fa9be6da6

+ 10 - 0
src/config.json

@@ -418,6 +418,16 @@
             "type": "component",
             "showDemo": true,
             "author": "liuguanglun"
+        },
+        {
+            "version": "1.0.0",
+            "name": "Pullrefreshloadmore",
+            "sort": "3",
+            "chnName": "下拉刷新&&无限加载",
+            "desc": "下拉刷新&&无限加载",
+            "type": "component",
+            "showDemo": true,
+            "author": "luobinbin"
         }
     ]
 }

+ 4 - 0
src/nutui.js

@@ -85,6 +85,9 @@ import Card from './packages/card/index.js';
 import './packages/card/card.scss';
 import Infiniteloading from './packages/infiniteloading/index.js';
 import './packages/infiniteloading/infiniteloading.scss';
+import Pullrefreshloadmore from './packages/pullrefreshloadmore/index.js';
+import './packages/pullrefreshloadmore/pullrefreshloadmore.scss';
+
 const packages = {
   Cell,
   Dialog,
@@ -129,6 +132,7 @@ const packages = {
   Field: Field,
   Card,
   Infiniteloading,
+  Pullrefreshloadmore,
 };
 
 const components = {};

+ 5 - 0
src/packages/pullrefreshloadmore/__test__/pullrefreshloadmore.spec.js

@@ -0,0 +1,5 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import Pullrefreshloadmore from '../pullrefreshloadmore.vue';
+import Vue from 'vue';
+
+describe('Pullrefreshloadmore.vue', () => {});

+ 58 - 0
src/packages/pullrefreshloadmore/components/Arrow.vue

@@ -0,0 +1,58 @@
+<template>
+  <svg viewBox="0 0 63.657 63.657" style="enable-background: new 0 0 63.657 63.657;" xml:space="preserve" width="512px" height="512px">
+    <g>
+      <g>
+        <g>
+          <g>
+            <polygon points="31.891,63.657 0.012,35.835 2.642,32.821 31.886,58.343 61.009,32.824 63.645,35.832" :fill="fillColor" />
+          </g>
+        </g>
+        <g>
+          <g>
+            <rect x="29.827" width="4" height="60" :fill="fillColor" />
+          </g>
+        </g>
+      </g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+      <g></g>
+    </g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+    <g></g>
+  </svg>
+</template>
+<script>
+export default {
+  props: {
+    fillColor: {
+      type: String,
+      default: '#AAA',
+    },
+  },
+};
+</script>

+ 42 - 0
src/packages/pullrefreshloadmore/components/Spinner.vue

@@ -0,0 +1,42 @@
+<template>
+  <svg class="spinner" viewBox="0 0 64 64">
+    <g stroke-width="4" stroke-linecap="round">
+      <line y1="17" y2="29" transform="translate(32,32) rotate(180)">
+        <animate attributeName="stroke-opacity" dur="750ms" values="1;.85;.7;.65;.55;.45;.35;.25;.15;.1;0;1" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(210)">
+        <animate attributeName="stroke-opacity" dur="750ms" values="0;1;.85;.7;.65;.55;.45;.35;.25;.15;.1;0" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(240)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".1;0;1;.85;.7;.65;.55;.45;.35;.25;.15;.1" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(270)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".15;.1;0;1;.85;.7;.65;.55;.45;.35;.25;.15" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(300)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".25;.15;.1;0;1;.85;.7;.65;.55;.45;.35;.25" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(330)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".35;.25;.15;.1;0;1;.85;.7;.65;.55;.45;.35" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(0)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".45;.35;.25;.15;.1;0;1;.85;.7;.65;.55;.45" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(30)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".55;.45;.35;.25;.15;.1;0;1;.85;.7;.65;.55" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(60)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".65;.55;.45;.35;.25;.15;.1;0;1;.85;.7;.65" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(90)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".7;.65;.55;.45;.35;.25;.15;.1;0;1;.85;.7" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(120)">
+        <animate attributeName="stroke-opacity" dur="750ms" values=".85;.7;.65;.55;.45;.35;.25;.15;.1;0;1;.85" repeatCount="indefinite"></animate>
+      </line>
+      <line y1="17" y2="29" transform="translate(32,32) rotate(150)">
+        <animate attributeName="stroke-opacity" dur="750ms" values="1;.85;.7;.65;.55;.45;.35;.25;.15;.1;0;1" repeatCount="indefinite"></animate>
+      </line>
+    </g>
+  </svg>
+</template>

+ 86 - 0
src/packages/pullrefreshloadmore/demo.vue

@@ -0,0 +1,86 @@
+<template>
+  <div
+    class="page has-navbar"
+    v-nav="{
+      title: 'nut-pullrefresh-loadmore',
+      showBackButton: true,
+      showMenuButton: true,
+      menuButtonText: menuButtonText,
+      onMenuButtonClick: clickFn,
+    }"
+  >
+    <nut-pullrefresh-loadmore class="page-content" :on-refresh="onRefresh" :on-infinite="onInfinite" ref="scroller">
+      <div
+        v-for="(item, index) in items"
+        :key="index"
+        @click="onItemClick(index)"
+        class="item item-borderless"
+        :class="{ 'item-stable': index % 2 == 0 }"
+      >
+        {{ item }}
+      </div>
+    </nut-pullrefresh-loadmore>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      items: [],
+      menuButtonText: '<span class="assertive">更多</span>',
+    };
+  },
+
+  mounted() {
+    for (let i = 1; i <= 20; i++) {
+      this.items.push(i + ' - keep walking, be 2 with you.');
+    }
+    this.top = 1;
+    this.bottom = 20;
+    setTimeout(() => {
+      if (this.$refs.scroller) this.$refs.scroller.resize();
+    });
+  },
+
+  methods: {
+    onRefresh() {
+      setTimeout(() => {
+        let start = this.top - 1;
+        for (let i = start; i > start - 10; i--) {
+          this.items.splice(0, 0, i + ' - keep walking, be 2 with you.');
+        }
+        this.top = this.top - 10;
+
+        setTimeout(() => {
+          if (this.$refs.scroller) this.$refs.scroller.finishPullToRefresh();
+        });
+      }, 1500);
+    },
+
+    onInfinite() {
+      setTimeout(() => {
+        let start = this.bottom + 1;
+        for (let i = start; i < start + 10; i++) {
+          this.items.push(i + ' - keep walking, be 2 with you.');
+        }
+        this.bottom = this.bottom + 10;
+
+        setTimeout(() => {
+          if (this.$refs.scroller) this.$refs.scroller.finishInfinite();
+        });
+      }, 1500);
+    },
+
+    onItemClick(index) {
+      console.log(index);
+    },
+
+    clickFn() {
+      console.log('do click');
+    },
+  },
+};
+</script>
+
+<style lang="scss"></style>

+ 66 - 0
src/packages/pullrefreshloadmore/doc.md

@@ -0,0 +1,66 @@
+# Icon 图标
+
+## 基本用法
+
+```html
+<nut-icon 
+  type="top"
+>
+</nut-icon>
+```
+## 自定义尺寸
+
+支持通过**size**设置图标大小,注意包含单位,如“40px”、“4rem”…
+
+```html
+<nut-icon 
+  type="action" 
+  size="40px"
+>
+</nut-icon>
+```
+
+## 自定义颜色
+
+支持通过**color**设置图标颜色,支持十六进制/RGB/RGBA等写法。
+
+```html
+<nut-icon 
+  type="trolley" 
+  color="#f0250f"
+>
+</nut-icon>
+```
+## 自定义svg图标
+
+支持通过**url**可以自定义添加额外的图片。
+
+```html
+<nut-icon type="self" :url="require('../../assets/svg/trolley.svg')"></nut-icon>
+```
+## type可选值
+
+nutui 自带图标库提供了下面的这些可选值。
+
+* top
+* down
+* right
+* action
+* more
+* trolley
+* search
+* tick
+* plus
+* minus
+* cross
+* circle-cross
+
+
+## Prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| type | 图标,可选值top/action/cross/down/right/more/plus/search/trolley/tick/minus/circle-cross | String | -
+| size | 尺寸,需要带单位 | String | -
+| color | 颜色 | String | -
+| url | 自定义图标路径,必须是svg格式,请用 require 方式引入 | String | - 

+ 9 - 0
src/packages/pullrefreshloadmore/index.js

@@ -0,0 +1,9 @@
+import Pullrefreshloadmore from './pullrefreshloadmore.vue';
+import './pullrefreshloadmore.scss';
+
+Pullrefreshloadmore.install = function (Vue) {
+  console.log('Pullrefreshloadmore.name', Pullrefreshloadmore.name);
+  Vue.component(Pullrefreshloadmore.name, Pullrefreshloadmore);
+};
+
+export default Pullrefreshloadmore;

File diff suppressed because it is too large
+ 1415 - 0
src/packages/pullrefreshloadmore/lib/core.js


+ 47 - 0
src/packages/pullrefreshloadmore/lib/render.js

@@ -0,0 +1,47 @@
+function getContentRender(content) {
+  var global = window;
+
+  var docStyle = document.documentElement.style;
+
+  var engine;
+  if (global.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
+    engine = 'presto';
+  } else if ('MozAppearance' in docStyle) {
+    engine = 'gecko';
+  } else if ('WebkitAppearance' in docStyle) {
+    engine = 'webkit';
+  } else if (typeof navigator.cpuClass === 'string') {
+    engine = 'trident';
+  }
+
+  var vendorPrefix = {
+    trident: 'ms',
+    gecko: 'Moz',
+    webkit: 'Webkit',
+    presto: 'O',
+  }[engine];
+
+  var helperElem = document.createElement('div');
+  var undef;
+
+  var perspectiveProperty = vendorPrefix + 'Perspective';
+  var transformProperty = vendorPrefix + 'Transform';
+
+  if (helperElem.style[perspectiveProperty] !== undef) {
+    return function (left, top, zoom) {
+      content.style[transformProperty] = 'translate3d(' + -left + 'px,' + -top + 'px,0) scale(' + zoom + ')';
+    };
+  } else if (helperElem.style[transformProperty] !== undef) {
+    return function (left, top, zoom) {
+      content.style[transformProperty] = 'translate(' + -left + 'px,' + -top + 'px) scale(' + zoom + ')';
+    };
+  } else {
+    return function (left, top, zoom) {
+      content.style.marginLeft = left ? -left / zoom + 'px' : '';
+      content.style.marginTop = top ? -top / zoom + 'px' : '';
+      content.style.zoom = zoom || '';
+    };
+  }
+}
+
+module.exports = getContentRender;

+ 112 - 0
src/packages/pullrefreshloadmore/pullrefreshloadmore.scss

@@ -0,0 +1,112 @@
+.nut-container {
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  overflow: hidden;
+
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -o-user-select: none;
+  user-select: none;
+}
+
+.nut-container > .nut-content {
+  width: 100%;
+  -webkit-transform-origin: left top;
+  -webkit-transform: translateZ(0);
+  -moz-transform-origin: left top;
+  -moz-transform: translateZ(0);
+  -ms-transform-origin: left top;
+  -ms-transform: translateZ(0);
+  -o-transform-origin: left top;
+  -o-transform: translateZ(0);
+  transform-origin: left top;
+  transform: translateZ(0);
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer {
+  width: 100%;
+  height: 60px;
+  margin-top: -60px;
+  text-align: center;
+  font-size: 16px;
+  color: #aaa;
+}
+
+.nut-container > .nut-content > .loading-layer {
+  width: 100%;
+  height: 60px;
+  text-align: center;
+  font-size: 16px;
+  line-height: 60px;
+  color: #aaa;
+  position: relative;
+}
+
+.nut-container > .nut-content > .loading-layer > .no-data-text {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+}
+
+.nut-container > .nut-content > .loading-layer > .spinner-holder,
+.nut-container > .nut-content > .loading-layer > .no-data-text {
+  opacity: 0;
+  transition: opacity 0.15s linear;
+  -webkit-transition: opacity 0.15s linear;
+}
+
+.nut-container > .nut-content > .loading-layer > .spinner-holder.active,
+.nut-container > .nut-content > .loading-layer > .no-data-text.active {
+  opacity: 1;
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer .spinner-holder,
+.nut-container > .nut-content > .loading-layer .spinner-holder {
+  text-align: center;
+  -webkit-font-smoothing: antialiased;
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer .spinner-holder .arrow,
+.nut-container > .nut-content > .loading-layer .spinner-holder .arrow {
+  width: 20px;
+  height: 20px;
+  margin: 8px auto 0 auto;
+
+  -webkit-transform: translate3d(0, 0, 0) rotate(0deg);
+  transform: translate3d(0, 0, 0) rotate(0deg);
+
+  -webkit-transition: -webkit-transform 0.2s linear;
+  transition: transform 0.2s linear;
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer .spinner-holder .text,
+.nut-container > .nut-content > .loading-layer .spinner-holder .text {
+  display: block;
+  margin: 0 auto;
+  font-size: 14px;
+  line-height: 20px;
+  color: #aaa;
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer .spinner-holder .spinner,
+.nut-container > .nut-content > .loading-layer .spinner-holder .spinner {
+  margin-top: 14px;
+  width: 32px;
+  height: 32px;
+  fill: #444;
+  stroke: #69717d;
+}
+
+.nut-container > .nut-content > .pull-to-refresh-layer.active .spinner-holder .arrow {
+  -webkit-transform: translate3d(0, 0, 0) rotate(180deg);
+  transform: translate3d(0, 0, 0) rotate(180deg);
+}

+ 389 - 0
src/packages/pullrefreshloadmore/pullrefreshloadmore.vue

@@ -0,0 +1,389 @@
+<template>
+  <div
+    class="nut-container"
+    :id="containerId"
+    @touchstart="touchStart($event)"
+    @touchmove="touchMove($event)"
+    @touchend="touchEnd($event)"
+    @mousedown="mouseDown($event)"
+    @mousemove="mouseMove($event)"
+    @mouseup="mouseUp($event)"
+  >
+    <div class="nut-content" :id="contentId">
+      <div v-if="onRefresh" class="pull-to-refresh-layer" :class="{ active: state == 1, 'active refreshing': state == 2 }">
+        <span class="spinner-holder">
+          <arrow class="arrow" :fillColor="refreshLayerColor" v-if="state != 2"></arrow>
+
+          <span class="text" v-if="state != 2" :style="{ color: refreshLayerColor }" v-text="refreshText"></span>
+
+          <span v-if="state == 2">
+            <slot name="refresh-spinner">
+              <spinner :style="{ fill: refreshLayerColor, stroke: refreshLayerColor }"></spinner>
+            </slot>
+          </span>
+        </span>
+      </div>
+
+      <slot></slot>
+
+      <div v-if="showInfiniteLayer" class="loading-layer">
+        <span class="spinner-holder" :class="{ active: showLoading }">
+          <slot name="infinite-spinner">
+            <spinner :style="{ fill: loadingLayerColor, stroke: loadingLayerColor }"></spinner>
+          </slot>
+        </span>
+
+        <div class="no-data-text" :class="{ active: !showLoading && loadingState == 2 }" :style="{ color: loadingLayerColor }" v-text="noDataText">
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import Scroller from './lib/core';
+import getContentRender from './lib/render';
+import Spinner from './components/Spinner';
+import Arrow from './components/Arrow';
+
+const re = /^[\d]+(\%)?$/;
+
+const widthAndHeightCoerce = (v) => {
+  if (v[v.length - 1] != '%') return v + 'px';
+  return v;
+};
+
+const widthAndHeightValidator = (v) => {
+  return re.test(v);
+};
+
+export default {
+  name: 'nut-pullrefresh-loadmore',
+  components: {
+    Spinner,
+    Arrow,
+  },
+
+  props: {
+    onRefresh: Function,
+    onInfinite: Function,
+
+    refreshText: {
+      type: String,
+      default: '下拉刷新',
+    },
+
+    noDataText: {
+      type: String,
+      default: '没有更多数据',
+    },
+
+    width: {
+      type: String,
+      default: '100%',
+      validator: widthAndHeightValidator,
+    },
+
+    height: {
+      type: String,
+      default: '100%',
+      validator: widthAndHeightValidator,
+    },
+
+    snapping: {
+      type: Boolean,
+      default: false,
+    },
+
+    snapWidth: {
+      type: Number,
+      default: 100,
+    },
+
+    snapHeight: {
+      type: Number,
+      default: 100,
+    },
+
+    animating: {
+      type: Boolean,
+      default: true,
+    },
+
+    animationDuration: {
+      type: Number,
+      default: 250,
+    },
+
+    bouncing: {
+      type: Boolean,
+      default: true,
+    },
+
+    refreshLayerColor: {
+      type: String,
+      default: '#AAA',
+    },
+
+    loadingLayerColor: {
+      type: String,
+      default: '#AAA',
+    },
+
+    cssClass: String, // content css class
+
+    minContentHeight: {
+      type: Number,
+      default: 0, // px
+    },
+  },
+
+  computed: {
+    w: function () {
+      return widthAndHeightCoerce(this.width);
+    },
+
+    h: function () {
+      return widthAndHeightCoerce(this.height);
+    },
+
+    showInfiniteLayer() {
+      let contentHeight = 0;
+      this.content ? (contentHeight = this.content.offsetHeight) : void 666;
+
+      return this.onInfinite ? contentHeight > this.minContentHeight : false;
+    },
+  },
+
+  data() {
+    return {
+      containerId: 'outer-' + Math.random().toString(36).substring(3, 8),
+      contentId: 'inner-' + Math.random().toString(36).substring(3, 8),
+      state: 0, // 0: pull to refresh, 1: release to refresh, 2: refreshing
+      loadingState: 0, // 0: stop, 1: loading, 2: stopping loading
+
+      showLoading: false,
+
+      container: undefined,
+      content: undefined,
+      scroller: undefined,
+      pullToRefreshLayer: undefined,
+      mousedown: false,
+      infiniteTimer: undefined,
+      resizeTimer: undefined,
+    };
+  },
+
+  mounted() {
+    this.container = document.getElementById(this.containerId);
+    this.container.style.width = this.w;
+    this.container.style.height = this.h;
+
+    this.content = document.getElementById(this.contentId);
+    if (this.cssClass) this.content.classList.add(this.cssClass);
+    this.pullToRefreshLayer = this.content.getElementsByTagName('div')[0];
+
+    let render = getContentRender(this.content);
+
+    let scrollerOptions = {
+      scrollingX: false,
+    };
+
+    this.scroller = new Scroller(render, {
+      scrollingX: false,
+      snapping: this.snapping,
+      animating: this.animating,
+      animationDuration: this.animationDuration,
+      bouncing: this.bouncing,
+    });
+
+    // enable PullToRefresh
+    if (this.onRefresh) {
+      this.scroller.activatePullToRefresh(
+        60,
+        () => {
+          this.state = 1;
+        },
+        () => {
+          this.state = 0;
+        },
+        () => {
+          this.state = 2;
+
+          this.$on('$finishPullToRefresh', () => {
+            setTimeout(() => {
+              this.state = 0;
+              this.finishPullToRefresh();
+            });
+          });
+
+          this.onRefresh(this.finishPullToRefresh);
+        }
+      );
+    }
+
+    // enable infinite loading
+    if (this.onInfinite) {
+      this.infiniteTimer = setInterval(() => {
+        let { left, top, zoom } = this.scroller.getValues();
+
+        // 在 keep alive 中 deactivated 的组件长宽变为 0
+        if (this.content.offsetHeight > 0 && top + 60 > this.content.offsetHeight - this.container.clientHeight) {
+          if (this.loadingState) return;
+          this.loadingState = 1;
+          this.showLoading = true;
+          this.onInfinite(this.finishInfinite);
+        }
+      }, 10);
+    }
+
+    // setup scroller
+    let rect = this.container.getBoundingClientRect();
+    this.scroller.setPosition(rect.left + this.container.clientLeft, rect.top + this.container.clientTop);
+
+    // snapping
+    if (this.snapping) {
+      // console.log(this.snapWidth, this.snapHeight)
+      this.scroller.setSnapSize(this.snapWidth, this.snapHeight);
+    }
+
+    // onContentResize
+    const contentSize = () => {
+      return {
+        width: this.content.offsetWidth,
+        height: this.content.offsetHeight,
+      };
+    };
+
+    let { content_width, content_height } = contentSize();
+
+    this.resizeTimer = setInterval(() => {
+      let { width, height } = contentSize();
+      if (width !== content_width || height !== content_height) {
+        content_width = width;
+        content_height = height;
+        this.resize();
+      }
+    }, 10);
+  },
+
+  destroyed() {
+    clearInterval(this.resizeTimer);
+    if (this.infiniteTimer) clearInterval(this.infiniteTimer);
+  },
+
+  methods: {
+    resize() {
+      let container = this.container;
+      let content = this.content;
+      this.scroller.setDimensions(container.clientWidth, container.clientHeight, content.offsetWidth, content.offsetHeight);
+    },
+
+    finishPullToRefresh() {
+      this.scroller.finishPullToRefresh();
+    },
+
+    finishInfinite(hideSpinner) {
+      this.loadingState = hideSpinner ? 2 : 0;
+      this.showLoading = false;
+
+      if (this.loadingState == 2) {
+        this.resetLoadingState();
+      }
+    },
+
+    triggerPullToRefresh() {
+      this.scroller.triggerPullToRefresh();
+    },
+
+    scrollTo(x, y, animate) {
+      this.scroller.scrollTo(x, y, animate);
+    },
+
+    scrollBy(x, y, animate) {
+      this.scroller.scrollBy(x, y, animate);
+    },
+
+    touchStart(e) {
+      // Don't react if initial down happens on a form element
+      if (e.target.tagName.match(/input|textarea|select/i)) {
+        return;
+      }
+      this.scroller.doTouchStart(e.touches, e.timeStamp);
+    },
+
+    touchMove(e) {
+      e.preventDefault();
+      this.scroller.doTouchMove(e.touches, e.timeStamp);
+    },
+
+    touchEnd(e) {
+      this.scroller.doTouchEnd(e.timeStamp);
+    },
+
+    mouseDown(e) {
+      // Don't react if initial down happens on a form element
+      if (e.target.tagName.match(/input|textarea|select/i)) {
+        return;
+      }
+      this.scroller.doTouchStart(
+        [
+          {
+            pageX: e.pageX,
+            pageY: e.pageY,
+          },
+        ],
+        e.timeStamp
+      );
+      this.mousedown = true;
+    },
+
+    mouseMove(e) {
+      if (!this.mousedown) {
+        return;
+      }
+      this.scroller.doTouchMove(
+        [
+          {
+            pageX: e.pageX,
+            pageY: e.pageY,
+          },
+        ],
+        e.timeStamp
+      );
+      this.mousedown = true;
+    },
+
+    mouseUp(e) {
+      if (!this.mousedown) {
+        return;
+      }
+      this.scroller.doTouchEnd(e.timeStamp);
+      this.mousedown = false;
+    },
+
+    // 获取位置
+    getPosition() {
+      let v = this.scroller.getValues();
+
+      return {
+        left: parseInt(v.left),
+        top: parseInt(v.top),
+      };
+    },
+
+    resetLoadingState() {
+      let { left, top, zoom } = this.scroller.getValues();
+      let container = this.container;
+      let content = this.content;
+
+      if (top + 60 > this.content.offsetHeight - this.container.clientHeight) {
+        setTimeout(() => {
+          this.resetLoadingState();
+        }, 1000);
+      } else {
+        this.loadingState = 0;
+      }
+    },
+  },
+};
+</script>