Actions.tsx 6.6 KB

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