mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-02 23:27:00 +02:00
Compare commits
5 Commits
zsviczian-
...
aakansha/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b9463b82bc | ||
![]() |
d710507283 | ||
![]() |
54e5532004 | ||
![]() |
80e40003f8 | ||
![]() |
ba420d36ac |
@@ -7,6 +7,9 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||
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_APP=https://app.excalidraw.com
|
||||
|
||||
|
@@ -4,13 +4,16 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
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_APP=https://app.excalidraw.com
|
||||
|
||||
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||
|
||||
# socket server URL used for collaboration
|
||||
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||
# Fill to set socket server URL used for collaboration.
|
||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||
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"}'
|
||||
|
||||
|
@@ -36,6 +36,7 @@ import {
|
||||
import {
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getCollabServer,
|
||||
getSyncableElements,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
@@ -451,9 +452,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||
|
||||
try {
|
||||
const socketServerData = await getCollabServer();
|
||||
|
||||
this.portal.socket = this.portal.open(
|
||||
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||
transports: ["websocket", "polling"],
|
||||
socketIOClient(socketServerData.url, {
|
||||
transports: socketServerData.polling
|
||||
? ["websocket", "polling"]
|
||||
: ["websocket"],
|
||||
}),
|
||||
roomId,
|
||||
roomKey,
|
||||
|
@@ -65,6 +65,35 @@ const generateRoomId = async () => {
|
||||
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 = {
|
||||
data: ArrayBuffer;
|
||||
iv: Uint8Array;
|
||||
|
@@ -20,6 +20,17 @@ 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", () => {
|
||||
const loadFromFirebase = async () => null;
|
||||
const saveToFirebase = () => {};
|
||||
|
@@ -13,11 +13,8 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## 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)
|
||||
- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
getOriginalContainerHeightFromCache,
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "../element/containerCache";
|
||||
} from "../element/textWysiwyg";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isTextBindableContainer,
|
||||
|
84
packages/excalidraw/actions/actionChangeFontSize.tsx
Normal file
84
packages/excalidraw/actions/actionChangeFontSize.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import {
|
||||
FontSizeExtraLargeIcon,
|
||||
FontSizeLargeIcon,
|
||||
FontSizeMediumIcon,
|
||||
FontSizeSmallIcon,
|
||||
} from "../components/icons";
|
||||
import { DEFAULT_FONT_SIZE } from "../constants";
|
||||
import { isTextElement } from "../element";
|
||||
import { getBoundTextElement } from "../element/textElement";
|
||||
import { t } from "../i18n";
|
||||
import { changeFontSize } from "./utils";
|
||||
import { getFormValue } from "./actionProperties";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, () => value, value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<ButtonIconSelect
|
||||
group="font-size"
|
||||
options={[
|
||||
{
|
||||
value: 16,
|
||||
text: t("labels.small"),
|
||||
icon: FontSizeSmallIcon,
|
||||
testId: "fontSize-small",
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
text: t("labels.medium"),
|
||||
icon: FontSizeMediumIcon,
|
||||
testId: "fontSize-medium",
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
text: t("labels.large"),
|
||||
icon: FontSizeLargeIcon,
|
||||
testId: "fontSize-large",
|
||||
},
|
||||
{
|
||||
value: 36,
|
||||
text: t("labels.veryLarge"),
|
||||
icon: FontSizeExtraLargeIcon,
|
||||
testId: "fontSize-veryLarge",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
</fieldset>
|
||||
),
|
||||
});
|
26
packages/excalidraw/actions/actionDecreaseFontSize.ts
Normal file
26
packages/excalidraw/actions/actionDecreaseFontSize.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
import { register } from "./register";
|
||||
import { changeFontSize, FONT_SIZE_RELATIVE_INCREASE_STEP } from "./utils";
|
||||
|
||||
export const actionDecreaseFontSize = register({
|
||||
name: "decreaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(
|
||||
// get previous value before relative increase (doesn't work fully
|
||||
// due to rounding and float precision issues)
|
||||
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
|
||||
),
|
||||
);
|
||||
},
|
||||
keyTest: (event) => {
|
||||
return (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
// KEYS.COMMA needed for MacOS
|
||||
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
|
||||
);
|
||||
},
|
||||
});
|
21
packages/excalidraw/actions/actionIncreaseFontSize.ts
Normal file
21
packages/excalidraw/actions/actionIncreaseFontSize.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { changeFontSize, FONT_SIZE_RELATIVE_INCREASE_STEP } from "./utils";
|
||||
|
||||
export const actionIncreaseFontSize = register({
|
||||
name: "increaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||
);
|
||||
},
|
||||
keyTest: (event) => {
|
||||
return (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
// KEYS.PERIOD needed for MacOS
|
||||
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
|
||||
);
|
||||
},
|
||||
});
|
@@ -1,4 +1,4 @@
|
||||
import { AppClassProperties, AppState, Primitive } from "../types";
|
||||
import { AppState, Primitive } from "../types";
|
||||
import {
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||
@@ -32,10 +32,6 @@ import {
|
||||
StrokeWidthBaseIcon,
|
||||
StrokeWidthBoldIcon,
|
||||
StrokeWidthExtraBoldIcon,
|
||||
FontSizeSmallIcon,
|
||||
FontSizeMediumIcon,
|
||||
FontSizeLargeIcon,
|
||||
FontSizeExtraLargeIcon,
|
||||
EdgeSharpIcon,
|
||||
EdgeRoundIcon,
|
||||
FreedrawIcon,
|
||||
@@ -52,7 +48,6 @@ import {
|
||||
} from "../components/icons";
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
FONT_FAMILY,
|
||||
ROUNDNESS,
|
||||
STROKE_WIDTH,
|
||||
@@ -63,16 +58,12 @@ import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getDefaultLineHeight,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
isUsingAdaptiveRadius,
|
||||
} from "../element/typeChecks";
|
||||
import { isLinearElement, isUsingAdaptiveRadius } from "../element/typeChecks";
|
||||
import {
|
||||
Arrowhead,
|
||||
ExcalidrawElement,
|
||||
@@ -83,7 +74,6 @@ import {
|
||||
VerticalAlign,
|
||||
} from "../element/types";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
canHaveArrowheads,
|
||||
@@ -96,8 +86,6 @@ import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { arrayToMap, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||
|
||||
export const changeProperty = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@@ -161,78 +149,6 @@ export const getFormValue = function <T extends Primitive>(
|
||||
return ret;
|
||||
};
|
||||
|
||||
const offsetElementAfterFontResize = (
|
||||
prevElement: ExcalidrawTextElement,
|
||||
nextElement: ExcalidrawTextElement,
|
||||
) => {
|
||||
if (isBoundToContainer(nextElement)) {
|
||||
return nextElement;
|
||||
}
|
||||
return mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
},
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
const changeFontSize = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||
) => {
|
||||
const newFontSizes = new Set<number>();
|
||||
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(oldElement) => {
|
||||
if (isTextElement(oldElement)) {
|
||||
const newFontSize = getNewFontSize(oldElement);
|
||||
newFontSizes.add(newFontSize);
|
||||
|
||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
|
||||
return oldElement;
|
||||
},
|
||||
true,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
// update state only if we've set all select text elements to
|
||||
// the same font size
|
||||
currentItemFontSize:
|
||||
newFontSizes.size === 1
|
||||
? [...newFontSizes][0]
|
||||
: fallbackValue ?? appState.currentItemFontSize,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const actionChangeStrokeColor = register({
|
||||
@@ -600,116 +516,6 @@ export const actionChangeOpacity = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, () => value, value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<ButtonIconSelect
|
||||
group="font-size"
|
||||
options={[
|
||||
{
|
||||
value: 16,
|
||||
text: t("labels.small"),
|
||||
icon: FontSizeSmallIcon,
|
||||
testId: "fontSize-small",
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
text: t("labels.medium"),
|
||||
icon: FontSizeMediumIcon,
|
||||
testId: "fontSize-medium",
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
text: t("labels.large"),
|
||||
icon: FontSizeLargeIcon,
|
||||
testId: "fontSize-large",
|
||||
},
|
||||
{
|
||||
value: 36,
|
||||
text: t("labels.veryLarge"),
|
||||
icon: FontSizeExtraLargeIcon,
|
||||
testId: "fontSize-veryLarge",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
</fieldset>
|
||||
),
|
||||
});
|
||||
|
||||
export const actionDecreaseFontSize = register({
|
||||
name: "decreaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(
|
||||
// get previous value before relative increase (doesn't work fully
|
||||
// due to rounding and float precision issues)
|
||||
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
|
||||
),
|
||||
);
|
||||
},
|
||||
keyTest: (event) => {
|
||||
return (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
// KEYS.COMMA needed for MacOS
|
||||
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionIncreaseFontSize = register({
|
||||
name: "increaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||
);
|
||||
},
|
||||
keyTest: (event) => {
|
||||
return (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
// KEYS.PERIOD needed for MacOS
|
||||
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeFontFamily = register({
|
||||
name: "changeFontFamily",
|
||||
trackEvent: false,
|
||||
|
@@ -14,12 +14,17 @@ export {
|
||||
actionChangeFillStyle,
|
||||
actionChangeSloppiness,
|
||||
actionChangeOpacity,
|
||||
actionChangeFontSize,
|
||||
actionChangeFontFamily,
|
||||
actionChangeTextAlign,
|
||||
actionChangeVerticalAlign,
|
||||
} from "./actionProperties";
|
||||
|
||||
export { actionDecreaseFontSize } from "./actionDecreaseFontSize";
|
||||
|
||||
export { actionIncreaseFontSize } from "./actionIncreaseFontSize";
|
||||
|
||||
export { actionChangeFontSize } from "./actionChangeFontSize";
|
||||
|
||||
export {
|
||||
actionChangeViewBackgroundColor,
|
||||
actionClearCanvas,
|
||||
|
80
packages/excalidraw/actions/utils.ts
Normal file
80
packages/excalidraw/actions/utils.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { mutateElement, newElementWith } from "..";
|
||||
import { isTextElement, redrawTextBoundingBox } from "../element";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { changeProperty } from "./actionProperties";
|
||||
|
||||
export const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||
|
||||
const offsetElementAfterFontResize = (
|
||||
prevElement: ExcalidrawTextElement,
|
||||
nextElement: ExcalidrawTextElement,
|
||||
) => {
|
||||
if (isBoundToContainer(nextElement)) {
|
||||
return nextElement;
|
||||
}
|
||||
return mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
},
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
export const changeFontSize = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||
) => {
|
||||
const newFontSizes = new Set<number>();
|
||||
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(oldElement) => {
|
||||
if (isTextElement(oldElement)) {
|
||||
const newFontSize = getNewFontSize(oldElement);
|
||||
newFontSizes.add(newFontSize);
|
||||
|
||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
|
||||
return oldElement;
|
||||
},
|
||||
true,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
// update state only if we've set all select text elements to
|
||||
// the same font size
|
||||
currentItemFontSize:
|
||||
newFontSizes.size === 1
|
||||
? [...newFontSizes][0]
|
||||
: fallbackValue ?? appState.currentItemFontSize,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
};
|
@@ -115,6 +115,7 @@ import {
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
newImageElement,
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -216,6 +217,7 @@ import {
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
hasBackground,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
@@ -407,8 +409,6 @@ import { AnimatedTrail } from "../animated-trail";
|
||||
import { LaserTrails } from "../laser-trails";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { getRenderOpacity } from "../renderer/renderElement";
|
||||
import { textWysiwyg } from "../element/textWysiwyg";
|
||||
import { isOverScrollBars } from "../scene/scrollbars";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@@ -1299,7 +1299,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const FRAME_NAME_EDIT_PADDING = 6;
|
||||
|
||||
const reset = () => {
|
||||
mutateElement(f, { name: f.name?.trim() || null });
|
||||
if (f.name?.trim() === "") {
|
||||
mutateElement(f, { name: null });
|
||||
}
|
||||
|
||||
this.setState({ editingFrame: null });
|
||||
};
|
||||
|
||||
@@ -1322,7 +1325,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onBlur={() => reset()}
|
||||
onKeyDown={(event) => {
|
||||
// for some inexplicable reason, `onBlur` triggered on ESC
|
||||
@@ -6501,11 +6503,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (embedLink.error instanceof URIError) {
|
||||
this.setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
if (embedLink.warning) {
|
||||
this.setToast({ message: embedLink.warning, closable: true });
|
||||
}
|
||||
|
||||
const element = newEmbeddableElement({
|
||||
@@ -7567,7 +7566,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ pendingImageElementId: null });
|
||||
}
|
||||
|
||||
this.props?.onPointerUp?.(activeTool, pointerDownState);
|
||||
this.onPointerUpEmitter.trigger(
|
||||
this.state.activeTool,
|
||||
pointerDownState,
|
||||
|
@@ -16,20 +16,25 @@ const FollowMode = ({
|
||||
onDisconnect,
|
||||
}: FollowModeProps) => {
|
||||
return (
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
<div style={{ position: "relative" }}>
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="follow-mode__disconnect-btn"
|
||||
>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
{CloseIcon}
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
||||
{CloseIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -142,7 +142,6 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||
export const DEFAULT_TEXT_ALIGN = "left";
|
||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||
export const DEFAULT_VERSION = "{version}";
|
||||
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||
|
||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||
|
||||
|
@@ -120,11 +120,8 @@ export const Hyperlink = ({
|
||||
} else {
|
||||
const { width, height } = element;
|
||||
const embedLink = getEmbedLink(link);
|
||||
if (embedLink?.error instanceof URIError) {
|
||||
setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
if (embedLink?.warning) {
|
||||
setToast({ message: embedLink.warning, closable: true });
|
||||
}
|
||||
const ar = embedLink
|
||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||
|
@@ -1,33 +0,0 @@
|
||||
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,5 +1,6 @@
|
||||
import { register } from "../actions/register";
|
||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { ExcalidrawProps } from "../types";
|
||||
import { getFontString, updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
@@ -106,8 +107,8 @@ export const getEmbedLink = (
|
||||
const vimeoLink = link.match(RE_VIMEO);
|
||||
if (vimeoLink?.[1]) {
|
||||
const target = vimeoLink?.[1];
|
||||
const error = !/^\d+$/.test(target)
|
||||
? new URIError("Invalid embed link format")
|
||||
const warning = !/^\d+$/.test(target)
|
||||
? t("toast.unrecognizedLinkFormat")
|
||||
: undefined;
|
||||
type = "video";
|
||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||
@@ -119,7 +120,7 @@ export const getEmbedLink = (
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type, error };
|
||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
||||
}
|
||||
|
||||
const figmaLink = link.match(RE_FIGMA);
|
||||
|
@@ -50,6 +50,7 @@ export {
|
||||
dragNewElement,
|
||||
} from "./dragElements";
|
||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||
export { textWysiwyg } from "./textWysiwyg";
|
||||
export { redrawTextBoundingBox } from "./textElement";
|
||||
export {
|
||||
getPerfectElementSize,
|
||||
|
@@ -31,12 +31,11 @@ import { isTextBindableContainer } from "./typeChecks";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
} from "./textWysiwyg";
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
} from "./types";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextContainer,
|
||||
} from "./types";
|
||||
import { AppState } from "../types";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
@@ -35,18 +36,12 @@ import {
|
||||
computeBoundTextPosition,
|
||||
getBoundTextElement,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
actionIncreaseFontSize,
|
||||
} from "../actions/actionProperties";
|
||||
import { actionDecreaseFontSize } from "../actions/actionDecreaseFontSize";
|
||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
originalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import { actionIncreaseFontSize } from "../actions";
|
||||
|
||||
const getTransform = (
|
||||
width: number,
|
||||
@@ -69,6 +64,38 @@ const getTransform = (
|
||||
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 = ({
|
||||
id,
|
||||
onChange,
|
||||
|
@@ -9,7 +9,7 @@ import { rotate } from "../math";
|
||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
@@ -106,8 +106,7 @@ export const getTransformHandlesFromCoords = (
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
const dashedLineMargin = margin / zoom.value;
|
||||
const centeringOffset =
|
||||
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
|
||||
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||
|
||||
const transformHandles: TransformHandles = {
|
||||
nw: omitSides.nw
|
||||
@@ -264,8 +263,8 @@ export const getTransformHandles = (
|
||||
};
|
||||
}
|
||||
const dashedLineMargin = isLinearElement(element)
|
||||
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
|
||||
: DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||
? DEFAULT_SPACING + 8
|
||||
: DEFAULT_SPACING;
|
||||
return getTransformHandlesFromCoords(
|
||||
getElementAbsoluteCoords(element, true),
|
||||
element.angle,
|
||||
|
@@ -214,10 +214,7 @@ export const isBoundToContainer = (
|
||||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" ||
|
||||
type === "embeddable" ||
|
||||
type === "iframe" ||
|
||||
type === "image";
|
||||
type === "rectangle" || type === "embeddable" || type === "iframe";
|
||||
|
||||
export const isUsingProportionalRadius = (type: string) =>
|
||||
type === "line" || type === "arrow" || type === "diamond";
|
||||
|
@@ -104,7 +104,7 @@ export type ExcalidrawIframeLikeElement =
|
||||
export type IframeData =
|
||||
| {
|
||||
intrinsicSize: { w: number; h: number };
|
||||
error?: Error;
|
||||
warning?: string;
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
|
@@ -746,7 +746,7 @@ export const getFrameLikeTitle = (
|
||||
element: ExcalidrawFrameLikeElement,
|
||||
frameIdx: number,
|
||||
) => {
|
||||
// TODO name frames "AI" only if specific to AI frames
|
||||
// TODO name frames AI only is specific to AI frames
|
||||
return element.name === null
|
||||
? isFrameElement(element)
|
||||
? `Frame ${frameIdx}`
|
||||
|
@@ -44,7 +44,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onScrollChange,
|
||||
children,
|
||||
validateEmbeddable,
|
||||
@@ -132,7 +131,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
|
@@ -344,17 +344,6 @@ const drawElementOnCanvas = (
|
||||
? renderConfig.imageCache.get(element.fileId)?.image
|
||||
: undefined;
|
||||
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(
|
||||
img,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
@@ -1312,31 +1301,6 @@ export const renderElementToSvg = (
|
||||
}) 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(
|
||||
element,
|
||||
root,
|
||||
|
@@ -64,11 +64,7 @@ import {
|
||||
} from "../element/transformHandles";
|
||||
import { arrayToMap, throttleRAF } from "../utils";
|
||||
import { UserIdleState } from "../types";
|
||||
import {
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
FRAME_STYLE,
|
||||
THEME_FILTER,
|
||||
} from "../constants";
|
||||
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||
import {
|
||||
EXTERNAL_LINK_IMG,
|
||||
getLinkHandleFromCoords,
|
||||
@@ -87,6 +83,8 @@ import {
|
||||
isElementInFrame,
|
||||
} from "../frame";
|
||||
|
||||
export const DEFAULT_SPACING = 2;
|
||||
|
||||
const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
@@ -678,8 +676,7 @@ const _renderInteractiveScene = ({
|
||||
);
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||
const dashedLinePadding =
|
||||
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||
const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value;
|
||||
context.fillStyle = oc.white;
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
const initialLineDash = context.getLineDash();
|
||||
@@ -1194,7 +1191,7 @@ const renderSelectionBorder = (
|
||||
cy: number;
|
||||
activeEmbeddable: boolean;
|
||||
},
|
||||
padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2,
|
||||
padding = DEFAULT_SPACING * 2,
|
||||
) => {
|
||||
const {
|
||||
angle,
|
||||
|
@@ -42,8 +42,7 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
||||
type === "embeddable" ||
|
||||
type === "arrow" ||
|
||||
type === "line" ||
|
||||
type === "diamond" ||
|
||||
type === "image";
|
||||
type === "diamond";
|
||||
|
||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export { isOverScrollBars } from "./scrollbars";
|
||||
export {
|
||||
isSomeElementSelected,
|
||||
getElementsWithinSelection,
|
||||
|
@@ -21,5 +21,5 @@ exports[`export > exporting svg containing transformed images > svg export outpu
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
<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>"
|
||||
<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>"
|
||||
`;
|
||||
|
@@ -31,7 +31,7 @@ import type { throttleRAF } from "./utils";
|
||||
import { Spreadsheet } from "./charts";
|
||||
import { Language } from "./i18n";
|
||||
import { ClipboardData } from "./clipboard";
|
||||
import { isOverScrollBars } from "./scene/scrollbars";
|
||||
import { isOverScrollBars } from "./scene";
|
||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||
import Library from "./data/library";
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
@@ -456,10 +456,6 @@ export interface ExcalidrawProps {
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
) => void;
|
||||
onPointerUp?: (
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
) => void;
|
||||
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
||||
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
||||
children?: React.ReactNode;
|
||||
|
Reference in New Issue
Block a user