import { useState, useEffect } from "react"; import clsx from "clsx"; import { KEYS, capitalizeString } from "@excalidraw/common"; import { t } from "../i18n"; import { useTunnels } from "../context/tunnels"; import DropdownMenu from "./dropdownMenu/DropdownMenu"; import { ToolPopover } from "./ToolPopover"; import { EraserToolButton, FrameToolButton, FreedrawToolPopover, getToolShortcut, HandToolButton, ImageToolButton, isToolButtonDisabled, SelectionToolPopover, TextToolButton, TOOLS, } from "./Tools"; import { TextIcon, ImageIcon, DotsIcon, frameToolIcon, EmbedIcon, laserPointerToolIcon, drawShapeToolIcon, bucketFillIcon, mermaidLogoIcon, MagicIcon, stickyNoteToolIcon, } from "./icons"; import "./ToolIcon.scss"; import "./MobileToolbar.scss"; import type { AppClassProperties, UIAppState } from "../types"; type MobileToolbarProps = { app: AppClassProperties; setAppState: React.Component["setState"]; }; export const MobileToolbar = ({ app, setAppState }: MobileToolbarProps) => { const activeTool = app.state.activeTool; const [isOtherShapesMenuOpen, setIsOtherShapesMenuOpen] = useState(false); const [lastActiveGenericShape, setLastActiveGenericShape] = useState< "rectangle" | "diamond" | "ellipse" >("rectangle"); const [lastActiveLinearElement, setLastActiveLinearElement] = useState< "arrow" | "line" >("arrow"); // keep lastActiveGenericShape in sync with active tool if user switches via other UI useEffect(() => { if ( activeTool.type === "rectangle" || activeTool.type === "diamond" || activeTool.type === "ellipse" ) { setLastActiveGenericShape(activeTool.type); } }, [activeTool.type]); // keep lastActiveLinearElement in sync with active tool if user switches via other UI useEffect(() => { if (activeTool.type === "arrow" || activeTool.type === "line") { setLastActiveLinearElement(activeTool.type); } }, [activeTool.type]); const frameToolSelected = activeTool.type === "frame"; const drawShapeToolSelected = activeTool.type === "autoshape"; const laserToolSelected = activeTool.type === "laser"; const embeddableToolSelected = activeTool.type === "embeddable"; const bucketFillToolSelected = activeTool.type === "bucketfill"; const stickyNoteToolSelected = activeTool.type === "stickynote"; const { TTDDialogTriggerTunnel } = useTunnels(); const SHAPE_TOOLS = (["rectangle", "diamond", "ellipse"] as const).map( (type) => ({ type, icon: TOOLS[type].icon, title: capitalizeString(t(`toolBar.${type}`)), fillable: TOOLS[type].fillable, }), ); const LINEAR_ELEMENT_TOOLS = (["arrow", "line"] as const).map((type) => ({ type, icon: TOOLS[type].icon, title: capitalizeString(t(`toolBar.${type}`)), fillable: TOOLS[type].fillable, })); const [toolbarWidth, setToolbarWidth] = useState(0); const WIDTH = 36; const GAP = 4; // hand, selection, freedraw, eraser, rectangle, arrow, others const MIN_TOOLS = 7; const MIN_WIDTH = MIN_TOOLS * WIDTH + (MIN_TOOLS - 1) * GAP; const ADDITIONAL_WIDTH = WIDTH + GAP; const showTextToolOutside = toolbarWidth >= MIN_WIDTH + 1 * ADDITIONAL_WIDTH; const showImageToolOutside = toolbarWidth >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH; const showFrameToolOutside = toolbarWidth >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH; const extraTools: readonly typeof activeTool.type[] = ( [ "text", "stickynote", "frame", "embeddable", "laser", "bucketfill", "magicframe", ] as const ).filter((tool) => { if (showTextToolOutside && tool === "text") { return false; } if (showFrameToolOutside && tool === "frame") { return false; } return true; }); const extraToolSelected = extraTools.includes(activeTool.type); const extraIcon = extraToolSelected ? activeTool.type === "text" ? TextIcon : activeTool.type === "image" ? ImageIcon : activeTool.type === "frame" ? frameToolIcon : activeTool.type === "stickynote" ? stickyNoteToolIcon : activeTool.type === "embeddable" ? EmbedIcon : activeTool.type === "laser" ? laserPointerToolIcon : activeTool.type === "bucketfill" ? bucketFillIcon : activeTool.type === "magicframe" ? MagicIcon : DotsIcon : DotsIcon; const toolProps = { app, activeTool }; return (
{ if (div) { setToolbarWidth(div.getBoundingClientRect().width); } }} > {/* Hand Tool */} {/* Selection Tool */} {/* Free Draw */} {/* Eraser */} {/* Rectangle/Diamond/Ellipse */} { if ( type === "rectangle" || type === "diamond" || type === "ellipse" ) { setLastActiveGenericShape(type); app.setActiveTool({ type }); } }} displayedOption={ SHAPE_TOOLS.find((tool) => tool.type === lastActiveGenericShape) || SHAPE_TOOLS[0] } /> {/* Arrow/Line */} { if (type === "arrow" || type === "line") { setLastActiveLinearElement(type); app.setActiveTool({ type }); } }} displayedOption={ LINEAR_ELEMENT_TOOLS.find( (tool) => tool.type === lastActiveLinearElement, ) || LINEAR_ELEMENT_TOOLS[0] } /> {/* Text Tool */} {showTextToolOutside && } {/* Image */} {showImageToolOutside && } {/* Frame Tool */} {showFrameToolOutside && } {/* Other Shapes */} { setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen); setAppState({ openMenu: null, openPopup: null }); }} title={t("toolBar.extraTools")} style={{ width: WIDTH, height: WIDTH, display: "flex", alignItems: "center", justifyContent: "center", }} > {extraIcon} setIsOtherShapesMenuOpen(false)} onSelect={() => setIsOtherShapesMenuOpen(false)} className="App-toolbar__extra-tools-dropdown" align="start" > {!showTextToolOutside && ( app.setActiveTool({ type: "text" })} icon={TextIcon} shortcut={KEYS.T.toLocaleUpperCase()} data-testid="toolbar-text" selected={activeTool.type === "text"} disabled={isToolButtonDisabled(app, "text")} > {t("toolBar.text")} )} {!showImageToolOutside && ( app.setActiveTool({ type: "image" })} icon={ImageIcon} data-testid="toolbar-image" selected={activeTool.type === "image"} disabled={isToolButtonDisabled(app, "image")} > {t("toolBar.image")} )} app.setActiveTool({ type: "stickynote" })} icon={stickyNoteToolIcon} shortcut={KEYS.N.toLocaleUpperCase()} data-testid="toolbar-stickynote" selected={stickyNoteToolSelected} disabled={isToolButtonDisabled(app, "stickynote")} > {t("toolBar.stickynote")} {!showFrameToolOutside && ( app.setActiveTool({ type: "frame" })} icon={frameToolIcon} shortcut={KEYS.F.toLocaleUpperCase()} data-testid="toolbar-frame" selected={frameToolSelected} disabled={isToolButtonDisabled(app, "frame")} > {t("toolBar.frame")} )} app.setActiveTool({ type: "embeddable" })} icon={EmbedIcon} data-testid="toolbar-embeddable" selected={embeddableToolSelected} disabled={isToolButtonDisabled(app, "embeddable")} > {t("toolBar.embeddable")} app.setActiveTool({ type: "autoshape" })} icon={drawShapeToolIcon} shortcut={getToolShortcut("autoshape")} data-testid="toolbar-autoshape" selected={drawShapeToolSelected} disabled={isToolButtonDisabled(app, "autoshape")} > {t("toolBar.autoshape")} app.setActiveTool({ type: "laser" })} icon={laserPointerToolIcon} data-testid="toolbar-laser" selected={laserToolSelected} shortcut={KEYS.K.toLocaleUpperCase()} disabled={isToolButtonDisabled(app, "laser")} > {t("toolBar.laser")} app.setActiveTool({ type: "bucketfill" })} icon={bucketFillIcon} data-testid="toolbar-bucketfill" selected={bucketFillToolSelected} shortcut={KEYS.B.toLocaleUpperCase()} disabled={isToolButtonDisabled(app, "bucketfill")} > {t("toolBar.bucketfill")}
Generate
{app.props.aiEnabled !== false && } app.setOpenDialog({ name: "ttd", tab: "mermaid" })} icon={mermaidLogoIcon} data-testid="toolbar-embeddable" > {t("toolBar.mermaidToExcalidraw")} {app.props.aiEnabled !== false && app.plugins.diagramToCode && ( <> app.onMagicframeToolSelect()} icon={MagicIcon} data-testid="toolbar-magicframe" badge={AI} disabled={isToolButtonDisabled(app, "magicframe")} > {t("toolBar.magicframe")} )}
); };