mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge remote-tracking branch 'origin/master' into danieljgeiger-mathjax
This commit is contained in:
commit
5a3f4fd08f
93 changed files with 1308 additions and 567 deletions
|
@ -223,9 +223,10 @@ export const ShapesSwitcher = ({
|
|||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||
const letter =
|
||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||
const shortcut = letter
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
return (
|
||||
<ToolButton
|
||||
|
@ -236,7 +237,7 @@ export const ShapesSwitcher = ({
|
|||
checked={activeTool.type === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||
keyBindingLabel={numericKey}
|
||||
keyBindingLabel={numericKey || letter}
|
||||
aria-label={capitalizeString(label)}
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={`toolbar-${value}`}
|
||||
|
|
35
src/components/ActiveConfirmDialog.tsx
Normal file
35
src/components/ActiveConfirmDialog.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { atom, useAtom } from "jotai";
|
||||
import { actionClearCanvas } from "../actions";
|
||||
import { t } from "../i18n";
|
||||
import { useExcalidrawActionManager } from "./App";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
|
||||
export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
||||
|
||||
export const ActiveConfirmDialog = () => {
|
||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||
activeConfirmDialogAtom,
|
||||
);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!activeConfirmDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeConfirmDialog === "clearCanvas") {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
actionManager.executeAction(actionClearCanvas);
|
||||
setActiveConfirmDialog(null);
|
||||
}}
|
||||
onCancel={() => setActiveConfirmDialog(null)}
|
||||
title={t("clearCanvasDialog.title")}
|
||||
>
|
||||
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -41,7 +41,11 @@ import { ActionManager } from "../actions/manager";
|
|||
import { getActions } from "../actions/register";
|
||||
import { ActionResult } from "../actions/types";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||
import {
|
||||
getDefaultAppState,
|
||||
isEraserActive,
|
||||
isHandToolActive,
|
||||
} from "../appState";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
|
@ -57,6 +61,7 @@ import {
|
|||
EVENT,
|
||||
GRID_SIZE,
|
||||
IMAGE_RENDER_TIMEOUT,
|
||||
isAndroid,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MAX_ALLOWED_FILE_BYTES,
|
||||
MIME_TYPES,
|
||||
|
@ -167,7 +172,6 @@ import {
|
|||
shouldRotateWithDiscreteAngle,
|
||||
isArrowKey,
|
||||
KEYS,
|
||||
isAndroid,
|
||||
} from "../keys";
|
||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||
import { renderScene } from "../renderer/renderScene";
|
||||
|
@ -283,6 +287,7 @@ import {
|
|||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||
import { Fonts } from "../scene/Fonts";
|
||||
import { actionPaste } from "../actions/actionClipboard";
|
||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
|
@ -622,6 +627,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
elements={this.scene.getNonDeletedElements()}
|
||||
onLockToggle={this.toggleLock}
|
||||
onPenModeToggle={this.togglePenMode}
|
||||
onHandToolToggle={this.onHandToolToggle}
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
|
@ -1875,6 +1881,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
onHandToolToggle = () => {
|
||||
this.actionManager.executeAction(actionToggleHandTool);
|
||||
};
|
||||
|
||||
scrollToContent = (
|
||||
target:
|
||||
| ExcalidrawElement
|
||||
|
@ -2292,11 +2302,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private setActiveTool = (
|
||||
tool:
|
||||
| { type: typeof SHAPES[number]["value"] | "eraser" }
|
||||
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
|
||||
| { type: "custom"; customType: string },
|
||||
) => {
|
||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||
if (!isHoldingSpace) {
|
||||
if (nextActiveTool.type === "hand") {
|
||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||
} else if (!isHoldingSpace) {
|
||||
setCursorForShape(this.canvas, this.state);
|
||||
}
|
||||
if (isToolIcon(document.activeElement)) {
|
||||
|
@ -2968,7 +2980,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
null;
|
||||
}
|
||||
|
||||
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
||||
if (
|
||||
isHoldingSpace ||
|
||||
isPanning ||
|
||||
isDraggingScrollBar ||
|
||||
isHandToolActive(this.state)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3560,7 +3577,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
} else if (this.state.activeTool.type === "custom") {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
} else if (this.state.activeTool.type !== "eraser") {
|
||||
} else if (
|
||||
this.state.activeTool.type !== "eraser" &&
|
||||
this.state.activeTool.type !== "hand"
|
||||
) {
|
||||
this.createGenericElementOnPointerDown(
|
||||
this.state.activeTool.type,
|
||||
pointerDownState,
|
||||
|
@ -3671,6 +3691,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
gesture.pointers.size <= 1 &&
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||
isHandToolActive(this.state) ||
|
||||
this.state.viewModeEnabled)
|
||||
) ||
|
||||
isTextElement(this.state.editingElement)
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 0 0.2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 1rem;
|
||||
background-color: var(--button-color);
|
||||
|
|
32
src/components/HandButton.tsx
Normal file
32
src/components/HandButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import "./ToolIcon.scss";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { handIcon } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
type LockIconProps = {
|
||||
title?: string;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
export const HandButton = (props: LockIconProps) => {
|
||||
return (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
icon={handIcon}
|
||||
name="editor-current-shape"
|
||||
checked={props.checked}
|
||||
title={`${props.title} — H`}
|
||||
keyBindingLabel={!props.isMobile ? KEYS.H.toLocaleUpperCase() : undefined}
|
||||
aria-label={`${props.title} — H`}
|
||||
aria-keyshortcuts={KEYS.H}
|
||||
data-testid={`toolbar-hand`}
|
||||
onChange={() => props.onChange?.()}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,10 +1,12 @@
|
|||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
||||
import { KEYS } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
import { ExternalLinkIcon } from "./icons";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { isDarwin, isFirefox, isWindows } from "../constants";
|
||||
|
||||
const Header = () => (
|
||||
<div className="HelpDialog__header">
|
||||
|
@ -67,6 +69,10 @@ function* intersperse(as: JSX.Element[][], delim: string | null) {
|
|||
}
|
||||
}
|
||||
|
||||
const upperCaseSingleChars = (str: string) => {
|
||||
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
const Shortcut = ({
|
||||
label,
|
||||
shortcuts,
|
||||
|
@ -81,7 +87,9 @@ const Shortcut = ({
|
|||
? [...shortcut.slice(0, -2).split("+"), "+"]
|
||||
: shortcut.split("+");
|
||||
|
||||
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
|
||||
return keys.map((key) => (
|
||||
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
|
||||
));
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
className="HelpDialog__island--tools"
|
||||
caption={t("helpDialog.tools")}
|
||||
>
|
||||
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.selection")}
|
||||
shortcuts={[KEYS.V, KEYS["1"]]}
|
||||
|
@ -304,10 +313,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||
label={t("labels.pasteAsPlaintext")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
{/* firefox supports clipboard API under a flag, so we'll
|
||||
show users what they can do in the error message */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<Shortcut
|
||||
label={t("labels.copyAsPng")}
|
||||
shortcuts={[getShortcutKey("Shift+Alt+C")]}
|
||||
/>
|
||||
)}
|
||||
<Shortcut
|
||||
label={t("labels.copyStyles")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Stack from "./Stack";
|
|||
import "./ExportDialog.scss";
|
||||
import OpenColor from "open-color";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
|
||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
|
||||
|
@ -190,7 +190,9 @@ const ImageExportModal = ({
|
|||
>
|
||||
SVG
|
||||
</ExportButton>
|
||||
{probablySupportsClipboardBlob && (
|
||||
{/* firefox supports clipboard API under a flag,
|
||||
so let's throw and tell people what they can do */}
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<ExportButton
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
onClick={() => onExportToClipboard(exportedElements)}
|
||||
|
|
|
@ -50,6 +50,9 @@ import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
|||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
|
@ -59,6 +62,7 @@ interface LayerUIProps {
|
|||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
showExitZenModeBtn: boolean;
|
||||
|
@ -85,6 +89,7 @@ const LayerUI = ({
|
|||
elements,
|
||||
canvas,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
showExitZenModeBtn,
|
||||
|
@ -304,13 +309,20 @@ const LayerUI = ({
|
|||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={() => onLockToggle()}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
|
||||
<div className="App-toolbar__divider"></div>
|
||||
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
|
@ -322,9 +334,6 @@ const LayerUI = ({
|
|||
});
|
||||
}}
|
||||
/>
|
||||
{/* {actionManager.renderAction("eraser", {
|
||||
// size: "small",
|
||||
})} */}
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
</Stack.Row>
|
||||
|
@ -386,6 +395,7 @@ const LayerUI = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<ActiveConfirmDialog />
|
||||
{renderImageExportDialog()}
|
||||
{renderJSONExportDialog()}
|
||||
{appState.pasteDialog.shown && (
|
||||
|
@ -408,7 +418,8 @@ const LayerUI = ({
|
|||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
onImageAction={onImageAction}
|
||||
|
|
|
@ -187,6 +187,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||
className="library-menu"
|
||||
>
|
||||
{!itemsSelected && (
|
||||
|
|
|
@ -9,7 +9,6 @@ type LockIconProps = {
|
|||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
zenModeEnabled?: boolean;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import { LibraryButton } from "./LibraryButton";
|
|||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
|
@ -31,6 +33,7 @@ type MobileMenuProps = {
|
|||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
|
||||
|
@ -52,6 +55,7 @@ export const MobileMenu = ({
|
|||
actionManager,
|
||||
setAppState,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
onImageAction,
|
||||
|
@ -88,6 +92,13 @@ export const MobileMenu = ({
|
|||
</Island>
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
isMobile
|
||||
/>
|
||||
)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
|
@ -101,13 +112,12 @@ export const MobileMenu = ({
|
|||
title={t("toolBar.lock")}
|
||||
isMobile
|
||||
/>
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
isMobile
|
||||
/>
|
||||
)}
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
</div>
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
|
|
|
@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
|
|||
name?: string;
|
||||
id?: string;
|
||||
size?: ToolButtonSize;
|
||||
keyBindingLabel?: string;
|
||||
keyBindingLabel?: string | null;
|
||||
showAriaLabel?: boolean;
|
||||
hidden?: boolean;
|
||||
visible?: boolean;
|
||||
|
|
|
@ -4,16 +4,23 @@ import { Island } from "../Island";
|
|||
import { useDevice } from "../App";
|
||||
import clsx from "clsx";
|
||||
import Stack from "../Stack";
|
||||
import React from "react";
|
||||
import { DropdownMenuContentPropsContext } from "./common";
|
||||
|
||||
const MenuContent = ({
|
||||
children,
|
||||
onClickOutside,
|
||||
className = "",
|
||||
onSelect,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onClickOutside?: () => void;
|
||||
className?: string;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
|
@ -24,28 +31,32 @@ const MenuContent = ({
|
|||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": device.isMobile,
|
||||
}).trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{device.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
{device.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContentPropsContext.Provider>
|
||||
);
|
||||
};
|
||||
export default MenuContent;
|
||||
MenuContent.displayName = "DropdownMenuContent";
|
||||
|
||||
export default MenuContent;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from "react";
|
||||
import {
|
||||
getDrodownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
|
||||
export const getDrodownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
icon,
|
||||
onSelect,
|
||||
|
@ -14,15 +14,17 @@ const DropdownMenuItem = ({
|
|||
...rest
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
onSelect: () => void;
|
||||
onSelect: (event: Event) => void;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
onClick={onSelect}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={getDrodownMenuItemClassName(className)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
import React from "react";
|
||||
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
|
||||
import {
|
||||
getDrodownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
} from "./common";
|
||||
|
||||
const DropdownMenuItemLink = ({
|
||||
icon,
|
||||
shortcut,
|
||||
href,
|
||||
children,
|
||||
onSelect,
|
||||
className = "",
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
href: string;
|
||||
onSelect?: (event: Event) => void;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
|
@ -23,6 +31,7 @@ const DropdownMenuItemLink = ({
|
|||
rel="noreferrer"
|
||||
className={getDrodownMenuItemClassName(className)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
|
|
31
src/components/dropdownMenu/common.ts
Normal file
31
src/components/dropdownMenu/common.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React, { useContext } from "react";
|
||||
import { EVENT } from "../../constants";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
|
||||
export const DropdownMenuContentPropsContext = React.createContext<{
|
||||
onSelect?: (event: Event) => void;
|
||||
}>({});
|
||||
|
||||
export const getDrodownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
};
|
||||
|
||||
export const useHandleDropdownMenuItemClick = (
|
||||
origOnClick:
|
||||
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
|
||||
| undefined,
|
||||
onSelect: ((event: Event) => void) | undefined,
|
||||
) => {
|
||||
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
|
||||
|
||||
return composeEventHandlers(origOnClick, (event) => {
|
||||
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
onSelect?.(itemSelectEvent);
|
||||
if (!itemSelectEvent.defaultPrevented) {
|
||||
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1532,3 +1532,14 @@ export const publishIcon = createIcon(
|
|||
export const eraser = createIcon(
|
||||
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
|
||||
);
|
||||
|
||||
export const handIcon = createIcon(
|
||||
<g strokeWidth={1.25}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5"></path>
|
||||
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5"></path>
|
||||
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
|
@ -28,9 +28,9 @@ import {
|
|||
} from "../../actions";
|
||||
|
||||
import "./DefaultItems.scss";
|
||||
import { useState } from "react";
|
||||
import ConfirmDialog from "../ConfirmDialog";
|
||||
import clsx from "clsx";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
|
||||
export const LoadScene = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
|
@ -122,41 +122,22 @@ export const ClearCanvas = () => {
|
|||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const toggleDialog = () => setShowDialog(!showDialog);
|
||||
|
||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
icon={TrashIcon}
|
||||
onSelect={toggleDialog}
|
||||
data-testid="clear-canvas-button"
|
||||
aria-label={t("buttons.clearReset")}
|
||||
>
|
||||
{t("buttons.clearReset")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* FIXME this should live outside MainMenu so it stays open
|
||||
if menu is closed */}
|
||||
{showDialog && (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
actionManager.executeAction(actionClearCanvas);
|
||||
toggleDialog();
|
||||
}}
|
||||
onCancel={toggleDialog}
|
||||
title={t("clearCanvasDialog.title")}
|
||||
>
|
||||
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
</>
|
||||
<DropdownMenuItem
|
||||
icon={TrashIcon}
|
||||
onSelect={() => setActiveConfirmDialog("clearCanvas")}
|
||||
data-testid="clear-canvas-button"
|
||||
aria-label={t("buttons.clearReset")}
|
||||
>
|
||||
{t("buttons.clearReset")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
ClearCanvas.displayName = "ClearCanvas";
|
||||
|
@ -171,7 +152,9 @@ export const ToggleTheme = () => {
|
|||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onSelect={(event) => {
|
||||
// do not close the menu when changing theme
|
||||
event.preventDefault();
|
||||
return actionManager.executeAction(actionToggleTheme);
|
||||
}}
|
||||
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
||||
|
|
|
@ -11,14 +11,25 @@ import * as DefaultItems from "./DefaultItems";
|
|||
import { UserList } from "../UserList";
|
||||
import { t } from "../../i18n";
|
||||
import { HamburgerMenuIcon } from "../icons";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
|
||||
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
|
||||
const MainMenu = ({
|
||||
children,
|
||||
onSelect,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const onClickOutside = device.isMobile
|
||||
? undefined
|
||||
: () => setAppState({ openMenu: null });
|
||||
|
||||
return (
|
||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||
<DropdownMenu.Trigger
|
||||
|
@ -30,7 +41,12 @@ const MainMenu = ({ children }: { children?: React.ReactNode }) => {
|
|||
>
|
||||
{HamburgerMenuIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content onClickOutside={onClickOutside}>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={onClickOutside}
|
||||
onSelect={composeEventHandlers(onSelect, () => {
|
||||
setAppState({ openMenu: null });
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{device.isMobile && appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue