src/Page.ts

import Component from "@egjs/component";
import {splitSpace, splitUnit, isArray, $, isObject, splitBracket, IObject} from "@daybrush/utils";
import * as WindowSize from "./WindowSize";
import Pages from "./Pages";

/**
 * @memberof Page
 * @typedef EventParameter
 * @property {string} type - The name of event
 * @property {Element} target - An element that represents a page
 * @property {Page} currentTarget - The page on which the event occurred.
 */

/**
 * @memberof Page
 * @typedef
 */
export interface PageState {
  enter: boolean;
  firstEnter: boolean;
  firstExit: boolean;
}
/**
 * @memberof Page
 * @typedef
 */
export interface PageOptions {
  range?: Array<number | string>;
  margin?: number | string |  Array<number | string>;
  events?: string[];
}
/**
 * @memberof Page
 * @typedef
 */
export interface Rect {
  top: number;
  height: number;
}
/**
 * You can check the page in and out of the screen.
 * @extends eg.Component
 * @sort 1
 * @example
const page = new Page(".page1", {
  range: ["0%", "100%"],
  margin: [0, 0],
  // Registers events automatically.
  events: ["resize", "scroll"]
});
 */
class Page extends Component {
  public static s: typeof Pages;
  private ranges: IObject<Page> = {};
  private _range: Array<string | number> = [0, "100%"];
  private margin: Array<string | number> = [0, 0];
  private pages: Page[] = [];
  private el: Element | null;
  private state: PageState = {
    enter: false,
    firstEnter: false,
    firstExit: false,
  };
  /**
   */
  constructor(el?: string | Element, options: PageOptions = {}) {
    super();

    this.el = el ? (isObject(el) ? el : $(el)) : null;
    if ("range" in options) {
      this._range = options.range!;
    }
    if ("margin" in options) {
      const margin = options.margin!;

      if (isArray(margin)) {
        this.margin = margin;
      } else {
        this.margin = [margin, margin];
      }
    }
    if ("events" in options) {
      options.events!.forEach(name => {
        if (name === "resize") {
          window.addEventListener("resize", this.resize);
        } else if (name === "scroll") {
          window.addEventListener("scroll", this.scroll);
        } else {
          this.el && this.el.addEventListener(name, this.scroll);
        }
      });
    }
  }
  /**
   */
  public add(page: Page) {
    this.pages.push(page);

    return this;
  }
  /**
   */
  public range(range: Array<string | number> | number | string = [0, "100%"]) {
    const rangeArr = isArray(range) ? range : [range, range];
    const id = `[${rangeArr.join(",")}]`;

    if (this.ranges[id]) {
      return this.ranges[id];
    }
    const page = new Page(this.el!, {
      range: rangeArr,
    });

    this.ranges[id] = page;
    this.add(page);
    return page;
  }
  /**
   * @method
   */
  public scroll = () => {
    this.onCheck();
  }
  /**
   * @method
   */
  public resize = () => {
    this.onCheck();
  }
  public triggerEvent(name: string) {
    this.trigger(name, {
      target: this.el,
    });
  }
  public onEnter(rect: Rect) {
    const state = this.state;

    if (!state.enter) {
      state.enter = true;

      if (!state.firstEnter) {
        state.firstEnter = true;

        /**
         * An event that occurs when you first enter a page.
         * @param {Page.EventParameter} event - Event object
         * @event Page#firstEnter
         */
        this.triggerEvent("firstEnter");
      }
      /**
       * An event that occurs when you enter a page.
       * @param {Page.EventParameter} event - Event object
       * @event Page#enter
       */
      this.triggerEvent("enter");
    }
    this.pages.forEach(page => {
      page.onCheck(page.el === this.el ? rect : undefined);
    });
  }
  public onExit() {
    const state = this.state;

    if (state.enter) {
      state.enter = false;

      if (!state.firstExit) {
        state.firstExit = true;
        /**
         * An event that occurs when you first exit a page.
         * @param {Page.EventParameter} event - Event object
         * @event Page#firstExit
         */
        this.triggerEvent("firstExit");
      }
      /**
       * An event that occurs when you exit a page.
       * @param {Page.EventParameter} event - Event object
       * @event Page#exit
       */
      this.triggerEvent("exit");
    }
    this.pages.forEach(page => {
      page.onExit();
    });
  }
  public calcSize(size: string | number, rect: Rect) {
    if (typeof size === "number") {
      return size;
    }
    const sizeInfos: Array<string | number> = splitSpace(size);

    if (!sizeInfos) {
      return 0;
    }
    const length = sizeInfos.length;
    const stack: number[] = [];
    let sign = 1;

    for (let i = 0; i < length; ++i) {
      const v = sizeInfos[i];

      if (v === "+") {
        sign = 1;
      } else if (v === "-") {
        sign = -1;
      } else if (v === "*") {
        stack.push((stack.pop()! || 0) * this._calcSize(sizeInfos[i + 1], rect));
        ++i;
      } else if (v === "/") {
        stack.push((stack.pop()! || 0) / this._calcSize(sizeInfos[i + 1], rect));
        ++i;
      } else {
        stack.push(sign * this._calcSize(v, rect));
        sign = 1;
      }
    }
    return stack.reduce((prev, cur) => {
      return prev + cur;
    }, 0);
  }
  /**
   */
  public getRect(isAbsolute?: boolean): Rect | undefined {
    const rect = this.el ? this.el.getBoundingClientRect() : undefined;

    if (!rect) {
      return;
    }
    const top = rect.top + (isAbsolute ? document.body.scrollTop || document.documentElement.scrollTop : 0);
    const height = rect.height;

    return {top, height};
  }
  public onCheck(rect: Rect | undefined = this.getRect()) {
    if (rect) {
      const {top} = rect;

      const rangeStart = this.calcSize(this._range[0], rect);
      const rangeEnd = this.calcSize(this._range[1], rect);
      const marginTop = this.calcSize(this.margin[0], rect);
      const marginBottom = this.calcSize(this.margin[1], rect);

      if (top + rangeEnd + marginBottom <= 0 || top + rangeStart - marginTop >= WindowSize.height) {
        this.onExit();
      } else {
        this.onEnter(rect);
      }
    } else {
      this.pages.forEach(page => {
        page.onCheck();
      });
    }
  }
  private _calcSize(size: string | number, rect: Rect) {
    if (!size) {
      return 0;
    }
    if (typeof size === "number") {
      return size;
    }
    if (size === "window") {
      return WindowSize.height;
    }
    if (size.indexOf("(") > -1) {
      return this.calcSize(splitBracket(size).value!, rect);
    }
    const info = splitUnit(size);

    if (info.unit === "%") {
      return rect.height * info.value / 100;
    } else {
      return info.value;
    }
  }
}

export default Page;