@aurora-design/my-scroll-pull-down
Version:
pull down to refresh, behave likes App list refreshing
329 lines (278 loc) • 9.13 kB
text/typescript
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
)
}
}