Actions.tsx 7.0 KB

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