stage/appear-stage.ts

import { IStage, StageOption, Actor, ActorElement } from "../common/types";

/**
 * 화면(지정된 root 영역)에 진입 여부를 판단하고 알리기 위한 클래스.
 * 등록된 Actor(s)를 IntersectionObserver 를 통해 관찰하고 관찰된 상태에 따라 Actor 에게 알림.
 * @template T Actor
 */
export class AppearStage<T extends Actor = Actor> implements IStage<T> {
  /**
   * Actor 맵
   */
  protected actorMap: Map<ActorElement, T>;

  /**
   * IntersectionObserver
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
   */
  protected observer: IntersectionObserver;

  /**
   * 초기화
   * @param [option] 초기 옵션. 인터섹션 옵저버는 생성시에만 옵션 지정이 가능
   */
  init(option: StageOption = {}): void {
    if (!this.observer) {
      this.actorMap = new Map<ActorElement, T>();
      this.observer = new IntersectionObserver(
        this.onObserveEntries.bind(this),
        option
      );
    }
  }

  /**
   * 전달된 actor 를 옵저버에 등록합니다.
   * @param actor 등록할 Actor
   */
  observe(actor: T): void {
    const { element } = actor;
    if (this.actorMap && !this.actorMap.has(element)) {
      actor.bind(this);
      this.actorMap.set(element, actor);
      this.intersectionObserver.observe(element);
    }
  }

  /**
   * 전달된 actor 를 옵저버에서 제외합니다.
   * @param actor 제외할 Actor
   */
  unobserve(actor: T): void {
    const { element } = actor;
    if (this.actorMap && this.actorMap.has(element)) {
      this.actorMap.delete(element);
      this.intersectionObserver.unobserve(element);
    }
  }

  /**
   * 옵저버에 등록(관찰) 중 인 Actor 의 수
   */
  get actorSize() {
    return this.actorMap ? this.actorMap.size : 0;
  }

  /**
   * 등록 되어있는 Actor 를 반환 합니다.
   * @returns T[]
   */
  getActors(): T[] {
    return Array.from(this.actorMap ? this.actorMap.values() : []);
  }

  /**
   * 생성된 intersection observer 인스턴스
   */
  get intersectionObserver(): IntersectionObserver {
    if (!this.observer) throw new Error("uninitialize");
    return this.observer;
  }

  /**
   * 옵저버의 콜백 핸들러
   * @param entries 옵저버의 콜백으로 전달받는 엔트리 값
   */
  protected onObserveEntries(entries: IntersectionObserverEntry[]) {
    entries.forEach((entry: IntersectionObserverEntry) => {
      if (!this.actorMap) return;
      const { isIntersecting, target } = entry;
      const actor = this.actorMap.get(target);
      if (actor) {
        if (isIntersecting) {
          actor.appear(entry);
        } else {
          actor.disappear(entry);
        }
      }
    });
  }

  /**
   * 파기
   */
  dispose(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
    if (this.actorMap) {
      this.actorMap.clear();
      this.actorMap = null;
    }
  }
}