import clsx from "clsx"; import { THEME } from "@excalidraw/common"; import type { Theme } from "@excalidraw/element/types"; import { actionClearCanvas, actionLoadScene, actionSaveToActiveFile, actionShortcuts, actionToggleArrowBinding, actionToggleGridMode, actionToggleMidpointSnapping, actionToggleObjectsSnapMode, actionToggleSearchMenu, actionToggleStats, actionToggleTheme, actionToggleZenMode, } from "../../actions"; import { actionToggleViewMode } from "../../actions/actionToggleViewMode"; import { resolveInputDevice } from "../../appState"; import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { trackEvent } from "../../analytics"; import { useUIAppState } from "../../context/ui-appState"; import { useSetAtom } from "../../editor-jotai"; import { useI18n } from "../../i18n"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { useExcalidrawSetAppState, useExcalidrawActionManager, useExcalidrawElements, useAppProps, useApp, } from "../App"; import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState"; import Trans from "../Trans"; import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem"; import DropdownMenuItemCheckbox from "../dropdownMenu/DropdownMenuItemCheckbox"; import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio"; import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink"; import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub"; import { GithubIcon, DiscordIcon, XBrandIcon, settingsIcon, emptyIcon, } from "../icons"; import { boltIcon, DeviceDesktopIcon, ExportIcon, ExportImageIcon, HelpIcon, LoadIcon, MoonIcon, save, searchIcon, SunIcon, TrashIcon, usersIcon, } from "../icons"; import "./DefaultItems.scss"; import type { InputDevice } from "../../types"; export const LoadScene = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const elements = useExcalidrawElements(); if (!actionManager.isActionEnabled(actionLoadScene)) { return null; } const handleSelect = async () => { if ( !elements.length || (await openConfirmModal({ title: t("overwriteConfirm.modal.loadFromFile.title"), actionLabel: t("overwriteConfirm.modal.loadFromFile.button"), color: "warning", description: ( {text}} br={() =>
} /> ), })) ) { actionManager.executeAction(actionLoadScene); } }; return ( {t("buttons.load")} ); }; LoadScene.displayName = "LoadScene"; export const SaveToActiveFile = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); if (!actionManager.isActionEnabled(actionSaveToActiveFile)) { return null; } return ( actionManager.executeAction(actionSaveToActiveFile)} icon={save} aria-label={`${t("buttons.save")}`} >{`${t("buttons.save")}`} ); }; SaveToActiveFile.displayName = "SaveToActiveFile"; export const SaveAsImage = () => { const setAppState = useExcalidrawSetAppState(); const { t } = useI18n(); return ( setAppState({ openDialog: { name: "imageExport" } })} shortcut={getShortcutFromShortcutName("imageExport")} aria-label={t("buttons.exportImage")} > {t("buttons.exportImage")} ); }; SaveAsImage.displayName = "SaveAsImage"; export const CommandPalette = (opts?: { className?: string }) => { const setAppState = useExcalidrawSetAppState(); const { t } = useI18n(); return ( { trackEvent("command_palette", "open", "menu"); setAppState({ openDialog: { name: "commandPalette" } }); }} shortcut={getShortcutFromShortcutName("commandPalette")} aria-label={t("commandPalette.title")} className={opts?.className} > {t("commandPalette.title")} ); }; CommandPalette.displayName = "CommandPalette"; export const SearchMenu = (opts?: { className?: string }) => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); return ( { actionManager.executeAction(actionToggleSearchMenu); }} shortcut={getShortcutFromShortcutName("searchMenu")} aria-label={t("search.title")} className={opts?.className} > {t("search.title")} ); }; SearchMenu.displayName = "SearchMenu"; export const Help = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); return ( actionManager.executeAction(actionShortcuts)} shortcut="?" aria-label={t("helpDialog.title")} > {t("helpDialog.title")} ); }; Help.displayName = "Help"; export const ClearCanvas = () => { const { t } = useI18n(); const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom); const actionManager = useExcalidrawActionManager(); if (!actionManager.isActionEnabled(actionClearCanvas)) { return null; } return ( setActiveConfirmDialog("clearCanvas")} data-testid="clear-canvas-button" aria-label={t("buttons.clearReset")} > {t("buttons.clearReset")} ); }; ClearCanvas.displayName = "ClearCanvas"; export const ToggleTheme = ( props: | { allowSystemTheme: true; /** * Controls the theme of this UI component only. * You should subscribe to `props.onThemeChange` and control the theme * upstream. */ theme: Theme | "system"; } | { allowSystemTheme: false; }, ) => { const { t } = useI18n(); const appState = useUIAppState(); const actionManager = useExcalidrawActionManager(); const shortcut = getShortcutFromShortcutName("toggleTheme"); const appProps = useAppProps(); if (!actionManager.isActionEnabled(actionToggleTheme)) { return null; } if (props?.allowSystemTheme) { return ( { if (appProps.onThemeChange) { appProps.onThemeChange(value); return; } console.warn( "MainMenu.DefaultItems.ToggleTheme: ` props.onThemeChange` must be defined to use system theme selection.", ); }} choices={[ { value: THEME.LIGHT, label: SunIcon, ariaLabel: `${t("buttons.lightMode")} - ${shortcut}`, }, { value: THEME.DARK, label: MoonIcon, ariaLabel: `${t("buttons.darkMode")} - ${shortcut}`, }, { value: "system", label: DeviceDesktopIcon, ariaLabel: t("buttons.systemMode"), }, ]} > {t("labels.theme")} ); } return ( { // do not close the menu when changing theme event.preventDefault(); actionManager.executeAction(actionToggleTheme); }} icon={appState.theme === THEME.DARK ? SunIcon : MoonIcon} data-testid="toggle-dark-mode" shortcut={shortcut} aria-label={ appState.theme === THEME.DARK ? t("buttons.lightMode") : t("buttons.darkMode") } > {appState.theme === THEME.DARK ? t("buttons.lightMode") : t("buttons.darkMode")} ); }; ToggleTheme.displayName = "ToggleTheme"; export const ChangeCanvasBackground = () => { const { t } = useI18n(); const appState = useUIAppState(); const actionManager = useExcalidrawActionManager(); const appProps = useAppProps(); if ( appState.viewModeEnabled || !appProps.UIOptions.canvasActions.changeViewBackgroundColor ) { return null; } return (
{t("labels.canvasBackground")}
{actionManager.renderAction("changeViewBackgroundColor")}
); }; ChangeCanvasBackground.displayName = "ChangeCanvasBackground"; export const Export = () => { const { t } = useI18n(); const setAppState = useExcalidrawSetAppState(); return ( { setAppState({ openDialog: { name: "jsonExport" } }); }} data-testid="json-export-button" aria-label={t("buttons.export")} > {t("buttons.export")} ); }; Export.displayName = "Export"; export const Socials = () => { const { t } = useI18n(); return ( <> GitHub {t("labels.followUs")} {t("labels.discordChat")} ); }; Socials.displayName = "Socials"; export const LiveCollaborationTrigger = ({ onSelect, isCollaborating, }: { onSelect: () => void; isCollaborating: boolean; }) => { const { t } = useI18n(); return ( {t("labels.liveCollaboration")} ); }; LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger"; const PreferencesToggleToolLockItem = () => { const { t } = useI18n(); const app = useApp(); const appState = useUIAppState(); return ( { app.toggleLock(); event.preventDefault(); }} > {t("labels.preferences_toolLock")} ); }; const PreferencesBoxSelectionModeItem = () => { const { t } = useI18n(); const appState = useUIAppState(); const setAppState = useExcalidrawSetAppState(); return ( name="boxSelectionMode" icon={emptyIcon} value={appState.boxSelectionMode} onChange={(value) => { setAppState({ boxSelectionMode: value, }); }} choices={[ { value: "contain", label: t("labels.boxSelectionContain"), ariaLabel: t("labels.boxSelectionContain"), }, { value: "overlap", label: t("labels.boxSelectionOverlap"), ariaLabel: t("labels.boxSelectionOverlap"), }, ]} > {t("labels.boxSelectionMode")} ); }; const PreferencesInputDeviceItem = () => { const { t } = useI18n(); const appState = useUIAppState(); const setAppState = useExcalidrawSetAppState(); return ( > name="inputDevice" icon={emptyIcon} // `auto` isn't offered yet; the radio shows what it resolves to value={resolveInputDevice(appState.inputDevice)} onChange={(value) => { setAppState({ inputDevice: value, }); }} choices={[ { value: "trackpad", label: t("labels.inputDeviceTrackpad"), ariaLabel: t("labels.inputDeviceTrackpad"), }, { value: "mouse", label: t("labels.inputDeviceMouse"), ariaLabel: t("labels.inputDeviceMouse"), }, ]} > {t("labels.inputDevice")} ); }; const PreferencesToggleSnapModeItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleObjectsSnapMode); event.preventDefault(); }} > {t("buttons.objectsSnapMode")} ); }; const PreferencesToggleArrowBindingItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleArrowBinding); event.preventDefault(); }} > {t("labels.arrowBinding")} ); }; const PreferencesToggleMidpointSnappingItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleMidpointSnapping); event.preventDefault(); }} > {t("labels.midpointSnapping")} ); }; export const PreferencesToggleGridModeItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleGridMode); event.preventDefault(); }} > {t("labels.toggleGrid")} ); }; export const PreferencesToggleZenModeItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleZenMode); event.preventDefault(); }} > {t("buttons.zenMode")} ); }; const PreferencesToggleViewModeItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); if (!actionManager.isActionEnabled(actionToggleViewMode)) { return null; } return ( { actionManager.executeAction(actionToggleViewMode); event.preventDefault(); }} > {t("labels.viewMode")} ); }; const PreferencesToggleElementPropertiesItem = () => { const { t } = useI18n(); const actionManager = useExcalidrawActionManager(); const appState = useUIAppState(); return ( { actionManager.executeAction(actionToggleStats); event.preventDefault(); }} > {t("stats.fullTitle")} ); }; export const Preferences = ({ children, additionalItems, }: { children?: React.ReactNode; additionalItems?: React.ReactNode; }) => { const { t } = useI18n(); return ( {t("labels.preferences")} {children || ( <> )} {additionalItems} ); }; Preferences.ToggleToolLock = PreferencesToggleToolLockItem; Preferences.BoxSelectionMode = PreferencesBoxSelectionModeItem; Preferences.InputDevice = PreferencesInputDeviceItem; Preferences.ToggleSnapMode = PreferencesToggleSnapModeItem; Preferences.ToggleArrowBinding = PreferencesToggleArrowBindingItem; Preferences.ToggleMidpointSnapping = PreferencesToggleMidpointSnappingItem; Preferences.ToggleGridMode = PreferencesToggleGridModeItem; Preferences.ToggleZenMode = PreferencesToggleZenModeItem; Preferences.ToggleViewMode = PreferencesToggleViewModeItem; Preferences.ToggleElementProperties = PreferencesToggleElementPropertiesItem; Preferences.displayName = "Preferences";