@better-scroll/indicators
Version:
used as parallax scrolling, magnifier effects
418 lines (368 loc) • 10.8 kB
text/typescript
import BScroll from '@better-scroll/core'
import { IndicatorOptions, Ratio, Postion, ValueSign } from './types'
import {
EventRegister,
EventEmitter,
getRect,
getClientSize,
getNow,
between,
Probe,
TouchEvent,
style,
maybePrevent,
} from '@better-scroll/shared-utils'
const resolveRatioOption = (ratioConfig?: Ratio) => {
let ret = {
ratioX: 0,
ratioY: 0,
}
/* istanbul ignore if */
if (!ratioConfig) {
return ret
}
if (typeof ratioConfig === 'number') {
ret.ratioX = ret.ratioY = ratioConfig
} else if (typeof ratioConfig === 'object' && ratioConfig) {
ret.ratioX = ratioConfig.x || 0
ret.ratioY = ratioConfig.y || 0
}
return ret
}
const handleBubbleAndCancelable = (e: TouchEvent) => {
maybePrevent(e)
e.stopPropagation()
}
export default class Indicator {
wrapper: HTMLElement
indicatorEl: HTMLElement
maxScrollX: number
minScrollX: number
ratioX: number
maxScrollY: number
minScrollY: number
translateXSign: ValueSign
translateYSign: ValueSign
ratioY: number
currentPos: Postion = {
x: 0,
y: 0,
}
moved: boolean
startTime: number
initiated: boolean
lastPointX: number
lastPointY: number
startEventRegister: EventRegister
moveEventRegister: EventRegister
endEventRegister: EventRegister
hooksFn: [EventEmitter, string, Function][] = []
constructor(public scroll: BScroll, public options: IndicatorOptions) {
this.handleDOM()
this.handleHooks()
this.handleInteractive()
}
private handleDOM() {
const { relationElement, relationElementHandleElementIndex = 0 } =
this.options
this.wrapper = relationElement
this.indicatorEl = this.wrapper.children[
relationElementHandleElementIndex
] as HTMLElement
}
private handleHooks() {
const scroll = this.scroll
const scrollHooks = scroll.hooks
const translaterHooks = scroll.scroller.translater.hooks
const animaterHooks = scroll.scroller.animater.hooks
this.registerHooks(
scrollHooks,
scrollHooks.eventTypes.refresh,
this.refresh
)
this.registerHooks(
translaterHooks,
translaterHooks.eventTypes.translate,
(pos: Postion) => {
this.updatePosition(pos)
}
)
this.registerHooks(
animaterHooks,
animaterHooks.eventTypes.time,
this.transitionTime
)
this.registerHooks(
animaterHooks,
animaterHooks.eventTypes.timeFunction,
this.transitionTimingFunction
)
}
private transitionTime(time: number = 0) {
this.indicatorEl.style[style.transitionDuration as any] = time + 'ms'
}
private transitionTimingFunction(easing: string) {
this.indicatorEl.style[style.transitionTimingFunction as any] = easing
}
private handleInteractive() {
if (this.options.interactive !== false) {
this.registerEvents()
}
}
private registerHooks(hooks: EventEmitter, name: string, handler: Function) {
hooks.on(name, handler, this)
this.hooksFn.push([hooks, name, handler])
}
private registerEvents() {
const { disableMouse, disableTouch } = this.scroll.options
const startEvents = []
const moveEvents = []
const endEvents = []
if (!disableMouse) {
startEvents.push({
name: 'mousedown',
handler: this.start.bind(this),
})
moveEvents.push({
name: 'mousemove',
handler: this.move.bind(this),
})
endEvents.push({
name: 'mouseup',
handler: this.end.bind(this),
})
}
if (!disableTouch) {
startEvents.push({
name: 'touchstart',
handler: this.start.bind(this),
})
moveEvents.push({
name: 'touchmove',
handler: this.move.bind(this),
})
endEvents.push(
{
name: 'touchend',
handler: this.end.bind(this),
},
{
name: 'touchcancel',
handler: this.end.bind(this),
}
)
}
this.startEventRegister = new EventRegister(this.indicatorEl, startEvents)
this.moveEventRegister = new EventRegister(window, moveEvents)
this.endEventRegister = new EventRegister(window, endEvents)
}
refresh() {
const {
x,
y,
hasHorizontalScroll,
hasVerticalScroll,
maxScrollX: maxBScrollX,
maxScrollY: maxBScrollY,
} = this.scroll
const { ratioX, ratioY } = resolveRatioOption(this.options.ratio)
const { width: wrapperWidth, height: wrapperHeight } = getClientSize(
this.wrapper
)
const { width: indicatorWidth, height: indicatorHeight } = getRect(
this.indicatorEl
)
if (hasHorizontalScroll) {
this.maxScrollX = wrapperWidth - indicatorWidth
this.translateXSign =
this.maxScrollX > 0 ? ValueSign.Positive : ValueSign.NotPositive
this.minScrollX = 0
// ensure positive
this.ratioX = ratioX ? ratioX : Math.abs(this.maxScrollX / maxBScrollX)
}
if (hasVerticalScroll) {
this.maxScrollY = wrapperHeight - indicatorHeight
this.translateYSign =
this.maxScrollY > 0 ? ValueSign.Positive : ValueSign.NotPositive
this.minScrollY = 0
this.ratioY = ratioY ? ratioY : Math.abs(this.maxScrollY / maxBScrollY)
}
this.updatePosition({
x,
y,
})
}
private start(e: TouchEvent) {
if (this.BScrollIsDisabled()) {
return
}
let point = (e.touches ? e.touches[0] : e) as Touch
handleBubbleAndCancelable(e)
this.initiated = true
this.moved = false
this.lastPointX = point.pageX
this.lastPointY = point.pageY
this.startTime = getNow()
this.scroll.scroller.hooks.trigger(
this.scroll.scroller.hooks.eventTypes.beforeScrollStart
)
}
private BScrollIsDisabled() {
return !this.scroll.enabled
}
private move(e: TouchEvent) {
if (!this.initiated) {
return
}
let point = (e.touches ? e.touches[0] : e) as Touch
const pointX = point.pageX
const pointY = point.pageY
handleBubbleAndCancelable(e)
let deltaX = pointX - this.lastPointX
let deltaY = pointY - this.lastPointY
this.lastPointX = pointX
this.lastPointY = pointY
if (!this.moved && !this.indicatorNotMoved(deltaX, deltaY)) {
this.moved = true
this.scroll.scroller.hooks.trigger(
this.scroll.scroller.hooks.eventTypes.scrollStart
)
}
if (this.moved) {
const newPos = this.getBScrollPosByRatio(this.currentPos, deltaX, deltaY)
this.syncBScroll(newPos)
}
}
private end(e: TouchEvent) {
if (!this.initiated) {
return
}
this.initiated = false
handleBubbleAndCancelable(e)
if (this.moved) {
const { x, y } = this.scroll
this.scroll.scroller.hooks.trigger(
this.scroll.scroller.hooks.eventTypes.scrollEnd,
{
x,
y,
}
)
}
}
private getBScrollPosByRatio(
currentPos: Postion,
deltaX: number,
deltaY: number
) {
const { x: currentX, y: currentY } = currentPos
const {
hasHorizontalScroll,
hasVerticalScroll,
minScrollX: BScrollMinScrollX,
maxScrollX: BScrollMaxScrollX,
minScrollY: BScrollMinScrollY,
maxScrollY: BScrollMaxScrollY,
} = this.scroll
let { x, y } = this.scroll
if (hasHorizontalScroll) {
const newPosX = between(
currentX + deltaX,
Math.min(this.minScrollX, this.maxScrollX),
Math.max(this.minScrollX, this.maxScrollX)
)
const roundX = Math.round((newPosX / this.ratioX) * this.translateXSign)
x = between(roundX, BScrollMaxScrollX, BScrollMinScrollX)
}
if (hasVerticalScroll) {
const newPosY = between(
currentY + deltaY,
Math.min(this.minScrollY, this.maxScrollY),
Math.max(this.minScrollY, this.maxScrollY)
)
const roundY = Math.round((newPosY / this.ratioY) * this.translateYSign)
y = between(roundY, BScrollMaxScrollY, BScrollMinScrollY)
}
return { x, y }
}
private indicatorNotMoved(deltaX: number, deltaY: number): boolean {
const { x, y } = this.currentPos
const xNotMoved =
(x === this.minScrollX && deltaX <= 0) ||
(x === this.maxScrollX && deltaX >= 0)
const yNotMoved =
(y === this.minScrollY && deltaY <= 0) ||
(y === this.maxScrollY && deltaY >= 0)
return xNotMoved && yNotMoved
}
private syncBScroll(newPos: Postion) {
const timestamp = getNow()
const { options, scroller } = this.scroll
const { probeType, momentumLimitTime } = options
scroller.translater.translate(newPos)
// dispatch scroll in interval time
if (timestamp - this.startTime > momentumLimitTime) {
this.startTime = timestamp
if (probeType === Probe.Throttle) {
scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, newPos)
}
}
// dispatch scroll all the time
if (probeType > Probe.Throttle) {
scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, newPos)
}
}
updatePosition(BScrollPos: Postion) {
const newIndicatorPos = this.getIndicatorPosByRatio(BScrollPos)
this.applyTransformProperty(newIndicatorPos)
this.currentPos = { ...newIndicatorPos }
}
private applyTransformProperty(pos: Postion) {
const translateZ = this.scroll.options.translateZ
const transformProperties = [
`translateX(${pos.x}px)`,
`translateY(${pos.y}px)`,
`${translateZ}`,
]
this.indicatorEl.style[style.transform as any] =
transformProperties.join(' ')
}
private getIndicatorPosByRatio(BScrollPos: Postion) {
const { x, y } = BScrollPos
const { hasHorizontalScroll, hasVerticalScroll } = this.scroll
const position = { ...this.currentPos }
if (hasHorizontalScroll) {
const roundX = Math.round(this.ratioX * x * this.translateXSign)
// maybe maxScrollX is negative
position.x = between(
roundX,
Math.min(this.minScrollX, this.maxScrollX),
Math.max(this.minScrollX, this.maxScrollX)
)
}
if (hasVerticalScroll) {
const roundY = Math.round(this.ratioY * y * this.translateYSign)
// maybe maxScrollY is negative
position.y = between(
roundY,
Math.min(this.minScrollY, this.maxScrollY),
Math.max(this.minScrollY, this.maxScrollY)
)
}
return position
}
destroy() {
if (this.options.interactive !== false) {
this.startEventRegister.destroy()
this.moveEventRegister.destroy()
this.endEventRegister.destroy()
}
this.hooksFn.forEach((item) => {
const hooks = item[0]
const hooksName = item[1]
const handlerFn = item[2]
hooks.off(hooksName, handlerFn)
})
this.hooksFn.length = 0
}
}