Skip to content

Commit

Permalink
feat: add "unlock all elements" to canvas contextMenu (excalidraw#5894)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwelle authored May 13, 2023
1 parent 5bf27a4 commit b1b325b
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 42 deletions.
68 changes: 68 additions & 0 deletions src/actions/actionElementLock.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Excalidraw } from "../packages/excalidraw/index";
import { queryByTestId, fireEvent } from "@testing-library/react";
import { render } from "../tests/test-utils";
import { Pointer, UI } from "../tests/helpers/ui";
import { API } from "../tests/helpers/api";

const { h } = window;
const mouse = new Pointer("mouse");

describe("element locking", () => {
it("should not show unlockAllElements action in contextMenu if no elements locked", async () => {
await render(<Excalidraw />);

mouse.rightClickAt(0, 0);

const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
expect(item).toBe(null);
});

it("should unlock all elements and select them when using unlockAllElements action in contextMenu", async () => {
await render(
<Excalidraw
initialData={{
elements: [
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: true,
}),
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: true,
}),
API.createElement({
x: 100,
y: 100,
width: 100,
height: 100,
locked: false,
}),
],
}}
/>,
);

mouse.rightClickAt(0, 0);

expect(Object.keys(h.state.selectedElementIds).length).toBe(0);
expect(h.elements.map((el) => el.locked)).toEqual([true, true, false]);

const item = queryByTestId(UI.queryContextMenu()!, "unlockAllElements");
expect(item).not.toBe(null);

fireEvent.click(item!.querySelector("button")!);

expect(h.elements.map((el) => el.locked)).toEqual([false, false, false]);
// should select the unlocked elements
expect(h.state.selectedElementIds).toEqual({
[h.elements[0].id]: true,
[h.elements[1].id]: true,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils";
import { register } from "./register";

export const actionToggleLock = register({
name: "toggleLock",
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
elements.every((el) => !el.locked);

export const actionToggleElementLock = register({
name: "toggleElementLock",
trackEvent: { category: "element" },
perform: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState, true);
Expand All @@ -15,20 +18,21 @@ export const actionToggleLock = register({
return false;
}

const operation = getOperation(selectedElements);
const nextLockState = shouldLock(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements);
const lock = operation === "lock";
return {
elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) {
return element;
}

return newElementWith(element, { locked: lock });
return newElementWith(element, { locked: nextLockState });
}),
appState: {
...appState,
selectedLinearElement: lock ? null : appState.selectedLinearElement,
selectedLinearElement: nextLockState
? null
: appState.selectedLinearElement,
},
commitToHistory: true,
};
Expand All @@ -41,7 +45,7 @@ export const actionToggleLock = register({
: "labels.elementLock.lock";
}

return getOperation(selected) === "lock"
return shouldLock(selected)
? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll";
},
Expand All @@ -55,6 +59,31 @@ export const actionToggleLock = register({
},
});

const getOperation = (
elements: readonly ExcalidrawElement[],
): "lock" | "unlock" => (elements.some((el) => !el.locked) ? "lock" : "unlock");
export const actionUnlockAllElements = register({
name: "unlockAllElements",
trackEvent: { category: "canvas" },
viewMode: false,
predicate: (elements) => {
return elements.some((element) => element.locked);
},
perform: (elements, appState) => {
const lockedElements = elements.filter((el) => el.locked);

return {
elements: elements.map((element) => {
if (element.locked) {
return newElementWith(element, { locked: false });
}
return element;
}),
appState: {
...appState,
selectedElementIds: Object.fromEntries(
lockedElements.map((el) => [el.id, true]),
),
},
commitToHistory: true,
};
},
contextItemLabel: "labels.elementLock.unlockAll",
});
2 changes: 1 addition & 1 deletion src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText";
export { actionLink } from "../element/Hyperlink";
export { actionToggleLock } from "./actionToggleLock";
export { actionToggleElementLock } from "./actionElementLock";
export { actionToggleLinearEditor } from "./actionLinearEditor";
4 changes: 2 additions & 2 deletions src/actions/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ShortcutName =
| "flipHorizontal"
| "flipVertical"
| "hyperlink"
| "toggleLock"
| "toggleElementLock"
>
| "saveScene"
| "imageExport";
Expand Down Expand Up @@ -80,7 +80,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
toggleLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
};

export const getShortcutFromShortcutName = (name: ShortcutName) => {
Expand Down
3 changes: 2 additions & 1 deletion src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export type ActionName =
| "unbindText"
| "hyperlink"
| "bindText"
| "toggleLock"
| "unlockAllElements"
| "toggleElementLock"
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool"
Expand Down
6 changes: 4 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
actionBindText,
actionUngroup,
actionLink,
actionToggleLock,
actionToggleElementLock,
actionToggleLinearEditor,
} from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
Expand Down Expand Up @@ -290,6 +290,7 @@ import {
isLocalLink,
} from "../element/Hyperlink";
import { shouldShowBoundingBox } from "../element/transformHandles";
import { actionUnlockAllElements } from "../actions/actionElementLock";
import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard";
import {
Expand Down Expand Up @@ -6347,6 +6348,7 @@ class App extends React.Component<AppProps, AppState> {
copyText,
CONTEXT_MENU_SEPARATOR,
actionSelectAll,
actionUnlockAllElements,
CONTEXT_MENU_SEPARATOR,
actionToggleGridMode,
actionToggleZenMode,
Expand Down Expand Up @@ -6393,7 +6395,7 @@ class App extends React.Component<AppProps, AppState> {
actionToggleLinearEditor,
actionLink,
actionDuplicateSelection,
actionToggleLock,
actionToggleElementLock,
CONTEXT_MENU_SEPARATOR,
actionDeleteSelected,
];
Expand Down
20 changes: 15 additions & 5 deletions src/tests/__snapshots__/contextmenu.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Object {
Object {
"contextItemLabel": [Function],
"keyTest": [Function],
"name": "toggleLock",
"name": "toggleElementLock",
"perform": [Function],
"trackEvent": Object {
"category": "element",
Expand Down Expand Up @@ -4644,7 +4644,7 @@ Object {
Object {
"contextItemLabel": [Function],
"keyTest": [Function],
"name": "toggleLock",
"name": "toggleElementLock",
"perform": [Function],
"trackEvent": Object {
"category": "element",
Expand Down Expand Up @@ -5194,7 +5194,7 @@ Object {
Object {
"contextItemLabel": [Function],
"keyTest": [Function],
"name": "toggleLock",
"name": "toggleElementLock",
"perform": [Function],
"trackEvent": Object {
"category": "element",
Expand Down Expand Up @@ -5642,6 +5642,16 @@ Object {
"category": "canvas",
},
},
Object {
"contextItemLabel": "labels.elementLock.unlockAll",
"name": "unlockAllElements",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "canvas",
},
"viewMode": false,
},
"separator",
Object {
"checked": [Function],
Expand Down Expand Up @@ -6043,7 +6053,7 @@ Object {
Object {
"contextItemLabel": [Function],
"keyTest": [Function],
"name": "toggleLock",
"name": "toggleElementLock",
"perform": [Function],
"trackEvent": Object {
"category": "element",
Expand Down Expand Up @@ -6389,7 +6399,7 @@ Object {
Object {
"contextItemLabel": [Function],
"keyTest": [Function],
"name": "toggleLock",
"name": "toggleElementLock",
"perform": [Function],
"trackEvent": Object {
"category": "element",
Expand Down
Loading

0 comments on commit b1b325b

Please sign in to comment.