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 type { ExcalidrawElement } from "../element/types";
|
||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { addElementsToFrame, removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameChildren } from "../frame";
|
import { getFrameChildren } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
||||||
|
@ -10,6 +10,8 @@ import { register } from "./register";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
import { frameToolIcon } from "../components/icons";
|
import { frameToolIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { newFrameElement } from "../element/newElement";
|
||||||
|
|
||||||
const isSingleFrameSelected = (
|
const isSingleFrameSelected = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
|
@ -144,3 +146,46 @@ export const actionSetFrameAsActiveTool = register({
|
||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
event.key.toLocaleLowerCase() === KEYS.F,
|
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"
|
| "saveFileToDisk"
|
||||||
| "saveToActiveFile"
|
| "saveToActiveFile"
|
||||||
| "toggleShortcuts"
|
| "toggleShortcuts"
|
||||||
|
| "wrapSelectionInFrame"
|
||||||
>
|
>
|
||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport"
|
| "imageExport"
|
||||||
|
@ -112,6 +113,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
toggleShortcuts: [getShortcutKey("?")],
|
toggleShortcuts: [getShortcutKey("?")],
|
||||||
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
||||||
|
wrapSelectionInFrame: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
||||||
|
|
|
@ -137,7 +137,8 @@ export type ActionName =
|
||||||
| "searchMenu"
|
| "searchMenu"
|
||||||
| "copyElementLink"
|
| "copyElementLink"
|
||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor";
|
| "cropEditor"
|
||||||
|
| "wrapSelectionInFrame";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|
|
@ -378,6 +378,7 @@ import { actionPaste } from "../actions/actionClipboard";
|
||||||
import {
|
import {
|
||||||
actionRemoveAllElementsFromFrame,
|
actionRemoveAllElementsFromFrame,
|
||||||
actionSelectAllElementsInFrame,
|
actionSelectAllElementsInFrame,
|
||||||
|
actionWrapSelectionInFrame,
|
||||||
} from "../actions/actionFrame";
|
} from "../actions/actionFrame";
|
||||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
|
@ -10664,8 +10665,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
actionCut,
|
actionCut,
|
||||||
actionCopy,
|
actionCopy,
|
||||||
actionPaste,
|
actionPaste,
|
||||||
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionSelectAllElementsInFrame,
|
actionSelectAllElementsInFrame,
|
||||||
actionRemoveAllElementsFromFrame,
|
actionRemoveAllElementsFromFrame,
|
||||||
|
actionWrapSelectionInFrame,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionToggleCropEditor,
|
actionToggleCropEditor,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
|
|
|
@ -263,6 +263,7 @@ function CommandPaletteInner({
|
||||||
actionManager.actions.cut,
|
actionManager.actions.cut,
|
||||||
actionManager.actions.copy,
|
actionManager.actions.copy,
|
||||||
actionManager.actions.deleteSelectedElements,
|
actionManager.actions.deleteSelectedElements,
|
||||||
|
actionManager.actions.wrapSelectionInFrame,
|
||||||
actionManager.actions.copyStyles,
|
actionManager.actions.copyStyles,
|
||||||
actionManager.actions.pasteStyles,
|
actionManager.actions.pasteStyles,
|
||||||
actionManager.actions.bringToFront,
|
actionManager.actions.bringToFront,
|
||||||
|
|
|
@ -237,6 +237,7 @@ const MultiPosition = ({
|
||||||
const [x1, y1] = getCommonBounds(elementsInUnit);
|
const [x1, y1] = getCommonBounds(elementsInUnit);
|
||||||
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
|
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
||||||
|
|
||||||
|
|
|
@ -164,7 +164,8 @@
|
||||||
"imageCropping": "Image cropping",
|
"imageCropping": "Image cropping",
|
||||||
"unCroppedDimension": "Uncropped dimension",
|
"unCroppedDimension": "Uncropped dimension",
|
||||||
"copyElementLink": "Copy link to object",
|
"copyElementLink": "Copy link to object",
|
||||||
"linkToElement": "Link to object"
|
"linkToElement": "Link to object",
|
||||||
|
"wrapSelectionInFrame": "Wrap selection in frame"
|
||||||
},
|
},
|
||||||
"elementLink": {
|
"elementLink": {
|
||||||
"title": "Link to object",
|
"title": "Link to object",
|
||||||
|
|
|
@ -97,6 +97,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"label": "labels.selectAllElementsInFrame",
|
"label": "labels.selectAllElementsInFrame",
|
||||||
"name": "selectAllElementsInFrame",
|
"name": "selectAllElementsInFrame",
|
||||||
|
@ -115,6 +116,15 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||||
"category": "history",
|
"category": "history",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "labels.wrapSelectionInFrame",
|
||||||
|
"name": "wrapSelectionInFrame",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
"separator",
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
|
@ -4731,6 +4741,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"label": "labels.selectAllElementsInFrame",
|
"label": "labels.selectAllElementsInFrame",
|
||||||
"name": "selectAllElementsInFrame",
|
"name": "selectAllElementsInFrame",
|
||||||
|
@ -4749,6 +4760,15 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"category": "history",
|
"category": "history",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "labels.wrapSelectionInFrame",
|
||||||
|
"name": "wrapSelectionInFrame",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
"separator",
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
|
@ -5942,6 +5962,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"label": "labels.selectAllElementsInFrame",
|
"label": "labels.selectAllElementsInFrame",
|
||||||
"name": "selectAllElementsInFrame",
|
"name": "selectAllElementsInFrame",
|
||||||
|
@ -5960,6 +5981,15 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"category": "history",
|
"category": "history",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "labels.wrapSelectionInFrame",
|
||||||
|
"name": "wrapSelectionInFrame",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
"separator",
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
|
@ -7876,6 +7906,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"label": "labels.selectAllElementsInFrame",
|
"label": "labels.selectAllElementsInFrame",
|
||||||
"name": "selectAllElementsInFrame",
|
"name": "selectAllElementsInFrame",
|
||||||
|
@ -7894,6 +7925,15 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"category": "history",
|
"category": "history",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "labels.wrapSelectionInFrame",
|
||||||
|
"name": "wrapSelectionInFrame",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
"separator",
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
|
@ -8854,6 +8894,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"label": "labels.selectAllElementsInFrame",
|
"label": "labels.selectAllElementsInFrame",
|
||||||
"name": "selectAllElementsInFrame",
|
"name": "selectAllElementsInFrame",
|
||||||
|
@ -8872,6 +8913,15 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"category": "history",
|
"category": "history",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "labels.wrapSelectionInFrame",
|
||||||
|
"name": "wrapSelectionInFrame",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
"separator",
|
"separator",
|
||||||
{
|
{
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
|
|
|
@ -120,6 +120,7 @@ describe("contextMenu element", () => {
|
||||||
"cut",
|
"cut",
|
||||||
"copy",
|
"copy",
|
||||||
"paste",
|
"paste",
|
||||||
|
"wrapSelectionInFrame",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
|
@ -213,6 +214,7 @@ describe("contextMenu element", () => {
|
||||||
"cut",
|
"cut",
|
||||||
"copy",
|
"copy",
|
||||||
"paste",
|
"paste",
|
||||||
|
"wrapSelectionInFrame",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
|
@ -269,6 +271,7 @@ describe("contextMenu element", () => {
|
||||||
"cut",
|
"cut",
|
||||||
"copy",
|
"copy",
|
||||||
"paste",
|
"paste",
|
||||||
|
"wrapSelectionInFrame",
|
||||||
"copyStyles",
|
"copyStyles",
|
||||||
"pasteStyles",
|
"pasteStyles",
|
||||||
"deleteSelectedElements",
|
"deleteSelectedElements",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue