select single element on cmd-click (#2087)

This commit is contained in:
David Luzar 2020-08-27 20:59:46 +02:00 committed by GitHub
parent b8f8bc2e32
commit 4c2d34ffd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 840 additions and 17 deletions

View File

@ -157,6 +157,7 @@ import {
isElementInGroup,
getSelectedGroupIdForElement,
getElementsInGroup,
editGroupForSelectedElement,
} from "../groups";
import { Library } from "../data/library";
import Scene from "../scene/Scene";
@ -939,10 +940,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private onTapEnd = (event: TouchEvent) => {
event.preventDefault();
if (event.touches.length > 0) {
const { previousSelectedElementIds } = this.state;
this.setState({
previousSelectedElementIds: {},
selectedElementIds: previousSelectedElementIds,
selectedElementIds: this.state.previousSelectedElementIds,
});
}
};
@ -1617,10 +1617,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
event.preventDefault();
const { previousSelectedElementIds } = this.state;
this.setState({
previousSelectedElementIds: {},
selectedElementIds: previousSelectedElementIds,
selectedElementIds: this.state.previousSelectedElementIds,
});
gesture.initialScale = null;
});
@ -1904,11 +1903,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
resetCursor();
this.startTextEditing({
sceneX,
sceneY,
insertAtParentCenter: !event.altKey,
});
if (!event[KEYS.CTRL_OR_CMD]) {
this.startTextEditing({
sceneX,
sceneY,
insertAtParentCenter: !event.altKey,
});
}
};
private handleCanvasPointerMove = (
@ -2485,7 +2486,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
};
// Returns whether the pointer event has been completely handled
/**
* @returns whether the pointer event has been completely handled
*/
private handleSelectionOnPointerDown = (
event: React.PointerEvent<HTMLCanvasElement>,
pointerDownState: PointerDownState,
@ -2587,6 +2590,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
// If we click on something
if (hitElement != null) {
// on CMD/CTRL, drill down to hit element regardless of groups etc.
if (event[KEYS.CTRL_OR_CMD]) {
this.setState((prevState) => ({
...editGroupForSelectedElement(prevState, hitElement),
previousSelectedElementIds: this.state.selectedElementIds,
}));
// mark as not completely handled so as to allow dragging etc.
return false;
}
// deselect if item is selected
// if shift is not clicked, this will always return true
// otherwise, it will trigger selection based on current
@ -2619,7 +2632,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
...prevState,
selectedElementIds: {
...prevState.selectedElementIds,
[hitElement!.id]: true,
[hitElement.id]: true,
},
},
this.scene.getElements(),
@ -2630,9 +2643,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
}
const { selectedElementIds } = this.state;
this.setState({
previousSelectedElementIds: selectedElementIds,
previousSelectedElementIds: this.state.selectedElementIds,
});
}
}
@ -3530,10 +3542,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
? prevState.editingGroupId
: null,
}));
const { selectedElementIds } = this.state;
this.setState({
selectedElementIds: {},
previousSelectedElementIds: selectedElementIds,
previousSelectedElementIds: this.state.selectedElementIds,
});
}

View File

@ -86,6 +86,20 @@ export function selectGroupsForSelectedElements(
return nextAppState;
}
export const editGroupForSelectedElement = (
appState: AppState,
element: NonDeleted<ExcalidrawElement>,
): AppState => {
return {
...appState,
editingGroupId: element.groupIds.length ? element.groupIds[0] : null,
selectedGroupIds: {},
selectedElementIds: {
[element.id]: true,
},
};
};
export function isElementInGroup(element: ExcalidrawElement, groupId: string) {
return element.groupIds.includes(groupId);
}

View File

