Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c3dffb082 | |||
| 0e0f34edd8 | |||
| 4888d9d355 | |||
| 1c39bd5781 | |||
| 90ad885446 | |||
| 1741c234a6 | |||
| 63b50b3586 | |||
| e0fefa8025 | |||
| d426cc968d | |||
| 2409c091ff | |||
| 626fe252ab |
@@ -7,9 +7,6 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
|||||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||||
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
|
|
||||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
|
||||||
VITE_APP_PORTAL_URL=
|
|
||||||
|
|
||||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
|||||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
|
||||||
|
|
||||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
|
||||||
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||||
|
|
||||||
# Fill to set socket server URL used for collaboration.
|
# socket server URL used for collaboration
|
||||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||||
VITE_APP_WS_SERVER_URL=
|
|
||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getCollabServer,
|
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
@@ -452,13 +451,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const socketServerData = await getCollabServer();
|
|
||||||
|
|
||||||
this.portal.socket = this.portal.open(
|
this.portal.socket = this.portal.open(
|
||||||
socketIOClient(socketServerData.url, {
|
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||||
transports: socketServerData.polling
|
transports: ["websocket", "polling"],
|
||||||
? ["websocket", "polling"]
|
|
||||||
: ["websocket"],
|
|
||||||
}),
|
}),
|
||||||
roomId,
|
roomId,
|
||||||
roomKey,
|
roomKey,
|
||||||
|
|||||||
@@ -65,35 +65,6 @@ const generateRoomId = async () => {
|
|||||||
return bytesToHexString(buffer);
|
return bytesToHexString(buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Right now the reason why we resolve connection params (url, polling...)
|
|
||||||
* from upstream is to allow changing the params immediately when needed without
|
|
||||||
* having to wait for clients to update the SW.
|
|
||||||
*
|
|
||||||
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
|
||||||
*/
|
|
||||||
export const getCollabServer = async (): Promise<{
|
|
||||||
url: string;
|
|
||||||
polling: boolean;
|
|
||||||
}> => {
|
|
||||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
|
||||||
return {
|
|
||||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
|
||||||
polling: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(
|
|
||||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
|
||||||
);
|
|
||||||
return await resp.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw new Error(t("errors.cannotResolveCollabServer"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EncryptedData = {
|
export type EncryptedData = {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
iv: Uint8Array;
|
iv: Uint8Array;
|
||||||
|
|||||||
@@ -20,17 +20,6 @@ Object.defineProperty(window, "crypto", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
|
|
||||||
const module = (await importActual()) as any;
|
|
||||||
return {
|
|
||||||
__esmodule: true,
|
|
||||||
...module,
|
|
||||||
getCollabServer: vi.fn(() => ({
|
|
||||||
url: /* doesn't really matter */ "http://localhost:3002",
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||||
const loadFromFirebase = async () => null;
|
const loadFromFirebase = async () => null;
|
||||||
const saveToFirebase = () => {};
|
const saveToFirebase = () => {};
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `onPointerUp` prop [#7638](https://github.com/excalidraw/excalidraw/pull/7638).
|
||||||
|
|
||||||
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
||||||
- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "../element/textWysiwyg";
|
} from "../element/containerCache";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ import {
|
|||||||
newLinearElement,
|
newLinearElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
textWysiwyg,
|
|
||||||
transformElements,
|
transformElements,
|
||||||
updateTextElement,
|
updateTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -217,7 +216,6 @@ import {
|
|||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
hasBackground,
|
hasBackground,
|
||||||
isOverScrollBars,
|
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
@@ -409,6 +407,8 @@ import { AnimatedTrail } from "../animated-trail";
|
|||||||
import { LaserTrails } from "../laser-trails";
|
import { LaserTrails } from "../laser-trails";
|
||||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||||
import { getRenderOpacity } from "../renderer/renderElement";
|
import { getRenderOpacity } from "../renderer/renderElement";
|
||||||
|
import { textWysiwyg } from "../element/textWysiwyg";
|
||||||
|
import { isOverScrollBars } from "../scene/scrollbars";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -1299,10 +1299,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const FRAME_NAME_EDIT_PADDING = 6;
|
const FRAME_NAME_EDIT_PADDING = 6;
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if (f.name?.trim() === "") {
|
mutateElement(f, { name: f.name?.trim() || null });
|
||||||
mutateElement(f, { name: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ editingFrame: null });
|
this.setState({ editingFrame: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1325,6 +1322,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
name: e.target.value,
|
name: e.target.value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
onBlur={() => reset()}
|
onBlur={() => reset()}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
// for some inexplicable reason, `onBlur` triggered on ESC
|
// for some inexplicable reason, `onBlur` triggered on ESC
|
||||||
@@ -6503,8 +6501,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embedLink.warning) {
|
if (embedLink.error instanceof URIError) {
|
||||||
this.setToast({ message: embedLink.warning, closable: true });
|
this.setToast({
|
||||||
|
message: t("toast.unrecognizedLinkFormat"),
|
||||||
|
closable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = newEmbeddableElement({
|
const element = newEmbeddableElement({
|
||||||
@@ -7566,6 +7567,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ pendingImageElementId: null });
|
this.setState({ pendingImageElementId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props?.onPointerUp?.(activeTool, pointerDownState);
|
||||||
this.onPointerUpEmitter.trigger(
|
this.onPointerUpEmitter.trigger(
|
||||||
this.state.activeTool,
|
this.state.activeTool,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
|
|||||||
@@ -16,25 +16,20 @@ const FollowMode = ({
|
|||||||
onDisconnect,
|
onDisconnect,
|
||||||
}: FollowModeProps) => {
|
}: FollowModeProps) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div className="follow-mode" style={{ width, height }}>
|
||||||
<div className="follow-mode" style={{ width, height }}>
|
<div className="follow-mode__badge">
|
||||||
<div className="follow-mode__badge">
|
<div className="follow-mode__badge__label">
|
||||||
<div className="follow-mode__badge__label">
|
Following{" "}
|
||||||
Following{" "}
|
<span
|
||||||
<span
|
className="follow-mode__badge__username"
|
||||||
className="follow-mode__badge__username"
|
title={userToFollow.username}
|
||||||
title={userToFollow.username}
|
|
||||||
>
|
|
||||||
{userToFollow.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onDisconnect}
|
|
||||||
className="follow-mode__disconnect-btn"
|
|
||||||
>
|
>
|
||||||
{CloseIcon}
|
{userToFollow.username}
|
||||||
</button>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
||||||
|
{CloseIcon}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
|||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
|
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||||
|
|
||||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,11 @@ export const Hyperlink = ({
|
|||||||
} else {
|
} else {
|
||||||
const { width, height } = element;
|
const { width, height } = element;
|
||||||
const embedLink = getEmbedLink(link);
|
const embedLink = getEmbedLink(link);
|
||||||
if (embedLink?.warning) {
|
if (embedLink?.error instanceof URIError) {
|
||||||
setToast({ message: embedLink.warning, closable: true });
|
setToast({
|
||||||
|
message: t("toast.unrecognizedLinkFormat"),
|
||||||
|
closable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const ar = embedLink
|
const ar = embedLink
|
||||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ExcalidrawTextContainer } from "./types";
|
||||||
|
|
||||||
|
export const originalContainerCache: {
|
||||||
|
[id: ExcalidrawTextContainer["id"]]:
|
||||||
|
| {
|
||||||
|
height: ExcalidrawTextContainer["height"];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
export const updateOriginalContainerCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
height: ExcalidrawTextContainer["height"],
|
||||||
|
) => {
|
||||||
|
const data =
|
||||||
|
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||||
|
data.height = height;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetOriginalContainerCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
) => {
|
||||||
|
if (originalContainerCache[id]) {
|
||||||
|
delete originalContainerCache[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOriginalContainerHeightFromCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
) => {
|
||||||
|
return originalContainerCache[id]?.height ?? null;
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { register } from "../actions/register";
|
import { register } from "../actions/register";
|
||||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||||
import { t } from "../i18n";
|
|
||||||
import { ExcalidrawProps } from "../types";
|
import { ExcalidrawProps } from "../types";
|
||||||
import { getFontString, updateActiveTool } from "../utils";
|
import { getFontString, updateActiveTool } from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
@@ -107,8 +106,8 @@ export const getEmbedLink = (
|
|||||||
const vimeoLink = link.match(RE_VIMEO);
|
const vimeoLink = link.match(RE_VIMEO);
|
||||||
if (vimeoLink?.[1]) {
|
if (vimeoLink?.[1]) {
|
||||||
const target = vimeoLink?.[1];
|
const target = vimeoLink?.[1];
|
||||||
const warning = !/^\d+$/.test(target)
|
const error = !/^\d+$/.test(target)
|
||||||
? t("toast.unrecognizedLinkFormat")
|
? new URIError("Invalid embed link format")
|
||||||
: undefined;
|
: undefined;
|
||||||
type = "video";
|
type = "video";
|
||||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||||
@@ -120,7 +119,7 @@ export const getEmbedLink = (
|
|||||||
intrinsicSize: aspectRatio,
|
intrinsicSize: aspectRatio,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
return { link, intrinsicSize: aspectRatio, type, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export {
|
|||||||
dragNewElement,
|
dragNewElement,
|
||||||
} from "./dragElements";
|
} from "./dragElements";
|
||||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||||
export { textWysiwyg } from "./textWysiwyg";
|
|
||||||
export { redrawTextBoundingBox } from "./textElement";
|
export { redrawTextBoundingBox } from "./textElement";
|
||||||
export {
|
export {
|
||||||
getPerfectElementSize,
|
getPerfectElementSize,
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ import { isTextBindableContainer } from "./typeChecks";
|
|||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||||
|
|
||||||
|
import { ExtractSetType } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./containerCache";
|
||||||
import { ExtractSetType } from "../utility-types";
|
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextContainer,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||||
@@ -44,6 +43,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
import {
|
||||||
|
originalContainerCache,
|
||||||
|
updateOriginalContainerCache,
|
||||||
|
} from "./containerCache";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@@ -66,38 +69,6 @@ const getTransform = (
|
|||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalContainerCache: {
|
|
||||||
[id: ExcalidrawTextContainer["id"]]:
|
|
||||||
| {
|
|
||||||
height: ExcalidrawTextContainer["height"];
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
export const updateOriginalContainerCache = (
|
|
||||||
id: ExcalidrawTextContainer["id"],
|
|
||||||
height: ExcalidrawTextContainer["height"],
|
|
||||||
) => {
|
|
||||||
const data =
|
|
||||||
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
|
||||||
data.height = height;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetOriginalContainerCache = (
|
|
||||||
id: ExcalidrawTextContainer["id"],
|
|
||||||
) => {
|
|
||||||
if (originalContainerCache[id]) {
|
|
||||||
delete originalContainerCache[id];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOriginalContainerHeightFromCache = (
|
|
||||||
id: ExcalidrawTextContainer["id"],
|
|
||||||
) => {
|
|
||||||
return originalContainerCache[id]?.height ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const textWysiwyg = ({
|
export const textWysiwyg = ({
|
||||||
id,
|
id,
|
||||||
onChange,
|
onChange,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { rotate } from "../math";
|
|||||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
@@ -106,7 +106,8 @@ export const getTransformHandlesFromCoords = (
|
|||||||
const width = x2 - x1;
|
const width = x2 - x1;
|
||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
const dashedLineMargin = margin / zoom.value;
|
const dashedLineMargin = margin / zoom.value;
|
||||||
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
const centeringOffset =
|
||||||
|
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
|
||||||
|
|
||||||
const transformHandles: TransformHandles = {
|
const transformHandles: TransformHandles = {
|
||||||
nw: omitSides.nw
|
nw: omitSides.nw
|
||||||
@@ -263,8 +264,8 @@ export const getTransformHandles = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const dashedLineMargin = isLinearElement(element)
|
const dashedLineMargin = isLinearElement(element)
|
||||||
? DEFAULT_SPACING + 8
|
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
|
||||||
: DEFAULT_SPACING;
|
: DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||||
return getTransformHandlesFromCoords(
|
return getTransformHandlesFromCoords(
|
||||||
getElementAbsoluteCoords(element, true),
|
getElementAbsoluteCoords(element, true),
|
||||||
element.angle,
|
element.angle,
|
||||||
|
|||||||
@@ -214,7 +214,10 @@ export const isBoundToContainer = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" || type === "embeddable" || type === "iframe";
|
type === "rectangle" ||
|
||||||
|
type === "embeddable" ||
|
||||||
|
type === "iframe" ||
|
||||||
|
type === "image";
|
||||||
|
|
||||||
export const isUsingProportionalRadius = (type: string) =>
|
export const isUsingProportionalRadius = (type: string) =>
|
||||||
type === "line" || type === "arrow" || type === "diamond";
|
type === "line" || type === "arrow" || type === "diamond";
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export type ExcalidrawIframeLikeElement =
|
|||||||
export type IframeData =
|
export type IframeData =
|
||||||
| {
|
| {
|
||||||
intrinsicSize: { w: number; h: number };
|
intrinsicSize: { w: number; h: number };
|
||||||
warning?: string;
|
error?: Error;
|
||||||
} & (
|
} & (
|
||||||
| { type: "video" | "generic"; link: string }
|
| { type: "video" | "generic"; link: string }
|
||||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||||
|
|||||||
@@ -746,7 +746,7 @@ export const getFrameLikeTitle = (
|
|||||||
element: ExcalidrawFrameLikeElement,
|
element: ExcalidrawFrameLikeElement,
|
||||||
frameIdx: number,
|
frameIdx: number,
|
||||||
) => {
|
) => {
|
||||||
// TODO name frames AI only is specific to AI frames
|
// TODO name frames "AI" only if specific to AI frames
|
||||||
return element.name === null
|
return element.name === null
|
||||||
? isFrameElement(element)
|
? isFrameElement(element)
|
||||||
? `Frame ${frameIdx}`
|
? `Frame ${frameIdx}`
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
generateIdForFile,
|
generateIdForFile,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
|
onPointerUp,
|
||||||
onScrollChange,
|
onScrollChange,
|
||||||
children,
|
children,
|
||||||
validateEmbeddable,
|
validateEmbeddable,
|
||||||
@@ -131,6 +132,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
generateIdForFile={generateIdForFile}
|
generateIdForFile={generateIdForFile}
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
onScrollChange={onScrollChange}
|
onScrollChange={onScrollChange}
|
||||||
validateEmbeddable={validateEmbeddable}
|
validateEmbeddable={validateEmbeddable}
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
|
|||||||
@@ -344,6 +344,17 @@ const drawElementOnCanvas = (
|
|||||||
? renderConfig.imageCache.get(element.fileId)?.image
|
? renderConfig.imageCache.get(element.fileId)?.image
|
||||||
: undefined;
|
: undefined;
|
||||||
if (img != null && !(img instanceof Promise)) {
|
if (img != null && !(img instanceof Promise)) {
|
||||||
|
if (element.roundness && context.roundRect) {
|
||||||
|
context.beginPath();
|
||||||
|
context.roundRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
getCornerRadius(Math.min(element.width, element.height), element),
|
||||||
|
);
|
||||||
|
context.clip();
|
||||||
|
}
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
img,
|
img,
|
||||||
0 /* hardcoded for the selection box*/,
|
0 /* hardcoded for the selection box*/,
|
||||||
@@ -1301,6 +1312,31 @@ export const renderElementToSvg = (
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (element.roundness) {
|
||||||
|
const clipPath = svgRoot.ownerDocument!.createElementNS(
|
||||||
|
SVG_NS,
|
||||||
|
"clipPath",
|
||||||
|
);
|
||||||
|
clipPath.id = `image-clipPath-${element.id}`;
|
||||||
|
|
||||||
|
const clipRect = svgRoot.ownerDocument!.createElementNS(
|
||||||
|
SVG_NS,
|
||||||
|
"rect",
|
||||||
|
);
|
||||||
|
const radius = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
clipRect.setAttribute("width", `${element.width}`);
|
||||||
|
clipRect.setAttribute("height", `${element.height}`);
|
||||||
|
clipRect.setAttribute("rx", `${radius}`);
|
||||||
|
clipRect.setAttribute("ry", `${radius}`);
|
||||||
|
clipPath.appendChild(clipRect);
|
||||||
|
addToRoot(clipPath, element);
|
||||||
|
|
||||||
|
g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
const clipG = maybeWrapNodesInFrameClipPath(
|
const clipG = maybeWrapNodesInFrameClipPath(
|
||||||
element,
|
element,
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ import {
|
|||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { arrayToMap, throttleRAF } from "../utils";
|
import { arrayToMap, throttleRAF } from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
import {
|
||||||
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
|
FRAME_STYLE,
|
||||||
|
THEME_FILTER,
|
||||||
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
@@ -83,8 +87,6 @@ import {
|
|||||||
isElementInFrame,
|
isElementInFrame,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
|
|
||||||
export const DEFAULT_SPACING = 2;
|
|
||||||
|
|
||||||
const strokeRectWithRotation = (
|
const strokeRectWithRotation = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@@ -676,7 +678,8 @@ const _renderInteractiveScene = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value;
|
const dashedLinePadding =
|
||||||
|
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||||
context.fillStyle = oc.white;
|
context.fillStyle = oc.white;
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||||
const initialLineDash = context.getLineDash();
|
const initialLineDash = context.getLineDash();
|
||||||
@@ -1191,7 +1194,7 @@ const renderSelectionBorder = (
|
|||||||
cy: number;
|
cy: number;
|
||||||
activeEmbeddable: boolean;
|
activeEmbeddable: boolean;
|
||||||
},
|
},
|
||||||
padding = DEFAULT_SPACING * 2,
|
padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
angle,
|
angle,
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
|||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "diamond";
|
type === "diamond" ||
|
||||||
|
type === "image";
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { isOverScrollBars } from "./scrollbars";
|
|
||||||
export {
|
export {
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ exports[`export > exporting svg containing transformed images > svg export outpu
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
<g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, 1) translate(-50 0)"></use></g><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="scale(1, -1) translate(0 -100)"></use></g><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, -1) translate(-50 -50)"></use></g></svg>"
|
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, 1) translate(-50 0)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="scale(1, -1) translate(0 -100)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, -1) translate(-50 -50)"></use></g></svg>"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import type { throttleRAF } from "./utils";
|
|||||||
import { Spreadsheet } from "./charts";
|
import { Spreadsheet } from "./charts";
|
||||||
import { Language } from "./i18n";
|
import { Language } from "./i18n";
|
||||||
import { ClipboardData } from "./clipboard";
|
import { ClipboardData } from "./clipboard";
|
||||||
import { isOverScrollBars } from "./scene";
|
import { isOverScrollBars } from "./scene/scrollbars";
|
||||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||||
import Library from "./data/library";
|
import Library from "./data/library";
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
@@ -456,6 +456,10 @@ export interface ExcalidrawProps {
|
|||||||
activeTool: AppState["activeTool"],
|
activeTool: AppState["activeTool"],
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) => void;
|
) => void;
|
||||||
|
onPointerUp?: (
|
||||||
|
activeTool: AppState["activeTool"],
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
) => void;
|
||||||
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
||||||
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|||||||
Reference in New Issue
Block a user