diff --git a/src/components/App.tsx b/src/components/App.tsx index 8146a8ee3..1bb9c7cce 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -223,6 +223,7 @@ import { withBatchedUpdatesThrottled, updateObject, setEraserCursor, + getCustomElementConfig, } from "../utils"; import ContextMenu, { ContextMenuOption } from "./ContextMenu"; import LayerUI from "./LayerUI"; @@ -422,10 +423,13 @@ class App extends React.Component { coords: { x: number; y: number }, name: string = "", ) => { - const config = this.props.customElementsConfig!.find( - (config) => config.name === name, - )!; - + const config = getCustomElementConfig( + this.props.customElementsConfig, + name, + ); + if (!config) { + return; + } const [gridX, gridY] = getGridPoint( coords.x, coords.y, @@ -3393,6 +3397,15 @@ class App extends React.Component { const elements = this.scene.getElements(); const selectedElements = getSelectedElements(elements, this.state); if (selectedElements.length === 1 && !this.state.editingLinearElement) { + if (selectedElements[0].type === "custom") { + const config = getCustomElementConfig( + this.props.customElementsConfig, + selectedElements[0].name, + ); + if (!config?.transformHandles) { + return false; + } + } const elementWithTransformHandleType = getElementWithTransformHandleType( elements, @@ -4209,6 +4222,7 @@ class App extends React.Component { const elementsWithinSelection = getElementsWithinSelection( elements, draggingElement, + this.props.customElementsConfig, ); this.setState((prevState) => selectGroupsForSelectedElements( diff --git a/src/constants.ts b/src/constants.ts index 78e70f656..257ad06bc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ import cssVariables from "./css/variables.module.scss"; -import { AppProps } from "./types"; +import { AppProps, CustomElementConfig } from "./types"; import { FontFamilyValues } from "./element/types"; export const APP_NAME = "Excalidraw"; @@ -152,6 +152,14 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { }, }; +export const DEFAULT_CUSTOM_ELEMENT_CONFIG: Required = { + type: "custom", + name: "custom", + transformHandles: true, + svg: "", + width: 40, + height: 40, +}; export const MQ_MAX_WIDTH_PORTRAIT = 730; export const MQ_MAX_WIDTH_LANDSCAPE = 1000; export const MQ_MAX_HEIGHT_LANDSCAPE = 500; diff --git a/src/packages/excalidraw/example/App.js b/src/packages/excalidraw/example/App.js index 5b50bba90..e859ff78a 100644 --- a/src/packages/excalidraw/example/App.js +++ b/src/packages/excalidraw/example/App.js @@ -196,6 +196,7 @@ export default function App() { }, ${encodeURIComponent(` `)}`, + transformHandles: false, }, ]; }; diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index f6a376a70..37572be46 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -9,7 +9,10 @@ import "../../css/styles.scss"; import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; import { defaultLang } from "../../i18n"; -import { DEFAULT_UI_OPTIONS } from "../../constants"; +import { + DEFAULT_UI_OPTIONS, + DEFAULT_CUSTOM_ELEMENT_CONFIG, +} from "../../constants"; const Excalidraw = (props: ExcalidrawProps) => { const { @@ -37,7 +40,6 @@ const Excalidraw = (props: ExcalidrawProps) => { generateIdForFile, onLinkOpen, renderCustomElementWidget, - customElementsConfig, } = props; const canvasActions = props.UIOptions?.canvasActions; @@ -48,6 +50,11 @@ const Excalidraw = (props: ExcalidrawProps) => { ...canvasActions, }, }; + const customElementsConfig: AppProps["customElementsConfig"] = + props.customElementsConfig?.map((customElementConfig) => ({ + ...DEFAULT_CUSTOM_ELEMENT_CONFIG, + ...customElementConfig, + })); if (canvasActions?.export) { UIOptions.canvasActions.export.saveFileToDisk = diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 3c3c6c376..3c3a8ad46 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -25,7 +25,13 @@ import { RoughSVG } from "roughjs/bin/svg"; import { RoughGenerator } from "roughjs/bin/generator"; import { RenderConfig } from "../scene/types"; -import { distance, getFontString, getFontFamilyString, isRTL } from "../utils"; +import { + distance, + getFontString, + getFontFamilyString, + isRTL, + getCustomElementConfig, +} from "../utils"; import { isPathALoop } from "../math"; import rough from "roughjs/bin/rough"; import { AppState, BinaryFiles, Zoom } from "../types"; @@ -256,9 +262,13 @@ const drawElementOnCanvas = ( } case "custom": { - const config = renderConfig.customElementsConfig?.find( - (config) => config.name === element.name, - )!; + const config = getCustomElementConfig( + renderConfig.customElementsConfig, + element.name, + ); + if (!config) { + break; + } if (!customElementImgCache[config.name]) { const img = document.createElement("img"); img.id = config.name; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 1dc789305..eeeabacbb 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -2,7 +2,7 @@ import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughSVG } from "roughjs/bin/svg"; import oc from "open-color"; -import { AppState, BinaryFiles, Zoom } from "../types"; +import { AppState, BinaryFiles, CustomElementConfig, Zoom } from "../types"; import { ExcalidrawElement, NonDeletedExcalidrawElement, @@ -47,7 +47,11 @@ import { TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils"; +import { + viewportCoordsToSceneCoords, + supportsEmoji, + getCustomElementConfig, +} from "../utils"; import { UserIdleState } from "../types"; import { THEME_FILTER } from "../constants"; import { @@ -304,24 +308,35 @@ export const renderScene = ( !appState.editingLinearElement ) { const selections = elements.reduce((acc, element) => { + const isCustom = element.type === "custom"; + let config: CustomElementConfig; const selectionColors = []; - // local user - if ( - appState.selectedElementIds[element.id] && - !isSelectedViaGroup(appState, element) - ) { - selectionColors.push(oc.black); + + if (element.type === "custom") { + config = getCustomElementConfig( + renderConfig.customElementsConfig, + element.name, + )!; } - // remote users - if (renderConfig.remoteSelectedElementIds[element.id]) { - selectionColors.push( - ...renderConfig.remoteSelectedElementIds[element.id].map( - (socketId) => { - const { background } = getClientColors(socketId, appState); - return background; - }, - ), - ); + if (!isCustom || (isCustom && config!.transformHandles)) { + // local user + if ( + appState.selectedElementIds[element.id] && + !isSelectedViaGroup(appState, element) + ) { + selectionColors.push(oc.black); + } + // remote users + if (renderConfig.remoteSelectedElementIds[element.id]) { + selectionColors.push( + ...renderConfig.remoteSelectedElementIds[element.id].map( + (socketId) => { + const { background } = getClientColors(socketId, appState); + return background; + }, + ), + ); + } } if (selectionColors.length) { const [elementX1, elementY1, elementX2, elementY2] = @@ -351,7 +366,6 @@ export const renderScene = ( selectionColors: [oc.black], }); }; - for (const groupId of getSelectedGroupIds(appState)) { // TODO: support multiplayer selected group IDs addSelectionForGroupId(groupId); @@ -371,19 +385,32 @@ export const renderScene = ( context.save(); context.translate(renderConfig.scrollX, renderConfig.scrollY); if (locallySelectedElements.length === 1) { - context.fillStyle = oc.white; - const transformHandles = getTransformHandles( - locallySelectedElements[0], - renderConfig.zoom, - "mouse", // when we render we don't know which pointer type so use mouse - ); - if (!appState.viewModeEnabled) { - renderTransformHandles( - context, - renderConfig, - transformHandles, - locallySelectedElements[0].angle, + let showTransformHandles = true; + if (locallySelectedElements[0].type === "custom") { + const config = getCustomElementConfig( + renderConfig.customElementsConfig, + locallySelectedElements[0].name, ); + if (!config || !config.transformHandles) { + showTransformHandles = false; + } + } + if (showTransformHandles) { + context.fillStyle = oc.white; + const transformHandles = getTransformHandles( + locallySelectedElements[0], + renderConfig.zoom, + "mouse", // when we render we don't know which pointer type so use mouse + ); + + if (!appState.viewModeEnabled) { + renderTransformHandles( + context, + renderConfig, + transformHandles, + locallySelectedElements[0].angle, + ); + } } } else if (locallySelectedElements.length > 1 && !appState.isRotating) { const dashedLinePadding = 4 / renderConfig.zoom.value; @@ -570,6 +597,7 @@ const renderTransformHandles = ( renderConfig: RenderConfig, transformHandles: TransformHandles, angle: number, + name?: string, ): void => { Object.keys(transformHandles).forEach((key) => { const transformHandle = transformHandles[key as TransformHandleType]; diff --git a/src/scene/selection.ts b/src/scene/selection.ts index a5abf7e43..5de25cd8d 100644 --- a/src/scene/selection.ts +++ b/src/scene/selection.ts @@ -3,20 +3,27 @@ import { NonDeletedExcalidrawElement, } from "../element/types"; import { getElementAbsoluteCoords, getElementBounds } from "../element"; -import { AppState } from "../types"; +import { AppState, ExcalidrawProps } from "../types"; import { isBoundToContainer } from "../element/typeChecks"; +import { getCustomElementConfig } from "../utils"; export const getElementsWithinSelection = ( elements: readonly NonDeletedExcalidrawElement[], selection: NonDeletedExcalidrawElement, + customElementConfig: ExcalidrawProps["customElementsConfig"], ) => { const [selectionX1, selectionY1, selectionX2, selectionY2] = getElementAbsoluteCoords(selection); return elements.filter((element) => { const [elementX1, elementY1, elementX2, elementY2] = getElementBounds(element); - + const isCustom = element.type === "custom"; + const allowSelection = isCustom + ? getCustomElementConfig(customElementConfig, element.name) + ?.transformHandles + : true; return ( + allowSelection && element.type !== "selection" && !isBoundToContainer(element) && selectionX1 <= elementX1 && diff --git a/src/types.ts b/src/types.ts index 577b9cc99..52f9eca20 100644 --- a/src/types.ts +++ b/src/types.ts @@ -206,11 +206,10 @@ export type ExcalidrawAPIRefValue = ready?: false; }; -type CustomElementConfig = { +export type CustomElementConfig = { type: "custom"; name: string; - resize?: boolean; - rotate?: boolean; + transformHandles?: boolean; svg: string; width?: number; height?: number; @@ -317,6 +316,7 @@ export type AppProps = ExcalidrawProps & { detectScroll: boolean; handleKeyboardGlobally: boolean; isCollaborating: boolean; + customElementsConfig: Required[] | undefined; }; /** A subset of App class properties that we need to use elsewhere diff --git a/src/utils.ts b/src/utils.ts index 364828672..c43bbe44f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,7 +11,7 @@ import { WINDOWS_EMOJI_FALLBACK_FONT, } from "./constants"; import { FontFamilyValues, FontString } from "./element/types"; -import { AppState, DataURL, Zoom } from "./types"; +import { AppState, DataURL, ExcalidrawProps, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { isDarwin } from "./keys"; @@ -612,3 +612,13 @@ export const updateObject = >( ...updates, }; }; + +export const getCustomElementConfig = ( + customElementConfig: ExcalidrawProps["customElementsConfig"], + name: string, +) => { + if (!customElementConfig) { + return null; + } + return customElementConfig.find((config) => config.name === name); +};