This commit is contained in:
Arnost Pleskot 2025-05-01 13:24:21 +02:00 committed by GitHub
commit 24fc02c29d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1918 additions and 68 deletions

View file

@ -4,6 +4,7 @@ import {
TTDDialogTrigger,
CaptureUpdateAction,
reconcileElements,
getCommonBounds,
} from "@excalidraw/excalidraw";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
@ -60,6 +61,7 @@ import {
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
import type {
ExcalidrawElement,
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
@ -70,8 +72,9 @@ import type {
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
ScrollConstraints,
} from "@excalidraw/excalidraw/types";
import type { ResolutionType } from "@excalidraw/common/utility-types";
import type { Merge, ResolutionType } from "@excalidraw/common/utility-types";
import type { ResolvablePromise } from "@excalidraw/common/utils";
import CustomStats from "./CustomStats";
@ -138,9 +141,156 @@ import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
import "./index.scss";
import type { CollabAPI } from "./collab/Collab";
import { getSelectedElements } from "@excalidraw/element/selection";
polyfill();
type DebugScrollConstraints = Merge<
ScrollConstraints,
{ viewportZoomFactor: number; enabled: boolean }
>;
const ConstraintsSettings = ({
initialConstraints,
excalidrawAPI,
}: {
initialConstraints: DebugScrollConstraints;
excalidrawAPI: ExcalidrawImperativeAPI;
}) => {
const [constraints, setConstraints] =
useState<DebugScrollConstraints>(initialConstraints);
useEffect(() => {
// add JSON-stringified constraints into url hash for easy sharing
const hash = new URLSearchParams(window.location.hash.slice(1));
hash.set(
"constraints",
encodeURIComponent(
window.btoa(JSON.stringify(constraints)).replace(/=+/, ""),
),
);
window.location.hash = decodeURIComponent(hash.toString());
excalidrawAPI.setScrollConstraints(constraints);
}, [constraints]);
const [selection, setSelection] = useState<ExcalidrawElement[]>([]);
useEffect(() => {
return excalidrawAPI.onChange((elements, appState) => {
setSelection(getSelectedElements(elements, appState));
});
}, [excalidrawAPI]);
return (
<div
style={{
display: "flex",
position: "fixed",
bottom: 10,
left: "calc(50%)",
transform: "translateX(-50%)",
gap: "0.6rem",
zIndex: 999999,
}}
>
enabled:{" "}
<input
type="checkbox"
defaultChecked={!!constraints.enabled}
onChange={(e) =>
setConstraints((s) => ({ ...s, enabled: e.target.checked }))
}
/>
x:{" "}
<input
placeholder="x"
size={4}
value={constraints.x.toString()}
onChange={(e) =>
setConstraints((s) => ({
...s,
x: parseInt(e.target.value) ?? 0,
}))
}
/>
y:{" "}
<input
placeholder="y"
size={4}
value={constraints.y.toString()}
onChange={(e) =>
setConstraints((s) => ({
...s,
y: parseInt(e.target.value) ?? 0,
}))
}
/>
w:{" "}
<input
placeholder="width"
size={4}
value={constraints.width.toString()}
onChange={(e) =>
setConstraints((s) => ({
...s,
width: parseInt(e.target.value) ?? 200,
}))
}
/>
h:{" "}
<input
placeholder="height"
size={4}
value={constraints.height.toString()}
onChange={(e) =>
setConstraints((s) => ({
...s,
height: parseInt(e.target.value) ?? 200,
}))
}
/>
zoomFactor:
<input
placeholder="height"
type="number"
min="0.1"
max="1"
step="0.1"
value={constraints.viewportZoomFactor.toString()}
onChange={(e) =>
setConstraints((s) => ({
...s,
viewportZoomFactor: parseFloat(e.target.value.toString()) ?? 0.7,
}))
}
/>
lockZoom:{" "}
<input
type="checkbox"
defaultChecked={!!constraints.lockZoom}
onChange={(e) =>
setConstraints((s) => ({ ...s, lockZoom: e.target.checked }))
}
/>
{selection.length > 0 && (
<button
onClick={() => {
const bbox = getCommonBounds(selection);
setConstraints((s) => ({
...s,
x: Math.round(bbox[0]),
y: Math.round(bbox[1]),
width: Math.round(bbox[2] - bbox[0]),
height: Math.round(bbox[3] - bbox[1]),
}));
}}
>
use selection
</button>
)}
</div>
);
};
window.EXCALIDRAW_THROTTLE_RENDER = true;
declare global {
@ -212,10 +362,22 @@ const initializeScene = async (opts: {
)
> => {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.slice(1));
const id = searchParams.get("id");
const jsonBackendMatch = window.location.hash.match(
/^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
);
const shareableLink = hashParams.get("json")?.split(",");
if (shareableLink) {
hashParams.delete("json");
const hash = `#${decodeURIComponent(hashParams.toString())}`;
window.history.replaceState(
{},
APP_NAME,
`${window.location.origin}${hash}`,
);
}
console.log({ shareableLink });
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
const localDataState = importFromLocalStorage();
@ -225,7 +387,7 @@ const initializeScene = async (opts: {
} = await loadScene(null, null, localDataState);
let roomLinkData = getCollaborationLinkData(window.location.href);
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
const isExternalScene = !!(id || shareableLink || roomLinkData);
if (isExternalScene) {
if (
// don't prompt if scene is empty
@ -235,16 +397,17 @@ const initializeScene = async (opts: {
// otherwise, prompt whether user wants to override current scene
(await openConfirmModal(shareableLinkConfirmDialog))
) {
if (jsonBackendMatch) {
if (shareableLink) {
scene = await loadScene(
jsonBackendMatch[1],
jsonBackendMatch[2],
shareableLink[0],
shareableLink[1],
localDataState,
);
console.log(">>>>", scene);
}
scene.scrollToContent = true;
if (!roomLinkData) {
window.history.replaceState({}, APP_NAME, window.location.origin);
// window.history.replaceState({}, APP_NAME, window.location.origin);
}
} else {
// https://github.com/excalidraw/excalidraw/issues/1919
@ -261,7 +424,7 @@ const initializeScene = async (opts: {
}
roomLinkData = null;
window.history.replaceState({}, APP_NAME, window.location.origin);
// window.history.replaceState({}, APP_NAME, window.location.origin);
}
} else if (externalUrlMatch) {
window.history.replaceState({}, APP_NAME, window.location.origin);
@ -322,12 +485,12 @@ const initializeScene = async (opts: {
key: roomLinkData.roomKey,
};
} else if (scene) {
return isExternalScene && jsonBackendMatch
return isExternalScene && shareableLink
? {
scene,
isExternalScene,
id: jsonBackendMatch[1],
key: jsonBackendMatch[2],
id: shareableLink[0],
key: shareableLink[1],
}
: { scene, isExternalScene: false };
}
@ -739,6 +902,31 @@ const ExcalidrawWrapper = () => {
[setShareDialogState],
);
const [constraints] = useState<DebugScrollConstraints>(() => {
const stored = new URLSearchParams(location.hash.slice(1)).get(
"constraints",
);
let storedConstraints = {};
if (stored) {
try {
storedConstraints = JSON.parse(window.atob(stored));
} catch {}
}
return {
x: 0,
y: 0,
width: document.body.clientWidth,
height: document.body.clientHeight,
lockZoom: false,
viewportZoomFactor: 0.7,
enabled: true,
...storedConstraints,
};
});
console.log(constraints);
// browsers generally prevent infinite self-embedding, there are
// cases where it still happens, and while we disallow self-embedding
// by not whitelisting our own origin, this serves as an additional guard
@ -864,6 +1052,7 @@ const ExcalidrawWrapper = () => {
</div>
);
}}
scrollConstraints={constraints.enabled ? constraints : undefined}
onLinkOpen={(element, event) => {
if (element.link && isElementLink(element.link)) {
event.preventDefault();
@ -871,6 +1060,12 @@ const ExcalidrawWrapper = () => {
}
}}
>
{excalidrawAPI && (
<ConstraintsSettings
excalidrawAPI={excalidrawAPI}
initialConstraints={constraints}
/>
)}
<AppMainMenu
onCollabDialogOpen={onCollabDialogOpen}
isCollaborating={isCollaborating}