diff options
Diffstat (limited to 'packages/excalidraw/actions/actionFinalize.tsx')
| -rw-r--r-- | packages/excalidraw/actions/actionFinalize.tsx | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx new file mode 100644 index 0000000..f2d8c01 --- /dev/null +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -0,0 +1,223 @@ +import { KEYS } from "../keys"; +import { isInvisiblySmallElement } from "../element"; +import { arrayToMap, updateActiveTool } from "../utils"; +import { ToolButton } from "../components/ToolButton"; +import { done } from "../components/icons"; +import { t } from "../i18n"; +import { register } from "./register"; +import { mutateElement } from "../element/mutateElement"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { + maybeBindLinearElement, + bindOrUnbindLinearElement, +} from "../element/binding"; +import { isBindingElement, isLinearElement } from "../element/typeChecks"; +import type { AppState } from "../types"; +import { resetCursor } from "../cursor"; +import { CaptureUpdateAction } from "../store"; +import { pointFrom } from "@excalidraw/math"; +import { isPathALoop } from "../shapes"; + +export const actionFinalize = register({ + name: "finalize", + label: "", + trackEvent: false, + perform: (elements, appState, _, app) => { + const { interactiveCanvas, focusContainer, scene } = app; + + const elementsMap = scene.getNonDeletedElementsMap(); + + if (appState.editingLinearElement) { + const { elementId, startBindingElement, endBindingElement } = + appState.editingLinearElement; + const element = LinearElementEditor.getElement(elementId, elementsMap); + + if (element) { + if (isBindingElement(element)) { + bindOrUnbindLinearElement( + element, + startBindingElement, + endBindingElement, + elementsMap, + scene, + ); + } + return { + elements: + element.points.length < 2 || isInvisiblySmallElement(element) + ? elements.filter((el) => el.id !== element.id) + : undefined, + appState: { + ...appState, + cursorButton: "up", + editingLinearElement: null, + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + } + } + + let newElements = elements; + + const pendingImageElement = + appState.pendingImageElementId && + scene.getElement(appState.pendingImageElementId); + + if (pendingImageElement) { + mutateElement(pendingImageElement, { isDeleted: true }, false); + } + + if (window.document.activeElement instanceof HTMLElement) { + focusContainer(); + } + + const multiPointElement = appState.multiElement + ? appState.multiElement + : appState.newElement?.type === "freedraw" + ? appState.newElement + : null; + + if (multiPointElement) { + // pen and mouse have hover + if ( + multiPointElement.type !== "freedraw" && + appState.lastPointerDownWith !== "touch" + ) { + const { points, lastCommittedPoint } = multiPointElement; + if ( + !lastCommittedPoint || + points[points.length - 1] !== lastCommittedPoint + ) { + mutateElement(multiPointElement, { + points: multiPointElement.points.slice(0, -1), + }); + } + } + + if (isInvisiblySmallElement(multiPointElement)) { + // TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want + newElements = newElements.filter( + (el) => el.id !== multiPointElement.id, + ); + } + + // If the multi point line closes the loop, + // set the last point to first point. + // This ensures that loop remains closed at different scales. + const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value); + if ( + multiPointElement.type === "line" || + multiPointElement.type === "freedraw" + ) { + if (isLoop) { + const linePoints = multiPointElement.points; + const firstPoint = linePoints[0]; + mutateElement(multiPointElement, { + points: linePoints.map((p, index) => + index === linePoints.length - 1 + ? pointFrom(firstPoint[0], firstPoint[1]) + : p, + ), + }); + } + } + + if ( + isBindingElement(multiPointElement) && + !isLoop && + multiPointElement.points.length > 1 + ) { + const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( + multiPointElement, + -1, + arrayToMap(elements), + ); + maybeBindLinearElement( + multiPointElement, + appState, + { x, y }, + elementsMap, + elements, + ); + } + } + + if ( + (!appState.activeTool.locked && + appState.activeTool.type !== "freedraw") || + !multiPointElement + ) { + resetCursor(interactiveCanvas); + } + + let activeTool: AppState["activeTool"]; + if (appState.activeTool.type === "eraser") { + activeTool = updateActiveTool(appState, { + ...(appState.activeTool.lastActiveTool || { + type: "selection", + }), + lastActiveToolBeforeEraser: null, + }); + } else { + activeTool = updateActiveTool(appState, { + type: "selection", + }); + } + + return { + elements: newElements, + appState: { + ...appState, + cursorButton: "up", + activeTool: + (appState.activeTool.locked || + appState.activeTool.type === "freedraw") && + multiPointElement + ? appState.activeTool + : activeTool, + activeEmbeddable: null, + newElement: null, + selectionElement: null, + multiElement: null, + editingTextElement: null, + startBoundElement: null, + suggestedBindings: [], + selectedElementIds: + multiPointElement && + !appState.activeTool.locked && + appState.activeTool.type !== "freedraw" + ? { + ...appState.selectedElementIds, + [multiPointElement.id]: true, + } + : appState.selectedElementIds, + // To select the linear element when user has finished mutipoint editing + selectedLinearElement: + multiPointElement && isLinearElement(multiPointElement) + ? new LinearElementEditor(multiPointElement) + : appState.selectedLinearElement, + pendingImageElementId: null, + }, + // TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, + keyTest: (event, appState) => + (event.key === KEYS.ESCAPE && + (appState.editingLinearElement !== null || + (!appState.newElement && appState.multiElement === null))) || + ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && + appState.multiElement !== null), + PanelComponent: ({ appState, updateData, data }) => ( + <ToolButton + type="button" + icon={done} + title={t("buttons.done")} + aria-label={t("buttons.done")} + onClick={updateData} + visible={appState.multiElement != null} + size={data?.size || "medium"} + style={{ pointerEvents: "all" }} + /> + ), +}); |
