import React from "react"; import { ActionManager } from "../actions/manager"; import { getNonDeletedElements } from "../element"; import { ExcalidrawElement, PointerType } from "../element/types"; import { t } from "../i18n"; import { useIsMobile } from "../components/App"; import { canChangeSharpness, canHaveArrowheads, getTargetElements, hasBackground, hasStrokeStyle, hasStrokeWidth, hasText, } from "../scene"; import { SHAPES } from "../shapes"; import { AppState, Zoom } from "../types"; import { capitalizeString, isTransparent, setCursorForShape } from "../utils"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import { hasStrokeColor } from "../scene/comparisons"; export const SelectedShapeActions = ({ appState, elements, renderAction, elementType, }: { appState: AppState; elements: readonly ExcalidrawElement[]; renderAction: ActionManager["renderAction"]; elementType: ExcalidrawElement["type"]; }) => { const targetElements = getTargetElements( getNonDeletedElements(elements), appState, ); const isEditing = Boolean(appState.editingElement); const isMobile = useIsMobile(); const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const showFillIcons = hasBackground(elementType) || targetElements.some( (element) => hasBackground(element.type) && !isTransparent(element.backgroundColor), ); const showChangeBackgroundIcons = hasBackground(elementType) || targetElements.some((element) => hasBackground(element.type)); let commonSelectedType: string | null = targetElements[0]?.type || null; for (const element of targetElements) { if (element.type !== commonSelectedType) { commonSelectedType = null; break; } } return (
{((hasStrokeColor(elementType) && elementType !== "image" && commonSelectedType !== "image") || targetElements.some((element) => hasStrokeColor(element.type))) && renderAction("changeStrokeColor")} {showChangeBackgroundIcons && renderAction("changeBackgroundColor")} {showFillIcons && renderAction("changeFillStyle")} {(hasStrokeWidth(elementType) || targetElements.some((element) => hasStrokeWidth(element.type))) && renderAction("changeStrokeWidth")} {(elementType === "freedraw" || targetElements.some((element) => element.type === "freedraw")) && renderAction("changeStrokeShape")} {(hasStrokeStyle(elementType) || targetElements.some((element) => hasStrokeStyle(element.type))) && ( <> {renderAction("changeStrokeStyle")} {renderAction("changeSloppiness")} )} {(canChangeSharpness(elementType) || targetElements.some((element) => canChangeSharpness(element.type))) && ( <>{renderAction("changeSharpness")} )} {(hasText(elementType) || targetElements.some((element) => hasText(element.type))) && ( <> {renderAction("changeFontSize")} {renderAction("changeFontFamily")} {renderAction("changeTextAlign")} )} {(canHaveArrowheads(elementType) || targetElements.some((element) => canHaveArrowheads(element.type))) && ( <>{renderAction("changeArrowhead")} )} {renderAction("changeOpacity")}
{t("labels.layers")}
{renderAction("sendToBack")} {renderAction("sendBackward")} {renderAction("bringToFront")} {renderAction("bringForward")}
{targetElements.length > 1 && (
{t("labels.align")}
{ // swap this order for RTL so the button positions always match their action // (i.e. the leftmost button aligns left) } {isRTL ? ( <> {renderAction("alignRight")} {renderAction("alignHorizontallyCentered")} {renderAction("alignLeft")} ) : ( <> {renderAction("alignLeft")} {renderAction("alignHorizontallyCentered")} {renderAction("alignRight")} )} {targetElements.length > 2 && renderAction("distributeHorizontally")}
{renderAction("alignTop")} {renderAction("alignVerticallyCentered")} {renderAction("alignBottom")} {targetElements.length > 2 && renderAction("distributeVertically")}
)} {!isEditing && targetElements.length > 0 && (
{t("labels.actions")}
{!isMobile && renderAction("duplicateSelection")} {!isMobile && renderAction("deleteSelectedElements")} {renderAction("group")} {renderAction("ungroup")} {targetElements.length === 1 && renderAction("hyperlink")}
)}
); }; export const ShapesSwitcher = ({ canvas, elementType, setAppState, onImageAction, }: { canvas: HTMLCanvasElement | null; elementType: ExcalidrawElement["type"]; setAppState: React.Component["setState"]; onImageAction: (data: { pointerType: PointerType | null }) => void; }) => ( <> {SHAPES.map(({ value, icon, key }, index) => { const label = t(`toolBar.${value}`); const letter = key && (typeof key === "string" ? key : key[0]); const shortcut = letter ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}` : `${index + 1}`; return ( { setAppState({ elementType: value, multiElement: null, selectedElementIds: {}, }); setCursorForShape(canvas, value); if (value === "image") { onImageAction({ pointerType }); } }} /> ); })} ); export const ZoomActions = ({ renderAction, zoom, }: { renderAction: ActionManager["renderAction"]; zoom: Zoom; }) => ( {renderAction("zoomOut")} {renderAction("zoomIn")} {renderAction("resetZoom")} );