123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import React from "react";
- import { ActionManager } from "../actions/manager";
- import { getNonDeletedElements } from "../element";
- import { ExcalidrawElement, PointerType } from "../element/types";
- import { t } from "../i18n";
- import { useIsMobile } from "../components/App";
- import {
- canChangeSharpness,
- canHaveArrowheads,
- getTargetElements,
- hasBackground,
- hasStrokeStyle,
- hasStrokeWidth,
- hasText,
- } from "../scene";
- import { SHAPES } from "../shapes";
- import { AppState, Zoom } from "../types";
- import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
- import Stack from "./Stack";
- import { ToolButton } from "./ToolButton";
- import { hasStrokeColor } from "../scene/comparisons";
- import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
- export const SelectedShapeActions = ({
- appState,
- elements,
- renderAction,
- elementType,
- }: {
- appState: AppState;
- elements: readonly ExcalidrawElement[];
- renderAction: ActionManager["renderAction"];
- elementType: AppState["elementType"];
- }) => {
- const targetElements = getTargetElements(
- getNonDeletedElements(elements),
- appState,
- );
- let isSingleElementBoundContainer = false;
- if (
- targetElements.length === 2 &&
- (hasBoundTextElement(targetElements[0]) ||
- hasBoundTextElement(targetElements[1]))
- ) {
- isSingleElementBoundContainer = true;
- }
- const isEditing = Boolean(appState.editingElement);
- const isMobile = useIsMobile();
- const isRTL = document.documentElement.getAttribute("dir") === "rtl";
- const showFillIcons =
- hasBackground(elementType) ||
- targetElements.some(
- (element) =>
- hasBackground(element.type) && !isTransparent(element.backgroundColor),
- );
- const showChangeBackgroundIcons =
- hasBackground(elementType) ||
- targetElements.some((element) => hasBackground(element.type));
- let commonSelectedType: string | null = targetElements[0]?.type || null;
- for (const element of targetElements) {
- if (element.type !== commonSelectedType) {
- commonSelectedType = null;
- break;
- }
- }
- return (
- <div className="panelColumn">
- {((hasStrokeColor(elementType) &&
- elementType !== "image" &&
- commonSelectedType !== "image") ||
- targetElements.some((element) => hasStrokeColor(element.type))) &&
- renderAction("changeStrokeColor")}
- {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
- {showFillIcons && renderAction("changeFillStyle")}
- {(hasStrokeWidth(elementType) ||
- targetElements.some((element) => hasStrokeWidth(element.type))) &&
- renderAction("changeStrokeWidth")}
- {(elementType === "freedraw" ||
- targetElements.some((element) => element.type === "freedraw")) &&
- renderAction("changeStrokeShape")}
- {(hasStrokeStyle(elementType) ||
- targetElements.some((element) => hasStrokeStyle(element.type))) && (
- <>
- {renderAction("changeStrokeStyle")}
- {renderAction("changeSloppiness")}
- </>
- )}
- {(canChangeSharpness(elementType) ||
- targetElements.some((element) => canChangeSharpness(element.type))) && (
- <>{renderAction("changeSharpness")}</>
- )}
- {(hasText(elementType) ||
- targetElements.some((element) => hasText(element.type))) && (
- <>
- {renderAction("changeFontSize")}
- {renderAction("changeFontFamily")}
- {renderAction("changeTextAlign")}
- </>
- )}
- {targetElements.some(
- (element) =>
- hasBoundTextElement(element) || isBoundToContainer(element),
- ) && renderAction("changeVerticalAlign")}
- {(canHaveArrowheads(elementType) ||
- targetElements.some((element) => canHaveArrowheads(element.type))) && (
- <>{renderAction("changeArrowhead")}</>
- )}
- {renderAction("changeOpacity")}
- <fieldset>
- <legend>{t("labels.layers")}</legend>
- <div className="buttonList">
- {renderAction("sendToBack")}
- {renderAction("sendBackward")}
- {renderAction("bringToFront")}
- {renderAction("bringForward")}
- </div>
- </fieldset>
- {targetElements.length > 1 && !isSingleElementBoundContainer && (
- <fieldset>
- <legend>{t("labels.align")}</legend>
- <div className="buttonList">
- {
- // 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")}
- </>
- )}
- {targetElements.length > 2 &&
- renderAction("distributeHorizontally")}
- <div className="iconRow">
- {renderAction("alignTop")}
- {renderAction("alignVerticallyCentered")}
- {renderAction("alignBottom")}
- {targetElements.length > 2 &&
- renderAction("distributeVertically")}
- </div>
- </div>
- </fieldset>
- )}
- {!isEditing && targetElements.length > 0 && (
- <fieldset>
- <legend>{t("labels.actions")}</legend>
- <div className="buttonList">
- {!isMobile && renderAction("duplicateSelection")}
- {!isMobile && renderAction("deleteSelectedElements")}
- {renderAction("group")}
- {renderAction("ungroup")}
- {targetElements.length === 1 && renderAction("hyperlink")}
- </div>
- </fieldset>
- )}
- </div>
- );
- };
- export const ShapesSwitcher = ({
- canvas,
- elementType,
- setAppState,
- onImageAction,
- }: {
- canvas: HTMLCanvasElement | null;
- elementType: AppState["elementType"];
- setAppState: React.Component<any, AppState>["setState"];
- onImageAction: (data: { pointerType: PointerType | null }) => void;
- }) => (
- <>
- {SHAPES.map(({ value, icon, key }, index) => {
- const label = t(`toolBar.${value}`);
- const letter = key && (typeof key === "string" ? key : key[0]);
- const shortcut = letter
- ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
- : `${index + 1}`;
- return (
- <ToolButton
- className="Shape"
- 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={shortcut}
- data-testid={value}
- onChange={({ pointerType }) => {
- setAppState({
- elementType: value,
- multiElement: null,
- selectedElementIds: {},
- });
- setCursorForShape(canvas, value);
- if (value === "image") {
- onImageAction({ pointerType });
- }
- }}
- />
- );
- })}
- </>
- );
- export const ZoomActions = ({
- renderAction,
- zoom,
- }: {
- renderAction: ActionManager["renderAction"];
- zoom: Zoom;
- }) => (
- <Stack.Col gap={1}>
- <Stack.Row gap={1} align="center">
- {renderAction("zoomOut")}
- {renderAction("zoomIn")}
- {renderAction("resetZoom")}
- </Stack.Row>
- </Stack.Col>
- );
|