UNPKG

@aurora-design/my-scroll-pull-down

Version:

pull down to refresh, behave likes App list refreshing

329 lines (278 loc) 9.13 kB
import BScroll, { Boundary } from '@better-scroll/core' import { MouseWheelConfig } from '@better-scroll/mouse-wheel' import { ease, extend, EventEmitter, Probe } from '@better-scroll/shared-utils' import propertiesConfig from './propertiesConfig' export type PullDownRefreshOptions = Partial<PullDownRefreshConfig> | true // pulldownRefresh phase will go through: // DEFAULT -> MOVING -> FETCHING // or // DEFAULT -> MOVING const enum PullDownPhase { DEFAULT, MOVING, FETCHING, } const enum ThresholdBoundary { DEFAULT, INSIDE, OUTSIDE, } export interface PullDownRefreshConfig { threshold: number stop: number } declare module '@better-scroll/core' { interface CustomOptions { pullDownRefresh?: PullDownRefreshOptions } interface CustomAPI { pullDownRefresh: PluginAPI } } interface PluginAPI { finishPullDown(): void openPullDown(config?: PullDownRefreshOptions): void closePullDown(): void autoPullDownRefresh(): void } const PULLING_DOWN_EVENT = 'pullingDown' const ENTER_THRESHOLD_EVENT = 'enterThreshold' const LEAVE_THRESHOLD_EVENT = 'leaveThreshold' export default class PullDown implements PluginAPI { static pluginName = 'pullDownRefresh' private hooksFn: Array<[EventEmitter, string, Function]> pulling: PullDownPhase = PullDownPhase.DEFAULT thresholdBoundary: ThresholdBoundary = ThresholdBoundary.DEFAULT watching: boolean options: PullDownRefreshConfig cachedOriginanMinScrollY: number currentMinScrollY: number constructor(public scroll: BScroll) { this.init() } private setPulling(status: PullDownPhase) { this.pulling = status } private setThresholdBoundary(boundary: ThresholdBoundary) { this.thresholdBoundary = boundary } private init() { this.handleBScroll() this.handleOptions(this.scroll.options.pullDownRefresh) this.handleHooks() this.watch() } private handleBScroll() { this.scroll.registerType([ PULLING_DOWN_EVENT, ENTER_THRESHOLD_EVENT, LEAVE_THRESHOLD_EVENT, ]) this.scroll.proxy(propertiesConfig) } private handleOptions(userOptions: PullDownRefreshOptions = {}) { userOptions = ( userOptions === true ? {} : userOptions ) as Partial<PullDownRefreshConfig> const defaultOptions: PullDownRefreshConfig = { threshold: 90, stop: 40, } this.options = extend(defaultOptions, userOptions) this.scroll.options.probeType = Probe.Realtime } private handleHooks() { this.hooksFn = [] const scroller = this.scroll.scroller const scrollBehaviorY = scroller.scrollBehaviorY this.currentMinScrollY = this.cachedOriginanMinScrollY = scrollBehaviorY.minScrollPos this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.contentChanged, () => { this.finishPullDown() } ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { // content is smaller than wrapper if (boundary.maxScrollPos > 0) { // allow scrolling when content is not full of wrapper boundary.maxScrollPos = -1 } boundary.minScrollPos = this.currentMinScrollY } ) // integrate with mousewheel if (this.hasMouseWheelPlugin()) { this.registerHooks( this.scroll, this.scroll.eventTypes.alterOptions, (mouseWheelOptions: MouseWheelConfig) => { const SANE_DISCRETE_TIME = 300 const SANE_EASE_TIME = 350 mouseWheelOptions.discreteTime = SANE_DISCRETE_TIME // easeTime > discreteTime ensure goInto checkPullDown function mouseWheelOptions.easeTime = SANE_EASE_TIME } ) this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelEnd, () => { // mouseWheel need trigger checkPullDown manually scroller.hooks.trigger(scroller.hooks.eventTypes.end) } ) } } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private hasMouseWheelPlugin() { return !!this.scroll.eventTypes.alterOptions } private watch() { const scroller = this.scroll.scroller this.watching = true this.registerHooks( scroller.hooks, scroller.hooks.eventTypes.end, this.checkPullDown ) this.registerHooks( this.scroll, this.scroll.eventTypes.scrollStart, this.resetStateBeforeScrollStart ) this.registerHooks( this.scroll, this.scroll.eventTypes.scroll, this.checkLocationOfThresholdBoundary ) if (this.hasMouseWheelPlugin()) { this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelStart, this.resetStateBeforeScrollStart ) } } private resetStateBeforeScrollStart() { // current fetching pulldownRefresh has ended if (!this.isFetchingStatus()) { this.setPulling(PullDownPhase.MOVING) this.setThresholdBoundary(ThresholdBoundary.DEFAULT) } } private checkLocationOfThresholdBoundary() { // pulldownRefresh is in the phase of Moving if (this.pulling === PullDownPhase.MOVING) { const scroll = this.scroll // enter threshold boundary const enteredThresholdBoundary = this.thresholdBoundary !== ThresholdBoundary.INSIDE && this.locateInsideThresholdBoundary() // leave threshold boundary const leftThresholdBoundary = this.thresholdBoundary !== ThresholdBoundary.OUTSIDE && !this.locateInsideThresholdBoundary() if (enteredThresholdBoundary) { this.setThresholdBoundary(ThresholdBoundary.INSIDE) scroll.trigger(ENTER_THRESHOLD_EVENT) } if (leftThresholdBoundary) { this.setThresholdBoundary(ThresholdBoundary.OUTSIDE) scroll.trigger(LEAVE_THRESHOLD_EVENT) } } } private locateInsideThresholdBoundary() { return this.scroll.y <= this.options.threshold } private unwatch() { const scroll = this.scroll const scroller = scroll.scroller this.watching = false scroller.hooks.off(scroller.hooks.eventTypes.end, this.checkPullDown) scroll.off(scroll.eventTypes.scrollStart, this.resetStateBeforeScrollStart) scroll.off(scroll.eventTypes.scroll, this.checkLocationOfThresholdBoundary) if (this.hasMouseWheelPlugin()) { scroll.off( scroll.eventTypes.mousewheelStart, this.resetStateBeforeScrollStart ) } } private checkPullDown() { const { threshold, stop } = this.options // check if a real pull down action if (this.scroll.y < threshold) { return false } if (this.pulling === PullDownPhase.MOVING) { this.modifyBehaviorYBoundary(stop) this.setPulling(PullDownPhase.FETCHING) this.scroll.trigger(PULLING_DOWN_EVENT) } this.scroll.scrollTo( this.scroll.x, stop, this.scroll.options.bounceTime, ease.bounce ) return this.isFetchingStatus() } private isFetchingStatus() { return this.pulling === PullDownPhase.FETCHING } private modifyBehaviorYBoundary(stopDistance: number) { const scrollBehaviorY = this.scroll.scroller.scrollBehaviorY // manually modify minScrollPos for a hang animation // to prevent from resetPosition this.cachedOriginanMinScrollY = scrollBehaviorY.minScrollPos this.currentMinScrollY = stopDistance scrollBehaviorY.computeBoundary() } finishPullDown() { if (this.isFetchingStatus()) { const scrollBehaviorY = this.scroll.scroller.scrollBehaviorY // restore minScrollY since the hang animation has ended this.currentMinScrollY = this.cachedOriginanMinScrollY scrollBehaviorY.computeBoundary() this.setPulling(PullDownPhase.DEFAULT) this.scroll.resetPosition(this.scroll.options.bounceTime, ease.bounce) } } // allow 'true' type is compat for beta version implements openPullDown(config: PullDownRefreshOptions = {}) { this.handleOptions(config) if (!this.watching) { this.watch() } } closePullDown() { this.unwatch() } autoPullDownRefresh() { const { threshold, stop } = this.options if (this.isFetchingStatus() || !this.watching) { return } this.modifyBehaviorYBoundary(stop) this.scroll.trigger(this.scroll.eventTypes.scrollStart) this.scroll.scrollTo(this.scroll.x, threshold) this.setPulling(PullDownPhase.FETCHING) this.scroll.trigger(PULLING_DOWN_EVENT) this.scroll.scrollTo( this.scroll.x, stop, this.scroll.options.bounceTime, ease.bounce ) } }