UNPKG

@scena/dragscroll

Version:
297 lines (266 loc) 8.75 kB
import EventEmitter from "@scena/event-emitter"; import { isFunction, isString, now } from "@daybrush/utils"; import { CheckScrollOptions, DragScrollEvents, DragScrollOptions, Rect } from "./types"; function getDefaultScrollPosition(e: { container: HTMLElement, direction: number[] }) { let container = e.container; if (container === document.body) { return [ container.scrollLeft || document.documentElement.scrollLeft, container.scrollTop || document.documentElement.scrollTop, ]; } return [ container.scrollLeft, container.scrollTop, ]; } function checkDefaultScrollEvent(container: HTMLElement | Window, callback: () => void) { container.addEventListener("scroll", callback); return () => { container.removeEventListener("scroll", callback); } } function getContainerElement(container: DragScrollOptions["container"]): HTMLElement { if (!container) { return null; } else if (isString(container)) { return document.querySelector<HTMLElement>(container); } if (isFunction(container)) { return container(); } else if (container instanceof Element) { return container; } else if ("current" in container) { return container.current; } else if ("value" in container) { return container.value; } } /** * @sort 1 */ class DragScroll extends EventEmitter<DragScrollEvents> { private _startRect: Rect | null = null; private _startPos: number[] = []; private _prevTime: number = 0; private _timer: number = 0; private _prevScrollPos: number[] = [0, 0]; private _isWait = false; private _flag = false; private _currentOptions: DragScrollOptions | null = null; private _lock = false; private _unregister: (() => void) | null = null; /** */ public dragStart(e: any, options: DragScrollOptions) { const container = getContainerElement(options.container); if (!container) { this._flag = false; return; } let top = 0; let left = 0; let width = 0; let height = 0; if (container === document.body) { width = window.innerWidth; height = window.innerHeight; } else { const rect = container.getBoundingClientRect(); top = rect.top; left = rect.left; width = rect.width; height = rect.height; } this._flag = true; this._startPos = [e.clientX, e.clientY]; this._startRect = { top, left, width, height }; this._prevScrollPos = this._getScrollPosition([0, 0], options); this._currentOptions = options; this._registerScrollEvent(options); } public drag(e: any, options: DragScrollOptions) { clearTimeout(this._timer); if (!this._flag) { return; } const { clientX, clientY, } = e; const { threshold = 0, } = options; const { _startRect, _startPos, } = this; this._currentOptions = options; const direction = [0, 0]; if (_startRect.top > clientY - threshold) { if (_startPos[1] > _startRect.top || clientY < _startPos[1]) { direction[1] = -1; } } else if (_startRect.top + _startRect.height < clientY + threshold) { if (_startPos[1] < _startRect.top + _startRect.height || clientY > _startPos[1]) { direction[1] = 1; } } if (_startRect.left > clientX - threshold) { if (_startPos[0] > _startRect.left || clientX < _startPos[0]) { direction[0] = -1; } } else if (_startRect.left + _startRect.width < clientX + threshold) { if (_startPos[0] < _startRect.left + _startRect.width || clientX > _startPos[0]) { direction[0] = 1; } } if (!direction[0] && !direction[1]) { return false; } return this._continueDrag({ ...options, direction, inputEvent: e, isDrag: true, }); } /** */ public checkScroll(options: CheckScrollOptions) { if (this._isWait) { return false; } const { prevScrollPos = this._prevScrollPos, direction, throttleTime = 0, inputEvent, isDrag, } = options; const nextScrollPos = this._getScrollPosition(direction || [0, 0], options); const offsetX = nextScrollPos[0] - prevScrollPos[0]; const offsetY = nextScrollPos[1] - prevScrollPos[1]; const nextDirection = direction || [ offsetX ? Math.abs(offsetX) / offsetX : 0, offsetY ? Math.abs(offsetY) / offsetY : 0, ]; this._prevScrollPos = nextScrollPos; this._lock = false; if (!offsetX && !offsetY) { return false; } /** * @event DragScroll#move */ this.emit("move", { offsetX: nextDirection[0] ? offsetX : 0, offsetY: nextDirection[1] ? offsetY : 0, inputEvent, }); if (throttleTime && isDrag) { clearTimeout(this._timer); this._timer = window.setTimeout(() => { this._continueDrag(options); }, throttleTime); } return true; } /** * */ public dragEnd() { this._flag = false; this._lock = false; clearTimeout(this._timer); this._unregisterScrollEvent(); } private _getScrollPosition(direction: number[], options: DragScrollOptions) { const { container, getScrollPosition = getDefaultScrollPosition, } = options; return getScrollPosition({ container: getContainerElement(container), direction }); } private _continueDrag(options: CheckScrollOptions) { const { container, direction, throttleTime, useScroll, isDrag, inputEvent, } = options; if (!this._flag || (isDrag && this._isWait)) { return; } const nowTime = now(); const distTime = Math.max(throttleTime + this._prevTime - nowTime, 0); if (distTime > 0) { clearTimeout(this._timer); this._timer = window.setTimeout(() => { this._continueDrag(options); }, distTime); return false; } this._prevTime = nowTime; const prevScrollPos = this._getScrollPosition(direction, options); this._prevScrollPos = prevScrollPos; if (isDrag) { this._isWait = true; } // unregister native scroll event if (!useScroll) { this._lock = true; } const param = { container: getContainerElement(container), direction, inputEvent, }; options.requestScroll?.(param); /** * @event DragScroll#scroll */ this.emit("scroll", param); this._isWait = false; return useScroll || this.checkScroll({ ...options, prevScrollPos, direction, inputEvent, }); } private _registerScrollEvent(options: DragScrollOptions) { this._unregisterScrollEvent(); const checkScrollEvent = options.checkScrollEvent; if (!checkScrollEvent) { return; } const callback = checkScrollEvent === true ? checkDefaultScrollEvent : checkScrollEvent; const container = getContainerElement(options.container); if (checkScrollEvent === true && (container === document.body || container === document.documentElement)) { this._unregister = checkDefaultScrollEvent(window, this._onScroll); } else { this._unregister = callback(container, this._onScroll); } } private _unregisterScrollEvent() { this._unregister?.(); this._unregister = null; } private _onScroll = () => { const options = this._currentOptions; if (this._lock || !options) { return; } this.emit("scrollDrag", { next: (inputEvent: any) => { this.checkScroll({ container: options.container, inputEvent, }); }, }); }; } export default DragScroll;