@better-scroll/core
Version:
Minimalistic core scrolling for BetterScroll, it is pure and tiny
313 lines (265 loc) • 8.23 kB
text/typescript
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()
}
}