Actions.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import React from "react";
  2. import { AppState } 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. {!isMobile && !isEditing && targetElements.length > 0 && (
  77. <fieldset>
  78. <legend>{t("labels.actions")}</legend>
  79. <div className="buttonList">
  80. {renderAction("duplicateSelection")}
  81. {renderAction("deleteSelectedElements")}
  82. {renderAction("group")}
  83. {renderAction("ungroup")}
  84. </div>
  85. </fieldset>
  86. )}
  87. </div>
  88. );
  89. };
  90. const LIBRARY_ICON = (
  91. // fa-th-large
  92. <svg viewBox="0 0 512 512">
  93. <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" />
  94. </svg>
  95. );
  96. export const ShapesSwitcher = ({
  97. elementType,
  98. setAppState,
  99. isLibraryOpen,
  100. }: {
  101. elementType: ExcalidrawElement["type"];
  102. setAppState: (appState: Partial<AppState>) => void;
  103. isLibraryOpen: boolean;
  104. }) => (
  105. <>
  106. {SHAPES.map(({ value, icon, key }, index) => {
  107. const label = t(`toolBar.${value}`);
  108. const letter = typeof key === "string" ? key : key[0];
  109. const letterShortcut = /[a-z]/.test(letter) ? letter : `Shift+${letter}`;
  110. const shortcut = `${capitalizeString(letterShortcut)} ${t(
  111. "shortcutsDialog.or",
  112. )} ${index + 1}`;
  113. return (
  114. <ToolButton
  115. className="Shape"
  116. key={value}
  117. type="radio"
  118. icon={icon}
  119. checked={elementType === value}
  120. name="editor-current-shape"
  121. title={`${capitalizeString(label)} — ${shortcut}`}
  122. keyBindingLabel={`${index + 1}`}
  123. aria-label={capitalizeString(label)}
  124. aria-keyshortcuts={`${key} ${index + 1}`}
  125. data-testid={value}
  126. onChange={() => {
  127. setAppState({
  128. elementType: value,
  129. multiElement: null,
  130. selectedElementIds: {},
  131. });
  132. setCursorForShape(value);
  133. setAppState({});
  134. }}
  135. />
  136. );
  137. })}
  138. <ToolButton
  139. className="Shape"
  140. type="button"
  141. icon={LIBRARY_ICON}
  142. name="editor-library"
  143. keyBindingLabel="9"
  144. aria-keyshortcuts="9"
  145. title={`${capitalizeString(t("toolBar.library"))} — 9`}
  146. aria-label={capitalizeString(t("toolBar.library"))}
  147. onClick={() => {
  148. setAppState({ isLibraryOpen: !isLibraryOpen });
  149. }}
  150. />
  151. </>
  152. );
  153. export const ZoomActions = ({
  154. renderAction,
  155. zoom,
  156. }: {
  157. renderAction: ActionManager["renderAction"];
  158. zoom: number;
  159. }) => (
  160. <Stack.Col gap={1}>
  161. <Stack.Row gap={1} align="center">
  162. {renderAction("zoomIn")}
  163. {renderAction("zoomOut")}
  164. {renderAction("resetZoom")}
  165. <div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
  166. </Stack.Row>
  167. </Stack.Col>
  168. );