refresher/refresher.vue

  1. <template>
  2. <div class="ion-refresher" :class="{'refresher-active':(state !== 'inactive')}" :style="{'top':top}" :state="state">
  3. <slot></slot>
  4. </div>
  5. </template>
  6. <style lang="scss" src="./style.scss"></style>
  7. <script type="text/javascript">
  8. /**
  9. * @component Refresher
  10. * @description
  11. *
  12. * ## 数据加载 / Refresher下拉刷新组件
  13. *
  14. * ### 说明
  15. *
  16. * Refresher组件是在Content组件下使用, 并提供了下拉刷新的功能. 使用前需要将Refresher组件放在Content组件内所有内容的前面, 并加上`slot="refresher"`属性. 如果Refresher组件使用完毕希望禁用, 请使用`enabled`属性, 而不是使用`v-if`指令.
  17. *
  18. * ### 事件
  19. *
  20. * 事件传递组件的this, 可用的两个方法为: complete/cancel. 当然可以使用ref获取组件的实例
  21. *
  22. * ### 注意
  23. *
  24. * 目前这个组件只适合在原生滚动下使用, 当使用js滚动时会异常
  25. *
  26. * ### 关于指示器RefresherContent
  27. *
  28. * 这个组件是可以定制化的
  29. *
  30. * @props {Number} [closeDuration=280] - 回复到 inactive 状态的动画时间
  31. * @props {Boolean} [enabled=true] - 组件是否可用
  32. * @props {Number} [pullMax=200] - 下拉的最大值, 超过则直接进入 refreshing状态
  33. * @props {Number} [pullMin=70] - 下拉进入refreshing状态的最小值
  34. * @props {Number} [snapbackDuration=280] - 回复到 refreshing 状态的动画时间
  35. *
  36. * @fires component:Refresher#onRefresh - 进入 refreshing 状态时触发, 事件传递组件的this
  37. * @fires component:Refresher#onPull - 下拉并且看到了refresher, 事件传递组件的this
  38. * @fires component:Refresher#onStart - 开始下拉的事件, 事件传递组件的this
  39. *
  40. * @slot 空 - 指示器RefresherContent组件的插槽
  41. *
  42. * @demo #/refresher
  43. * @see component:Content
  44. * @usage
  45. * <Page>
  46. * <Header>
  47. * <Navbar>
  48. * <Title>Refresher</Title>
  49. * </Navbar>
  50. * </Header>
  51. * <Content>
  52. * <Refresher slot="refresher" @onRefresh="doRefresh($event)" @onPull="doPulling($event)">
  53. * <RefresherContent pullingText="下拉刷新..." refreshingText="正在刷新..."></RefresherContent>
  54. * </Refresher>
  55. * <List>
  56. * <Item v-for="i in list">{{i}}</Item>
  57. * </List>
  58. * </Content>
  59. * </Page>
  60. *
  61. *
  62. * // ...
  63. *
  64. * methods: {
  65. * doRefresh(ins){
  66. * let _start = this.i;
  67. * setTimeout(() => {
  68. * for (; (10 + _start) > this.i; this.i++) {
  69. * this.list.unshift(`item - ${this.i}`)
  70. * }
  71. * // 当前异步完成
  72. * ins.complete();
  73. * console.debug('onInfinite-complete')
  74. * }, 500)
  75. * },
  76. * },
  77. * */
  78. import { pointerCoord } from '../../util/util'
  79. import registerListener from '../../util/register-listener'
  80. import css from '../../util/get-css'
  81. import urlChange from '../../util/url-change'
  82. const STATE_INACTIVE = 'inactive'
  83. const STATE_PULLING = 'pulling'
  84. const STATE_READY = 'ready'
  85. const STATE_REFRESHING = 'refreshing'
  86. const STATE_CANCELLING = 'cancelling'
  87. const STATE_COMPLETING = 'completing'
  88. const DAMP = 0.5// 滑动阻尼
  89. export default {
  90. name: 'Refresher',
  91. // Content组件的实例注入
  92. inject: ['contentComponent', 'appComponent'],
  93. data () {
  94. return {
  95. // -------- public --------
  96. /**
  97. * @name state
  98. * @type {String}
  99. * @description Refresher 的状态, 可以使下面的一个值:
  100. * - `inactive` - Refresher 当前时隐藏状态, 没有下拉 或者 刷新
  101. * - `pulling` - Refresher 当前正在被下拉, 但是还没达到触发刷新的点
  102. * - `cancelling` - 还没达到触发刷新的点时, 用户松手, 动画恢复完毕后改为`inactive`状态
  103. * - `ready` - 用户已经下拉到达触发点, 如果用户松手, 则进入`refreshing`状态
  104. * - `refreshing` - Refresher 处于刷新状态, 等待异步操作的完成. 一旦执行了refresh的 `complete()` 方法, Refresher 将进入 `completing` 状态.
  105. * - `completing` - Refresher 的`refreshing` 状态结束, Refresher之后会执行关闭的动画, 如果动画结束, 则回退到`inactive` 状态.
  106. * */
  107. state: STATE_INACTIVE,
  108. /**
  109. * @name startY
  110. * @type {Number}
  111. * @description 下拉的起始点
  112. * */
  113. startY: null, // 下拉的起始点
  114. /**
  115. * @name currentY
  116. * @type {Number}
  117. * @description 当前的点
  118. * */
  119. currentY: null, // 当前的点
  120. /**
  121. * @name deltaY
  122. * @type {Number}
  123. * @description 起始点和当前点的距离
  124. * */
  125. deltaY: null, // 起始点和当前点的距离
  126. /**
  127. * @name progress
  128. * @type {Number}
  129. * @description 表示对当前进度.
  130. * - 0: 原始状态,为下拉
  131. * - =1: 到达refreshing状态的最小值
  132. * - \>1: 超过refreshing状态的最小值
  133. * */
  134. progress: null, // 表示对当前进度. 0:原始状态,为下拉; >1: 刷新开始
  135. // -------- private --------
  136. unReg: null,
  137. appliedStyles: false,
  138. didStart: false,
  139. lastCheck: 0,
  140. unregs: [], // 解绑的事件列表
  141. damp: DAMP, // 滑动阻尼
  142. top: null // style的top
  143. }
  144. },
  145. props: {
  146. // 回复到 inactive 状态的时间
  147. closeDuration: {
  148. type: Number,
  149. default: 280
  150. },
  151. enabled: {
  152. type: Boolean,
  153. default: true
  154. },
  155. // 下拉的最大值, 超过则拉不动
  156. pullMax: {
  157. type: Number,
  158. default: 200
  159. },
  160. // 下拉进入refreshing状态的最小值
  161. pullMin: {
  162. type: Number,
  163. default: 70
  164. },
  165. // 回复到 refreshing 状态的时间
  166. snapbackDuration: {
  167. type: Number,
  168. default: 280
  169. }
  170. },
  171. watch: {
  172. enabled (val) {
  173. this.$_setListeners(val)
  174. },
  175. state (val) {
  176. if (val === STATE_INACTIVE) {
  177. this.appComponent && this.appComponent.setDisableScroll(false)
  178. }
  179. if (val === STATE_PULLING) {
  180. this.appComponent && this.appComponent.setDisableScroll(true)
  181. }
  182. }
  183. },
  184. methods: {
  185. /**
  186. * @function complete
  187. * @description
  188. * 异步数据请求成功后, 调用这个方法; refresher将会关闭,
  189. * 状态由`refreshing` -> `completing`.
  190. */
  191. complete () {
  192. this.$_closeRefresher(STATE_COMPLETING, '120ms')
  193. // 重新计算尺寸, 必须
  194. this.contentComponent && this.contentComponent.resize()
  195. },
  196. /**
  197. * @function cancel
  198. * @description
  199. * 取消 refresher, 其状态由`refreshing` -> `cancelling`
  200. */
  201. cancel () {
  202. this.$_closeRefresher(STATE_CANCELLING, '')
  203. },
  204. /**
  205. * @param {Boolean} shouldListen -
  206. * */
  207. $_setListeners (shouldListen) {
  208. // 如果解绑函数存在则全部解绑
  209. if (this.unregs && this.unregs.length > 0) {
  210. this.unregs.forEach((_unreg) => {
  211. _unreg && _unreg()
  212. })
  213. }
  214. // 如果为true, 则添加事件监听
  215. // 等待Content完毕
  216. this.$nextTick(() => {
  217. if (shouldListen && this.contentComponent) {
  218. let contentElement = this.contentComponent.$_getScrollElement()
  219. console.assert(contentElement, 'Refresh Component need Content Ready!::<Component>$_setListeners()')
  220. // TODO: 对于点击事件应该同统一封装一层
  221. registerListener(contentElement, 'touchstart', this.$_pointerDownHandler.bind(this), {'passive': false}, this.unregs)
  222. registerListener(contentElement, 'mousedown', this.$_pointerDownHandler.bind(this), {'passive': false}, this.unregs)
  223. registerListener(contentElement, 'touchmove', this.$_pointerMoveHandler.bind(this), {'passive': false}, this.unregs)
  224. registerListener(contentElement, 'mousemove', this.$_pointerMoveHandler.bind(this), {'passive': false}, this.unregs)
  225. registerListener(contentElement, 'touchend', this.$_pointerUpHandler.bind(this), {'passive': false}, this.unregs)
  226. registerListener(contentElement, 'mouseup', this.$_pointerUpHandler.bind(this), {'passive': false}, this.unregs)
  227. }
  228. })
  229. },
  230. /**
  231. * @param {TouchEvent} ev - 点击事件
  232. * */
  233. $_pointerDownHandler (ev) {
  234. // 如果多点触摸, 则直接返回
  235. if (ev.touches && ev.touches.length > 1) {
  236. return false
  237. }
  238. if (this.state !== STATE_INACTIVE) {
  239. return false
  240. }
  241. let scrollHostScrollTop = this.contentComponent.scrollView.getTop()
  242. // 如果当前的scrollTop大于0, 意味着页面在别的位置, 不是停在顶部, 不是在refresher状态, 直接退出
  243. if (scrollHostScrollTop > 0) {
  244. return false
  245. }
  246. let coord = pointerCoord(ev)
  247. // 记录开始位置
  248. this.startY = this.currentY = coord.y
  249. this.progress = 0
  250. this.state = STATE_INACTIVE
  251. return true
  252. },
  253. /**
  254. * @param {TouchEvent} ev - 点击事件
  255. * */
  256. $_pointerMoveHandler (ev) {
  257. // 滑动部分的处理函数在滑动过程中会执行很多次, 因此会有节流的处理,
  258. // 另外DOM的操作除非必要, 否则不进行
  259. // 多点触摸则直接返回
  260. if (ev.touches && ev.touches.length > 1) {
  261. return 1
  262. }
  263. // 如果页面在滚动, 或者scrollTop != 0
  264. if (this.contentComponent && this.contentComponent.isScrolling) {
  265. return 9
  266. }
  267. // 不能存在scrollTop为负的情况
  268. if (this.contentComponent && this.contentComponent.scrollView && this.contentComponent.scrollView.ev.scrollTop < 0) {
  269. // fix: 有些机型存在scrollTop为负的情况
  270. return 10
  271. }
  272. // 以下状态将返回
  273. // 没有开始位置/正在refreshing状态/正在closing(cancelling/completing)
  274. if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_COMPLETING) {
  275. return 2
  276. }
  277. // 在16ms之内如果连续触发,则不处理(节流)
  278. let now = Date.now()
  279. const T = 16
  280. if (this.lastCheck + T > now) {
  281. return 3
  282. }
  283. // 节流 -- 记录上次的节流时间
  284. this.lastCheck = now
  285. let coord = pointerCoord(ev)
  286. this.currentY = coord.y
  287. // 记录拉动的距离
  288. this.deltaY = (coord.y - this.startY)
  289. // 当前是向上滑动, 所以应该设置state为inactive
  290. if (this.deltaY <= 0) {
  291. // 这个是向上滚动, 没使用 refresher 的功能, 故现在是inactive状态
  292. this.progress = 0
  293. if (this.state !== STATE_INACTIVE) {
  294. this.state = STATE_INACTIVE
  295. }
  296. if (this.appliedStyles) {
  297. // reset the styles only if they were applied
  298. this.$_setCss(0, '', false, '') // y/duration/overflow/delay
  299. return 5
  300. }
  301. return 6
  302. }
  303. // 正式进入 refresher
  304. if (this.state === STATE_INACTIVE) {
  305. let scrollHostScrollTop = this.contentComponent.scrollView.ev.scrollTop
  306. if (scrollHostScrollTop > 0) {
  307. this.progress = 0
  308. this.startY = null
  309. return 7
  310. }
  311. // 步入正题
  312. this.state = STATE_PULLING
  313. }
  314. if (!this.deltaY) {
  315. this.progress = 0
  316. return 8
  317. }
  318. // prevent native scroll events
  319. ev.preventDefault()
  320. // 进入 pulling 状态, 开始移动scroll element
  321. this.$_setCss((this.deltaY * this.damp), '0ms', true, '')
  322. // 进行滚动吧
  323. this.$_goScroll()
  324. },
  325. $_goScroll () {
  326. // set pull progress
  327. this.progress = (this.deltaY * this.damp / this.pullMin)
  328. // 对外发送 onStart 事件
  329. if (!this.didStart) {
  330. this.didStart = true
  331. /**
  332. * @event component:Refresher#onStart
  333. * @description 开始下拉的事件, 事件传递组件的this
  334. * @property {Component} this - 组件实例
  335. */
  336. this.$emit('onStart', this)
  337. }
  338. // 对外发送 onPull 事件
  339. /**
  340. * @event component:Refresher#onPull
  341. * @description 下拉并且看到了refresher, 事件传递组件的this
  342. * @property {Component} this - 组件实例
  343. */
  344. this.$emit('onPull', this)
  345. // do nothing if the delta is less than the pull threshold
  346. if (this.deltaY * this.damp < this.pullMin) {
  347. // ensure it stays in the pulling state, cuz its not ready yet
  348. this.state = STATE_PULLING
  349. return 2
  350. }
  351. if (this.deltaY * this.damp > this.pullMax) {
  352. // they pulled farther than the max, so kick off the refresh
  353. this.$_beginRefresh()
  354. return 3
  355. }
  356. // 正好在最大和最小值之间, 进入 ready 状态吧
  357. this.state = STATE_READY
  358. return 4
  359. },
  360. $_pointerUpHandler () {
  361. if (this.state === STATE_READY) {
  362. this.$_beginRefresh()
  363. } else if (this.state === STATE_PULLING) {
  364. // 返回原点
  365. this.cancel()
  366. }
  367. // reset
  368. this.startY = null
  369. },
  370. $_beginRefresh () {
  371. this.state = STATE_REFRESHING
  372. // 将content放置在开口处
  373. this.$_setCss(this.pullMin, (this.snapbackDuration + 'ms'), true, '')
  374. // 发送事件 onRefresh
  375. /**
  376. * @event component:Refresher#onRefresh
  377. * @description 进入 refreshing 状态时触发, 事件传递组件的this
  378. * @property {Component} this - 组件实例
  379. */
  380. this.$emit('onRefresh', this)
  381. },
  382. /**
  383. * @param {string} state
  384. * @param {string} delay
  385. * */
  386. $_closeRefresher (state, delay) {
  387. var timer // number
  388. function close (ev) {
  389. // closing is done, return to inactive state
  390. if (ev) {
  391. clearTimeout(timer)
  392. }
  393. this.state = STATE_INACTIVE
  394. this.progress = 0
  395. this.didStart = this.startY = this.currentY = this.deltaY = null
  396. this.$_setCss(0, '0ms', false, '')
  397. }
  398. // create fallback timer incase something goes wrong with transitionEnd event
  399. timer = window.setTimeout(close.bind(this), 600)
  400. // create transition end event on the content's scroll element
  401. this.contentComponent.$_onScrollElementTransitionEnd(close.bind(this))
  402. // reset set the styles on the scroll element
  403. // set that the refresh is actively cancelling/completing
  404. this.state = state
  405. this.$_setCss(0, '', true, delay)
  406. },
  407. /**
  408. * @param {Number} y - 纵坐标
  409. * @param {String} duration - duration
  410. * @param {Boolean} overflowVisible - overflowVisible
  411. * @param {String} delay - delay
  412. **/
  413. $_setCss (y, duration, overflowVisible, delay) {
  414. this.appliedStyles = (y > 0)
  415. if (this.contentComponent) {
  416. this.contentComponent.$_setScrollElementStyle(css.transform, ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)'))
  417. this.contentComponent.$_setScrollElementStyle(css.transitionDuration, duration)
  418. this.contentComponent.$_setScrollElementStyle(css.transitionDelay, delay)
  419. this.contentComponent.$_setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : ''))
  420. }
  421. }
  422. },
  423. created () {
  424. this.unReg = urlChange(() => {
  425. this.$app && this.$app.$_enableScroll && this.$app.$_enableScroll()
  426. this.unReg && this.unReg()
  427. this.cancel()
  428. })
  429. },
  430. mounted () {
  431. if (this.contentComponent) {
  432. this.contentComponent.refreshClass['has-refresher'] = true
  433. }
  434. this.$_setListeners(this.enabled)
  435. this.$root.$on('content:mounted', () => {
  436. // refresher定位
  437. if (this.contentComponent) {
  438. let contentTop = this.contentComponent.headerBarHeight || 0
  439. this.top = `${contentTop}px`
  440. }
  441. })
  442. }
  443. }
  444. </script>