rewrite router (related #33)
This commit is contained in:
parent
634a69e79c
commit
94b4cdd3b8
|
@ -5,9 +5,9 @@ import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer";
|
|||
import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext";
|
||||
import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom";
|
||||
import throttle from "lodash.throttle";
|
||||
import { Routes } from "../lib/routes";
|
||||
import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net";
|
||||
import { IPosition } from "@sc07-canvas/lib/src/net";
|
||||
import { Template } from "./Template";
|
||||
import { IRouterData, Router } from "../lib/router";
|
||||
|
||||
export const CanvasWrapper = () => {
|
||||
// to prevent safari from blurring things, use the zoom css property
|
||||
|
@ -21,79 +21,60 @@ export const CanvasWrapper = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const parseHashParams = (canvas: Canvas) => {
|
||||
// maybe move this to a utility inside routes.ts
|
||||
|
||||
let { hash } = new URL(window.location.href);
|
||||
if (hash.indexOf("#") === 0) {
|
||||
hash = hash.slice(1);
|
||||
}
|
||||
let params = new URLSearchParams(hash);
|
||||
|
||||
let position: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
zoom?: number;
|
||||
} = {};
|
||||
|
||||
if (params.has("x") && !isNaN(parseInt(params.get("x")!)))
|
||||
position.x = parseInt(params.get("x")!);
|
||||
if (params.has("y") && !isNaN(parseInt(params.get("y")!)))
|
||||
position.y = parseInt(params.get("y")!);
|
||||
if (params.has("zoom") && !isNaN(parseInt(params.get("zoom")!)))
|
||||
position.zoom = parseInt(params.get("zoom")!);
|
||||
|
||||
if (
|
||||
typeof position.x === "number" &&
|
||||
typeof position.y === "number" &&
|
||||
typeof position.zoom === "number"
|
||||
) {
|
||||
const { transformX, transformY } = canvas.canvasToPanZoomTransform(
|
||||
position.x,
|
||||
position.y
|
||||
);
|
||||
|
||||
return {
|
||||
x: transformX,
|
||||
y: transformY,
|
||||
zoom: position.zoom,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const CanvasInner = () => {
|
||||
const canvasRef = createRef<HTMLCanvasElement>();
|
||||
const { config, setCanvasPosition, setCursorPosition } = useAppContext();
|
||||
const PanZoom = useContext(RendererContext);
|
||||
|
||||
useEffect(() => {
|
||||
Router.PanZoom = PanZoom;
|
||||
|
||||
// @ts-ignore
|
||||
window.TEST_Router = Router;
|
||||
}, [PanZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.canvas || !canvasRef.current) return;
|
||||
const canvas = canvasRef.current!;
|
||||
const canvasInstance = new Canvas(config, canvas, PanZoom);
|
||||
const initAt = Date.now();
|
||||
|
||||
{
|
||||
// TODO: handle hash changes and move viewport
|
||||
// NOTE: this will need to be cancelled if handleViewportMove was executed recently
|
||||
const handleNavigate = (data: IRouterData) => {
|
||||
if (data.canvas) {
|
||||
const position = canvasInstance.canvasToPanZoomTransform(
|
||||
data.canvas.x,
|
||||
data.canvas.y
|
||||
);
|
||||
|
||||
const position = parseHashParams(canvasInstance);
|
||||
if (position) {
|
||||
PanZoom.setPosition(position, { suppressEmit: true });
|
||||
PanZoom.setPosition(
|
||||
{
|
||||
x: position.transformX,
|
||||
y: position.transformY,
|
||||
zoom: data.canvas.zoom || 0, // TODO: fit canvas to viewport instead of defaulting
|
||||
},
|
||||
{ suppressEmit: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewportMove = throttle((state: ViewportMoveEvent) => {
|
||||
const pos = canvasInstance.panZoomTransformToCanvas();
|
||||
// initial load
|
||||
const initialRouter = Router.get();
|
||||
console.log(
|
||||
"[CanvasWrapper] Initial router data, handling navigate",
|
||||
initialRouter
|
||||
);
|
||||
handleNavigate(initialRouter);
|
||||
|
||||
const canvasPosition: ICanvasPosition = {
|
||||
x: pos.canvasX,
|
||||
y: pos.canvasY,
|
||||
zoom: state.scale >> 0,
|
||||
};
|
||||
const handleViewportMove = (state: ViewportMoveEvent) => {
|
||||
if (Date.now() - initAt < 60 * 1000) {
|
||||
console.debug(
|
||||
"[CanvasWrapper] handleViewportMove called soon after init",
|
||||
Date.now() - initAt
|
||||
);
|
||||
}
|
||||
|
||||
setCanvasPosition(canvasPosition);
|
||||
|
||||
window.location.replace(Routes.canvas({ pos: canvasPosition }));
|
||||
}, 1000);
|
||||
Router.queueUpdate();
|
||||
};
|
||||
|
||||
const handleCursorPos = throttle((pos: IPosition) => {
|
||||
if (
|
||||
|
@ -111,11 +92,13 @@ const CanvasInner = () => {
|
|||
|
||||
PanZoom.addListener("viewportMove", handleViewportMove);
|
||||
canvasInstance.on("cursorPos", handleCursorPos);
|
||||
Router.on("navigate", handleNavigate);
|
||||
|
||||
return () => {
|
||||
canvasInstance.destroy();
|
||||
PanZoom.removeListener("viewportMove", handleViewportMove);
|
||||
canvasInstance.off("cursorPos", handleCursorPos);
|
||||
Router.off("navigate", handleNavigate);
|
||||
};
|
||||
|
||||
// ! do not include canvasRef, it causes infinite re-renders
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { PropsWithChildren, createContext, useContext, useState } from "react";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { IRouterData, Router } from "../lib/router";
|
||||
|
||||
interface ITemplate {
|
||||
/**
|
||||
|
@ -35,13 +42,41 @@ const templateContext = createContext<ITemplate>({} as any);
|
|||
export const useTemplateContext = () => useContext(templateContext);
|
||||
|
||||
export const TemplateContext = ({ children }: PropsWithChildren) => {
|
||||
const [enable, setEnable] = useState(false);
|
||||
const [url, setURL] = useState<string>();
|
||||
const [width, setWidth] = useState<number>();
|
||||
const [x, setX] = useState(0);
|
||||
const [y, setY] = useState(0);
|
||||
const routerData = Router.get();
|
||||
const [enable, setEnable] = useState(!!routerData.template?.url);
|
||||
const [url, setURL] = useState<string | undefined>(routerData.template?.url);
|
||||
const [width, setWidth] = useState<number | undefined>(
|
||||
routerData.template?.width
|
||||
);
|
||||
const [x, setX] = useState(routerData.template?.x || 0);
|
||||
const [y, setY] = useState(routerData.template?.y || 0);
|
||||
const [opacity, setOpacity] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNavigate = (data: IRouterData) => {
|
||||
if (data.template) {
|
||||
setEnable(true);
|
||||
setURL(data.template.url);
|
||||
setWidth(data.template.width);
|
||||
setX(data.template.x || 0);
|
||||
setY(data.template.y || 0);
|
||||
} else {
|
||||
setEnable(false);
|
||||
}
|
||||
};
|
||||
|
||||
Router.on("navigate", handleNavigate);
|
||||
|
||||
return () => {
|
||||
Router.off("navigate", handleNavigate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Router.setTemplate({ enabled: enable, width, x, y, url });
|
||||
Router.queueUpdate();
|
||||
}, [enable, width, x, y, url]);
|
||||
|
||||
return (
|
||||
<templateContext.Provider
|
||||
value={{
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
import { PanZoom } from "@sc07-canvas/lib/src/renderer/PanZoom";
|
||||
import { Canvas } from "./canvas";
|
||||
import throttle from "lodash.throttle";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
const CLIENT_PARAMS = {
|
||||
canvas_x: "x",
|
||||
canvas_y: "y",
|
||||
canvas_zoom: "zoom",
|
||||
template_url: "tu",
|
||||
template_width: "tw",
|
||||
template_x: "tx",
|
||||
template_y: "ty",
|
||||
};
|
||||
|
||||
export interface IRouterData {
|
||||
canvas?: {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom?: number;
|
||||
};
|
||||
template?: {
|
||||
url: string;
|
||||
width?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
} = {
|
||||
enabled: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
_hashChange(e: HashChangeEvent) {
|
||||
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");
|
||||
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 + "");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
| 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}) {
|
||||
this.templateState = args;
|
||||
}
|
||||
}
|
||||
|
||||
export const Router = new _Router();
|
|
@ -1,38 +0,0 @@
|
|||
import { ICanvasPosition } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
export interface ITemplateState {
|
||||
url: string;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export const Routes = {
|
||||
canvas: ({
|
||||
pos,
|
||||
template,
|
||||
}: {
|
||||
pos?: ICanvasPosition;
|
||||
template?: ITemplateState;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (pos) {
|
||||
params.set("x", pos.x + "");
|
||||
params.set("y", pos.y + "");
|
||||
params.set("zoom", pos.zoom + "");
|
||||
}
|
||||
|
||||
if (template) {
|
||||
let { url, width, x, y, opacity } = template;
|
||||
params.set("template.url", url);
|
||||
params.set("template.width", width + "");
|
||||
params.set("template.x", x + "");
|
||||
params.set("template.y", y + "");
|
||||
params.set("template.opacity", opacity + "");
|
||||
}
|
||||
|
||||
return "/#" + params;
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue