@jood/appearer
Version:
javascript intersection observer
318 lines (310 loc) • 8.42 kB
JavaScript
import 'intersection-observer';
import { Subject } from 'rxjs';
/**
* 화면(지정된 root 영역)에 진입 여부를 판단하고 알리기 위한 클래스.
* 등록된 Actor(s)를 IntersectionObserver 를 통해 관찰하고 관찰된 상태에 따라 Actor 에게 알림.
* @template T Actor
*/
class AppearStage {
/**
* 초기화
* @param [option] 초기 옵션. 인터섹션 옵저버는 생성시에만 옵션 지정이 가능
*/
init(option = {}) {
if (!this.observer) {
this.actorMap = new Map();
this.observer = new IntersectionObserver(this.onObserveEntries.bind(this), option);
}
}
/**
* 전달된 actor 를 옵저버에 등록합니다.
* @param actor 등록할 Actor
*/
observe(actor) {
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) {
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() {
return Array.from(this.actorMap ? this.actorMap.values() : []);
}
/**
* 생성된 intersection observer 인스턴스
*/
get intersectionObserver() {
if (!this.observer)
throw new Error("uninitialize");
return this.observer;
}
/**
* 옵저버의 콜백 핸들러
* @param entries 옵저버의 콜백으로 전달받는 엔트리 값
*/
onObserveEntries(entries) {
entries.forEach((entry) => {
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() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
if (this.actorMap) {
this.actorMap.clear();
this.actorMap = null;
}
}
}
/**
* Actor type 구분
* @export
* @enum {number}
*/
var AppearType;
(function (AppearType) {
AppearType["BASE"] = "base";
AppearType["ONCE"] = "once";
AppearType["LAZY"] = "lazy";
})(AppearType || (AppearType = {}));
/**
* 관찰대상의 이벤트
* @implements {AppearEventData<T>}
* @template T
*/
class AppearEvent {
/**
* @param type 이벤트 타입
* @param option 이벤트 데이터
*/
constructor(type, option) {
const { actor, entry } = option;
this.type = type;
this.actor = actor;
this.entry = entry;
}
}
/**
* 이벤트 타입 - 진입
*/
AppearEvent.APPEAR = "APPEAR";
/**
* 이벤트 타입 - 이탈
*/
AppearEvent.DISAPPEAR = "DISAPPEAR";
/**
* Stage 에 등록될 Actor.
* 스테이지에 진입, 이탈 시 계속 알려주는 기본형.
* @class BaseActor
* @implements {IActor}
*/
class BaseActor {
/**
* @param element 옵저버에 등록되어야 하는 native element
*/
constructor(element) {
/**
* 이벤트 Observable
* @see https://rxjs-dev.firebaseapp.com/guide/subject
*/
this.events = new Subject();
/**
* 현재 진입 여부 상태
*/
this.isAppear = null;
this.element = element;
}
/**
* 해당 인스턴스를 관찰하는 스테이지를 연결
* @param stage 스테이지
*/
bind(stage) {
this.stage = stage;
}
/**
* 진입, 이탈 등 이벤트 알림
* @param type 이벤트 타입
* @param [entry] 상태 변경시 관찰된 상태
*/
dispatch(type, entry) {
this.events.next(new AppearEvent(type, {
actor: this,
entry,
}));
}
/**
* 스테이지 진입
* @param [entry] 스테이지 진입시 관찰 상태
*/
appear(entry) {
if (this.isAppear === true)
return;
this.isAppear = true;
this.dispatch(AppearEvent.APPEAR, entry);
}
/**
* 스테이지 이탈
* @param [entry] 스테이지 이탈시 관찰 상태
*/
disappear(entry) {
if (this.isAppear === false)
return;
this.isAppear = false;
this.dispatch(AppearEvent.DISAPPEAR, entry);
}
/**
* 파기
*/
dispose() {
if (this.stage) {
this.stage.unobserve(this);
}
}
}
/**
* Stage 에 등록될 Actor.
* 스테이지 진입을 한번만 감지한 후 본인 스스로 관찰 해제하는 감지형.
* (사용 예: 화면 진입시 한번만 애니메이션 한다, 이미지 로드를 한다)
* @class OnceActor
* @extends {BaseActor}
*/
class OnceActor extends BaseActor {
/**
* 스테이지 진입. 진입시 자동 관찰 해제.
* @override
* @param [entry]
*/
appear(entry) {
if (this.isAppear === true)
return;
this.isAppear = true;
this.dispatch(AppearEvent.APPEAR, entry);
if (this.stage) {
this.stage.unobserve(this);
}
}
}
/**
* Stage 에 등록될 Actor.
* 스테이지에 진입을 한번만 감지하되, 진입 후 너무 빠르게 이탈시에는 감지 처리를 하지 않는 느린 감지형.
* (사용 예: 촘촘한 상품 목록과 같이 빠르게 스크롤 하여 지나칠 수 있는 곳)
* @class LazyActor
* @extends {BaseActor}
*/
class LazyActor extends BaseActor {
constructor() {
super(...arguments);
this.appearTimer = null;
this.checkoutDelay = 1000;
this.appearDelay = 150;
}
/**
* 느린 감지를 시작하기 전 대기 시간.
* 지정된 시간 전에 감지된 진입은 느린 감지를 하지 않고 바로 진입을 알림.
* @param [delay=1000]
*/
setCheckoutDelay(delay = 1000) {
this.checkoutDelay = delay;
}
/**
* 지정된 시간 사이에 진입 후 진출을 하는 경우 진입 알림을 하지 않는 대기 시간.
* @param [delay=150]
*/
setAppearDelay(delay = 150) {
this.appearDelay = delay;
}
/**
* 진입 대기 타이머 파기
*/
clearAppearTimer() {
if (this.appearTimer) {
clearTimeout(this.appearTimer);
this.appearTimer = null;
}
}
/**
* 스테이지 진입. 진입 후 일정시간 (appearDelay) 전에 이탈하는 경우는 진입으로 취급하지 않음.
* @override
* @param [entry]
*/
appear(entry) {
this.clearAppearTimer();
if (this.isAppear === true)
return;
if (this.checkoutDelay <= entry.time) {
this.appearTimer = setTimeout(() => {
this.doAppear(entry);
}, this.appearDelay);
}
else {
this.doAppear(entry);
}
}
/**
* 실제 진입 처리.
* @private
* @param entry
*/
doAppear(entry) {
this.isAppear = true;
this.dispatch(AppearEvent.APPEAR, entry);
if (this.stage) {
this.stage.unobserve(this);
}
}
/**
* 스테이지 이탈.
* @override
* @param entry
*/
disappear(entry) {
this.clearAppearTimer();
if (!this.isAppear)
return;
this.isAppear = false;
this.dispatch(AppearEvent.DISAPPEAR, entry);
}
}
/**
* Generated bundle index. Do not edit.
*/
export { AppearEvent, AppearStage, AppearType, BaseActor, LazyActor, OnceActor };
//# sourceMappingURL=jood-appearer.js.map