@ -878,6 +878,757 @@ exports[`given element A and group of elements B and given both are selected whe
exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `22`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = `
Object {
"appearance": "light",
"collaborators": Map {},
"currentItemBackgroundColor": "transparent",
"currentItemFillStyle": "hachure",
"currentItemFontFamily": 1,
"currentItemFontSize": 20,
"currentItemLinearStrokeSharpness": "round",
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemStrokeColor": "#000000",
"currentItemStrokeSharpness": "sharp",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 1,
"currentItemTextAlign": "left",
"cursorButton": "up",
"cursorX": 0,
"cursorY": 0,
"draggingElement": null,
"editingElement": null,
"editingGroupId": "id10",
"editingLinearElement": null,
"elementLocked": false,
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isCollaborating": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
"previousSelectedElementIds": Object {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": Object {
"id12": true,
"id7": true,
},
"selectedGroupIds": Object {},
"selectionElement": null,
"shouldAddWatermark": false,
"shouldCacheIgnoreZoom": false,
"showShortcutsDialog": false,
"startBoundElement": null,
"suggestedBindings": Array [],
"username": "",
"viewBackgroundColor": "#ffffff",
"width": 1024,
"zenModeEnabled": false,
"zoom": 1,
}
`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 0 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 493213705,
"width": 10,
"x": 0,
"y": 0,
}
`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 1 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 915032327,
"width": 10,
"x": 30,
"y": 30,
}
`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] element 2 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id10",
],
"height": 10,
"id": "id7",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 400692809,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"width": 10,
"x": 60,
"y": 60,
}
`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] history 1`] = `
Object {
"recording": false,
"redoStack": Array [],
"stateHistory": Array [
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 0,
"y": 0,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 453191,
"width": 10,
"x": 30,
"y": 30,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1150084233,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1116226695,
"width": 10,
"x": 30,
"y": 30,
},
],
},
Object {
"appState": Object {
"editingGroupId": "id4",
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
"id5": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1150084233,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1116226695,
"width": 10,
"x": 30,
"y": 30,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id7": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1150084233,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 1116226695,
"width": 10,
"x": 30,
"y": 30,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id7",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 400692809,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1604849351,
"width": 10,
"x": 60,
"y": 60,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id7": true,
"id8": true,
"id9": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 493213705,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 915032327,
"width": 10,
"x": 30,
"y": 30,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id10",
],
"height": 10,
"id": "id7",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 400692809,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"width": 10,
"x": 60,
"y": 60,
},
],
},
Object {
"appState": Object {
"editingGroupId": "id4",
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
"id11": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 493213705,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 915032327,
"width": 10,
"x": 30,
"y": 30,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id10",
],
"height": 10,
"id": "id7",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 400692809,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"width": 10,
"x": 60,
"y": 60,
},
],
},
Object {
"appState": Object {
"editingGroupId": "id10",
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id12": true,
"id7": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 493213705,
"width": 10,
"x": 0,
"y": 0,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id4",
"id10",
],
"height": 10,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 915032327,
"width": 10,
"x": 30,
"y": 30,
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [
"id10",
],
"height": 10,
"id": "id7",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 400692809,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"width": 10,
"x": 60,
"y": 60,
},
],
},
],
}
`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of elements 1`] = `3`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `41`;
exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = `
Object {
"appearance": "light",

View File

@ -50,6 +50,20 @@ const group = (elements: ExcalidrawElement[]) => {
});
};
const assertSelectedElements = (...elements: ExcalidrawElement[]) => {
expect(
getSelectedElements().map((element) => {
return element.id;
}),
).toEqual(expect.arrayContaining(elements.map((element) => element.id)));
};
const clearSelection = () => {
// @ts-ignore
h.app.clearSelection(null);
expect(getSelectedElements().length).toBe(0);
};
let altKey = false;
let shiftKey = false;
let ctrlKey = false;
@ -191,8 +205,7 @@ class Pointer {
/** if multiple elements supplied, they're shift-selected */
elements: ExcalidrawElement | ExcalidrawElement[],
) {
// @ts-ignore
h.app.clearSelection(null);
clearSelection();
withModifierKeys({ shift: true }, () => {
elements = Array.isArray(elements) ? elements : [elements];
elements.forEach((element) => {
@ -1649,6 +1662,40 @@ describe("regression tests", () => {
expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev);
expect(getSelectedElements()).toEqual(selectedElements_prev);
});
it("Cmd/Ctrl-click exclusively select element under pointer", () => {
const rect1 = createElement("rectangle", { x: 0 });
const rect2 = createElement("rectangle", { x: 30 });
group([rect1, rect2]);
assertSelectedElements(rect1, rect2);
withModifierKeys({ ctrl: true }, () => {
mouse.clickOn(rect1);
});
assertSelectedElements(rect1);
clearSelection();
withModifierKeys({ ctrl: true }, () => {
mouse.clickOn(rect1);
});
assertSelectedElements(rect1);
const rect3 = createElement("rectangle", { x: 60 });
group([rect1, rect3]);
assertSelectedElements(rect1, rect2, rect3);
withModifierKeys({ ctrl: true }, () => {
mouse.clickOn(rect1);
});
assertSelectedElements(rect1);
clearSelection();
withModifierKeys({ ctrl: true }, () => {
mouse.clickOn(rect3);
});
assertSelectedElements(rect3);
});
});
it(