fix: remove image preview on image insertion (#9626)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Marcel Mraz
2025-06-10 21:31:11 +02:00
committed by GitHub
parent 0d4abd1ddc
commit a7b64f02b3
17 changed files with 35 additions and 344 deletions
@@ -352,7 +352,6 @@ export const ShapesSwitcher = ({
if (value === "image") {
app.setActiveTool({
type: value,
insertOnCanvasDirectly: pointerType !== "mouse",
});
} else {
app.setActiveTool({ type: value });
+23 -157
View File
@@ -161,7 +161,6 @@ import {
maybeParseEmbedSrc,
getEmbedLink,
getInitializedImageElements,
loadHTMLImageElement,
normalizeSVG,
updateImageCache as _updateImageCache,
getBoundTextElement,
@@ -258,7 +257,6 @@ import type {
ExcalidrawEmbeddableElement,
Ordered,
MagicGenerationData,
ExcalidrawNonSelectionElement,
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
} from "@excalidraw/element/types";
@@ -338,7 +336,6 @@ import {
} from "../scene";
import { getStateForZoom } from "../scene/zoom";
import {
dataURLToFile,
dataURLToString,
generateIdFromFile,
getDataURL,
@@ -440,7 +437,6 @@ import type {
AppProps,
AppState,
BinaryFileData,
DataURL,
ExcalidrawImperativeAPI,
BinaryFiles,
Gesture,
@@ -1520,7 +1516,6 @@ class App extends React.Component<AppProps, AppState> {
width: this.state.width,
editingTextElement: this.state.editingTextElement,
newElementId: this.state.newElement?.id,
pendingImageElementId: this.state.pendingImageElementId,
});
this.visibleElements = visibleElements;
@@ -4711,16 +4706,10 @@ class App extends React.Component<AppProps, AppState> {
};
setActiveTool = (
tool: (
| (
| { type: Exclude<ToolType, "image"> }
| {
type: Extract<ToolType, "image">;
insertOnCanvasDirectly?: boolean;
}
)
| { type: "custom"; customType: string }
) & { locked?: boolean; fromSelection?: boolean },
tool: ({ type: ToolType } | { type: "custom"; customType: string }) & {
locked?: boolean;
fromSelection?: boolean;
},
keepSelection = false,
) => {
if (!this.isToolSupported(tool.type)) {
@@ -4746,10 +4735,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings: [] });
}
if (nextActiveTool.type === "image") {
this.onImageAction({
insertOnCanvasDirectly:
(tool.type === "image" && tool.insertOnCanvasDirectly) ?? false,
});
this.onImageAction();
}
this.setState((prevState) => {
@@ -6595,34 +6581,6 @@ class App extends React.Component<AppProps, AppState> {
this.state.activeTool.type,
pointerDownState,
);
} else if (this.state.activeTool.type === "image") {
// reset image preview on pointerdown
setCursor(this.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
// retrieve the latest element as the state may be stale
const pendingImageElement =
this.state.pendingImageElementId &&
this.scene.getElement(this.state.pendingImageElementId);
if (!pendingImageElement) {
return;
}
this.setState({
newElement: pendingImageElement as ExcalidrawNonSelectionElement,
pendingImageElementId: null,
multiElement: null,
});
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
this.scene.mutateElement(pendingImageElement, {
x,
y,
frameId: frame ? frame.id : null,
});
} else if (this.state.activeTool.type === "freedraw") {
this.handleFreeDrawElementOnPointerDown(
event,
@@ -6646,7 +6604,8 @@ class App extends React.Component<AppProps, AppState> {
);
} else if (
this.state.activeTool.type !== "eraser" &&
this.state.activeTool.type !== "hand"
this.state.activeTool.type !== "hand" &&
this.state.activeTool.type !== "image"
) {
this.createGenericElementOnPointerDown(
this.state.activeTool.type,
@@ -9092,10 +9051,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.eventListeners.onKeyUp!,
);
if (this.state.pendingImageElementId) {
this.setState({ pendingImageElementId: null });
}
this.props?.onPointerUp?.(activeTool, pointerDownState);
this.onPointerUpEmitter.trigger(
this.state.activeTool,
@@ -9873,11 +9828,9 @@ class App extends React.Component<AppProps, AppState> {
private initializeImage = async ({
imageFile,
imageElement: _imageElement,
showCursorImagePreview = false,
}: {
imageFile: File;
imageElement: ExcalidrawImageElement;
showCursorImagePreview?: boolean;
}) => {
// at this point this should be guaranteed image file, but we do this check
// to satisfy TS down the line
@@ -9935,16 +9888,6 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (showCursorImagePreview) {
const dataURL = this.files[fileId]?.dataURL;
// optimization so that we don't unnecessarily resize the original
// full-size file for cursor preview
// (it's much faster to convert the resized dataURL to File)
const resizedFile = dataURL && dataURLToFile(dataURL);
this.setImagePreviewCursor(resizedFile || imageFile);
}
const dataURL =
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
@@ -9983,11 +9926,7 @@ class App extends React.Component<AppProps, AppState> {
const imageHTML = await cachedImageData?.image;
if (
imageHTML &&
this.state.pendingImageElementId !== imageElement.id &&
this.state.newElement?.id !== imageElement.id
) {
if (imageHTML && this.state.newElement?.id !== imageElement.id) {
const naturalDimensions = this.getImageNaturalDimensions(
imageElement,
imageHTML,
@@ -10000,10 +9939,6 @@ class App extends React.Component<AppProps, AppState> {
} catch (error: any) {
console.error(error);
reject(new Error(t("errors.imageInsertError")));
} finally {
if (!showCursorImagePreview) {
resetCursor(this.interactiveCanvas);
}
}
},
);
@@ -10015,7 +9950,6 @@ class App extends React.Component<AppProps, AppState> {
insertImageElement = async (
imageElement: ExcalidrawImageElement,
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
@@ -10030,7 +9964,6 @@ class App extends React.Component<AppProps, AppState> {
const image = await this.initializeImage({
imageFile,
imageElement,
showCursorImagePreview,
});
const nextElements = this.scene
@@ -10063,58 +9996,7 @@ class App extends React.Component<AppProps, AppState> {
}
};
private setImagePreviewCursor = async (imageFile: File) => {
// mustn't be larger than 128 px
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property
const cursorImageSizePx = 96;
let imagePreview;
try {
imagePreview = await resizeImageFile(imageFile, {
maxWidthOrHeight: cursorImageSizePx,
});
} catch (e: any) {
if (e.cause === "UNSUPPORTED") {
throw new Error(t("errors.unsupportedFileType"));
}
throw e;
}
let previewDataURL = await getDataURL(imagePreview);
// SVG cannot be resized via `resizeImageFile` so we resize by rendering to
// a small canvas
if (imageFile.type === MIME_TYPES.svg) {
const img = await loadHTMLImageElement(previewDataURL);
let height = Math.min(img.height, cursorImageSizePx);
let width = height * (img.width / img.height);
if (width > cursorImageSizePx) {
width = cursorImageSizePx;
height = width * (img.height / img.width);
}
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d")!;
context.drawImage(img, 0, 0, width, height);
previewDataURL = canvas.toDataURL(MIME_TYPES.svg) as DataURL;
}
if (this.state.pendingImageElementId) {
setCursor(this.interactiveCanvas, `url(${previewDataURL}) 4 4, auto`);
}
};
private onImageAction = async ({
insertOnCanvasDirectly,
}: {
insertOnCanvasDirectly: boolean;
}) => {
private onImageAction = async () => {
try {
const clientX = this.state.width / 2 + this.state.offsetLeft;
const clientY = this.state.height / 2 + this.state.offsetTop;
@@ -10137,35 +10019,20 @@ class App extends React.Component<AppProps, AppState> {
addToFrameUnderCursor: false,
});
if (insertOnCanvasDirectly) {
this.insertImageElement(imageElement, imageFile);
this.initializeImageDimensions(imageElement);
this.store.scheduleCapture();
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
},
() => {
this.actionManager.executeAction(actionFinalize);
},
);
} else {
this.setState(
{
pendingImageElementId: imageElement.id,
},
() => {
this.insertImageElement(
imageElement,
imageFile,
/* showCursorImagePreview */ true,
);
},
);
}
this.insertImageElement(imageElement, imageFile);
this.initializeImageDimensions(imageElement);
this.store.scheduleCapture();
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
},
() => {
this.actionManager.executeAction(actionFinalize);
},
);
} catch (error: any) {
if (error.name !== "AbortError") {
console.error(error);
@@ -10174,7 +10041,6 @@ class App extends React.Component<AppProps, AppState> {
}
this.setState(
{
pendingImageElementId: null,
newElement: null,
activeTool: updateActiveTool(this.state, { type: "selection" }),
},
@@ -503,7 +503,6 @@ function CommandPaletteInner({
if (value === "image") {
app.setActiveTool({
type: value,
insertOnCanvasDirectly: event.type === EVENT.KEYDOWN,
});
} else {
app.setActiveTool({ type: value });
@@ -74,10 +74,6 @@ const getHints = ({
return t("hints.embeddable");
}
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
return t("hints.placeImage");
}
const selectedElements = app.scene.getSelectedElements(appState);
if (
@@ -198,7 +198,6 @@ const getRelevantAppStateProps = (
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
selectionElement: appState.selectionElement,
selectedGroupIds: appState.selectedGroupIds,
selectedLinearElement: appState.selectedLinearElement,
@@ -100,7 +100,6 @@ const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
viewBackgroundColor: appState.viewBackgroundColor,
exportScale: appState.exportScale,