snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
212
packages/excalidraw/actions/actionLinearEditor.tsx
Normal file
212
packages/excalidraw/actions/actionLinearEditor.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
isElbowArrow,
|
||||
isLinearElement,
|
||||
isLineElement,
|
||||
} from "@excalidraw/element";
|
||||
import { arrayToMap, invariant } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
toggleLinePolygonState,
|
||||
CaptureUpdateAction,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawLineElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||
import { IconButton } from "../components/IconButton";
|
||||
import { lineEditorIcon, polygonIcon } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import { newElementWith } from "../../element/src/mutateElement";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleLinearEditor = register({
|
||||
name: "toggleLinearEditor",
|
||||
category: DEFAULT_CATEGORIES.elements,
|
||||
label: (elements, appState, app) => {
|
||||
const selectedElement = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
})[0] as ExcalidrawLinearElement | undefined;
|
||||
|
||||
return selectedElement?.type === "arrow"
|
||||
? "labels.lineEditor.editArrow"
|
||||
: "labels.lineEditor.edit";
|
||||
},
|
||||
keywords: ["line"],
|
||||
trackEvent: {
|
||||
category: "element",
|
||||
},
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
if (
|
||||
!appState.selectedLinearElement?.isEditing &&
|
||||
selectedElements.length === 1 &&
|
||||
isLinearElement(selectedElements[0]) &&
|
||||
!isElbowArrow(selectedElements[0])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
perform(elements, appState, _, app) {
|
||||
invariant(
|
||||
appState.selectedLinearElement,
|
||||
"No selected linear element found",
|
||||
);
|
||||
|
||||
const selectedElement = app.scene.getElement(
|
||||
appState.selectedLinearElement.elementId,
|
||||
) as ExcalidrawLinearElement | null;
|
||||
|
||||
invariant(selectedElement, "No selected element found");
|
||||
|
||||
const selectedLinearElement = {
|
||||
...appState.selectedLinearElement,
|
||||
isEditing: !appState.selectedLinearElement.isEditing,
|
||||
};
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
selectedLinearElement,
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData, app }) => {
|
||||
const selectedElement = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
})[0] as ExcalidrawLinearElement;
|
||||
|
||||
if (!selectedElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = t(
|
||||
selectedElement.type === "arrow"
|
||||
? "labels.lineEditor.editArrow"
|
||||
: "labels.lineEditor.edit",
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
type="button"
|
||||
icon={lineEditorIcon}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
onClick={() => updateData(null)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionTogglePolygon = register({
|
||||
name: "togglePolygon",
|
||||
category: DEFAULT_CATEGORIES.elements,
|
||||
icon: polygonIcon,
|
||||
keywords: ["loop"],
|
||||
label: (elements, appState, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
});
|
||||
|
||||
const allPolygons = !selectedElements.some(
|
||||
(element) => !isLineElement(element) || !element.polygon,
|
||||
);
|
||||
|
||||
return allPolygons
|
||||
? "labels.polygon.breakPolygon"
|
||||
: "labels.polygon.convertToPolygon";
|
||||
},
|
||||
trackEvent: {
|
||||
category: "element",
|
||||
},
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
});
|
||||
|
||||
return (
|
||||
selectedElements.length > 0 &&
|
||||
selectedElements.every(
|
||||
(element) => isLineElement(element) && element.points.length >= 4,
|
||||
)
|
||||
);
|
||||
},
|
||||
perform(elements, appState, _, app) {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
||||
if (selectedElements.some((element) => !isLineElement(element))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetElements = selectedElements as ExcalidrawLineElement[];
|
||||
|
||||
// if one element not a polygon, convert all to polygon
|
||||
const nextPolygonState = targetElements.some((element) => !element.polygon);
|
||||
|
||||
const targetElementsMap = arrayToMap(targetElements);
|
||||
|
||||
return {
|
||||
elements: elements.map((element) => {
|
||||
if (!targetElementsMap.has(element.id) || !isLineElement(element)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return newElementWith(element, {
|
||||
backgroundColor: nextPolygonState
|
||||
? element.backgroundColor
|
||||
: "transparent",
|
||||
...toggleLinePolygonState(element, nextPolygonState),
|
||||
});
|
||||
}),
|
||||
appState,
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData, app }) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
});
|
||||
|
||||
if (
|
||||
selectedElements.length === 0 ||
|
||||
selectedElements.some(
|
||||
(element) =>
|
||||
!isLineElement(element) ||
|
||||
// only show polygon button if every selected element is already
|
||||
// a polygon, effectively showing this button only to allow for
|
||||
// disabling the polygon state
|
||||
!element.polygon ||
|
||||
element.points.length < 3,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allPolygon = selectedElements.every(
|
||||
(element) => isLineElement(element) && element.polygon,
|
||||
);
|
||||
|
||||
const label = t(
|
||||
allPolygon
|
||||
? "labels.polygon.breakPolygon"
|
||||
: "labels.polygon.convertToPolygon",
|
||||
);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
type="toggle"
|
||||
icon={polygonIcon}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
checked={allPolygon}
|
||||
onSelect={() => updateData(null)}
|
||||
style={{ marginLeft: "auto" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user