import { fromEvent, Subscription, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { IBrowserScroll, IScrollState, ScrollDirection, ScrollType } from './types';
/**
* 브라우져(window) 스크롤
* @export
* @class BrowserScroll
*/
export class BrowserScroll implements IBrowserScroll {
protected handleScroll: () => void;
protected handleResize: () => void;
protected resizeObserver: Observable<Event>;
protected scrollObserver: Observable<Event>;
protected eventListener: Subscription;
protected subjectScroll: Subject<ScrollType>;
protected observableScroll: Observable<ScrollType>;
protected yCurrent = 0;
protected yDirection = 0;
protected yDirectionLooseBefore = 0;
protected yDirectionLooseGap = 20;
protected yEndDispatchHold = false;
protected xCurrent = 0;
protected xDirection = 0;
protected xDirectionLooseBefore = 0;
protected xDirectionLooseGap = 20;
protected xEndDispatchHold = false;
constructor() {
this.eventListener = new Subscription();
this.subjectScroll = new Subject();
this.observableScroll = this.subjectScroll.asObservable();
}
/**
* 초기화
*/
init(): void {
this.initScroll();
this.initResize();
}
protected initScroll() {
this.handleScroll = this.onScroll.bind(this);
this.scrollObserver = fromEvent(window, 'scroll');
this.eventListener.add(this.scrollObserver.subscribe(this.handleScroll));
}
protected initResize() {
this.handleResize = this.onResize.bind(this);
this.resizeObserver = fromEvent(window, 'resize');
this.eventListener.add(this.resizeObserver.subscribe(this.handleResize));
}
get scrollTop(): number {
return document.documentElement.scrollTop || document.body.scrollTop;
}
get scrollLeft(): number {
return document.documentElement.scrollLeft || document.body.scrollLeft;
}
get scrollHeight(): number {
return document.documentElement.scrollHeight || document.body.scrollHeight;
}
get scrollWidth(): number {
return document.documentElement.scrollWidth || document.body.scrollWidth;
}
get innerHeight(): number {
return window.innerHeight;
}
get innerWidth(): number {
return window.innerWidth;
}
/**
* 구독자에게 전달되는 데이터
* @returns {IScrollState}
*/
getState(): IScrollState {
const scrollTop = this.scrollTop;
const scrollHeight = this.scrollHeight;
const innerHeight = this.innerHeight;
const directionY = this.yDirection;
const holdEndY = this.yEndDispatchHold;
const endY = scrollHeight - innerHeight;
let percentY = scrollTop / endY;
percentY = isNaN(percentY) ? 0 : percentY;
const scrollLeft = this.scrollLeft;
const scrollWidth = this.scrollWidth;
const innerWidth = this.innerWidth;
const directionX = this.xDirection;
const holdEndX = this.xEndDispatchHold;
const endX = scrollWidth - innerWidth;
let percentX = scrollLeft / endX;
percentX = isNaN(percentX) ? 0 : percentX;
return {
scrollTop,
scrollHeight,
innerHeight,
directionY,
holdEndY,
percentY,
endY,
scrollLeft,
scrollWidth,
innerWidth,
directionX,
holdEndX,
percentX,
endX,
};
}
/**
* 스크롤 loose Y 방향 변경에 대한 알림 조건 중, 이전 스크롤 위치와 현재 스크롤 위치의 여백 차이값
* @param {number} gap
*/
setDirectionLooseGapY(gap: number): void {
this.yDirectionLooseGap = gap;
}
/**
* 스크롤 loose X 방향 변경에 대한 알림 조건 중, 이전 스크롤 위치와 현재 스크롤 위치의 여백 차이값
* @param {number} gap
*/
setDirectionLooseGapX(gap: number): void {
this.xDirectionLooseGap = gap;
}
/**
* 지정한 위치로 스크롤
* @param {number} top
* @param {number} [left=0]
*/
setScroll(top: number, left: number = 0): void {
window.scroll(left, top);
}
/**
* 지정한 위치로 스크롤 + 애니메이션
* @param {number} top
* @param {number} [left=0]
* @param {boolean} [behavior=true] 'smooth'
*/
setScrollTo(top: number, left: number = 0, behavior: boolean = true): void {
window.scrollTo({ top, left, behavior: behavior ? 'smooth' : 'auto' });
}
/**
* 스크롤 y 끝에 도달할 때 알림을 일시 보류한다.
* @param {boolean} is
*/
holdDispatchEndY(is: boolean): void {
this.yEndDispatchHold = is;
}
/**
* 스크롤 x 끝에 도달할 때 알림을 일시 보류한다.
* @param {boolean} is
*/
holdDispatchEndX(is: boolean): void {
this.xEndDispatchHold = is;
}
/**
* 트리거: observeScroll() 의 구독자들에게 알림
*/
dispatchScroll(type: ScrollType): void {
this.subjectScroll.next(type);
}
protected pipeScroll(observable: Observable<ScrollType>, filterType: ScrollType) {
return observable.pipe(
filter((type) => type === filterType),
map(() => this.getState())
);
}
/**
* 옵저버블: 스크롤이 발생하면 알림
* @returns {Observable<IScrollState>}
*/
observeScroll(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.SCROLL);
}
/**
* 옵저버블: 스크롤의 Y 방향이 바뀌면 알림
* @returns {Observable<IScrollState>}
*/
observeDirectionY(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.DIRECTION_Y);
}
/**
* 옵저버블: 스크롤의 Y 방향이 바뀌고, 일정 조건에 도달시 발생하는 알림
* @returns {Observable<IScrollState>}
*/
observeDirectionLooseY(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.DIRECTION_LOOSE_Y);
}
/**
* 옵저버블: 스크롤이 Y 끝에 도달할 때 알림
* @returns {Observable<IScrollState>}
*/
observeEndY(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.END_Y);
}
/**
* 옵저버블: 스크롤의 X 방향이 바뀌면 알림
* @returns {Observable<IScrollState>}
*/
observeDirectionX(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.DIRECTION_X);
}
/**
* 옵저버블: 스크롤의 X 방향이 바뀌고, 일정 조건에 도달시 발생하는 알림
* @returns {Observable<IScrollState>}
*/
observeDirectionLooseX(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.DIRECTION_LOOSE_X);
}
/**
* 옵저버블: 스크롤이 X 끝에 도달할 때 알림
* @returns {Observable<IScrollState>}
*/
observeEndX(): Observable<IScrollState> {
return this.pipeScroll(this.observableScroll, ScrollType.END_X);
}
/**
* 핸들러: 리사이즈
* @protected
*/
protected onResize(): void {
this.onScroll();
}
/**
* 핸들러: 스크롤
* @protected
*/
protected onScroll(): void {
this.onScrollX();
this.onScrollY();
this.dispatchScroll(ScrollType.SCROLL);
}
/**
* 스크롤 X 축
* @protected
*/
protected onScrollX() {
const { scrollLeft, endX, directionX } = this.getState();
const xCurrent = this.xCurrent;
let xDirection = 0;
if (xCurrent < scrollLeft) {
xDirection = ScrollDirection.RIGHT;
} else if (scrollLeft < xCurrent) {
xDirection = ScrollDirection.LEFT;
} else {
xDirection = ScrollDirection.NONE;
}
this.xDirection = xDirection;
this.xCurrent = scrollLeft;
if (this.xDirection !== directionX) {
this.dispatchScroll(ScrollType.DIRECTION_X);
}
if (this.xDirectionLooseBefore !== this.xDirection && this.xDirectionLooseGap <= Math.abs(scrollLeft - xCurrent)) {
this.dispatchScroll(ScrollType.DIRECTION_LOOSE_X);
this.xDirectionLooseBefore = this.xDirection;
}
if (endX <= scrollLeft) {
if (!this.xEndDispatchHold) {
this.dispatchScroll(ScrollType.END_X);
}
this.holdDispatchEndX(true);
} else {
this.holdDispatchEndX(false);
}
}
/**
* 스크롤 Y 축
* @protected
*/
protected onScrollY() {
const { scrollTop, endY, directionY } = this.getState();
const yCurrent = this.yCurrent;
let yDirection = 0;
if (yCurrent < scrollTop) {
yDirection = ScrollDirection.DOWN;
} else if (scrollTop < yCurrent) {
yDirection = ScrollDirection.UP;
} else {
yDirection = ScrollDirection.NONE;
}
this.yDirection = yDirection;
this.yCurrent = scrollTop;
if (this.yDirection !== directionY) {
this.dispatchScroll(ScrollType.DIRECTION_Y);
}
if (this.yDirectionLooseBefore !== this.yDirection && this.yDirectionLooseGap <= Math.abs(scrollTop - yCurrent)) {
this.dispatchScroll(ScrollType.DIRECTION_LOOSE_Y);
this.yDirectionLooseBefore = this.yDirection;
}
if (endY <= scrollTop) {
if (!this.yEndDispatchHold) {
this.dispatchScroll(ScrollType.END_Y);
}
this.holdDispatchEndY(true);
} else {
this.holdDispatchEndY(false);
}
}
/**
* 파기
*/
destroy(): void {
if (this.eventListener) {
this.eventListener.unsubscribe();
this.eventListener = null;
}
if (this.subjectScroll) {
this.subjectScroll.unsubscribe();
this.subjectScroll = null;
}
}
}