Refactor
This commit is contained in:
parent
624500b091
commit
c71ccaf17a
@ -878,6 +878,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
theme={appTheme}
|
theme={appTheme}
|
||||||
setTheme={(theme) => setAppTheme(theme)}
|
setTheme={(theme) => setAppTheme(theme)}
|
||||||
refresh={() => forceRefresh((prev) => !prev)}
|
refresh={() => forceRefresh((prev) => !prev)}
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
/>
|
/>
|
||||||
<AppWelcomeScreen
|
<AppWelcomeScreen
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
onCollabDialogOpen={onCollabDialogOpen}
|
||||||
@ -1154,25 +1155,38 @@ const ExcalidrawRecorderWrapper = ({ children }: React.PropsWithChildren) => {
|
|||||||
{ type: string; listener: EventListener }
|
{ type: string; listener: EventListener }
|
||||||
>
|
>
|
||||||
>(new WeakMap());
|
>(new WeakMap());
|
||||||
const dataRef = useRef<object[]>([]);
|
const replayRef = useRef<Map<string, EventListenerOrEventListenerObject[]>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
const dataRef = useRef<object[] | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dataRef.current.push({
|
/**
|
||||||
time: new Date().getTime(),
|
* Run the event listeners for the type
|
||||||
type: "start",
|
*/
|
||||||
localStorage: JSON.parse(JSON.stringify(window.localStorage)),
|
window.runReplay = (type: string, payload: any) => {
|
||||||
dimensions: {
|
replayRef.current
|
||||||
outerWidth: window.outerWidth,
|
.get(type)
|
||||||
outerHeight: window.outerHeight,
|
?.forEach((listener) =>
|
||||||
},
|
Object.hasOwn(listener, "handleEvent")
|
||||||
chromeVersion: window.navigator.userAgent
|
? (listener as EventListenerObject).handleEvent(payload)
|
||||||
.split(" ")
|
: (listener as EventListener)(payload),
|
||||||
.find((v) => v.startsWith("Chrome/"))
|
);
|
||||||
?.substring(7),
|
};
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Access to the recorded data
|
||||||
|
*/
|
||||||
|
window.getRecordedDataRef = (): object[] | null => dataRef.current;
|
||||||
|
window.setRecordedDataRef = (data: object[] | null) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
};
|
||||||
|
|
||||||
Window.prototype._removeEventListener =
|
Window.prototype._removeEventListener =
|
||||||
Window.prototype.removeEventListener;
|
Window.prototype.removeEventListener;
|
||||||
|
/**
|
||||||
|
* removeEventListener
|
||||||
|
*/
|
||||||
window.removeEventListener = function <K extends keyof WindowEventMap>(
|
window.removeEventListener = function <K extends keyof WindowEventMap>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: EventListenerOrEventListenerObject,
|
listener: EventListenerOrEventListenerObject,
|
||||||
@ -1182,12 +1196,25 @@ const ExcalidrawRecorderWrapper = ({ children }: React.PropsWithChildren) => {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
window._removeEventListener(type, existing.listener, options);
|
window._removeEventListener(type, existing.listener, options);
|
||||||
listenerRef.current.delete(listener);
|
listenerRef.current.delete(listener);
|
||||||
|
const eventListeners = replayRef.current.get(type);
|
||||||
|
if (eventListeners) {
|
||||||
|
const index = eventListeners.indexOf(existing.listener);
|
||||||
|
if (index !== -1) {
|
||||||
|
eventListeners.splice(index, 1);
|
||||||
|
if (eventListeners.length === 0) {
|
||||||
|
replayRef.current.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window._removeEventListener(type, listener, options);
|
window._removeEventListener(type, listener, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Window.prototype._addEventListener = Window.prototype.addEventListener;
|
Window.prototype._addEventListener = Window.prototype.addEventListener;
|
||||||
|
/**
|
||||||
|
* addEventListener
|
||||||
|
*/
|
||||||
window.addEventListener = function <K extends keyof WindowEventMap>(
|
window.addEventListener = function <K extends keyof WindowEventMap>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: EventListenerOrEventListenerObject,
|
listener: EventListenerOrEventListenerObject,
|
||||||
@ -1198,7 +1225,7 @@ const ExcalidrawRecorderWrapper = ({ children }: React.PropsWithChildren) => {
|
|||||||
|
|
||||||
if (!existing || existing?.type !== type) {
|
if (!existing || existing?.type !== type) {
|
||||||
wrappedListener = function (...args) {
|
wrappedListener = function (...args) {
|
||||||
dataRef.current.push({
|
dataRef.current?.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
type: "event",
|
type: "event",
|
||||||
name: type,
|
name: type,
|
||||||
@ -1217,6 +1244,11 @@ const ExcalidrawRecorderWrapper = ({ children }: React.PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listenerRef.current.set(listener, { type, listener: wrappedListener });
|
listenerRef.current.set(listener, { type, listener: wrappedListener });
|
||||||
|
if (replayRef.current.has(type)) {
|
||||||
|
replayRef.current.get(type)?.push(listener);
|
||||||
|
} else {
|
||||||
|
replayRef.current.set(type, [listener]);
|
||||||
|
}
|
||||||
|
|
||||||
window._addEventListener(type, wrappedListener, options);
|
window._addEventListener(type, wrappedListener, options);
|
||||||
};
|
};
|
||||||
@ -1230,14 +1262,6 @@ const ExcalidrawRecorderWrapper = ({ children }: React.PropsWithChildren) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handle = setInterval(() => {
|
|
||||||
console.log(dataRef.current);
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => clearInterval(handle);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,13 +2,21 @@ import {
|
|||||||
loginIcon,
|
loginIcon,
|
||||||
ExcalLogo,
|
ExcalLogo,
|
||||||
eyeIcon,
|
eyeIcon,
|
||||||
|
pngIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { isDevEnv } from "@excalidraw/common";
|
import { getVersion, isDevEnv } from "@excalidraw/common";
|
||||||
|
import { fileOpen, fileSave } from "@excalidraw/excalidraw/data/filesystem";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||||
|
|
||||||
import type { Theme } from "@excalidraw/element/types";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
@ -22,6 +30,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
theme: Theme | "system";
|
theme: Theme | "system";
|
||||||
setTheme: (theme: Theme | "system") => void;
|
setTheme: (theme: Theme | "system") => void;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
|
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
@ -60,6 +69,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
{isDevEnv() && (
|
{isDevEnv() && (
|
||||||
|
<>
|
||||||
<MainMenu.Item
|
<MainMenu.Item
|
||||||
icon={eyeIcon}
|
icon={eyeIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -75,6 +85,118 @@ export const AppMainMenu: React.FC<{
|
|||||||
>
|
>
|
||||||
Visual Debug
|
Visual Debug
|
||||||
</MainMenu.Item>
|
</MainMenu.Item>
|
||||||
|
{props.excalidrawAPI && (
|
||||||
|
<>
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.Item
|
||||||
|
icon={pngIcon}
|
||||||
|
onClick={async () => {
|
||||||
|
const blob = await fileOpen({
|
||||||
|
description: "Excalidraw test case recording",
|
||||||
|
extensions: ["json"],
|
||||||
|
});
|
||||||
|
const text = await blob.text();
|
||||||
|
const recording = superjson.parse<any>(text);
|
||||||
|
|
||||||
|
window.setRecordedDataRef(null);
|
||||||
|
|
||||||
|
const start = recording.shift();
|
||||||
|
|
||||||
|
window.resizeTo(
|
||||||
|
start.dimensions.innerWidth,
|
||||||
|
start.dimensions.innerHeight,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
Math.abs(start.dimensions.innerWidth - window.innerWidth) >
|
||||||
|
1 ||
|
||||||
|
Math.abs(
|
||||||
|
start.dimensions.innerHeight - window.innerHeight,
|
||||||
|
) > 1
|
||||||
|
) {
|
||||||
|
console.error("Window dimensions do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.excalidrawAPI!.resetScene();
|
||||||
|
props.excalidrawAPI!.updateScene({
|
||||||
|
elements: superjson.parse(start.scene),
|
||||||
|
appState: {
|
||||||
|
...getDefaultAppState(),
|
||||||
|
...superjson.parse<AppState>(start.state),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastTime = start.time;
|
||||||
|
for (const item of recording) {
|
||||||
|
if (item.type === "event") {
|
||||||
|
const { time, type, name, ...rest } = item;
|
||||||
|
const delay = time - lastTime;
|
||||||
|
lastTime = time;
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, delay),
|
||||||
|
);
|
||||||
|
console.log(type, name, rest);
|
||||||
|
window.runReplay(name, rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run Recording...
|
||||||
|
</MainMenu.Item>
|
||||||
|
<MainMenu.Item
|
||||||
|
icon={pngIcon}
|
||||||
|
onClick={async () => {
|
||||||
|
window.setRecordedDataRef([
|
||||||
|
{
|
||||||
|
time: new Date().getTime(),
|
||||||
|
type: "start",
|
||||||
|
excalidrawVersion: getVersion(),
|
||||||
|
dimensions: {
|
||||||
|
innerWidth: window.innerWidth,
|
||||||
|
innerHeight: window.innerHeight,
|
||||||
|
},
|
||||||
|
chromeVersion: window.navigator.userAgent
|
||||||
|
.split(" ")
|
||||||
|
.find((v) => v.startsWith("Chrome/"))
|
||||||
|
?.substring(7),
|
||||||
|
state: superjson.stringify(
|
||||||
|
props.excalidrawAPI!.getAppState(),
|
||||||
|
),
|
||||||
|
scene: superjson.stringify(
|
||||||
|
props.excalidrawAPI!.getSceneElementsIncludingDeleted(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start Recording
|
||||||
|
</MainMenu.Item>
|
||||||
|
<MainMenu.Item
|
||||||
|
icon={pngIcon}
|
||||||
|
onClick={async () => {
|
||||||
|
const blob = new Blob(
|
||||||
|
[superjson.stringify(window.getRecordedDataRef())],
|
||||||
|
{
|
||||||
|
type: "text/json",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fileSave(blob, {
|
||||||
|
name: `testcase-${new Date().getTime()}${Math.floor(
|
||||||
|
Math.random() * 10000,
|
||||||
|
)}`,
|
||||||
|
extension: "json",
|
||||||
|
description: "Excalidraw test case recording",
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Recording...
|
||||||
|
</MainMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.DefaultItems.ToggleTheme
|
<MainMenu.DefaultItems.ToggleTheme
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"socket.io-client": "4.7.2",
|
"socket.io-client": "4.7.2",
|
||||||
|
"superjson": "2.2.2",
|
||||||
"vite-plugin-html": "3.2.2"
|
"vite-plugin-html": "3.2.2"
|
||||||
},
|
},
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
|
@ -11427,6 +11427,9 @@ declare global {
|
|||||||
};
|
};
|
||||||
_addEventListener: typeof window.addEventListener;
|
_addEventListener: typeof window.addEventListener;
|
||||||
_removeEventListener: typeof window.removeEventListener;
|
_removeEventListener: typeof window.removeEventListener;
|
||||||
|
getRecordedDataRef: () => object[] | null;
|
||||||
|
setRecordedDataRef: (data: object[] | null) => void;
|
||||||
|
runReplay: (type: string, payload: any) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
yarn.lock
19
yarn.lock
@ -4189,6 +4189,13 @@ convert-source-map@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||||
|
|
||||||
|
copy-anything@^3.0.2:
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
|
||||||
|
integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
|
||||||
|
dependencies:
|
||||||
|
is-what "^4.1.8"
|
||||||
|
|
||||||
core-js-compat@^3.38.0, core-js-compat@^3.40.0:
|
core-js-compat@^3.38.0, core-js-compat@^3.40.0:
|
||||||
version "3.41.0"
|
version "3.41.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.41.0.tgz#4cdfce95f39a8f27759b667cf693d96e5dda3d17"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.41.0.tgz#4cdfce95f39a8f27759b667cf693d96e5dda3d17"
|
||||||
@ -6501,6 +6508,11 @@ is-weakset@^2.0.3:
|
|||||||
call-bound "^1.0.3"
|
call-bound "^1.0.3"
|
||||||
get-intrinsic "^1.2.6"
|
get-intrinsic "^1.2.6"
|
||||||
|
|
||||||
|
is-what@^4.1.8:
|
||||||
|
version "4.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
|
||||||
|
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
|
||||||
|
|
||||||
isarray@^2.0.5:
|
isarray@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||||
@ -8941,6 +8953,13 @@ stylis@^4.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320"
|
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320"
|
||||||
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
|
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
|
||||||
|
|
||||||
|
superjson@2.2.2:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
|
||||||
|
integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==
|
||||||
|
dependencies:
|
||||||
|
copy-anything "^3.0.2"
|
||||||
|
|
||||||
supports-color@^5.3.0:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user