Actions.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import React from "react";
  2. import { ActionManager } from "../actions/manager";
  3. import { getNonDeletedElements } from "../element";
  4. import { ExcalidrawElement, PointerType } from "../element/types";
  5. import { t } from "../i18n";
  6. import { useIsMobile } from "../components/App";
  7. import {
  8. canChangeSharpness,
  9. canHaveArrowheads,
  10. getTargetElements,
  11. hasBackground,
  12. hasStrokeStyle,
  13. hasStrokeWidth,
  14. hasText,
  15. } from "../scene";
  16. import { SHAPES } from "../shapes";
  17. import { AppState, Zoom } from "../types";
  18. import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
  19. import Stack from "./Stack";
  20. import { ToolButton } from "./ToolButton";
  21. import { hasStrokeColor } from "../scene/comparisons";
  22. import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
  23. export const SelectedShapeActions = ({
  24. appState,
  25. elements,
  26. renderAction,
  27. elementType,
  28. }: {
  29. appState: AppState;
  30. elements: readonly ExcalidrawElement[];
  31. renderAction: ActionManager["renderAction"];
  32. elementType: AppState["elementType"];
  33. }) => {
  34. const targetElements = getTargetElements(
  35. getNonDeletedElements(elements),
  36. appState,
  37. );
  38. let isSingleElementBoundContainer = false;
  39. if (
  40. targetElements.length === 2 &&
  41. (hasBoundTextElement(targetElements[0]) ||
  42. hasBoundTextElement(targetElements[1]))
  43. ) {
  44. isSingleElementBoundContainer = true;
  45. }
  46. const isEditing = Boolean(appState.editingElement);
  47. const isMobile = useIsMobile();
  48. const isRTL = document.documentElement.getAttribute("dir") === "rtl";
  49. const showFillIcons =
  50. hasBackground(elementType) ||
  51. targetElements.some(
  52. (element) =>
  53. hasBackground(element.type) && !isTransparent(element.backgroundColor),
  54. );
  55. const showChangeBackgroundIcons =
  56. hasBackground(elementType) ||
  57. targetElements.some((element) => hasBackground(element.type));
  58. let commonSelectedType: string | null = targetElements[0]?.type || null;
  59. for (const element of targetElements) {
  60. if (element.type !== commonSelectedType) {
  61. commonSelectedType = null;
  62. break;
  63. }
  64. }
  65. return (
  66. <div className="panelColumn">
  67. {((hasStrokeColor(elementType) &&
  68. elementType !== "image" &&
  69. commonSelectedType !== "image") ||
  70. targetElements.some((element) => hasStrokeColor(element.type))) &&
  71. renderAction("changeStrokeColor")}
  72. {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
  73. {showFillIcons && renderAction("changeFillStyle")}
  74. {(hasStrokeWidth(elementType) ||
  75. targetElements.some((element) => hasStrokeWidth(element.type))) &&
  76. renderAction("changeStrokeWidth")}
  77. {(elementType === "freedraw" ||
  78. targetElements.some((element) => element.type === "freedraw")) &&
  79. renderAction("changeStrokeShape")}
  80. {(hasStrokeStyle(elementType) ||
  81. targetElements.some((element) => hasStrokeStyle(element.type))) && (
  82. <>
  83. {renderAction("changeStrokeStyle")}
  84. {renderAction("changeSloppiness")}
  85. </>
  86. )}
  87. {(canChangeSharpness(elementType) ||
  88. targetElements.some((element) => canChangeSharpness(element.type))) && (
  89. <>{renderAction("changeSharpness")}</>
  90. )}
  91. {(hasText(elementType) ||
  92. targetElements.some((element) => hasText(element.type))) && (
  93. <>
  94. {renderAction("changeFontSize")}
  95. {renderAction("changeFontFamily")}
  96. {renderAction("changeTextAlign")}
  97. </>
  98. )}
  99. {targetElements.some(
  100. (element) =>
  101. hasBoundTextElement(element) || isBoundToContainer(element),
  102. ) && renderAction("changeVerticalAlign")}
  103. {(canHaveArrowheads(elementType) ||
  104. targetElements.some((element) => canHaveArrowheads(element.type))) && (
  105. <>{renderAction("changeArrowhead")}</>
  106. )}
  107. {renderAction("changeOpacity")}
  108. <fieldset>
  109. <legend>{t("labels.layers")}</legend>
  110. <div className="buttonList">
  111. {renderAction("sendToBack")}
  112. {renderAction("sendBackward")}
  113. {renderAction("bringToFront")}
  114. {renderAction("bringForward")}
  115. </div>
  116. </fieldset>
  117. {targetElements.length > 1 && !isSingleElementBoundContainer && (
  118. <fieldset>
  119. <legend>{t("labels.align")}</legend>
  120. <div className="buttonList">
  121. {
  122. // swap this order for RTL so the button positions always match their action
  123. // (i.e. the leftmost button aligns left)
  124. }
  125. {isRTL ? (
  126. <>
  127. {renderAction("alignRight")}
  128. {renderAction("alignHorizontallyCentered")}
  129. {renderAction("alignLeft")}
  130. </>
  131. ) : (
  132. <>
  133. {renderAction("alignLeft")}
  134. {renderAction("alignHorizontallyCentered")}
  135. {renderAction("alignRight")}
  136. </>
  137. )}
  138. {targetElements.length > 2 &&
  139. renderAction("distributeHorizontally")}
  140. <div className="iconRow">
  141. {renderAction("alignTop")}
  142. {renderAction("alignVerticallyCentered")}
  143. {renderAction("alignBottom")}
  144. {targetElements.length > 2 &&
  145. renderAction("distributeVertically")}
  146. </div>
  147. </div>
  148. </fieldset>
  149. )}
  150. {!isEditing && targetElements.length > 0 && (
  151. <fieldset>
  152. <legend>{t("labels.actions")}</legend>
  153. <div className="buttonList">
  154. {!isMobile && renderAction("duplicateSelection")}
  155. {!isMobile && renderAction("deleteSelectedElements")}
  156. {renderAction("group")}
  157. {renderAction("ungroup")}
  158. {targetElements.length === 1 && renderAction("hyperlink")}
  159. </div>
  160. </fieldset>
  161. )}
  162. </div>
  163. );
  164. };
  165. export const ShapesSwitcher = ({
  166. canvas,
  167. elementType,
  168. setAppState,
  169. onImageAction,
  170. }: {
  171. canvas: HTMLCanvasElement | null;
  172. elementType: AppState["elementType"];
  173. setAppState: React.Component<any, AppState>["setState"];
  174. onImageAction: (data: { pointerType: PointerType | null }) => void;
  175. }) => (
  176. <>
  177. {SHAPES.map(({ value, icon, key }, index) => {
  178. const label = t(`toolBar.${value}`);
  179. const letter = key && (typeof key === "string" ? key : key[0]);
  180. const shortcut = letter
  181. ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
  182. : `${index + 1}`;
  183. return (
  184. <ToolButton
  185. className="Shape"
  186. key={value}
  187. type="radio"
  188. icon={icon}
  189. checked={elementType === value}
  190. name="editor-current-shape"
  191. title={`${capitalizeString(label)} — ${shortcut}`}
  192. keyBindingLabel={`${index + 1}`}
  193. aria-label={capitalizeString(label)}
  194. aria-keyshortcuts={shortcut}
  195. data-testid={value}
  196. onChange={({ pointerType }) => {
  197. setAppState({
  198. elementType: value,
  199. multiElement: null,
  200. selectedElementIds: {},
  201. });
  202. setCursorForShape(canvas, value);
  203. if (value === "image") {
  204. onImageAction({ pointerType });
  205. }
  206. }}
  207. />
  208. );
  209. })}
  210. </>
  211. );
  212. export const ZoomActions = ({
  213. renderAction,
  214. zoom,
  215. }: {
  216. renderAction: ActionManager["renderAction"];
  217. zoom: Zoom;
  218. }) => (
  219. <Stack.Col gap={1}>
  220. <Stack.Row gap={1} align="center">
  221. {renderAction("zoomOut")}
  222. {renderAction("zoomIn")}
  223. {renderAction("resetZoom")}
  224. </Stack.Row>
  225. </Stack.Col>
  226. );