mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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>
This commit is contained in:
parent
c92f3bebf5
commit
00b5b0a0ca
9 changed files with 111 additions and 4 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue