Actions.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. return (
  37. <div className="panelColumn">
  38. {renderAction("changeStrokeColor")}
  39. {(hasBackground(elementType) ||
  40. targetElements.some((element) => hasBackground(element.type))) && (
  41. <>
  42. {renderAction("changeBackgroundColor")}
  43. {renderAction("changeFillStyle")}
  44. </>
  45. )}
  46. {(hasStroke(elementType) ||
  47. targetElements.some((element) => hasStroke(element.type))) && (
  48. <>
  49. {renderAction("changeStrokeWidth")}
  50. {renderAction("changeStrokeStyle")}
  51. {renderAction("changeSloppiness")}
  52. </>
  53. )}
  54. {(canChangeSharpness(elementType) ||
  55. targetElements.some((element) => canChangeSharpness(element.type))) && (
  56. <>{renderAction("changeSharpness")}</>
  57. )}
  58. {(hasText(elementType) ||
  59. targetElements.some((element) => hasText(element.type))) && (
  60. <>
  61. {renderAction("changeFontSize")}
  62. {renderAction("changeFontFamily")}
  63. {renderAction("changeTextAlign")}
  64. </>
  65. )}
  66. {renderAction("changeOpacity")}
  67. <fieldset>
  68. <legend>{t("labels.layers")}</legend>
  69. <div className="buttonList">
  70. {renderAction("sendToBack")}
  71. {renderAction("sendBackward")}
  72. {renderAction("bringToFront")}
  73. {renderAction("bringForward")}
  74. </div>
  75. </fieldset>
  76. {targetElements.length > 1 && (
  77. <fieldset>
  78. <legend>{t("labels.align")}</legend>
  79. <div className="buttonList">
  80. {renderAction("alignLeft")}
  81. {renderAction("alignHorizontallyCentered")}
  82. {renderAction("alignRight")}
  83. {renderAction("alignTop")}
  84. {renderAction("alignVerticallyCentered")}
  85. {renderAction("alignBottom")}
  86. </div>
  87. </fieldset>
  88. )}
  89. {!isMobile && !isEditing && targetElements.length > 0 && (
  90. <fieldset>
  91. <legend>{t("labels.actions")}</legend>
  92. <div className="buttonList">
  93. {renderAction("duplicateSelection")}
  94. {renderAction("deleteSelectedElements")}
  95. {renderAction("group")}
  96. {renderAction("ungroup")}
  97. </div>
  98. </fieldset>
  99. )}
  100. </div>
  101. );
  102. };
  103. const LIBRARY_ICON = (
  104. // fa-th-large
  105. <svg viewBox="0 0 512 512">
  106. <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" />
  107. </svg>
  108. );
  109. export const ShapesSwitcher = ({
  110. elementType,
  111. setAppState,
  112. isLibraryOpen,
  113. }: {
  114. elementType: ExcalidrawElement["type"];
  115. setAppState: React.Component<any, AppState>["setState"];
  116. isLibraryOpen: boolean;
  117. }) => (
  118. <>
  119. {SHAPES.map(({ value, icon, key }, index) => {
  120. const label = t(`toolBar.${value}`);
  121. const letter = typeof key === "string" ? key : key[0];
  122. const letterShortcut = /[a-z]/.test(letter) ? letter : `Shift+${letter}`;
  123. const shortcut = `${capitalizeString(letterShortcut)} ${t(
  124. "shortcutsDialog.or",
  125. )} ${index + 1}`;
  126. return (
  127. <ToolButton
  128. className="Shape"
  129. key={value}
  130. type="radio"
  131. icon={icon}
  132. checked={elementType === value}
  133. name="editor-current-shape"
  134. title={`${capitalizeString(label)} — ${shortcut}`}
  135. keyBindingLabel={`${index + 1}`}
  136. aria-label={capitalizeString(label)}
  137. aria-keyshortcuts={`${key} ${index + 1}`}
  138. data-testid={value}
  139. onChange={() => {
  140. setAppState({
  141. elementType: value,
  142. multiElement: null,
  143. selectedElementIds: {},
  144. });
  145. setCursorForShape(value);
  146. setAppState({});
  147. }}
  148. />
  149. );
  150. })}
  151. <ToolButton
  152. className="Shape"
  153. type="button"
  154. icon={LIBRARY_ICON}
  155. name="editor-library"
  156. keyBindingLabel="9"
  157. aria-keyshortcuts="9"
  158. title={`${capitalizeString(t("toolBar.library"))} — 9`}
  159. aria-label={capitalizeString(t("toolBar.library"))}
  160. onClick={() => {
  161. setAppState({ isLibraryOpen: !isLibraryOpen });
  162. }}
  163. />
  164. </>
  165. );
  166. export const ZoomActions = ({
  167. renderAction,
  168. zoom,
  169. }: {
  170. renderAction: ActionManager["renderAction"];
  171. zoom: Zoom;
  172. }) => (
  173. <Stack.Col gap={1}>
  174. <Stack.Row gap={1} align="center">
  175. {renderAction("zoomIn")}
  176. {renderAction("zoomOut")}
  177. {renderAction("resetZoom")}
  178. <div style={{ marginInlineStart: 4 }}>
  179. {(zoom.value * 100).toFixed(0)}%
  180. </div>
  181. </Stack.Row>
  182. </Stack.Col>
  183. );