mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: allow inner-drag-selecting with cmd/ctrl (#3603)
* feat: allow inner-drag-selecting with cmd/ctrl * don't use cursor when pressing cmd/ctrl * ensure we reset deselected groups * add tests * add docs * couple fixes around group selection
This commit is contained in:
parent
f4e10c93e1
commit
6d40039f08
6 changed files with 228 additions and 20 deletions
|
@ -57,6 +57,7 @@ export class API {
|
|||
width = 100,
|
||||
height = width,
|
||||
isDeleted = false,
|
||||
groupIds = [],
|
||||
...rest
|
||||
}: {
|
||||
type: T;
|
||||
|
@ -66,6 +67,7 @@ export class API {
|
|||
width?: number;
|
||||
id?: string;
|
||||
isDeleted?: boolean;
|
||||
groupIds?: string[];
|
||||
// generic element props
|
||||
strokeColor?: ExcalidrawGenericElement["strokeColor"];
|
||||
backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
|
||||
|
@ -152,6 +154,9 @@ export class API {
|
|||
if (isDeleted) {
|
||||
element.isDeleted = isDeleted;
|
||||
}
|
||||
if (groupIds) {
|
||||
element.groupIds = groupIds;
|
||||
}
|
||||
return element as any;
|
||||
};
|
||||
|
||||
|
|
|
@ -122,6 +122,9 @@ export class Pointer {
|
|||
};
|
||||
}
|
||||
|
||||
// incremental (moving by deltas)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
move(dx: number, dy: number) {
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
this.clientX += dx;
|
||||
|
@ -150,6 +153,39 @@ export class Pointer {
|
|||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
||||
}
|
||||
|
||||
// absolute coords
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
moveTo(x: number, y: number) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
||||
}
|
||||
|
||||
downAt(x = this.clientX, y = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
||||
}
|
||||
|
||||
upAt(x = this.clientX, y = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
||||
}
|
||||
|
||||
clickAt(x: number, y: number) {
|
||||
this.downAt(x, y);
|
||||
this.upAt();
|
||||
}
|
||||
|
||||
doubleClickAt(x: number, y: number) {
|
||||
this.moveTo(x, y);
|
||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
select(
|
||||
/** if multiple elements supplied, they're shift-selected */
|
||||
elements: ExcalidrawElement | ExcalidrawElement[],
|
||||
|
|
|
@ -10,6 +10,9 @@ import ExcalidrawApp from "../excalidraw-app";
|
|||
import * as Renderer from "../renderer/renderScene";
|
||||
import { KEYS } from "../keys";
|
||||
import { reseed } from "../random";
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard, Pointer } from "./helpers/ui";
|
||||
import { getSelectedElements } from "../scene";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
@ -23,6 +26,130 @@ beforeEach(() => {
|
|||
|
||||
const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
const assertSelectedIds = (ids: string[]) => {
|
||||
expect(
|
||||
getSelectedElements(h.app.getSceneElements(), h.state).map((el) => el.id),
|
||||
).toEqual(ids);
|
||||
};
|
||||
|
||||
describe("inner box-selection", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
});
|
||||
it("selecting elements visually nested inside another", async () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 300,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 50,
|
||||
height: 50,
|
||||
});
|
||||
const rect3 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 150,
|
||||
y: 150,
|
||||
width: 50,
|
||||
height: 50,
|
||||
});
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(40, 40);
|
||||
mouse.moveTo(290, 290);
|
||||
mouse.up();
|
||||
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
});
|
||||
});
|
||||
|
||||
it("selecting grouped elements visually nested inside another", async () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 300,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["A"],
|
||||
});
|
||||
const rect3 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 150,
|
||||
y: 150,
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["A"],
|
||||
});
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(40, 40);
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
mouse.up();
|
||||
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
});
|
||||
});
|
||||
|
||||
it("selecting & deselecting grouped elements visually nested inside another", async () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 300,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["A"],
|
||||
});
|
||||
const rect3 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 150,
|
||||
y: 150,
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["A"],
|
||||
});
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(rect2.x - 20, rect2.x - 20);
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
assertSelectedIds([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
mouse.moveTo(rect2.x - 10, rect2.y - 10);
|
||||
assertSelectedIds([rect1.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({});
|
||||
mouse.up();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selection element", () => {
|
||||
it("create selection element on pointer down", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
|
|
|
@ -56,9 +56,8 @@ const populateElements = (
|
|||
y,
|
||||
width,
|
||||
height,
|
||||
groupIds,
|
||||
});
|
||||
// @ts-ignore
|
||||
element.groupIds = groupIds;
|
||||
if (isSelected) {
|
||||
selectedElementIds[element.id] = true;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue