Compare commits

..

5 Commits

Author SHA1 Message Date
Aakansha Doshi
b9463b82bc lint 2024-01-30 13:18:39 +05:30
Aakansha Doshi
d710507283 move actionChangeFontSize as well 2024-01-30 13:08:53 +05:30
Aakansha Doshi
54e5532004 fix 2024-01-30 12:46:11 +05:30
Aakansha Doshi
80e40003f8 fix 2024-01-30 12:42:11 +05:30
Aakansha Doshi
ba420d36ac fix: move actions related to font size to its own file 2024-01-30 12:28:51 +05:30
34 changed files with 363 additions and 348 deletions

View File

@@ -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

View File

@@ -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"}'

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 = () => {};

View File

@@ -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

View File

@@ -17,7 +17,7 @@ import {
getOriginalContainerHeightFromCache,
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "../element/containerCache";
} from "../element/textWysiwyg";
import {
hasBoundTextElement,
isTextBindableContainer,

View 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>
),
});

View 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)
);
},
});

View 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)
);
},
});

View File

@@ -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,

View File

@@ -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,

View 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,
};
};

View File

@@ -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,

View File

@@ -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>
);

View File

@@ -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"];

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -50,6 +50,7 @@ export {
dragNewElement,
} from "./dragElements";
export { isTextElement, isExcalidrawElement } from "./typeChecks";
export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement";
export {
getPerfectElementSize,

View File

@@ -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 (

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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 }

View File

@@ -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}`

View File

@@ -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}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -1,3 +1,4 @@
export { isOverScrollBars } from "./scrollbars";
export {
isSomeElementSelected,
getElementsWithinSelection,

View File

@@ -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>"
`;

View File

@@ -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;