mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-17 11:14:23 +01:00
Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax-maint-stage
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
hasStrokeWidth,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
@@ -220,10 +220,12 @@ export const ShapesSwitcher = ({
|
||||
activeTool,
|
||||
appState,
|
||||
app,
|
||||
UIOptions,
|
||||
}: {
|
||||
activeTool: UIAppState["activeTool"];
|
||||
appState: UIAppState;
|
||||
app: AppClassProperties;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
|
||||
@@ -234,6 +236,14 @@ export const ShapesSwitcher = ({
|
||||
return (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
if (
|
||||
UIOptions.tools?.[
|
||||
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
|
||||
] === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter =
|
||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||
|
||||
@@ -351,6 +351,7 @@ import {
|
||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { ImageSceneDataError } from "../errors";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
snapDraggedElements,
|
||||
@@ -2299,6 +2300,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||
if (!this.isToolSupported("image")) {
|
||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||
return;
|
||||
}
|
||||
|
||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||
this.insertImageElement(imageElement, file);
|
||||
this.initializeImageDimensions(imageElement);
|
||||
@@ -2504,7 +2510,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
mixedContent.some((node) => node.type === "imageUrl")
|
||||
mixedContent.some((node) => node.type === "imageUrl") &&
|
||||
this.isToolSupported("image")
|
||||
) {
|
||||
const imageURLs = mixedContent
|
||||
.filter((node) => node.type === "imageUrl")
|
||||
@@ -2744,7 +2751,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
togglePenMode = (force?: boolean) => {
|
||||
togglePenMode = (force: boolean | null) => {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
penMode: force ?? !prevState.penMode,
|
||||
@@ -3312,6 +3319,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
// We purposely widen the `tool` type so this helper can be called with
|
||||
// any tool without having to type check it
|
||||
private isToolSupported = <T extends ToolType | "custom">(tool: T) => {
|
||||
return (
|
||||
this.props.UIOptions.tools?.[
|
||||
tool as Extract<T, keyof AppProps["UIOptions"]["tools"]>
|
||||
] !== false
|
||||
);
|
||||
};
|
||||
|
||||
setActiveTool = (
|
||||
tool: (
|
||||
| (
|
||||
@@ -3324,6 +3341,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) => {
|
||||
if (!this.isToolSupported(tool.type)) {
|
||||
console.warn(
|
||||
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||
if (nextActiveTool.type === "hand") {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
@@ -4769,9 +4793,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||
|
||||
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
mutateElement(pendingImageElement, {
|
||||
x,
|
||||
y,
|
||||
frameId: frame ? frame.id : null,
|
||||
});
|
||||
} else if (this.state.activeTool.type === "freedraw") {
|
||||
this.handleFreeDrawElementOnPointerDown(
|
||||
@@ -5638,9 +5666,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private createImageElement = ({
|
||||
sceneX,
|
||||
sceneY,
|
||||
addToFrameUnderCursor = true,
|
||||
}: {
|
||||
sceneX: number;
|
||||
sceneY: number;
|
||||
addToFrameUnderCursor?: boolean;
|
||||
}) => {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
sceneX,
|
||||
@@ -5650,10 +5680,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
});
|
||||
const topLayerFrame = addToFrameUnderCursor
|
||||
? this.getTopLayerFrameAtSceneCoords({
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
})
|
||||
: null;
|
||||
|
||||
const element = newImageElement({
|
||||
type: "image",
|
||||
@@ -7503,6 +7535,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageFile: File,
|
||||
showCursorImagePreview?: boolean,
|
||||
) => {
|
||||
// we should be handling all cases upstream, but in case we forget to handle
|
||||
// a future case, let's throw here
|
||||
if (!this.isToolSupported("image")) {
|
||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.addNewElement(imageElement);
|
||||
|
||||
try {
|
||||
@@ -7586,6 +7625,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const imageElement = this.createImageElement({
|
||||
sceneX: x,
|
||||
sceneY: y,
|
||||
addToFrameUnderCursor: false,
|
||||
});
|
||||
|
||||
if (insertOnCanvasDirectly) {
|
||||
@@ -7886,7 +7926,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
try {
|
||||
if (isSupportedImageFile(file)) {
|
||||
// if image tool not supported, don't show an error here and let it fall
|
||||
// through so we still support importing scene data from images. If no
|
||||
// scene data encoded, we'll show an error then
|
||||
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
|
||||
// first attempt to decode scene from the image if it's embedded
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -8014,6 +8057,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof ImageSceneDataError &&
|
||||
error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
|
||||
!this.isToolSupported("image")
|
||||
) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMessage: t("errors.imageToolNotSupported"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({ isLoading: false, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ interface LayerUIProps {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||
showExitZenModeBtn: boolean;
|
||||
langCode: Language["code"];
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
@@ -258,7 +258,7 @@ const LayerUI = ({
|
||||
<PenModeButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
@@ -280,6 +280,7 @@ const LayerUI = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
@@ -470,6 +471,7 @@ const LayerUI = ({
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
)}
|
||||
{!device.editor.isMobile && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
@@ -35,7 +36,7 @@ type MobileMenuProps = {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||
|
||||
renderTopRightUI?: (
|
||||
isMobile: boolean,
|
||||
@@ -45,6 +46,7 @@ type MobileMenuProps = {
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen: boolean;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
app: AppClassProperties;
|
||||
};
|
||||
|
||||
@@ -62,6 +64,7 @@ export const MobileMenu = ({
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
UIOptions,
|
||||
app,
|
||||
}: MobileMenuProps) => {
|
||||
const {
|
||||
@@ -83,6 +86,7 @@ export const MobileMenu = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
@@ -94,7 +98,7 @@ export const MobileMenu = ({
|
||||
)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
|
||||
@@ -222,6 +222,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
toggleTheme: null,
|
||||
saveAsImage: true,
|
||||
},
|
||||
tools: {
|
||||
image: true,
|
||||
},
|
||||
};
|
||||
|
||||
// breakpoints
|
||||
|
||||
@@ -99,7 +99,7 @@ export const setCursorForShape = (
|
||||
interactiveCanvas.style.cursor = `url(${url}), auto`;
|
||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||
} else {
|
||||
} else if (appState.activeTool.type !== "image") {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
|
||||
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
import { ExcalidrawElement, FileId } from "../element/types";
|
||||
import { CanvasError } from "../errors";
|
||||
import { CanvasError, ImageSceneDataError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { AppState, DataURL, LibraryItem } from "../types";
|
||||
@@ -24,15 +24,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
||||
).decodePngMetadata(blob);
|
||||
} catch (error: any) {
|
||||
if (error.message === "INVALID") {
|
||||
throw new DOMException(
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"EncodingError",
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
} else {
|
||||
throw new DOMException(
|
||||
t("alerts.cannotRestoreFromImage"),
|
||||
"EncodingError",
|
||||
);
|
||||
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -58,15 +55,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message === "INVALID") {
|
||||
throw new DOMException(
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"EncodingError",
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
} else {
|
||||
throw new DOMException(
|
||||
t("alerts.cannotRestoreFromImage"),
|
||||
"EncodingError",
|
||||
);
|
||||
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,8 +125,19 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
fileHandle?: FileSystemHandle | null,
|
||||
) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
let data;
|
||||
try {
|
||||
const data = JSON.parse(contents);
|
||||
try {
|
||||
data = JSON.parse(contents);
|
||||
} catch (error: any) {
|
||||
if (isSupportedImageFile(blob)) {
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (isValidExcalidrawData(data)) {
|
||||
return {
|
||||
type: MIME_TYPES.excalidraw,
|
||||
@@ -162,7 +167,9 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
if (error instanceof ImageSceneDataError) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,3 +16,19 @@ export class AbortError extends DOMException {
|
||||
super(message, "AbortError");
|
||||
}
|
||||
}
|
||||
|
||||
type ImageSceneDataErrorCode =
|
||||
| "IMAGE_NOT_CONTAINS_SCENE_DATA"
|
||||
| "IMAGE_SCENE_DATA_ERROR";
|
||||
|
||||
export class ImageSceneDataError extends Error {
|
||||
public code;
|
||||
constructor(
|
||||
message = "Image Scene Data Error",
|
||||
code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "EncodingError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"importLibraryError": "Couldn't load library",
|
||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||
"imageToolNotSupported": "Images are disabled.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
|
||||
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||
|
||||
@@ -11,18 +11,22 @@ The change should be grouped under one of the below section and must contain PR
|
||||
Please add the latest change on the top under the correct section.
|
||||
-->
|
||||
|
||||
## Unreleased
|
||||
## 0.17.0 (2023-11-14)
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for disabling `image` tool (also disabling image insertion in general, though keeps support for importing from `.excalidraw` files) [#6320](https://github.com/excalidraw/excalidraw/pull/6320).
|
||||
|
||||
For disabling `image` you need to set 👇
|
||||
|
||||
```
|
||||
UIOptions.tools = {
|
||||
image: false
|
||||
}
|
||||
```
|
||||
|
||||
- Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api)
|
||||
|
||||
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0.
|
||||
|
||||
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
|
||||
|
||||
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205).
|
||||
@@ -31,12 +35,156 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
- Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195)
|
||||
|
||||
- Add onChange, onPointerDown, onPointerUp api subscribers [#7154](https://github.com/excalidraw/excalidraw/pull/7154).
|
||||
|
||||
- Support props.locked for setActiveTool [#7153](https://github.com/excalidraw/excalidraw/pull/7153).
|
||||
|
||||
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078)
|
||||
|
||||
#### BREAKING CHANGES
|
||||
### Fixes
|
||||
|
||||
- Double image dialog on image insertion [#7152](https://github.com/excalidraw/excalidraw/pull/7152).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api) [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0 [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
- [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243)
|
||||
|
||||
### Build
|
||||
|
||||
- Support Preact [#7255](https://github.com/excalidraw/excalidraw/pull/7255). The host needs to set `process.env.IS_PREACT` to `true`
|
||||
|
||||
When using `vite` or any build tools, you will have to make sure the `process` is accessible as we are accessing `process.env.IS_PREACT` to decide whether to use the `preact` build.
|
||||
|
||||
Since `Vite` removes env variables by default, you can update the Vite config to ensure its available :point_down:
|
||||
|
||||
```
|
||||
define: {
|
||||
"process.env.IS_PREACT": process.env.IS_PREACT,
|
||||
},
|
||||
```
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Features
|
||||
|
||||
- Allow D&D dice app domain for embeds [#7263](https://github.com/excalidraw/excalidraw/pull/7263)
|
||||
|
||||
- Remove full screen shortcut [#7222](https://github.com/excalidraw/excalidraw/pull/7222)
|
||||
|
||||
- Make adaptive-roughness less aggressive [#7250](https://github.com/excalidraw/excalidraw/pull/7250)
|
||||
|
||||
- Render frames on export [#7210](https://github.com/excalidraw/excalidraw/pull/7210)
|
||||
|
||||
- Support mermaid flowchart and sequence diagrams to excalidraw diagrams 🥳 [#6920](https://github.com/excalidraw/excalidraw/pull/6920)
|
||||
|
||||
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205)
|
||||
|
||||
- Make clipboard more robust and reintroduce contextmenu actions [#7198](https://github.com/excalidraw/excalidraw/pull/7198)
|
||||
|
||||
- Support giphy.com embed domain [#7192](https://github.com/excalidraw/excalidraw/pull/7192)
|
||||
|
||||
- Renderer tweaks [#6698](https://github.com/excalidraw/excalidraw/pull/6698)
|
||||
|
||||
- Closing of "Save to.." Dialog on Save To Disk [#7168](https://github.com/excalidraw/excalidraw/pull/7168)
|
||||
|
||||
- Added Copy/Paste from Google Docs [#7136](https://github.com/excalidraw/excalidraw/pull/7136)
|
||||
|
||||
- Remove bound-arrows from frames [#7157](https://github.com/excalidraw/excalidraw/pull/7157)
|
||||
|
||||
- New dark mode theme & light theme tweaks [#7104](https://github.com/excalidraw/excalidraw/pull/7104)
|
||||
|
||||
- Better laser cursor for dark mode [#7132](https://github.com/excalidraw/excalidraw/pull/7132)
|
||||
|
||||
- Laser pointer improvements [#7128](https://github.com/excalidraw/excalidraw/pull/7128)
|
||||
|
||||
- Initial Laser Pointer MVP [#6739](https://github.com/excalidraw/excalidraw/pull/6739)
|
||||
|
||||
- Export `iconFillColor()` [#6996](https://github.com/excalidraw/excalidraw/pull/6996)
|
||||
|
||||
- Element alignments - snapping [#6256](https://github.com/excalidraw/excalidraw/pull/6256)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Image insertion bugs [#7278](https://github.com/excalidraw/excalidraw/pull/7278)
|
||||
|
||||
- ExportToSvg to honor frameRendering also for name not only for frame itself [#7270](https://github.com/excalidraw/excalidraw/pull/7270)
|
||||
|
||||
- Can't toggle penMode off due to missing typecheck in togglePenMode [#7273](https://github.com/excalidraw/excalidraw/pull/7273)
|
||||
|
||||
- Replace hard coded font family with const value in addFrameLabelsAsTextElements [#7269](https://github.com/excalidraw/excalidraw/pull/7269)
|
||||
|
||||
- Perf issue when ungrouping elements within frame [#7265](https://github.com/excalidraw/excalidraw/pull/7265)
|
||||
|
||||
- Fixes the shortcut collision between "toggleHandTool" and "distributeHorizontally" [#7189](https://github.com/excalidraw/excalidraw/pull/7189)
|
||||
|
||||
- Allow pointer events when editing a linear element [#7238](https://github.com/excalidraw/excalidraw/pull/7238)
|
||||
|
||||
- Make modal use viewport breakpoints [#7246](https://github.com/excalidraw/excalidraw/pull/7246)
|
||||
|
||||
- Align input `:hover`/`:focus` with spec [#7225](https://github.com/excalidraw/excalidraw/pull/7225)
|
||||
|
||||
- Dialog remounting on className updates [#7224](https://github.com/excalidraw/excalidraw/pull/7224)
|
||||
|
||||
- Don't update label position when dragging labelled arrows [#6891](https://github.com/excalidraw/excalidraw/pull/6891)
|
||||
|
||||
- Frame add/remove/z-index ordering changes [#7194](https://github.com/excalidraw/excalidraw/pull/7194)
|
||||
|
||||
- Element relative position when dragging multiple elements on grid [#7107](https://github.com/excalidraw/excalidraw/pull/7107)
|
||||
|
||||
- Freedraw non-solid bg hitbox not working [#7193](https://github.com/excalidraw/excalidraw/pull/7193)
|
||||
|
||||
- Actions panel ux improvement [#6850](https://github.com/excalidraw/excalidraw/pull/6850)
|
||||
|
||||
- Better fill rendering with latest RoughJS [#7031](https://github.com/excalidraw/excalidraw/pull/7031)
|
||||
|
||||
- Fix for Strange Symbol Appearing on Canvas after Deleting Grouped Graphics (Issue #7116) [#7170](https://github.com/excalidraw/excalidraw/pull/7170)
|
||||
|
||||
- Attempt to fix flake in wysiwyg tests [#7173](https://github.com/excalidraw/excalidraw/pull/7173)
|
||||
|
||||
- Ensure `ClipboardItem` created in the same tick to fix safari [#7066](https://github.com/excalidraw/excalidraw/pull/7066)
|
||||
|
||||
- Wysiwyg left in undefined state on reload [#7123](https://github.com/excalidraw/excalidraw/pull/7123)
|
||||
|
||||
- Ensure relative z-index of elements added to frame is retained [#7134](https://github.com/excalidraw/excalidraw/pull/7134)
|
||||
|
||||
- Memoize static canvas on `props.renderConfig` [#7131](https://github.com/excalidraw/excalidraw/pull/7131)
|
||||
|
||||
- Regression from #6739 preventing redirect link in view mode [#7120](https://github.com/excalidraw/excalidraw/pull/7120)
|
||||
|
||||
- Update links to excalidraw-app [#7072](https://github.com/excalidraw/excalidraw/pull/7072)
|
||||
|
||||
- Ensure we do not stop laser update prematurely [#7100](https://github.com/excalidraw/excalidraw/pull/7100)
|
||||
|
||||
- Remove invisible elements safely [#7083](https://github.com/excalidraw/excalidraw/pull/7083)
|
||||
|
||||
- Icon size in manifest [#7073](https://github.com/excalidraw/excalidraw/pull/7073)
|
||||
|
||||
- Elements being dropped/duplicated when added to frame [#7057](https://github.com/excalidraw/excalidraw/pull/7057)
|
||||
|
||||
- Frame name not editable on dbl-click [#7037](https://github.com/excalidraw/excalidraw/pull/7037)
|
||||
|
||||
- Polyfill `Element.replaceChildren` [#7034](https://github.com/excalidraw/excalidraw/pull/7034)
|
||||
|
||||
### Refactor
|
||||
|
||||
- DRY out tool typing [#7086](https://github.com/excalidraw/excalidraw/pull/7086)
|
||||
|
||||
- Refactor event globals to differentiate from `lastPointerUp` [#7084](https://github.com/excalidraw/excalidraw/pull/7084)
|
||||
|
||||
- DRY out and simplify setting active tool from toolbar [#7079](https://github.com/excalidraw/excalidraw/pull/7079)
|
||||
|
||||
### Performance
|
||||
|
||||
- Improve element in frame check [#7124](https://github.com/excalidraw/excalidraw/pull/7124)
|
||||
|
||||
---
|
||||
|
||||
## 0.16.1 (2023-09-21)
|
||||
|
||||
## Excalidraw Library
|
||||
@@ -59,7 +207,7 @@ Please add the latest change on the top under the correct section.
|
||||
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
||||
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
||||
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
||||
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||
@@ -338,7 +486,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
|
||||
- [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
|
||||
|
||||
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [disableImageTool, setDisableImageTool] = useState(false);
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
@@ -606,6 +607,16 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableImageTool === true}
|
||||
onChange={() => {
|
||||
setDisableImageTool(!disableImageTool);
|
||||
}}
|
||||
/>
|
||||
Disable Image Tool
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -686,6 +697,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
|
||||
@@ -56,6 +56,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
tools: {
|
||||
image: props.UIOptions?.tools?.image ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
if (canvasActions?.export) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.IS_PREACT === "true") {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
module.exports = require("./dist/excalidraw-with-preact.production.min.js");
|
||||
} else {
|
||||
module.exports = require("./dist/excalidraw-with-preact.development.js");
|
||||
}
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
module.exports = require("./dist/excalidraw.production.min.js");
|
||||
} else {
|
||||
module.exports = require("./dist/excalidraw.development.js");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@excalidraw/excalidraw",
|
||||
"version": "0.16.1",
|
||||
"version": "0.17.0",
|
||||
"main": "main.js",
|
||||
"types": "types/packages/excalidraw/index.d.ts",
|
||||
"files": [
|
||||
@@ -78,7 +78,7 @@
|
||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "tsc --project ../../../tsconfig-types.json",
|
||||
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
|
||||
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && NODE_ENV=development webpack --config webpack.preact.config.js && NODE_ENV=production webpack --config webpack.preact.config.js && yarn gen:types",
|
||||
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
||||
"pack": "yarn build:umd && yarn pack",
|
||||
"start": "webpack serve --config webpack.dev-server.config.js",
|
||||
|
||||
33
src/packages/excalidraw/webpack.preact.config.js
Normal file
33
src/packages/excalidraw/webpack.preact.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const prodConfig = require("./webpack.prod.config");
|
||||
const devConfig = require("./webpack.dev.config");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
const config = isProd ? prodConfig : devConfig;
|
||||
const outputFile = isProd
|
||||
? "excalidraw-with-preact.production.min"
|
||||
: "excalidraw-with-preact.development";
|
||||
|
||||
const preactWebpackConfig = {
|
||||
entry: {
|
||||
[outputFile]: "./entry.js",
|
||||
},
|
||||
externals: {
|
||||
...config.externals,
|
||||
"react-dom/client": {
|
||||
root: "ReactDOMClient",
|
||||
commonjs2: "react-dom/client",
|
||||
commonjs: "react-dom/client",
|
||||
amd: "react-dom/client",
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
root: "ReactJSXRuntime",
|
||||
commonjs2: "react/jsx-runtime",
|
||||
commonjs: "react/jsx-runtime",
|
||||
amd: "react/jsx-runtime",
|
||||
},
|
||||
},
|
||||
};
|
||||
module.exports = merge(config, preactWebpackConfig);
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
getElementAbsoluteCoords,
|
||||
} from "../element/bounds";
|
||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||
import { distance, getFontString } from "../utils";
|
||||
import { cloneJSON, distance, getFontString } from "../utils";
|
||||
import { AppState, BinaryFiles } from "../types";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
FONT_FAMILY,
|
||||
FRAME_STYLE,
|
||||
SVG_NS,
|
||||
THEME_FILTER,
|
||||
@@ -51,8 +52,9 @@ const __createSceneForElementsHack__ = (
|
||||
// we can't duplicate elements to regenerate ids because we need the
|
||||
// orig ids when embedding. So we do another hack of not mapping element
|
||||
// ids to Scene instances so that we don't override the editor elements
|
||||
// mapping
|
||||
scene.replaceAllElements(elements, false);
|
||||
// mapping.
|
||||
// We still need to clone the objects themselves to regen references.
|
||||
scene.replaceAllElements(cloneJSON(elements), false);
|
||||
return scene;
|
||||
};
|
||||
|
||||
@@ -105,7 +107,7 @@ const addFrameLabelsAsTextElements = (
|
||||
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
||||
x: element.x,
|
||||
y: element.y - FRAME_STYLE.nameOffsetY,
|
||||
fontFamily: 4,
|
||||
fontFamily: FONT_FAMILY.Assistant,
|
||||
fontSize: FRAME_STYLE.nameFontSize,
|
||||
lineHeight:
|
||||
FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"],
|
||||
@@ -139,6 +141,36 @@ const getFrameRenderingConfig = (
|
||||
};
|
||||
};
|
||||
|
||||
const prepareElementsForRender = ({
|
||||
elements,
|
||||
exportingFrame,
|
||||
frameRendering,
|
||||
exportWithDarkMode,
|
||||
}: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
exportingFrame: ExcalidrawFrameElement | null | undefined;
|
||||
frameRendering: AppState["frameRendering"];
|
||||
exportWithDarkMode: AppState["exportWithDarkMode"];
|
||||
}) => {
|
||||
let nextElements: readonly ExcalidrawElement[];
|
||||
|
||||
if (exportingFrame) {
|
||||
nextElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
} else if (frameRendering.enabled && frameRendering.name) {
|
||||
nextElements = addFrameLabelsAsTextElements(elements, {
|
||||
exportWithDarkMode,
|
||||
});
|
||||
} else {
|
||||
nextElements = elements;
|
||||
}
|
||||
|
||||
return nextElements;
|
||||
};
|
||||
|
||||
export const exportToCanvas = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@@ -167,21 +199,24 @@ export const exportToCanvas = async (
|
||||
const tempScene = __createSceneForElementsHack__(elements);
|
||||
elements = tempScene.getNonDeletedElements();
|
||||
|
||||
let nextElements: ExcalidrawElement[];
|
||||
const frameRendering = getFrameRenderingConfig(
|
||||
exportingFrame ?? null,
|
||||
appState.frameRendering ?? null,
|
||||
);
|
||||
|
||||
const elementsForRender = prepareElementsForRender({
|
||||
elements,
|
||||
exportingFrame,
|
||||
exportWithDarkMode: appState.exportWithDarkMode,
|
||||
frameRendering,
|
||||
});
|
||||
|
||||
if (exportingFrame) {
|
||||
exportPadding = 0;
|
||||
nextElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
} else {
|
||||
nextElements = addFrameLabelsAsTextElements(elements, appState);
|
||||
}
|
||||
|
||||
const [minX, minY, width, height] = getCanvasSize(
|
||||
exportingFrame ? [exportingFrame] : getRootElements(nextElements),
|
||||
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||
exportPadding,
|
||||
);
|
||||
|
||||
@@ -191,7 +226,7 @@ export const exportToCanvas = async (
|
||||
|
||||
const { imageCache } = await updateImageCache({
|
||||
imageCache: new Map(),
|
||||
fileIds: getInitializedImageElements(nextElements).map(
|
||||
fileIds: getInitializedImageElements(elementsForRender).map(
|
||||
(element) => element.fileId,
|
||||
),
|
||||
files,
|
||||
@@ -200,15 +235,12 @@ export const exportToCanvas = async (
|
||||
renderStaticScene({
|
||||
canvas,
|
||||
rc: rough.canvas(canvas),
|
||||
elements: nextElements,
|
||||
visibleElements: nextElements,
|
||||
elements: elementsForRender,
|
||||
visibleElements: elementsForRender,
|
||||
scale,
|
||||
appState: {
|
||||
...appState,
|
||||
frameRendering: getFrameRenderingConfig(
|
||||
exportingFrame ?? null,
|
||||
appState.frameRendering ?? null,
|
||||
),
|
||||
frameRendering,
|
||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: -minX + exportPadding,
|
||||
scrollY: -minY + exportPadding,
|
||||
@@ -248,8 +280,14 @@ export const exportToSvg = async (
|
||||
const tempScene = __createSceneForElementsHack__(elements);
|
||||
elements = tempScene.getNonDeletedElements();
|
||||
|
||||
const frameRendering = getFrameRenderingConfig(
|
||||
opts?.exportingFrame ?? null,
|
||||
appState.frameRendering ?? null,
|
||||
);
|
||||
|
||||
let {
|
||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||
exportWithDarkMode = false,
|
||||
viewBackgroundColor,
|
||||
exportScale = 1,
|
||||
exportEmbedScene,
|
||||
@@ -257,19 +295,15 @@ export const exportToSvg = async (
|
||||
|
||||
const { exportingFrame = null } = opts || {};
|
||||
|
||||
let nextElements: ExcalidrawElement[] = [];
|
||||
const elementsForRender = prepareElementsForRender({
|
||||
elements,
|
||||
exportingFrame,
|
||||
exportWithDarkMode,
|
||||
frameRendering,
|
||||
});
|
||||
|
||||
if (exportingFrame) {
|
||||
exportPadding = 0;
|
||||
nextElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
} else {
|
||||
nextElements = addFrameLabelsAsTextElements(elements, {
|
||||
exportWithDarkMode: appState.exportWithDarkMode ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
let metadata = "";
|
||||
@@ -293,7 +327,7 @@ export const exportToSvg = async (
|
||||
}
|
||||
|
||||
const [minX, minY, width, height] = getCanvasSize(
|
||||
exportingFrame ? [exportingFrame] : getRootElements(nextElements),
|
||||
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||
exportPadding,
|
||||
);
|
||||
|
||||
@@ -304,7 +338,7 @@ export const exportToSvg = async (
|
||||
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||
if (appState.exportWithDarkMode) {
|
||||
if (exportWithDarkMode) {
|
||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||
}
|
||||
|
||||
@@ -379,15 +413,12 @@ export const exportToSvg = async (
|
||||
}
|
||||
|
||||
const rsvg = rough.svg(svgRoot);
|
||||
renderSceneToSvg(nextElements, rsvg, svgRoot, files || {}, {
|
||||
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
||||
offsetX,
|
||||
offsetY,
|
||||
exportWithDarkMode: appState.exportWithDarkMode ?? false,
|
||||
exportWithDarkMode,
|
||||
renderEmbeddables: opts?.renderEmbeddables ?? false,
|
||||
frameRendering: getFrameRenderingConfig(
|
||||
exportingFrame ?? null,
|
||||
appState.frameRendering ?? null,
|
||||
),
|
||||
frameRendering,
|
||||
});
|
||||
|
||||
tempScene.destroy();
|
||||
|
||||
@@ -482,7 +482,7 @@ export type ExportOpts = {
|
||||
// truthiness value will determine whether the action is rendered or not
|
||||
// (see manager renderAction). We also override canvasAction values in
|
||||
// excalidraw package index.tsx.
|
||||
type CanvasActions = Partial<{
|
||||
export type CanvasActions = Partial<{
|
||||
changeViewBackgroundColor: boolean;
|
||||
clearCanvas: boolean;
|
||||
export: false | ExportOpts;
|
||||
@@ -492,9 +492,12 @@ type CanvasActions = Partial<{
|
||||
saveAsImage: boolean;
|
||||
}>;
|
||||
|
||||
type UIOptions = Partial<{
|
||||
export type UIOptions = Partial<{
|
||||
dockedSidebarBreakpoint: number;
|
||||
canvasActions: CanvasActions;
|
||||
tools: {
|
||||
image: boolean;
|
||||
};
|
||||
/** @deprecated does nothing. Will be removed in 0.15 */
|
||||
welcomeScreen?: boolean;
|
||||
}>;
|
||||
|
||||
Reference in New Issue
Block a user