UNPKG

@better-scroll/core

Version:

Minimalistic core scrolling for BetterScroll, it is pure and tiny

313 lines (265 loc) 8.23 kB
import ActionsHandler from '../base/ActionsHandler' import { Behavior } from './Behavior' import DirectionLockAction from './DirectionLock' import { Animater } from '../animater' import { OptionsConstructor as BScrollOptions } from '../Options' import { TranslaterPoint } from '../translater' import { preventDefaultExceptionFn, TouchEvent, getNow, Probe, EventEmitter, between, Quadrant, maybePrevent, } from '@better-scroll/shared-utils' const applyQuadrantTransformation = ( deltaX: number, deltaY: number, quadrant: Quadrant ) => { if (quadrant === Quadrant.Second) { return [deltaY, -deltaX] } else if (quadrant === Quadrant.Third) { return [-deltaX, -deltaY] } else if (quadrant === Quadrant.Forth) { return [-deltaY, deltaX] } else { return [deltaX, deltaY] } } export default class ScrollerActions { hooks: EventEmitter scrollBehaviorX: Behavior scrollBehaviorY: Behavior actionsHandler: ActionsHandler animater: Animater options: BScrollOptions directionLockAction: DirectionLockAction fingerMoved: boolean contentMoved: boolean enabled: boolean startTime: number endTime: number ensuringInteger: boolean constructor( scrollBehaviorX: Behavior, scrollBehaviorY: Behavior, actionsHandler: ActionsHandler, animater: Animater, options: BScrollOptions ) { this.hooks = new EventEmitter([ 'start', 'beforeMove', 'scrollStart', 'scroll', 'beforeEnd', 'end', 'scrollEnd', 'contentNotMoved', 'detectMovingDirection', 'coordinateTransformation', ]) this.scrollBehaviorX = scrollBehaviorX this.scrollBehaviorY = scrollBehaviorY this.actionsHandler = actionsHandler this.animater = animater this.options = options this.directionLockAction = new DirectionLockAction( options.directionLockThreshold, options.freeScroll, options.eventPassthrough ) this.enabled = true this.bindActionsHandler() } private bindActionsHandler() { // [mouse|touch]start event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.start, (e: TouchEvent) => { if (!this.enabled) return true return this.handleStart(e) } ) // [mouse|touch]move event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.move, ({ deltaX, deltaY, e, }: { deltaX: number deltaY: number e: TouchEvent }) => { if (!this.enabled) return true const [transformateDeltaX, transformateDeltaY] = applyQuadrantTransformation(deltaX, deltaY, this.options.quadrant) const transformateDeltaData = { deltaX: transformateDeltaX, deltaY: transformateDeltaY, } this.hooks.trigger( this.hooks.eventTypes.coordinateTransformation, transformateDeltaData ) return this.handleMove( transformateDeltaData.deltaX, transformateDeltaData.deltaY, e ) } ) // [mouse|touch]end event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.end, (e: TouchEvent) => { if (!this.enabled) return true return this.handleEnd(e) } ) // click this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.click, (e: TouchEvent) => { // handle native click event if (this.enabled && !e._constructed) { this.handleClick(e) } } ) } private handleStart(e: TouchEvent) { const timestamp = getNow() this.fingerMoved = false this.contentMoved = false this.startTime = timestamp this.directionLockAction.reset() this.scrollBehaviorX.start() this.scrollBehaviorY.start() // force stopping last transition or animation this.animater.doStop() this.scrollBehaviorX.resetStartPos() this.scrollBehaviorY.resetStartPos() this.hooks.trigger(this.hooks.eventTypes.start, e) } private handleMove(deltaX: number, deltaY: number, e: TouchEvent) { if (this.hooks.trigger(this.hooks.eventTypes.beforeMove, e)) { return } const absDistX = this.scrollBehaviorX.getAbsDist(deltaX) const absDistY = this.scrollBehaviorY.getAbsDist(deltaY) const timestamp = getNow() // We need to move at least momentumLimitDistance pixels // for the scrolling to initiate if (this.checkMomentum(absDistX, absDistY, timestamp)) { return true } if (this.directionLockAction.checkMovingDirection(absDistX, absDistY, e)) { this.actionsHandler.setInitiated() return true } const delta = this.directionLockAction.adjustDelta(deltaX, deltaY) const prevX = this.scrollBehaviorX.getCurrentPos() const newX = this.scrollBehaviorX.move(delta.deltaX) const prevY = this.scrollBehaviorY.getCurrentPos() const newY = this.scrollBehaviorY.move(delta.deltaY) if (this.hooks.trigger(this.hooks.eventTypes.detectMovingDirection)) { return } if (!this.fingerMoved) { this.fingerMoved = true } const positionChanged = newX !== prevX || newY !== prevY if (!this.contentMoved && !positionChanged) { this.hooks.trigger(this.hooks.eventTypes.contentNotMoved) } if (!this.contentMoved && positionChanged) { this.contentMoved = true this.hooks.trigger(this.hooks.eventTypes.scrollStart) } if (this.contentMoved && positionChanged) { this.animater.translate({ x: newX, y: newY, }) this.dispatchScroll(timestamp) } } private dispatchScroll(timestamp: number) { // dispatch scroll in interval time if (timestamp - this.startTime > this.options.momentumLimitTime) { // refresh time and starting position to initiate a momentum this.startTime = timestamp this.scrollBehaviorX.updateStartPos() this.scrollBehaviorY.updateStartPos() if (this.options.probeType === Probe.Throttle) { this.hooks.trigger(this.hooks.eventTypes.scroll, this.getCurrentPos()) } } // dispatch scroll all the time if (this.options.probeType > Probe.Throttle) { this.hooks.trigger(this.hooks.eventTypes.scroll, this.getCurrentPos()) } } private checkMomentum(absDistX: number, absDistY: number, timestamp: number) { return ( timestamp - this.endTime > this.options.momentumLimitTime && absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance ) } private handleEnd(e: TouchEvent) { if (this.hooks.trigger(this.hooks.eventTypes.beforeEnd, e)) { return } let currentPos = this.getCurrentPos() this.scrollBehaviorX.updateDirection() this.scrollBehaviorY.updateDirection() if (this.hooks.trigger(this.hooks.eventTypes.end, e, currentPos)) { return true } currentPos = this.ensureIntegerPos(currentPos) this.animater.translate(currentPos) this.endTime = getNow() const duration = this.endTime - this.startTime this.hooks.trigger(this.hooks.eventTypes.scrollEnd, currentPos, duration) } private ensureIntegerPos(currentPos: TranslaterPoint) { this.ensuringInteger = true let { x, y } = currentPos const { minScrollPos: minScrollPosX, maxScrollPos: maxScrollPosX } = this.scrollBehaviorX const { minScrollPos: minScrollPosY, maxScrollPos: maxScrollPosY } = this.scrollBehaviorY x = x > 0 ? Math.ceil(x) : Math.floor(x) y = y > 0 ? Math.ceil(y) : Math.floor(y) x = between(x, maxScrollPosX, minScrollPosX) y = between(y, maxScrollPosY, minScrollPosY) return { x, y } } private handleClick(e: TouchEvent) { if ( !preventDefaultExceptionFn(e.target, this.options.preventDefaultException) ) { maybePrevent(e) e.stopPropagation() } } getCurrentPos(): TranslaterPoint { return { x: this.scrollBehaviorX.getCurrentPos(), y: this.scrollBehaviorY.getCurrentPos(), } } refresh() { this.endTime = 0 } destroy() { this.hooks.destroy() } }