snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
326
packages/excalidraw/components/Toolbar.tsx
Normal file
326
packages/excalidraw/components/Toolbar.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import { useEditorInterface, useStylesPanelMode } from "./App";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import { Island } from "./Island";
|
||||
import { LockButton } from "./LockButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import Stack from "./Stack";
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
import {
|
||||
drawShapeToolIcon,
|
||||
EmbedIcon,
|
||||
frameToolIcon,
|
||||
ImageIcon,
|
||||
LassoIcon,
|
||||
laserPointerToolIcon,
|
||||
bucketFillIcon,
|
||||
MagicIcon,
|
||||
mermaidLogoIcon,
|
||||
DotsIcon,
|
||||
} from "./icons";
|
||||
import {
|
||||
ArrowToolButton,
|
||||
DiamondToolButton,
|
||||
EllipseToolButton,
|
||||
EraserToolButton,
|
||||
FreedrawToolPopover,
|
||||
FreedrawToolButton,
|
||||
getToolShortcut,
|
||||
HandToolButton,
|
||||
isToolButtonDisabled,
|
||||
LassoToolButton,
|
||||
LineToolButton,
|
||||
RectangleToolButton,
|
||||
SelectionToolButton,
|
||||
SelectionToolPopover,
|
||||
StickyNoteToolButton,
|
||||
TextToolButton,
|
||||
} from "./Tools";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
AppState,
|
||||
UIAppState,
|
||||
} from "../types";
|
||||
|
||||
const ExtraToolsDropdown = ({
|
||||
app,
|
||||
activeTool,
|
||||
setAppState,
|
||||
UIOptions,
|
||||
}: {
|
||||
app: AppClassProperties;
|
||||
activeTool: UIAppState["activeTool"];
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
const isFullStylesPanel = useStylesPanelMode() === "full";
|
||||
const { TTDDialogTriggerTunnel } = useTunnels();
|
||||
|
||||
const imageToolSelected = activeTool.type === "image";
|
||||
const frameToolSelected = activeTool.type === "frame";
|
||||
const drawShapeToolSelected = activeTool.type === "autoshape";
|
||||
const laserToolSelected = activeTool.type === "laser";
|
||||
const bucketFillToolSelected = activeTool.type === "bucketfill";
|
||||
const lassoToolSelected =
|
||||
isFullStylesPanel &&
|
||||
activeTool.type === "lasso" &&
|
||||
app.state.preferredSelectionTool.type !== "lasso";
|
||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||
"App-toolbar__extra-tools-trigger--selected":
|
||||
imageToolSelected ||
|
||||
frameToolSelected ||
|
||||
embeddableToolSelected ||
|
||||
(isFullStylesPanel && drawShapeToolSelected) ||
|
||||
lassoToolSelected ||
|
||||
bucketFillToolSelected ||
|
||||
// in collab we're already highlighting the laser button
|
||||
// outside toolbar, so let's not highlight extra-tools button
|
||||
// on top of it
|
||||
(laserToolSelected && !app.props.isCollaborating),
|
||||
})}
|
||||
onToggle={() => {
|
||||
setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen);
|
||||
setAppState({ openMenu: null, openPopup: null });
|
||||
}}
|
||||
title={t("toolBar.extraTools")}
|
||||
>
|
||||
{imageToolSelected
|
||||
? ImageIcon
|
||||
: frameToolSelected
|
||||
? frameToolIcon
|
||||
: embeddableToolSelected
|
||||
? EmbedIcon
|
||||
: isFullStylesPanel && drawShapeToolSelected
|
||||
? drawShapeToolIcon
|
||||
: laserToolSelected && !app.props.isCollaborating
|
||||
? laserPointerToolIcon
|
||||
: lassoToolSelected
|
||||
? LassoIcon
|
||||
: bucketFillToolSelected
|
||||
? bucketFillIcon
|
||||
: DotsIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||
className="App-toolbar__extra-tools-dropdown"
|
||||
>
|
||||
{UIOptions.tools?.image !== false && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "image" })}
|
||||
icon={ImageIcon}
|
||||
shortcut={KEYS["9"]}
|
||||
data-testid="toolbar-image"
|
||||
selected={imageToolSelected}
|
||||
disabled={isToolButtonDisabled(app, "image")}
|
||||
>
|
||||
{t("toolBar.image")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "frame" })}
|
||||
icon={frameToolIcon}
|
||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid="toolbar-frame"
|
||||
selected={frameToolSelected}
|
||||
disabled={isToolButtonDisabled(app, "frame")}
|
||||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
||||
icon={EmbedIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
selected={embeddableToolSelected}
|
||||
disabled={isToolButtonDisabled(app, "embeddable")}
|
||||
>
|
||||
{t("toolBar.embeddable")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "autoshape" })}
|
||||
icon={drawShapeToolIcon}
|
||||
shortcut={getToolShortcut("autoshape")}
|
||||
data-testid="toolbar-autoshape"
|
||||
selected={drawShapeToolSelected}
|
||||
disabled={isToolButtonDisabled(app, "autoshape")}
|
||||
>
|
||||
{t("toolBar.autoshape")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "laser" })}
|
||||
icon={laserPointerToolIcon}
|
||||
data-testid="toolbar-laser"
|
||||
selected={laserToolSelected}
|
||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||
disabled={isToolButtonDisabled(app, "laser")}
|
||||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "bucketfill" })}
|
||||
icon={bucketFillIcon}
|
||||
data-testid="toolbar-bucketfill"
|
||||
selected={bucketFillToolSelected}
|
||||
shortcut={KEYS.B.toLocaleUpperCase()}
|
||||
disabled={isToolButtonDisabled(app, "bucketfill")}
|
||||
>
|
||||
{t("toolBar.bucketfill")}
|
||||
</DropdownMenu.Item>
|
||||
{isFullStylesPanel && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||
icon={LassoIcon}
|
||||
data-testid="toolbar-lasso"
|
||||
selected={lassoToolSelected}
|
||||
disabled={isToolButtonDisabled(app, "lasso")}
|
||||
>
|
||||
{t("toolBar.lasso")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||
Generate
|
||||
</div>
|
||||
{app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
|
||||
icon={mermaidLogoIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
>
|
||||
{t("toolBar.mermaidToExcalidraw")}
|
||||
</DropdownMenu.Item>
|
||||
{app.props.aiEnabled !== false && app.plugins.diagramToCode && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.onMagicframeToolSelect()}
|
||||
icon={MagicIcon}
|
||||
data-testid="toolbar-magicframe"
|
||||
badge={<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>}
|
||||
disabled={isToolButtonDisabled(app, "magicframe")}
|
||||
>
|
||||
{t("toolBar.magicframe")}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
/** the main (desktop/tablet) toolbar island */
|
||||
export const Toolbar = ({
|
||||
app,
|
||||
appState,
|
||||
setAppState,
|
||||
UIOptions,
|
||||
onPenModeToggle,
|
||||
onLockToggle,
|
||||
heading,
|
||||
}: {
|
||||
app: AppClassProperties;
|
||||
appState: UIAppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||
onLockToggle: () => void;
|
||||
heading: React.ReactNode;
|
||||
}) => {
|
||||
const editorInterface = useEditorInterface();
|
||||
const isCompactStylesPanel = useStylesPanelMode() === "compact";
|
||||
|
||||
const activeTool = appState.activeTool;
|
||||
const toolProps = { app, activeTool };
|
||||
|
||||
return (
|
||||
<Island
|
||||
padding={1}
|
||||
className={clsx("App-toolbar", {
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
"App-toolbar--compact": isCompactStylesPanel,
|
||||
})}
|
||||
data-viewport-ui="top"
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
isMobile={editorInterface.formFactor === "phone"}
|
||||
editorInterface={editorInterface}
|
||||
app={app}
|
||||
/>
|
||||
{heading}
|
||||
<Stack.Row gap={isCompactStylesPanel ? 0.5 : 1}>
|
||||
{/* in compact UI the pen mode button is rendered as a separate
|
||||
floating button below the compact actions menu */}
|
||||
{!isCompactStylesPanel && (
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
)}
|
||||
{app.props.activeTool == null && (
|
||||
<>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
// the active tool — including its lock state — is host-controlled
|
||||
disabled={app.props.activeTool != null}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="App-toolbar__divider"
|
||||
style={{ marginRight: "0.25rem" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<HandToolButton {...toolProps} hideKeyBinding />
|
||||
{isCompactStylesPanel ? (
|
||||
<SelectionToolPopover {...toolProps} setAppState={setAppState} />
|
||||
) : appState.preferredSelectionTool.type === "lasso" ? (
|
||||
<LassoToolButton {...toolProps} />
|
||||
) : (
|
||||
<SelectionToolButton {...toolProps} />
|
||||
)}
|
||||
<RectangleToolButton {...toolProps} />
|
||||
<DiamondToolButton {...toolProps} />
|
||||
<EllipseToolButton {...toolProps} />
|
||||
<ArrowToolButton {...toolProps} />
|
||||
<LineToolButton {...toolProps} />
|
||||
{isCompactStylesPanel ? (
|
||||
<FreedrawToolPopover {...toolProps} />
|
||||
) : (
|
||||
<FreedrawToolButton {...toolProps} />
|
||||
)}
|
||||
<TextToolButton {...toolProps} />
|
||||
<StickyNoteToolButton {...toolProps} />
|
||||
<EraserToolButton {...toolProps} />
|
||||
|
||||
<div
|
||||
className="App-toolbar__divider"
|
||||
style={{ marginLeft: "0.25rem" }}
|
||||
/>
|
||||
|
||||
<ExtraToolsDropdown
|
||||
app={app}
|
||||
activeTool={activeTool}
|
||||
setAppState={setAppState}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user