From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/actions/actionFrame.ts | 214 +++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 packages/excalidraw/actions/actionFrame.ts (limited to 'packages/excalidraw/actions/actionFrame.ts') diff --git a/packages/excalidraw/actions/actionFrame.ts b/packages/excalidraw/actions/actionFrame.ts new file mode 100644 index 0000000..ce05b53 --- /dev/null +++ b/packages/excalidraw/actions/actionFrame.ts @@ -0,0 +1,214 @@ +import { getCommonBounds, getNonDeletedElements } from "../element"; +import type { ExcalidrawElement } from "../element/types"; +import { addElementsToFrame, removeAllElementsFromFrame } from "../frame"; +import { getFrameChildren } from "../frame"; +import { KEYS } from "../keys"; +import type { AppClassProperties, AppState, UIAppState } from "../types"; +import { updateActiveTool } from "../utils"; +import { setCursorForShape } from "../cursor"; +import { register } from "./register"; +import { isFrameLikeElement } from "../element/typeChecks"; +import { frameToolIcon } from "../components/icons"; +import { CaptureUpdateAction } from "../store"; +import { getSelectedElements } from "../scene"; +import { newFrameElement } from "../element/newElement"; +import { getElementsInGroup } from "../groups"; +import { mutateElement } from "../element/mutateElement"; + +const isSingleFrameSelected = ( + appState: UIAppState, + app: AppClassProperties, +) => { + const selectedElements = app.scene.getSelectedElements(appState); + + return ( + selectedElements.length === 1 && isFrameLikeElement(selectedElements[0]) + ); +}; + +export const actionSelectAllElementsInFrame = register({ + name: "selectAllElementsInFrame", + label: "labels.selectAllElementsInFrame", + trackEvent: { category: "canvas" }, + perform: (elements, appState, _, app) => { + const selectedElement = + app.scene.getSelectedElements(appState).at(0) || null; + + if (isFrameLikeElement(selectedElement)) { + const elementsInFrame = getFrameChildren( + getNonDeletedElements(elements), + selectedElement.id, + ).filter((element) => !(element.type === "text" && element.containerId)); + + return { + elements, + appState: { + ...appState, + selectedElementIds: elementsInFrame.reduce((acc, element) => { + acc[element.id] = true; + return acc; + }, {} as Record), + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + } + + return { + elements, + appState, + captureUpdate: CaptureUpdateAction.EVENTUALLY, + }; + }, + predicate: (elements, appState, _, app) => + isSingleFrameSelected(appState, app), +}); + +export const actionRemoveAllElementsFromFrame = register({ + name: "removeAllElementsFromFrame", + label: "labels.removeAllElementsFromFrame", + trackEvent: { category: "history" }, + perform: (elements, appState, _, app) => { + const selectedElement = + app.scene.getSelectedElements(appState).at(0) || null; + + if (isFrameLikeElement(selectedElement)) { + return { + elements: removeAllElementsFromFrame(elements, selectedElement), + appState: { + ...appState, + selectedElementIds: { + [selectedElement.id]: true, + }, + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + } + + return { + elements, + appState, + captureUpdate: CaptureUpdateAction.EVENTUALLY, + }; + }, + predicate: (elements, appState, _, app) => + isSingleFrameSelected(appState, app), +}); + +export const actionupdateFrameRendering = register({ + name: "updateFrameRendering", + label: "labels.updateFrameRendering", + viewMode: true, + trackEvent: { category: "canvas" }, + perform: (elements, appState) => { + return { + elements, + appState: { + ...appState, + frameRendering: { + ...appState.frameRendering, + enabled: !appState.frameRendering.enabled, + }, + }, + captureUpdate: CaptureUpdateAction.EVENTUALLY, + }; + }, + checked: (appState: AppState) => appState.frameRendering.enabled, +}); + +export const actionSetFrameAsActiveTool = register({ + name: "setFrameAsActiveTool", + label: "toolBar.frame", + trackEvent: { category: "toolbar" }, + icon: frameToolIcon, + viewMode: false, + perform: (elements, appState, _, app) => { + const nextActiveTool = updateActiveTool(appState, { + type: "frame", + }); + + setCursorForShape(app.interactiveCanvas, { + ...appState, + activeTool: nextActiveTool, + }); + + return { + elements, + appState: { + ...appState, + activeTool: updateActiveTool(appState, { + type: "frame", + }), + }, + captureUpdate: CaptureUpdateAction.EVENTUALLY, + }; + }, + keyTest: (event) => + !event[KEYS.CTRL_OR_CMD] && + !event.shiftKey && + !event.altKey && + event.key.toLocaleLowerCase() === KEYS.F, +}); + +export const actionWrapSelectionInFrame = register({ + name: "wrapSelectionInFrame", + label: "labels.wrapSelectionInFrame", + trackEvent: { category: "element" }, + predicate: (elements, appState, _, app) => { + const selectedElements = getSelectedElements(elements, appState); + + return ( + selectedElements.length > 0 && + !selectedElements.some((element) => isFrameLikeElement(element)) + ); + }, + perform: (elements, appState, _, app) => { + const selectedElements = getSelectedElements(elements, appState); + + const [x1, y1, x2, y2] = getCommonBounds( + selectedElements, + app.scene.getNonDeletedElementsMap(), + ); + const PADDING = 16; + const frame = newFrameElement({ + x: x1 - PADDING, + y: y1 - PADDING, + width: x2 - x1 + PADDING * 2, + height: y2 - y1 + PADDING * 2, + }); + + // for a selected partial group, we want to remove it from the remainder of the group + if (appState.editingGroupId) { + const elementsInGroup = getElementsInGroup( + selectedElements, + appState.editingGroupId, + ); + + for (const elementInGroup of elementsInGroup) { + const index = elementInGroup.groupIds.indexOf(appState.editingGroupId); + + mutateElement( + elementInGroup, + { + groupIds: elementInGroup.groupIds.slice(0, index), + }, + false, + ); + } + } + + const nextElements = addElementsToFrame( + [...app.scene.getElementsIncludingDeleted(), frame], + selectedElements, + frame, + appState, + ); + + return { + elements: nextElements, + appState: { + selectedElementIds: { [frame.id]: true }, + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, +}); -- cgit v1.2.3