feat: add action to wrap selected items in a frame (#9005)

* feat: add action to wrap selected items in a frame

* fix type

* select frame on wrap & refactor

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
pull/8627/merge
Ryan Di 3 weeks ago committed by GitHub
parent c92f3bebf5
commit 00b5b0a0ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,6 @@
import { getNonDeletedElements } from "../element";
import { getCommonBounds, getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { addElementsToFrame, removeAllElementsFromFrame } from "../frame";
import { getFrameChildren } from "../frame";
import { KEYS } from "../keys";
import type { AppClassProperties, AppState, UIAppState } from "../types";
@ -10,6 +10,8 @@ import { register } from "./register";
import { isFrameLikeElement } from "../element/typeChecks";
import { frameToolIcon } from "../components/icons";
import { StoreAction } from "../store";
import { getSelectedElements } from "../scene";
import { newFrameElement } from "../element/newElement";
const isSingleFrameSelected = (
appState: UIAppState,
@ -144,3 +146,46 @@ export const actionSetFrameAsActiveTool = register({
!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,
});
const nextElements = addElementsToFrame(
[...app.scene.getElementsIncludingDeleted(), frame],
selectedElements,
frame,
);
return {
elements: nextElements,
appState: {
selectedElementIds: { [frame.id]: true },
},
storeAction: StoreAction.CAPTURE,
};
},
});

@ -47,6 +47,7 @@ export type ShortcutName =
| "saveFileToDisk"
| "saveToActiveFile"
| "toggleShortcuts"
| "wrapSelectionInFrame"
>
| "saveScene"
| "imageExport"
@ -112,6 +113,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
toggleShortcuts: [getShortcutKey("?")],
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
wrapSelectionInFrame: [],
};
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {

@ -137,7 +137,8 @@ export type ActionName =
| "searchMenu"
| "copyElementLink"
| "linkToElement"
| "cropEditor";
| "cropEditor"
| "wrapSelectionInFrame";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

@ -378,6 +378,7 @@ import { actionPaste } from "../actions/actionClipboard";
import {
actionRemoveAllElementsFromFrame,
actionSelectAllElementsInFrame,
actionWrapSelectionInFrame,
} from "../actions/actionFrame";
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
@ -10664,8 +10665,10 @@ class App extends React.Component<AppProps, AppState> {
actionCut,
actionCopy,
actionPaste,
CONTEXT_MENU_SEPARATOR,
actionSelectAllElementsInFrame,
actionRemoveAllElementsFromFrame,
actionWrapSelectionInFrame,
CONTEXT_MENU_SEPARATOR,
actionToggleCropEditor,
CONTEXT_MENU_SEPARATOR,

@ -263,6 +263,7 @@ function CommandPaletteInner({
actionManager.actions.cut,
actionManager.actions.copy,
actionManager.actions.deleteSelectedElements,
actionManager.actions.wrapSelectionInFrame,
actionManager.actions.copyStyles,
actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,

@ -237,6 +237,7 @@ const MultiPosition = ({
const [x1, y1] = getCommonBounds(elementsInUnit);
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
}
const [el] = elementsInUnit;
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];

@ -164,7 +164,8 @@
"imageCropping": "Image cropping",
"unCroppedDimension": "Uncropped dimension",
"copyElementLink": "Copy link to object",
"linkToElement": "Link to object"
"linkToElement": "Link to object",
"wrapSelectionInFrame": "Wrap selection in frame"
},
"elementLink": {
"title": "Link to object",

@ -97,6 +97,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"category": "element",
},
},
"separator",
{
"label": "labels.selectAllElementsInFrame",
"name": "selectAllElementsInFrame",
@ -115,6 +116,15 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"category": "history",
},
},
{
"label": "labels.wrapSelectionInFrame",
"name": "wrapSelectionInFrame",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
"separator",
{
"PanelComponent": [Function],
@ -4731,6 +4741,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"category": "element",
},
},
"separator",
{
"label": "labels.selectAllElementsInFrame",
"name": "selectAllElementsInFrame",
@ -4749,6 +4760,15 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"category": "history",
},
},
{
"label": "labels.wrapSelectionInFrame",
"name": "wrapSelectionInFrame",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
"separator",
{
"PanelComponent": [Function],
@ -5942,6 +5962,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"category": "element",
},
},
"separator",
{
"label": "labels.selectAllElementsInFrame",
"name": "selectAllElementsInFrame",
@ -5960,6 +5981,15 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"category": "history",
},
},
{
"label": "labels.wrapSelectionInFrame",
"name": "wrapSelectionInFrame",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
"separator",
{
"PanelComponent": [Function],
@ -7876,6 +7906,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "element",
},
},
"separator",
{
"label": "labels.selectAllElementsInFrame",
"name": "selectAllElementsInFrame",
@ -7894,6 +7925,15 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "history",
},
},
{
"label": "labels.wrapSelectionInFrame",
"name": "wrapSelectionInFrame",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
"separator",
{
"PanelComponent": [Function],
@ -8854,6 +8894,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "element",
},
},
"separator",
{
"label": "labels.selectAllElementsInFrame",
"name": "selectAllElementsInFrame",
@ -8872,6 +8913,15 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "history",
},
},
{
"label": "labels.wrapSelectionInFrame",
"name": "wrapSelectionInFrame",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
"separator",
{
"PanelComponent": [Function],

@ -120,6 +120,7 @@ describe("contextMenu element", () => {
"cut",
"copy",
"paste",
"wrapSelectionInFrame",
"copyStyles",
"pasteStyles",
"deleteSelectedElements",
@ -213,6 +214,7 @@ describe("contextMenu element", () => {
"cut",
"copy",
"paste",
"wrapSelectionInFrame",
"copyStyles",
"pasteStyles",
"deleteSelectedElements",
@ -269,6 +271,7 @@ describe("contextMenu element", () => {
"cut",
"copy",
"paste",
"wrapSelectionInFrame",
"copyStyles",
"pasteStyles",
"deleteSelectedElements",

Loading…
Cancel
Save