Compare commits
5 Commits
| 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)
|
# 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
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -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_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
|
||||||
|
|
||||||
# socket server URL used for collaboration
|
# Fill to set socket server URL used for collaboration.
|
||||||
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
# 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"}'
|
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 {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
|
getCollabServer,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
@@ -451,9 +452,13 @@ 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(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
socketIOClient(socketServerData.url, {
|
||||||
transports: ["websocket", "polling"],
|
transports: socketServerData.polling
|
||||||
|
? ["websocket", "polling"]
|
||||||
|
: ["websocket"],
|
||||||
}),
|
}),
|
||||||
roomId,
|
roomId,
|
||||||
roomKey,
|
roomKey,
|
||||||
|
|||||||
@@ -65,6 +65,35 @@ 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,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", () => {
|
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||||
const loadFromFirebase = async () => null;
|
const loadFromFirebase = async () => null;
|
||||||
const saveToFirebase = () => {};
|
const saveToFirebase = () => {};
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ 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/containerCache";
|
} from "../element/textWysiwyg";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -32,10 +32,6 @@ import {
|
|||||||
StrokeWidthBaseIcon,
|
StrokeWidthBaseIcon,
|
||||||
StrokeWidthBoldIcon,
|
StrokeWidthBoldIcon,
|
||||||
StrokeWidthExtraBoldIcon,
|
StrokeWidthExtraBoldIcon,
|
||||||
FontSizeSmallIcon,
|
|
||||||
FontSizeMediumIcon,
|
|
||||||
FontSizeLargeIcon,
|
|
||||||
FontSizeExtraLargeIcon,
|
|
||||||
EdgeSharpIcon,
|
EdgeSharpIcon,
|
||||||
EdgeRoundIcon,
|
EdgeRoundIcon,
|
||||||
FreedrawIcon,
|
FreedrawIcon,
|
||||||
@@ -52,7 +48,6 @@ import {
|
|||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
STROKE_WIDTH,
|
STROKE_WIDTH,
|
||||||
@@ -63,16 +58,12 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import { isLinearElement, isUsingAdaptiveRadius } from "../element/typeChecks";
|
||||||
isBoundToContainer,
|
|
||||||
isLinearElement,
|
|
||||||
isUsingAdaptiveRadius,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import {
|
import {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -83,7 +74,6 @@ import {
|
|||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
@@ -96,8 +86,6 @@ import { hasStrokeColor } from "../scene/comparisons";
|
|||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
|
||||||
|
|
||||||
export const changeProperty = (
|
export const changeProperty = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@@ -161,78 +149,6 @@ export const getFormValue = function <T extends Primitive>(
|
|||||||
return ret;
|
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({
|
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({
|
export const actionChangeFontFamily = register({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ export {
|
|||||||
actionChangeFillStyle,
|
actionChangeFillStyle,
|
||||||
actionChangeSloppiness,
|
actionChangeSloppiness,
|
||||||
actionChangeOpacity,
|
actionChangeOpacity,
|
||||||
actionChangeFontSize,
|
|
||||||
actionChangeFontFamily,
|
actionChangeFontFamily,
|
||||||
actionChangeTextAlign,
|
actionChangeTextAlign,
|
||||||
actionChangeVerticalAlign,
|
actionChangeVerticalAlign,
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
|
export { actionDecreaseFontSize } from "./actionDecreaseFontSize";
|
||||||
|
|
||||||
|
export { actionIncreaseFontSize } from "./actionIncreaseFontSize";
|
||||||
|
|
||||||
|
export { actionChangeFontSize } from "./actionChangeFontSize";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
actionChangeViewBackgroundColor,
|
actionChangeViewBackgroundColor,
|
||||||
actionClearCanvas,
|
actionClearCanvas,
|
||||||
|
|||||||
@@ -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,
|
newLinearElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
|
textWysiwyg,
|
||||||
transformElements,
|
transformElements,
|
||||||
updateTextElement,
|
updateTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -216,6 +217,7 @@ import {
|
|||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
hasBackground,
|
hasBackground,
|
||||||
|
isOverScrollBars,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
@@ -407,8 +409,6 @@ 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,7 +1299,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const FRAME_NAME_EDIT_PADDING = 6;
|
const FRAME_NAME_EDIT_PADDING = 6;
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
mutateElement(f, { name: f.name?.trim() || null });
|
if (f.name?.trim() === "") {
|
||||||
|
mutateElement(f, { name: null });
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ editingFrame: null });
|
this.setState({ editingFrame: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1322,7 +1325,6 @@ 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
|
||||||
@@ -6501,11 +6503,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embedLink.error instanceof URIError) {
|
if (embedLink.warning) {
|
||||||
this.setToast({
|
this.setToast({ message: embedLink.warning, closable: true });
|
||||||
message: t("toast.unrecognizedLinkFormat"),
|
|
||||||
closable: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = newEmbeddableElement({
|
const element = newEmbeddableElement({
|
||||||
@@ -7567,7 +7566,6 @@ 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,20 +16,25 @@ const FollowMode = ({
|
|||||||
onDisconnect,
|
onDisconnect,
|
||||||
}: FollowModeProps) => {
|
}: FollowModeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="follow-mode" style={{ width, height }}>
|
<div style={{ position: "relative" }}>
|
||||||
<div className="follow-mode__badge">
|
<div className="follow-mode" style={{ width, height }}>
|
||||||
<div className="follow-mode__badge__label">
|
<div className="follow-mode__badge">
|
||||||
Following{" "}
|
<div className="follow-mode__badge__label">
|
||||||
<span
|
Following{" "}
|
||||||
className="follow-mode__badge__username"
|
<span
|
||||||
title={userToFollow.username}
|
className="follow-mode__badge__username"
|
||||||
|
title={userToFollow.username}
|
||||||
|
>
|
||||||
|
{userToFollow.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onDisconnect}
|
||||||
|
className="follow-mode__disconnect-btn"
|
||||||
>
|
>
|
||||||
{userToFollow.username}
|
{CloseIcon}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
|
||||||
{CloseIcon}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ 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,11 +120,8 @@ export const Hyperlink = ({
|
|||||||
} else {
|
} else {
|
||||||
const { width, height } = element;
|
const { width, height } = element;
|
||||||
const embedLink = getEmbedLink(link);
|
const embedLink = getEmbedLink(link);
|
||||||
if (embedLink?.error instanceof URIError) {
|
if (embedLink?.warning) {
|
||||||
setToast({
|
setToast({ message: embedLink.warning, closable: true });
|
||||||
message: t("toast.unrecognizedLinkFormat"),
|
|
||||||
closable: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const ar = embedLink
|
const ar = embedLink
|
||||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
? 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 { 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";
|
||||||
@@ -106,8 +107,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 error = !/^\d+$/.test(target)
|
const warning = !/^\d+$/.test(target)
|
||||||
? new URIError("Invalid embed link format")
|
? t("toast.unrecognizedLinkFormat")
|
||||||
: undefined;
|
: undefined;
|
||||||
type = "video";
|
type = "video";
|
||||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||||
@@ -119,7 +120,7 @@ export const getEmbedLink = (
|
|||||||
intrinsicSize: aspectRatio,
|
intrinsicSize: aspectRatio,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
return { link, intrinsicSize: aspectRatio, type, error };
|
return { link, intrinsicSize: aspectRatio, type, warning };
|
||||||
}
|
}
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ 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,12 +31,11 @@ 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 "./containerCache";
|
} from "./textWysiwyg";
|
||||||
|
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 "./containerCache";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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";
|
||||||
@@ -35,18 +36,12 @@ import {
|
|||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import { actionDecreaseFontSize } from "../actions/actionDecreaseFontSize";
|
||||||
actionDecreaseFontSize,
|
|
||||||
actionIncreaseFontSize,
|
|
||||||
} from "../actions/actionProperties";
|
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
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 {
|
import { actionIncreaseFontSize } from "../actions";
|
||||||
originalContainerCache,
|
|
||||||
updateOriginalContainerCache,
|
|
||||||
} from "./containerCache";
|
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@@ -69,6 +64,38 @@ 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_TRANSFORM_HANDLE_SPACING } from "../constants";
|
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
@@ -106,8 +106,7 @@ 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 =
|
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||||
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
|
|
||||||
|
|
||||||
const transformHandles: TransformHandles = {
|
const transformHandles: TransformHandles = {
|
||||||
nw: omitSides.nw
|
nw: omitSides.nw
|
||||||
@@ -264,8 +263,8 @@ export const getTransformHandles = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const dashedLineMargin = isLinearElement(element)
|
const dashedLineMargin = isLinearElement(element)
|
||||||
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
|
? DEFAULT_SPACING + 8
|
||||||
: DEFAULT_TRANSFORM_HANDLE_SPACING;
|
: DEFAULT_SPACING;
|
||||||
return getTransformHandlesFromCoords(
|
return getTransformHandlesFromCoords(
|
||||||
getElementAbsoluteCoords(element, true),
|
getElementAbsoluteCoords(element, true),
|
||||||
element.angle,
|
element.angle,
|
||||||
|
|||||||
@@ -214,10 +214,7 @@ export const isBoundToContainer = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" || type === "embeddable" || type === "iframe";
|
||||||
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 };
|
||||||
error?: Error;
|
warning?: string;
|
||||||
} & (
|
} & (
|
||||||
| { 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 if specific to AI frames
|
// TODO name frames AI only is specific to AI frames
|
||||||
return element.name === null
|
return element.name === null
|
||||||
? isFrameElement(element)
|
? isFrameElement(element)
|
||||||
? `Frame ${frameIdx}`
|
? `Frame ${frameIdx}`
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
generateIdForFile,
|
generateIdForFile,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
onPointerUp,
|
|
||||||
onScrollChange,
|
onScrollChange,
|
||||||
children,
|
children,
|
||||||
validateEmbeddable,
|
validateEmbeddable,
|
||||||
@@ -132,7 +131,6 @@ 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,17 +344,6 @@ 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*/,
|
||||||
@@ -1312,31 +1301,6 @@ 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,11 +64,7 @@ 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 {
|
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
|
||||||
FRAME_STYLE,
|
|
||||||
THEME_FILTER,
|
|
||||||
} from "../constants";
|
|
||||||
import {
|
import {
|
||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
@@ -87,6 +83,8 @@ import {
|
|||||||
isElementInFrame,
|
isElementInFrame,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
|
|
||||||
|
export const DEFAULT_SPACING = 2;
|
||||||
|
|
||||||
const strokeRectWithRotation = (
|
const strokeRectWithRotation = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@@ -678,8 +676,7 @@ const _renderInteractiveScene = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding =
|
const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value;
|
||||||
(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();
|
||||||
@@ -1194,7 +1191,7 @@ const renderSelectionBorder = (
|
|||||||
cy: number;
|
cy: number;
|
||||||
activeEmbeddable: boolean;
|
activeEmbeddable: boolean;
|
||||||
},
|
},
|
||||||
padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2,
|
padding = DEFAULT_SPACING * 2,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
angle,
|
angle,
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ 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,3 +1,4 @@
|
|||||||
|
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>
|
||||||
<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 { Spreadsheet } from "./charts";
|
||||||
import { Language } from "./i18n";
|
import { Language } from "./i18n";
|
||||||
import { ClipboardData } from "./clipboard";
|
import { ClipboardData } from "./clipboard";
|
||||||
import { isOverScrollBars } from "./scene/scrollbars";
|
import { isOverScrollBars } from "./scene";
|
||||||
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,10 +456,6 @@ 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