2020-03-07 10:20:38 -05:00
|
|
|
import React from "react";
|
2020-11-04 17:49:15 +00:00
|
|
|
import { AppState, Zoom } 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,
|
2020-12-07 18:35:16 +02:00
|
|
|
getTargetElements,
|
2020-08-15 00:59:43 +09:00
|
|
|
} from "../scene";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { t } from "../i18n";
|
|
|
|
import { SHAPES } from "../shapes";
|
|
|
|
import { ToolButton } from "./ToolButton";
|
2020-11-27 02:13:38 +05:30
|
|
|
import { capitalizeString, isTransparent, 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-12-04 19:18:20 +02:00
|
|
|
import { trackEvent, EVENT_SHAPE, EVENT_DIALOG } from "../analytics";
|
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-12-07 18:35:16 +02:00
|
|
|
const targetElements = getTargetElements(
|
2020-04-08 09:49:52 -07:00
|
|
|
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-11-25 18:21:33 -05:00
|
|
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
|
|
|
|
2020-11-27 02:13:38 +05:30
|
|
|
const showFillIcons =
|
|
|
|
hasBackground(elementType) ||
|
|
|
|
targetElements.some(
|
|
|
|
(element) =>
|
|
|
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
|
|
|
);
|
|
|
|
const showChangeBackgroundIcons =
|
|
|
|
hasBackground(elementType) ||
|
|
|
|
targetElements.some((element) => hasBackground(element.type));
|
2020-03-07 10:20:38 -05:00
|
|
|
return (
|
|
|
|
<div className="panelColumn">
|
|
|
|
{renderAction("changeStrokeColor")}
|
2020-11-27 02:13:38 +05:30
|
|
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
|
|
|
{showFillIcons && renderAction("changeFillStyle")}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
{(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-10-31 11:40:06 +01:00
|
|
|
|
|
|
|
{targetElements.length > 1 && (
|
|
|
|
<fieldset>
|
|
|
|
<legend>{t("labels.align")}</legend>
|
|
|
|
<div className="buttonList">
|
2020-11-25 18:21:33 -05:00
|
|
|
{
|
|
|
|
// swap this order for RTL so the button positions always match their action
|
|
|
|
// (i.e. the leftmost button aligns left)
|
|
|
|
}
|
|
|
|
{isRTL ? (
|
|
|
|
<>
|
|
|
|
{renderAction("alignRight")}
|
|
|
|
{renderAction("alignHorizontallyCentered")}
|
|
|
|
{renderAction("alignLeft")}
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
{renderAction("alignLeft")}
|
|
|
|
{renderAction("alignHorizontallyCentered")}
|
|
|
|
{renderAction("alignRight")}
|
|
|
|
</>
|
|
|
|
)}
|
2020-11-23 18:16:23 +00:00
|
|
|
{targetElements.length > 2 &&
|
|
|
|
renderAction("distributeHorizontally")}
|
|
|
|
<div className="iconRow">
|
|
|
|
{renderAction("alignTop")}
|
|
|
|
{renderAction("alignVerticallyCentered")}
|
|
|
|
{renderAction("alignBottom")}
|
|
|
|
{targetElements.length > 2 &&
|
|
|
|
renderAction("distributeVertically")}
|
|
|
|
</div>
|
2020-10-31 11:40:06 +01:00
|
|
|
</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];
|
2020-12-01 23:36:06 +02:00
|
|
|
const shortcut = `${capitalizeString(letter)} ${t(
|
2020-07-24 15:47:46 +02:00
|
|
|
"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)}
|
2020-12-01 23:36:06 +02:00
|
|
|
aria-keyshortcuts={shortcut}
|
2020-05-20 16:21:37 +03:00
|
|
|
data-testid={value}
|
|
|
|
onChange={() => {
|
2020-12-02 23:57:51 +02:00
|
|
|
trackEvent(EVENT_SHAPE, value, "toolbar");
|
2020-05-20 16:21:37 +03:00
|
|
|
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-11-27 03:02:40 +08:00
|
|
|
className="Shape ToolIcon_type_button__library"
|
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={() => {
|
2020-12-04 19:18:20 +02:00
|
|
|
if (!isLibraryOpen) {
|
|
|
|
trackEvent(EVENT_DIALOG, "library");
|
|
|
|
}
|
2020-07-10 02:20:23 -07:00
|
|
|
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"];
|
2020-11-04 17:49:15 +00:00
|
|
|
zoom: Zoom;
|
2020-05-20 16:21:37 +03:00
|
|
|
}) => (
|
|
|
|
<Stack.Col gap={1}>
|
|
|
|
<Stack.Row gap={1} align="center">
|
|
|
|
{renderAction("zoomIn")}
|
|
|
|
{renderAction("zoomOut")}
|
|
|
|
{renderAction("resetZoom")}
|
2020-11-04 17:49:15 +00:00
|
|
|
<div style={{ marginInlineStart: 4 }}>
|
|
|
|
{(zoom.value * 100).toFixed(0)}%
|
|
|
|
</div>
|
2020-05-20 16:21:37 +03:00
|
|
|
</Stack.Row>
|
|
|
|
</Stack.Col>
|
|
|
|
);
|