feat: add "unlock all elements" to canvas contextMenu (#5894)

This commit is contained in:
David Luzar
2023-05-13 22:52:03 +02:00
committed by GitHub
parent 5bf27a463c
commit b1b325b9a7
10 changed files with 148 additions and 42 deletions

View File

@ -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,
});
});
});

View File

@ -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);
@ -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,
};
@ -41,7 +45,7 @@ export const actionToggleLock = register({
: "labels.elementLock.lock";
}
return getOperation(selected) === "lock"
return shouldLock(selected)
? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll";
},
@ -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",
});

View File

@ -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";

View File

@ -34,7 +34,7 @@ export type ShortcutName =
| "flipHorizontal"
| "flipVertical"
| "hyperlink"
| "toggleLock"
| "toggleElementLock"
>
| "saveScene"
| "imageExport";
@ -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) => {

View File

@ -111,7 +111,8 @@ export type ActionName =
| "unbindText"
| "hyperlink"
| "bindText"
| "toggleLock"
| "unlockAllElements"
| "toggleElementLock"
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool"