2020-03-07 10:20:38 -05:00
|
|
|
import React from "react";
|
2020-04-02 16:52:24 +09:00
|
|
|
import { AppState } from "../types";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { ExcalidrawElement } from "../element/types";
|
|
|
|
import { ActionManager } from "../actions/manager";
|
2020-08-15 00:59:43 +09:00
|
|
|
import {
|
|
|
|
hasBackground,
|
|
|
|
hasStroke,
|
|
|
|
canChangeSharpness,
|
|
|
|
hasText,
|
|
|
|
getTargetElement,
|
|
|
|
} from "../scene";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { t } from "../i18n";
|
|
|
|
import { SHAPES } from "../shapes";
|
|
|
|
import { ToolButton } from "./ToolButton";
|
2020-04-07 14:39:06 +03:00
|
|
|
import { capitalizeString, setCursorForShape } from "../utils";
|
2020-03-07 10:20:38 -05:00
|
|
|
import Stack from "./Stack";
|
2020-04-02 00:13:53 +09:00
|
|
|
import useIsMobile from "../is-mobile";
|
2020-04-08 09:49:52 -07:00
|
|
|
import { getNonDeletedElements } from "../element";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const SelectedShapeActions = ({
|
2020-04-02 16:52:24 +09:00
|
|
|
appState,
|
|
|
|
elements,
|
2020-03-07 10:20:38 -05:00
|
|
|
renderAction,
|
|
|
|
elementType,
|
|
|
|
}: {
|
2020-04-02 16:52:24 +09:00
|
|
|
appState: AppState;
|
|
|
|
elements: readonly ExcalidrawElement[];
|
2020-03-07 10:20:38 -05:00
|
|
|
renderAction: ActionManager["renderAction"];
|
|
|
|
elementType: ExcalidrawElement["type"];
|
2020-05-20 16:21:37 +03:00
|
|
|
}) => {
|
2020-04-08 09:49:52 -07:00
|
|
|
const targetElements = getTargetElement(
|
|
|
|
getNonDeletedElements(elements),
|
|
|
|
appState,
|
|
|
|
);
|
2020-04-02 16:52:24 +09:00
|
|
|
const isEditing = Boolean(appState.editingElement);
|
2020-04-02 00:13:53 +09:00
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
return (
|
|
|
|
<div className="panelColumn">
|
|
|
|
{renderAction("changeStrokeColor")}
|
|
|
|
{(hasBackground(elementType) ||
|
2020-03-23 13:05:07 +02:00
|
|
|
targetElements.some((element) => hasBackground(element.type))) && (
|
2020-03-07 10:20:38 -05:00
|
|
|
<>
|
|
|
|
{renderAction("changeBackgroundColor")}
|
|
|
|
|
|
|
|
{renderAction("changeFillStyle")}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{(hasStroke(elementType) ||
|
2020-03-23 13:05:07 +02:00
|
|
|
targetElements.some((element) => hasStroke(element.type))) && (
|
2020-03-07 10:20:38 -05:00
|
|
|
<>
|
|
|
|
{renderAction("changeStrokeWidth")}
|
2020-05-14 17:04:33 +02:00
|
|
|
{renderAction("changeStrokeStyle")}
|
2020-03-07 10:20:38 -05:00
|
|
|
{renderAction("changeSloppiness")}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
2020-08-15 00:59:43 +09:00
|
|
|
{(canChangeSharpness(elementType) ||
|
|
|
|
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
|
|
|
<>{renderAction("changeSharpness")}</>
|
|
|
|
)}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
{(hasText(elementType) ||
|
2020-03-23 13:05:07 +02:00
|
|
|
targetElements.some((element) => hasText(element.type))) && (
|
2020-03-07 10:20:38 -05:00
|
|
|
<>
|
|
|
|
{renderAction("changeFontSize")}
|
|
|
|
|
|
|
|
{renderAction("changeFontFamily")}
|
2020-04-08 21:00:27 +01:00
|
|
|
|
|
|
|
{renderAction("changeTextAlign")}
|
2020-03-07 10:20:38 -05:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{renderAction("changeOpacity")}
|
|
|
|
|
|
|
|
<fieldset>
|
|
|
|
<legend>{t("labels.layers")}</legend>
|
|
|
|
<div className="buttonList">
|
|
|
|
{renderAction("sendToBack")}
|
|
|
|
{renderAction("sendBackward")}
|
|
|
|
{renderAction("bringToFront")}
|
|
|
|
{renderAction("bringForward")}
|
|
|
|
</div>
|
|
|
|
</fieldset>
|
2020-04-02 16:52:24 +09:00
|
|
|
{!isMobile && !isEditing && targetElements.length > 0 && (
|
2020-04-02 00:13:53 +09:00
|
|
|
<fieldset>
|
|
|
|
<legend>{t("labels.actions")}</legend>
|
|
|
|
<div className="buttonList">
|
|
|
|
{renderAction("duplicateSelection")}
|
|
|
|
{renderAction("deleteSelectedElements")}
|
2020-07-26 00:42:06 +02:00
|
|
|
{renderAction("group")}
|
|
|
|
{renderAction("ungroup")}
|
2020-04-02 00:13:53 +09:00
|
|
|
</div>
|
|
|
|
</fieldset>
|
|
|
|
)}
|
2020-03-07 10:20:38 -05:00
|
|
|
</div>
|
|
|
|
);
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-10 02:20:23 -07:00
|
|
|
const LIBRARY_ICON = (
|
|
|
|
// fa-th-large
|
|
|
|
<svg viewBox="0 0 512 512">
|
|
|
|
<path d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z" />
|
|
|
|
</svg>
|
|
|
|
);
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const ShapesSwitcher = ({
|
2020-03-07 10:20:38 -05:00
|
|
|
elementType,
|
|
|
|
setAppState,
|
2020-07-10 02:20:23 -07:00
|
|
|
isLibraryOpen,
|
2020-03-07 10:20:38 -05:00
|
|
|
}: {
|
|
|
|
elementType: ExcalidrawElement["type"];
|
2020-10-16 11:53:40 +02:00
|
|
|
setAppState: React.Component<any, AppState>["setState"];
|
2020-07-10 02:20:23 -07:00
|
|
|
isLibraryOpen: boolean;
|
2020-05-20 16:21:37 +03:00
|
|
|
}) => (
|
|
|
|
<>
|
|
|
|
{SHAPES.map(({ value, icon, key }, index) => {
|
|
|
|
const label = t(`toolBar.${value}`);
|
2020-07-24 15:47:46 +02:00
|
|
|
const letter = typeof key === "string" ? key : key[0];
|
|
|
|
const letterShortcut = /[a-z]/.test(letter) ? letter : `Shift+${letter}`;
|
|
|
|
const shortcut = `${capitalizeString(letterShortcut)} ${t(
|
|
|
|
"shortcutsDialog.or",
|
|
|
|
)} ${index + 1}`;
|
2020-05-20 16:21:37 +03:00
|
|
|
return (
|
|
|
|
<ToolButton
|
2020-07-20 00:12:56 +03:00
|
|
|
className="Shape"
|
2020-05-20 16:21:37 +03:00
|
|
|
key={value}
|
|
|
|
type="radio"
|
|
|
|
icon={icon}
|
|
|
|
checked={elementType === value}
|
|
|
|
name="editor-current-shape"
|
|
|
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
|
|
|
keyBindingLabel={`${index + 1}`}
|
|
|
|
aria-label={capitalizeString(label)}
|
|
|
|
aria-keyshortcuts={`${key} ${index + 1}`}
|
|
|
|
data-testid={value}
|
|
|
|
onChange={() => {
|
|
|
|
setAppState({
|
|
|
|
elementType: value,
|
|
|
|
multiElement: null,
|
|
|
|
selectedElementIds: {},
|
|
|
|
});
|
|
|
|
setCursorForShape(value);
|
|
|
|
setAppState({});
|
|
|
|
}}
|
2020-07-10 02:20:23 -07:00
|
|
|
/>
|
2020-05-20 16:21:37 +03:00
|
|
|
);
|
|
|
|
})}
|
2020-07-10 02:20:23 -07:00
|
|
|
<ToolButton
|
2020-07-20 00:12:56 +03:00
|
|
|
className="Shape"
|
2020-07-10 02:20:23 -07:00
|
|
|
type="button"
|
|
|
|
icon={LIBRARY_ICON}
|
|
|
|
name="editor-library"
|
|
|
|
keyBindingLabel="9"
|
|
|
|
aria-keyshortcuts="9"
|
|
|
|
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
|
|
|
aria-label={capitalizeString(t("toolBar.library"))}
|
|
|
|
onClick={() => {
|
|
|
|
setAppState({ isLibraryOpen: !isLibraryOpen });
|
|
|
|
}}
|
|
|
|
/>
|
2020-05-20 16:21:37 +03:00
|
|
|
</>
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const ZoomActions = ({
|
2020-03-07 10:20:38 -05:00
|
|
|
renderAction,
|
|
|
|
zoom,
|
|
|
|
}: {
|
|
|
|
renderAction: ActionManager["renderAction"];
|
|
|
|
zoom: number;
|
2020-05-20 16:21:37 +03:00
|
|
|
}) => (
|
|
|
|
<Stack.Col gap={1}>
|
|
|
|
<Stack.Row gap={1} align="center">
|
|
|
|
{renderAction("zoomIn")}
|
|
|
|
{renderAction("zoomOut")}
|
|
|
|
{renderAction("resetZoom")}
|
|
|
|
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
|
|
|
|
</Stack.Row>
|
|
|
|
</Stack.Col>
|
|
|
|
);
|