Source: observer/VisibilityChangeObserver.ts

import { EventEmitter } from '../event';
import { IVisibilityStatus } from './types';

const doc = window.document as any;

/**
 * 실행된 browser 기준으로, visibilityChange event 에 맞는 vendorPrefix 반환
 * @memberof VisibilityChangeObserver
 * @export
 * @returns {String}
 */
export function getVendorPrefix(): string {
  let prefix = '';

  if (typeof doc.hidden !== 'undefined') {
    prefix = '';
  } /* istanbul ignore next */ else if (typeof doc.msHidden !== 'undefined') {
    prefix = 'ms';
  } /* istanbul ignore next */ else if (typeof doc.webkitHidden !== 'undefined') {
    prefix = 'webkit';
  }

  return prefix;
}

/**
 * getVendorPrefix 의 값
 * @constant
 * @memberof VisibilityChangeObserver
 * @type {String}
 */
export const VENDOR_PREFIX = getVendorPrefix();

/**
 * browser 상의 visibilitychange event 명
 * @constant
 * @memberof VisibilityChangeObserver
 * @type {String}
 */
export const VISIBILITY_EVENT_NAME = `${VENDOR_PREFIX}visibilitychange`;

/**
 * browser 상의 visibility hidden 시점의 event 명
 * @constant
 * @memberof VisibilityChangeObserver
 * @type {String}
 */
export const HIDDEN_METHOD_NAME = VENDOR_PREFIX ? /* istanbul ignore next */ `${VENDOR_PREFIX}Hidden` : 'hidden';

/**
 * VisibilityChange Event Types
 * @event VisibilityChangeObserver#VISIBILITY_EVENTS
 * @memberof VisibilityChangeObserver
 * @property {String} CHANGE - 변경 시점
 * @property {String} GET_STATUS - 현재 상태값 체크
 */
// for jsdoc
/**
 * @export
 * @readonly
 * @enum {VISIBILITY_EVENTS}
 */
export enum VISIBILITY_EVENTS {
  CHANGE = 'VISIBILITY_CHANGE-EVENTS-CHANGE',
  GET_STATUS = 'VISIBILITY_CHANGE-EVENTS-GET_STATUS'
}

/**
 * browser 의 visibility 변화를 감지
  <iframe
    src="https://codesandbox.io/embed/nonollcode-snippet-9gko8?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&initialpath=%2Fobserver-VisibilityChange.html&module=%2Fobserver-VisibilityChange.html&theme=dark"
    style="width:100%; height:500px; border:1px solid black; border-radius: 4px; overflow:hidden;"
    title="@nonoll/code-snippet"
    allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
    sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
  ></iframe>
 * @export
 * @class VisibilityChangeObserver
 * @alias observer/VisibilityChangeObserver
 * @extends {EventEmitter}
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
 * @see https://caniuse.com/#search=visibilityState
 * @throws {Error} browser 에서 지원하지 못하는 경우 - throw new Error('VisibilityChange 지원되지 않는 브라우저입니다.');
 * @example
import { VisibilityChangeObserver, VISIBILITY_EVENTS } from '@nonoll/code-snippet/observer';

const createElement = ({ tag = 'div', id = '', style = '', value = '', text = '' }) => {
  const doc = window.document;
  const target = doc.createElement(tag);
  target.setAttribute('id', id);
  target.setAttribute('style', style);
  target.setAttribute('value', value);
  if (text) {
    target.textContent = text;
  }
  return target;
}

let observer;

const forExample = () => {
  if (observer) {
    console.log('already example');
    return;
  }

  const doc = window.document;

  observer = new VisibilityChangeObserver();

  observer.on(VISIBILITY_EVENTS.CHANGE, ({ isHidden, isShow }) => {
    console.log('change', isHidden, isShow);
  });

  observer.attach();
  observer.emit(VISIBILITY_EVENTS.GET_STATUS);

  const statusButton = createElement({ tag: 'button', text: 'status' });
  const openButton = createElement({ tag: 'button', text: 'window open' });

  doc.body.appendChild(statusButton);
  doc.body.appendChild(openButton);

  statusButton.addEventListener('click', e => {
    e.preventDefault();
    console.log('statusButton clicked');
    if (!observer) {
      return;
    }
    // observer.emit(VISIBILITY_EVENTS.GET_STATUS);
    const { isHidden, isShow } = observer.getStatus();
    console.log('getStatus', isHidden, isShow);
  });

  openButton.addEventListener('click', e => {
    e.preventDefault();
    console.log('openButton clicked');
    if (!observer) {
      return;
    }
    const browser = window.open('https://nonoll.github.io/code-snippet/');
    if (!browser) {
      console.error('팝업이 차단되어 있습니다.');
    }
  });
}

forExample();
 */
