Skip to content
router.ts 5.61 KiB
Newer Older
Grant's avatar
Grant committed
import { PanZoom } from "@sc07-canvas/lib/src/renderer/PanZoom";
import { Canvas } from "./canvas";
import throttle from "lodash.throttle";
import EventEmitter from "eventemitter3";
Grant's avatar
Grant committed
import { TemplateStyle, TemplateStyles } from "./template";
Grant's avatar
Grant committed

const CLIENT_PARAMS = {
  canvas_x: "x",
  canvas_y: "y",
  canvas_zoom: "zoom",
  template_url: "tu",
  template_width: "tw",
  template_x: "tx",
  template_y: "ty",
Grant's avatar
Grant committed
  template_style: "ts",
Grant's avatar
Grant committed
};

export interface IRouterData {
  canvas?: {
    x: number;
    y: number;
    zoom?: number;
  };
  template?: {
    url: string;
    width?: number;
    x?: number;
    y?: number;
Grant's avatar
Grant committed
    style?: TemplateStyle;
Grant's avatar
Grant committed
  };
}

interface RouterEvents {
  navigate(route: IRouterData): void;
}

class _Router extends EventEmitter<RouterEvents> {
  PanZoom: PanZoom | undefined;

  // React TemplateContext
  templateState: {
    enabled: boolean;
    width?: number;
    x: number;
    y: number;
    url?: string;
Grant's avatar
Grant committed
    style: TemplateStyle;
Grant's avatar
Grant committed
  } = {
    enabled: false,
    x: 0,
    y: 0,
Grant's avatar
Grant committed
    style: "ONE_TO_ONE",
Grant's avatar
Grant committed
  };

  constructor() {
    super();

    window.addEventListener("hashchange", this._hashChange.bind(this));
  }

  destroy() {
    // NOTE: this method *never* gets called because this is intended to be global
    window.removeEventListener("hashchange", this._hashChange.bind(this));
  }

Grant's avatar
Grant committed
  _hashChange(_e: HashChangeEvent) {
Grant's avatar
Grant committed
    const data = this.get();
    console.info("[Router] Navigated", data);
    this.emit("navigate", data);
  }

  queueUpdate = throttle(this.update, 500);

  update() {
    const url = this.getURL();
    if (!url) return;

    console.log("[Router] Updating URL", url);
Grant's avatar
Grant committed
    window.history.replaceState({}, "", url);
  }

  getURL() {
    const canvas = Canvas.instance;
    // this is not that helpful because the data is more spread out
    // this gets replaced by using TemplateContext data
    // const template = Template.instance;

    if (!canvas) {
      console.warn("Router#update called but no canvas instance exists");
      return;
    }

    if (!this.PanZoom) {
      console.warn("Router#update called but no PanZoom instance exists");
    }

    const params = new URLSearchParams();

    const position = canvas.panZoomTransformToCanvas();
    params.set(CLIENT_PARAMS.canvas_x, position.canvasX + "");
    params.set(CLIENT_PARAMS.canvas_y, position.canvasY + "");
    params.set(
      CLIENT_PARAMS.canvas_zoom,
      (this.PanZoom!.transform.scale >> 0) + ""
    );

    if (this.templateState.enabled && this.templateState.url) {
      params.set(CLIENT_PARAMS.template_url, this.templateState.url + "");
      if (this.templateState.width)
        params.set(CLIENT_PARAMS.template_width, this.templateState.width + "");
      params.set(CLIENT_PARAMS.template_x, this.templateState.x + "");
      params.set(CLIENT_PARAMS.template_y, this.templateState.y + "");
Grant's avatar
Grant committed
      if (this.templateState.style)
        params.set(CLIENT_PARAMS.template_style, this.templateState.style + "");
Grant's avatar
Grant committed
    }

    return (
      window.location.protocol + "//" + window.location.host + "/#" + params
    );
  }

  /**
   * Parse the URL and return what was found, following specifications
   * There's no defaults, if it's not specified in the url, it's not specified in the return
   *
   * @returns
   */
  get(): IRouterData {
    const params = new URLSearchParams(window.location.hash.slice(1));

    let canvas:
      | {
          x: number;
          y: number;
          zoom?: number;
        }
      | undefined = undefined;

    if (
      params.has(CLIENT_PARAMS.canvas_x) &&
      params.has(CLIENT_PARAMS.canvas_y)
    ) {
      // params exist, now to validate
      // x & y or nothing; zoom is optional

      let x = parseInt(params.get(CLIENT_PARAMS.canvas_x) || "");
      let y = parseInt(params.get(CLIENT_PARAMS.canvas_y) || "");
      if (!isNaN(x) && !isNaN(y)) {
        // x & y are valid numbers
        canvas = {
          x,
          y,
        };

        if (params.has(CLIENT_PARAMS.canvas_zoom)) {
          let zoom = parseInt(params.get(CLIENT_PARAMS.canvas_zoom) || "");

          if (!isNaN(zoom)) {
            canvas.zoom = zoom;
          }
        }
      }
    }

    let template:
      | {
          url: string;
          width?: number;
          x?: number;
          y?: number;
Grant's avatar
Grant committed
          style?: TemplateStyle;
Grant's avatar
Grant committed
        }
      | undefined = undefined;

    if (params.has(CLIENT_PARAMS.template_url)) {
      const url = params.get(CLIENT_PARAMS.template_url)!;
      template = { url };

      if (params.has(CLIENT_PARAMS.template_width)) {
        let width = parseInt(params.get(CLIENT_PARAMS.template_width) || "");

        if (!isNaN(width)) {
          template.width = width;
        }
      }

      if (
        params.has(CLIENT_PARAMS.template_x) &&
        params.has(CLIENT_PARAMS.template_y)
      ) {
        // both x & y has to be set

        let x = parseInt(params.get(CLIENT_PARAMS.template_x) || "");
        let y = parseInt(params.get(CLIENT_PARAMS.template_y) || "");

        if (!isNaN(x) && !isNaN(y)) {
          template.x = x;
          template.y = y;
        }
      }
Grant's avatar
Grant committed

      if (params.has(CLIENT_PARAMS.template_style)) {
        let style = params.get(CLIENT_PARAMS.template_style);

        if (style && TemplateStyles.indexOf(style) > -1) {
          template.style = style as any;
        }
      }
Grant's avatar
Grant committed
    }

    return {
      canvas,
      template,
    };
  }

  /**
   * Accept updates to local copy of TemplateContext from React
   * @param args
   */
  setTemplate(args: {
    enabled: boolean;
    width?: number;
    x: number;
    y: number;
    url?: string;
Grant's avatar
Grant committed
    style: TemplateStyle;
Grant's avatar
Grant committed
  }) {
    this.templateState = args;
  }
}

export const Router = new _Router();