actionAlign.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import React from "react";
  2. import { KEYS } from "../keys";
  3. import { t } from "../i18n";
  4. import { register } from "./register";
  5. import {
  6. AlignBottomIcon,
  7. AlignLeftIcon,
  8. AlignRightIcon,
  9. AlignTopIcon,
  10. CenterHorizontallyIcon,
  11. CenterVerticallyIcon,
  12. } from "../components/icons";
  13. import { getSelectedElements, isSomeElementSelected } from "../scene";
  14. import { getElementMap, getNonDeletedElements } from "../element";
  15. import { ToolButton } from "../components/ToolButton";
  16. import { ExcalidrawElement } from "../element/types";
  17. import { AppState } from "../types";
  18. import { alignElements, Alignment } from "../align";
  19. import { getShortcutKey } from "../utils";
  20. import { trackEvent, EVENT_ALIGN } from "../analytics";
  21. const enableActionGroup = (
  22. elements: readonly ExcalidrawElement[],
  23. appState: AppState,
  24. ) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
  25. const alignSelectedElements = (
  26. elements: readonly ExcalidrawElement[],
  27. appState: Readonly<AppState>,
  28. alignment: Alignment,
  29. ) => {
  30. const selectedElements = getSelectedElements(
  31. getNonDeletedElements(elements),
  32. appState,
  33. );
  34. const updatedElements = alignElements(selectedElements, alignment);
  35. const updatedElementsMap = getElementMap(updatedElements);
  36. return elements.map((element) => updatedElementsMap[element.id] || element);
  37. };
  38. export const actionAlignTop = register({
  39. name: "alignTop",
  40. perform: (elements, appState) => {
  41. trackEvent(EVENT_ALIGN, "align", "top");
  42. return {
  43. appState,
  44. elements: alignSelectedElements(elements, appState, {
  45. position: "start",
  46. axis: "y",
  47. }),
  48. commitToHistory: true,
  49. };
  50. },
  51. keyTest: (event) =>
  52. event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
  53. PanelComponent: ({ elements, appState, updateData }) => (
  54. <ToolButton
  55. hidden={!enableActionGroup(elements, appState)}
  56. type="button"
  57. icon={<AlignTopIcon appearance={appState.appearance} />}
  58. onClick={() => updateData(null)}
  59. title={`${t("labels.alignTop")} — ${getShortcutKey(
  60. "CtrlOrCmd+Shift+Up",
  61. )}`}
  62. aria-label={t("labels.alignTop")}
  63. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  64. />
  65. ),
  66. });
  67. export const actionAlignBottom = register({
  68. name: "alignBottom",
  69. perform: (elements, appState) => {
  70. trackEvent(EVENT_ALIGN, "align", "bottom");
  71. return {
  72. appState,
  73. elements: alignSelectedElements(elements, appState, {
  74. position: "end",
  75. axis: "y",
  76. }),
  77. commitToHistory: true,
  78. };
  79. },
  80. keyTest: (event) =>
  81. event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
  82. PanelComponent: ({ elements, appState, updateData }) => (
  83. <ToolButton
  84. hidden={!enableActionGroup(elements, appState)}
  85. type="button"
  86. icon={<AlignBottomIcon appearance={appState.appearance} />}
  87. onClick={() => updateData(null)}
  88. title={`${t("labels.alignBottom")} — ${getShortcutKey(
  89. "CtrlOrCmd+Shift+Down",
  90. )}`}
  91. aria-label={t("labels.alignBottom")}
  92. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  93. />
  94. ),
  95. });
  96. export const actionAlignLeft = register({
  97. name: "alignLeft",
  98. perform: (elements, appState) => {
  99. trackEvent(EVENT_ALIGN, "align", "left");
  100. return {
  101. appState,
  102. elements: alignSelectedElements(elements, appState, {
  103. position: "start",
  104. axis: "x",
  105. }),
  106. commitToHistory: true,
  107. };
  108. },
  109. keyTest: (event) =>
  110. event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
  111. PanelComponent: ({ elements, appState, updateData }) => (
  112. <ToolButton
  113. hidden={!enableActionGroup(elements, appState)}
  114. type="button"
  115. icon={<AlignLeftIcon appearance={appState.appearance} />}
  116. onClick={() => updateData(null)}
  117. title={`${t("labels.alignLeft")} — ${getShortcutKey(
  118. "CtrlOrCmd+Shift+Left",
  119. )}`}
  120. aria-label={t("labels.alignLeft")}
  121. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  122. />
  123. ),
  124. });
  125. export const actionAlignRight = register({
  126. name: "alignRight",
  127. perform: (elements, appState) => {
  128. trackEvent(EVENT_ALIGN, "align", "right");
  129. return {
  130. appState,
  131. elements: alignSelectedElements(elements, appState, {
  132. position: "end",
  133. axis: "x",
  134. }),
  135. commitToHistory: true,
  136. };
  137. },
  138. keyTest: (event) =>
  139. event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
  140. PanelComponent: ({ elements, appState, updateData }) => (
  141. <ToolButton
  142. hidden={!enableActionGroup(elements, appState)}
  143. type="button"
  144. icon={<AlignRightIcon appearance={appState.appearance} />}
  145. onClick={() => updateData(null)}
  146. title={`${t("labels.alignRight")} — ${getShortcutKey(
  147. "CtrlOrCmd+Shift+Right",
  148. )}`}
  149. aria-label={t("labels.alignRight")}
  150. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  151. />
  152. ),
  153. });
  154. export const actionAlignVerticallyCentered = register({
  155. name: "alignVerticallyCentered",
  156. perform: (elements, appState) => {
  157. trackEvent(EVENT_ALIGN, "vertically", "center");
  158. return {
  159. appState,
  160. elements: alignSelectedElements(elements, appState, {
  161. position: "center",
  162. axis: "y",
  163. }),
  164. commitToHistory: true,
  165. };
  166. },
  167. PanelComponent: ({ elements, appState, updateData }) => (
  168. <ToolButton
  169. hidden={!enableActionGroup(elements, appState)}
  170. type="button"
  171. icon={<CenterVerticallyIcon appearance={appState.appearance} />}
  172. onClick={() => updateData(null)}
  173. title={t("labels.centerVertically")}
  174. aria-label={t("labels.centerVertically")}
  175. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  176. />
  177. ),
  178. });
  179. export const actionAlignHorizontallyCentered = register({
  180. name: "alignHorizontallyCentered",
  181. perform: (elements, appState) => {
  182. trackEvent(EVENT_ALIGN, "horizontally", "center");
  183. return {
  184. appState,
  185. elements: alignSelectedElements(elements, appState, {
  186. position: "center",
  187. axis: "x",
  188. }),
  189. commitToHistory: true,
  190. };
  191. },
  192. PanelComponent: ({ elements, appState, updateData }) => (
  193. <ToolButton
  194. hidden={!enableActionGroup(elements, appState)}
  195. type="button"
  196. icon={<CenterHorizontallyIcon appearance={appState.appearance} />}
  197. onClick={() => updateData(null)}
  198. title={t("labels.centerHorizontally")}
  199. aria-label={t("labels.centerHorizontally")}
  200. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  201. />
  202. ),
  203. });