aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/actions/actionAlign.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/actions/actionAlign.tsx')
-rw-r--r--packages/excalidraw/actions/actionAlign.tsx255
1 files changed, 255 insertions, 0 deletions
diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx
new file mode 100644
index 0000000..53e8e61
--- /dev/null
+++ b/packages/excalidraw/actions/actionAlign.tsx
@@ -0,0 +1,255 @@
+import type { Alignment } from "../align";
+import { alignElements } from "../align";
+import {
+ AlignBottomIcon,
+ AlignLeftIcon,
+ AlignRightIcon,
+ AlignTopIcon,
+ CenterHorizontallyIcon,
+ CenterVerticallyIcon,
+} from "../components/icons";
+import { ToolButton } from "../components/ToolButton";
+import { getNonDeletedElements } from "../element";
+import { isFrameLikeElement } from "../element/typeChecks";
+import type { ExcalidrawElement } from "../element/types";
+import { updateFrameMembershipOfSelectedElements } from "../frame";
+import { t } from "../i18n";
+import { KEYS } from "../keys";
+import { isSomeElementSelected } from "../scene";
+import { CaptureUpdateAction } from "../store";
+import type { AppClassProperties, AppState, UIAppState } from "../types";
+import { arrayToMap, getShortcutKey } from "../utils";
+import { register } from "./register";
+
+export const alignActionsPredicate = (
+ appState: UIAppState,
+ app: AppClassProperties,
+) => {
+ const selectedElements = app.scene.getSelectedElements(appState);
+ return (
+ selectedElements.length > 1 &&
+ // TODO enable aligning frames when implemented properly
+ !selectedElements.some((el) => isFrameLikeElement(el))
+ );
+};
+
+const alignSelectedElements = (
+ elements: readonly ExcalidrawElement[],
+ appState: Readonly<AppState>,
+ app: AppClassProperties,
+ alignment: Alignment,
+) => {
+ const selectedElements = app.scene.getSelectedElements(appState);
+ const elementsMap = arrayToMap(elements);
+
+ const updatedElements = alignElements(
+ selectedElements,
+ elementsMap,
+ alignment,
+ app.scene,
+ );
+
+ const updatedElementsMap = arrayToMap(updatedElements);
+
+ return updateFrameMembershipOfSelectedElements(
+ elements.map((element) => updatedElementsMap.get(element.id) || element),
+ appState,
+ app,
+ );
+};
+
+export const actionAlignTop = register({
+ name: "alignTop",
+ label: "labels.alignTop",
+ icon: AlignTopIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "start",
+ axis: "y",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={AlignTopIcon}
+ onClick={() => updateData(null)}
+ title={`${t("labels.alignTop")} — ${getShortcutKey(
+ "CtrlOrCmd+Shift+Up",
+ )}`}
+ aria-label={t("labels.alignTop")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});
+
+export const actionAlignBottom = register({
+ name: "alignBottom",
+ label: "labels.alignBottom",
+ icon: AlignBottomIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "end",
+ axis: "y",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={AlignBottomIcon}
+ onClick={() => updateData(null)}
+ title={`${t("labels.alignBottom")} — ${getShortcutKey(
+ "CtrlOrCmd+Shift+Down",
+ )}`}
+ aria-label={t("labels.alignBottom")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});
+
+export const actionAlignLeft = register({
+ name: "alignLeft",
+ label: "labels.alignLeft",
+ icon: AlignLeftIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "start",
+ axis: "x",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={AlignLeftIcon}
+ onClick={() => updateData(null)}
+ title={`${t("labels.alignLeft")} — ${getShortcutKey(
+ "CtrlOrCmd+Shift+Left",
+ )}`}
+ aria-label={t("labels.alignLeft")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});
+
+export const actionAlignRight = register({
+ name: "alignRight",
+ label: "labels.alignRight",
+ icon: AlignRightIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "end",
+ axis: "x",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ keyTest: (event) =>
+ event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={AlignRightIcon}
+ onClick={() => updateData(null)}
+ title={`${t("labels.alignRight")} — ${getShortcutKey(
+ "CtrlOrCmd+Shift+Right",
+ )}`}
+ aria-label={t("labels.alignRight")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});
+
+export const actionAlignVerticallyCentered = register({
+ name: "alignVerticallyCentered",
+ label: "labels.centerVertically",
+ icon: CenterVerticallyIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "center",
+ axis: "y",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={CenterVerticallyIcon}
+ onClick={() => updateData(null)}
+ title={t("labels.centerVertically")}
+ aria-label={t("labels.centerVertically")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});
+
+export const actionAlignHorizontallyCentered = register({
+ name: "alignHorizontallyCentered",
+ label: "labels.centerHorizontally",
+ icon: CenterHorizontallyIcon,
+ trackEvent: { category: "element" },
+ predicate: (elements, appState, appProps, app) =>
+ alignActionsPredicate(appState, app),
+ perform: (elements, appState, _, app) => {
+ return {
+ appState,
+ elements: alignSelectedElements(elements, appState, app, {
+ position: "center",
+ axis: "x",
+ }),
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ },
+ PanelComponent: ({ elements, appState, updateData, app }) => (
+ <ToolButton
+ hidden={!alignActionsPredicate(appState, app)}
+ type="button"
+ icon={CenterHorizontallyIcon}
+ onClick={() => updateData(null)}
+ title={t("labels.centerHorizontally")}
+ aria-label={t("labels.centerHorizontally")}
+ visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+ />
+ ),
+});