diff --git a/src/actions/actionAlign.tsx b/src/actions/actionAlign.tsx index 5697a707e2dd6..137f68ae9f25e 100644 --- a/src/actions/actionAlign.tsx +++ b/src/actions/actionAlign.tsx @@ -9,6 +9,7 @@ import { } from "../components/icons"; import { ToolButton } from "../components/ToolButton"; import { getNonDeletedElements } from "../element"; +import { isFrameLikeElement } from "../element/typeChecks"; import { ExcalidrawElement } from "../element/types"; import { updateFrameMembershipOfSelectedElements } from "../frame"; import { t } from "../i18n"; @@ -28,7 +29,7 @@ const alignActionsPredicate = ( return ( selectedElements.length > 1 && // TODO enable aligning frames when implemented properly - !selectedElements.some((el) => el.type === "frame") + !selectedElements.some((el) => isFrameLikeElement(el)) ); }; diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index 4d7ec6a7c3178..de25ed8986100 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -10,7 +10,7 @@ import { newElementWith } from "../element/mutateElement"; import { getElementsInGroup } from "../groups"; import { LinearElementEditor } from "../element/linearElementEditor"; import { fixBindingsAfterDeletion } from "../element/binding"; -import { isBoundToContainer } from "../element/typeChecks"; +import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks"; import { updateActiveTool } from "../utils"; import { TrashIcon } from "../components/icons"; @@ -20,7 +20,7 @@ const deleteSelectedElements = ( ) => { const framesToBeDeleted = new Set( getSelectedElements( - elements.filter((el) => el.type === "frame"), + elements.filter((el) => isFrameLikeElement(el)), appState, ).map((el) => el.id), ); diff --git a/src/actions/actionDistribute.tsx b/src/actions/actionDistribute.tsx index d3cdb5c9cdb71..bf51bedf4b48f 100644 --- a/src/actions/actionDistribute.tsx +++ b/src/actions/actionDistribute.tsx @@ -5,6 +5,7 @@ import { import { ToolButton } from "../components/ToolButton"; import { distributeElements, Distribution } from "../distribute"; import { getNonDeletedElements } from "../element"; +import { isFrameLikeElement } from "../element/typeChecks"; import { ExcalidrawElement } from "../element/types"; import { updateFrameMembershipOfSelectedElements } from "../frame"; import { t } from "../i18n"; @@ -19,7 +20,7 @@ const enableActionGroup = (appState: AppState, app: AppClassProperties) => { return ( selectedElements.length > 1 && // TODO enable distributing frames when implemented properly - !selectedElements.some((el) => el.type === "frame") + !selectedElements.some((el) => isFrameLikeElement(el)) ); }; diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index 060a286800f5c..ba079168e9c04 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -20,7 +20,7 @@ import { bindTextToShapeAfterDuplication, getBoundTextElement, } from "../element/textElement"; -import { isBoundToContainer, isFrameElement } from "../element/typeChecks"; +import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks"; import { normalizeElementOrder } from "../element/sortElements"; import { DuplicateIcon } from "../components/icons"; import { @@ -140,11 +140,11 @@ const duplicateElements = ( } const boundTextElement = getBoundTextElement(element); - const isElementAFrame = isFrameElement(element); + const isElementAFrameLike = isFrameLikeElement(element); if (idsOfElementsToDuplicate.get(element.id)) { // if a group or a container/bound-text or frame, duplicate atomically - if (element.groupIds.length || boundTextElement || isElementAFrame) { + if (element.groupIds.length || boundTextElement || isElementAFrameLike) { const groupId = getSelectedGroupForElement(appState, element); if (groupId) { // TODO: @@ -154,7 +154,7 @@ const duplicateElements = ( sortedElements, groupId, ).flatMap((element) => - isFrameElement(element) + isFrameLikeElement(element) ? [...getFrameChildren(elements, element.id), element] : [element], ); @@ -180,7 +180,7 @@ const duplicateElements = ( ); continue; } - if (isElementAFrame) { + if (isElementAFrameLike) { const elementsInFrame = getFrameChildren(sortedElements, element.id); elementsWithClones.push( diff --git a/src/actions/actionElementLock.ts b/src/actions/actionElementLock.ts index cd539c5a355df..164240b290b17 100644 --- a/src/actions/actionElementLock.ts +++ b/src/actions/actionElementLock.ts @@ -1,4 +1,5 @@ import { newElementWith } from "../element/mutateElement"; +import { isFrameLikeElement } from "../element/typeChecks"; import { ExcalidrawElement } from "../element/types"; import { KEYS } from "../keys"; import { arrayToMap } from "../utils"; @@ -51,7 +52,7 @@ export const actionToggleElementLock = register({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: false, }); - if (selected.length === 1 && selected[0].type !== "frame") { + if (selected.length === 1 && !isFrameLikeElement(selected[0])) { return selected[0].locked ? "labels.elementLock.unlock" : "labels.elementLock.lock"; diff --git a/src/actions/actionFrame.ts b/src/actions/actionFrame.ts index 9e8c16c23d4b7..4cddb2ac0f40e 100644 --- a/src/actions/actionFrame.ts +++ b/src/actions/actionFrame.ts @@ -7,23 +7,27 @@ import { AppClassProperties, AppState } from "../types"; import { updateActiveTool } from "../utils"; import { setCursorForShape } from "../cursor"; import { register } from "./register"; +import { isFrameLikeElement } from "../element/typeChecks"; const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => { const selectedElements = app.scene.getSelectedElements(appState); - return selectedElements.length === 1 && selectedElements[0].type === "frame"; + return ( + selectedElements.length === 1 && isFrameLikeElement(selectedElements[0]) + ); }; export const actionSelectAllElementsInFrame = register({ name: "selectAllElementsInFrame", trackEvent: { category: "canvas" }, perform: (elements, appState, _, app) => { - const selectedFrame = app.scene.getSelectedElements(appState)[0]; + const selectedElement = + app.scene.getSelectedElements(appState).at(0) || null; - if (selectedFrame && selectedFrame.type === "frame") { + if (isFrameLikeElement(selectedElement)) { const elementsInFrame = getFrameChildren( getNonDeletedElements(elements), - selectedFrame.id, + selectedElement.id, ).filter((element) => !(element.type === "text" && element.containerId)); return { @@ -54,15 +58,20 @@ export const actionRemoveAllElementsFromFrame = register({ name: "removeAllElementsFromFrame", trackEvent: { category: "history" }, perform: (elements, appState, _, app) => { - const selectedFrame = app.scene.getSelectedElements(appState)[0]; + const selectedElement = + app.scene.getSelectedElements(appState).at(0) || null; - if (selectedFrame && selectedFrame.type === "frame") { + if (isFrameLikeElement(selectedElement)) { return { - elements: removeAllElementsFromFrame(elements, selectedFrame, appState), + elements: removeAllElementsFromFrame( + elements, + selectedElement, + appState, + ), appState: { ...appState, selectedElementIds: { - [selectedFrame.id]: true, + [selectedElement.id]: true, }, }, commitToHistory: true, diff --git a/src/actions/actionGroup.tsx b/src/actions/actionGroup.tsx index 219f1444cbba2..e6cb058401bf3 100644 --- a/src/actions/actionGroup.tsx +++ b/src/actions/actionGroup.tsx @@ -22,8 +22,8 @@ import { AppClassProperties, AppState } from "../types"; import { isBoundToContainer } from "../element/typeChecks"; import { getElementsInResizingFrame, - getFrameElements, - groupByFrames, + getFrameLikeElements, + groupByFrameLikes, removeElementsFromFrame, replaceAllElementsInFrame, } from "../frame"; @@ -102,7 +102,7 @@ export const actionGroup = register({ // when it happens, we want to remove elements that are in the frame // and are going to be grouped from the frame (mouthful, I know) if (groupingElementsFromDifferentFrames) { - const frameElementsMap = groupByFrames(selectedElements); + const frameElementsMap = groupByFrameLikes(selectedElements); frameElementsMap.forEach((elementsInFrame, frameId) => { nextElements = removeElementsFromFrame( @@ -219,7 +219,7 @@ export const actionUngroup = register({ .map((element) => element.frameId!), ); - const targetFrames = getFrameElements(elements).filter((frame) => + const targetFrames = getFrameLikeElements(elements).filter((frame) => selectedElementFrameIds.has(frame.id), ); diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index 2b656b050e483..9c6589bbc70d5 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -20,7 +20,7 @@ import { hasBoundTextElement, canApplyRoundnessTypeToElement, getDefaultRoundnessTypeForElement, - isFrameElement, + isFrameLikeElement, isArrowElement, } from "../element/typeChecks"; import { getSelectedElements } from "../scene"; @@ -138,7 +138,7 @@ export const actionPasteStyles = register({ }); } - if (isFrameElement(element)) { + if (isFrameLikeElement(element)) { newElement = newElementWith(newElement, { roundness: null, backgroundColor: "transparent", diff --git a/src/clipboard.ts b/src/clipboard.ts index 32b0edf1a1fc9..a88402d694fa8 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -9,7 +9,10 @@ import { EXPORT_DATA_TYPES, MIME_TYPES, } from "./constants"; -import { isInitializedImageElement } from "./element/typeChecks"; +import { + isFrameLikeElement, + isInitializedImageElement, +} from "./element/typeChecks"; import { deepCopyElement } from "./element/newElement"; import { mutateElement } from "./element/mutateElement"; import { getContainingFrame } from "./frame"; @@ -124,7 +127,7 @@ export const serializeAsClipboardJSON = ({ files: BinaryFiles | null; }) => { const framesToCopy = new Set( - elements.filter((element) => element.type === "frame"), + elements.filter((element) => isFrameLikeElement(element)), ); let foundFile = false; diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 6d1d80b1e1eed..556dc4af7e465 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { ActionManager } from "../actions/manager"; import { getNonDeletedElements } from "../element"; -import { ExcalidrawElement } from "../element/types"; +import { ExcalidrawElement, ExcalidrawElementType } from "../element/types"; import { t } from "../i18n"; import { useDevice } from "../components/App"; import { @@ -36,6 +36,8 @@ import { frameToolIcon, mermaidLogoIcon, laserPointerToolIcon, + OpenAIIcon, + MagicIcon, } from "./icons"; import { KEYS } from "../keys"; @@ -79,7 +81,8 @@ export const SelectedShapeActions = ({ const showLinkIcon = targetElements.length === 1 || isSingleElementBoundContainer; - let commonSelectedType: string | null = targetElements[0]?.type || null; + let commonSelectedType: ExcalidrawElementType | null = + targetElements[0]?.type || null; for (const element of targetElements) { if (element.type !== commonSelectedType) { @@ -94,7 +97,8 @@ export const SelectedShapeActions = ({ {((hasStrokeColor(appState.activeTool.type) && appState.activeTool.type !== "image" && commonSelectedType !== "image" && - commonSelectedType !== "frame") || + commonSelectedType !== "frame" && + commonSelectedType !== "magicframe") || targetElements.some((element) => hasStrokeColor(element.type))) && renderAction("changeStrokeColor")} @@ -331,6 +335,9 @@ export const ShapesSwitcher = ({ > {t("toolBar.laser")} +