UNPKG

@better-scroll/indicators

Version:

used as parallax scrolling, magnifier effects

418 lines (368 loc) 10.8 kB
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 } }