export class VisibilityChangeObserver extends EventEmitter {
  constructor() {
    super();

    /* istanbul ignore next: for not support error */
    if (!VisibilityChangeObserver.isSupport()) {
      throw new Error('VisibilityChange 지원되지 않는 브라우저입니다.');
    }

    this.onVisibilityChangeListener = this.onVisibilityChangeListener.bind(this);
  }

  /**
   * visibilitychange event 명 반환
   * @static
   * @returns {String}
   * @memberof VisibilityChangeObserver
   */
  static visibilityEventName(): string {
    return `${VENDOR_PREFIX}visibilitychange`;
  }

  /**
   * visibility hidden event 명 반환
   * @static
   * @returns {String}
   * @memberof VisibilityChangeObserver
   */
  static hiddenMethodName(): string {
    return HIDDEN_METHOD_NAME;
  }

  /**
   * visibility change 지원 여부 반환
   * @static
   * @returns {boolean}
   * @memberof VisibilityChangeObserver
   */
  static isSupport(): boolean {
    return typeof doc[VisibilityChangeObserver.hiddenMethodName()] !== 'undefined';
  }

  /**
   * @private
   * @memberof VisibilityChangeObserver
   * @see IVisibilityStatus
   * @fires VisibilityChangeObserver#VISIBILITY_EVENTS
   */
  private onVisibilityChangeListener(): void {
    const isHidden = this.isHidden();
    this.emit(VISIBILITY_EVENTS.CHANGE, { isHidden, isShow: !isHidden });
  }

  /**
   * @private
   * @returns {Boolean}
   * @memberof VisibilityChangeObserver
   */
  private isHidden(): boolean {
    return doc[VisibilityChangeObserver.hiddenMethodName()];
  }

  /**
   * 현재 상태를 반환
   * @returns {IVisibilityStatus}
   * @memberof VisibilityChangeObserver
   */
  public getStatus(): IVisibilityStatus {
    this.emit(VISIBILITY_EVENTS.GET_STATUS);

    const isHidden = this.isHidden();
    return { isHidden, isShow: !isHidden };
  }

  /**
   * 이벤트 감지 설정
   * @returns {VisibilityChangeObserver}
   * @memberof VisibilityChangeObserver
   */
  public attach(): VisibilityChangeObserver {
    doc.addEventListener(VisibilityChangeObserver.visibilityEventName(), this.onVisibilityChangeListener, false);
    this.on(VISIBILITY_EVENTS.GET_STATUS, this.onVisibilityChangeListener);

    return this;
  }

  /**
   * 이벤트 감지 해제
   * @returns {VisibilityChangeObserver}
   * @memberof VisibilityChangeObserver
   */
  public detach(): VisibilityChangeObserver {
    doc.removeEventListener(VisibilityChangeObserver.visibilityEventName(), this.onVisibilityChangeListener, false);
    this.off(VISIBILITY_EVENTS.GET_STATUS, this.onVisibilityChangeListener);

    return this;
  }

  /**
   * destory
   * @returns {VisibilityChangeObserver}
   * @memberof VisibilityChangeObserver
   */
  public destroy(): VisibilityChangeObserver {
    this.detach();

    return this;
  }
}