Fix drag multiple elements bug (#2023)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
4718c31da5
commit
e7d186b439
@ -31,11 +31,12 @@ import {
|
|||||||
getDragOffsetXY,
|
getDragOffsetXY,
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
hitTest,
|
hitTest,
|
||||||
|
isHittingElementBoundingBoxWithoutHittingElement,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import {
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
isOverScrollBars,
|
isOverScrollBars,
|
||||||
getElementAtPosition,
|
getElementsAtPosition,
|
||||||
getElementContainingPosition,
|
getElementContainingPosition,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
@ -151,9 +152,11 @@ import throttle from "lodash.throttle";
|
|||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import {
|
||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
|
isSelectedViaGroup,
|
||||||
selectGroupsForSelectedElements,
|
selectGroupsForSelectedElements,
|
||||||
isElementInGroup,
|
isElementInGroup,
|
||||||
getSelectedGroupIdForElement,
|
getSelectedGroupIdForElement,
|
||||||
|
getElementsInGroup,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
import { Library } from "../data/library";
|
import { Library } from "../data/library";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
@ -231,12 +234,16 @@ type PointerDownState = Readonly<{
|
|||||||
hit: {
|
hit: {
|
||||||
// The element the pointer is "hitting", is determined on the initial
|
// The element the pointer is "hitting", is determined on the initial
|
||||||
// pointer down event
|
// pointer down event
|
||||||
element: ExcalidrawElement | null;
|
element: NonDeleted<ExcalidrawElement> | null;
|
||||||
|
// The elements the pointer is "hitting", is determined on the initial
|
||||||
|
// pointer down event
|
||||||
|
allHitElements: NonDeleted<ExcalidrawElement>[];
|
||||||
// This is determined on the initial pointer down event
|
// This is determined on the initial pointer down event
|
||||||
wasAddedToSelection: boolean;
|
wasAddedToSelection: boolean;
|
||||||
// Whether selected element(s) were duplicated, might change during the
|
// Whether selected element(s) were duplicated, might change during the
|
||||||
// pointer interation
|
// pointer interaction
|
||||||
hasBeenDuplicated: boolean;
|
hasBeenDuplicated: boolean;
|
||||||
|
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
||||||
};
|
};
|
||||||
drag: {
|
drag: {
|
||||||
// Might change during the pointer interation
|
// Might change during the pointer interation
|
||||||
@ -1713,7 +1720,32 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): NonDeleted<ExcalidrawElement> | null {
|
): NonDeleted<ExcalidrawElement> | null {
|
||||||
return getElementAtPosition(this.scene.getElements(), (element) =>
|
const allHitElements = this.getElementsAtPosition(x, y);
|
||||||
|
if (allHitElements.length > 1) {
|
||||||
|
const elementWithHighestZIndex =
|
||||||
|
allHitElements[allHitElements.length - 1];
|
||||||
|
// If we're hitting element with highest z-index only on its bounding box
|
||||||
|
// while also hitting other element figure, the latter should be considered.
|
||||||
|
return isHittingElementBoundingBoxWithoutHittingElement(
|
||||||
|
elementWithHighestZIndex,
|
||||||
|
this.state,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
)
|
||||||
|
? allHitElements[allHitElements.length - 2]
|
||||||
|
: elementWithHighestZIndex;
|
||||||
|
}
|
||||||
|
if (allHitElements.length === 1) {
|
||||||
|
return allHitElements[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getElementsAtPosition(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): NonDeleted<ExcalidrawElement>[] {
|
||||||
|
return getElementsAtPosition(this.scene.getElements(), (element) =>
|
||||||
hitTest(element, this.state, x, y),
|
hitTest(element, this.state, x, y),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2084,14 +2116,27 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hitElement = this.getElementAtPosition(scenePointerX, scenePointerY);
|
|
||||||
|
const hitElement = this.getElementAtPosition(
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
);
|
||||||
if (this.state.elementType === "text") {
|
if (this.state.elementType === "text") {
|
||||||
document.documentElement.style.cursor = isTextElement(hitElement)
|
document.documentElement.style.cursor = isTextElement(hitElement)
|
||||||
? CURSOR_TYPE.TEXT
|
? CURSOR_TYPE.TEXT
|
||||||
: CURSOR_TYPE.CROSSHAIR;
|
: CURSOR_TYPE.CROSSHAIR;
|
||||||
|
} else if (isOverScrollBar) {
|
||||||
|
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
|
||||||
|
} else if (
|
||||||
|
hitElement ||
|
||||||
|
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||||
|
scenePointer,
|
||||||
|
selectedElements,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
document.documentElement.style.cursor = CURSOR_TYPE.MOVE;
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.style.cursor =
|
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
|
||||||
hitElement && !isOverScrollBar ? "move" : "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2370,8 +2415,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
},
|
},
|
||||||
hit: {
|
hit: {
|
||||||
element: null,
|
element: null,
|
||||||
|
allHitElements: [],
|
||||||
wasAddedToSelection: false,
|
wasAddedToSelection: false,
|
||||||
hasBeenDuplicated: false,
|
hasBeenDuplicated: false,
|
||||||
|
hasHitCommonBoundingBoxOfSelectedElements: this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||||
|
origin,
|
||||||
|
selectedElements,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
drag: {
|
drag: {
|
||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
@ -2516,13 +2566,26 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.maybeClearSelectionWhenHittingElement(
|
// For overlapped elements one position may hit
|
||||||
event,
|
// multiple elements
|
||||||
pointerDownState.hit.element,
|
pointerDownState.hit.allHitElements = this.getElementsAtPosition(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we click on something
|
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hitElement = pointerDownState.hit.element;
|
||||||
|
const someHitElementIsSelected = pointerDownState.hit.allHitElements.some(
|
||||||
|
(element) => this.isASelectedElement(element),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
(hitElement === null || !someHitElementIsSelected) &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
|
) {
|
||||||
|
this.clearSelection(hitElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we click on something
|
||||||
if (hitElement != null) {
|
if (hitElement != null) {
|
||||||
// deselect if item is selected
|
// deselect if item is selected
|
||||||
// if shift is not clicked, this will always return true
|
// if shift is not clicked, this will always return true
|
||||||
@ -2542,23 +2605,28 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this.setState((prevState) => {
|
|
||||||
return selectGroupsForSelectedElements(
|
// Add hit element to selection. At this point if we're not holding
|
||||||
{
|
// SHIFT the previously selected element(s) were deselected above
|
||||||
...prevState,
|
// (make sure you use setState updater to use latest state)
|
||||||
selectedElementIds: {
|
if (
|
||||||
...prevState.selectedElementIds,
|
!someHitElementIsSelected &&
|
||||||
[hitElement!.id]: true,
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
|
) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
...prevState,
|
||||||
|
selectedElementIds: {
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[hitElement!.id]: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
this.scene.getElements(),
|
||||||
this.scene.getElements(),
|
);
|
||||||
);
|
});
|
||||||
});
|
pointerDownState.hit.wasAddedToSelection = true;
|
||||||
// TODO: this is strange...
|
}
|
||||||
this.scene.replaceAllElements(
|
|
||||||
this.scene.getElementsIncludingDeleted(),
|
|
||||||
);
|
|
||||||
pointerDownState.hit.wasAddedToSelection = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2571,6 +2639,29 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private isASelectedElement(hitElement: ExcalidrawElement | null): boolean {
|
||||||
|
return hitElement != null && this.state.selectedElementIds[hitElement.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isHittingCommonBoundingBoxOfSelectedElements(
|
||||||
|
point: Readonly<{ x: number; y: number }>,
|
||||||
|
selectedElements: readonly ExcalidrawElement[],
|
||||||
|
): boolean {
|
||||||
|
if (selectedElements.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many pixels off the shape boundary we still consider a hit
|
||||||
|
const threshold = 10 / this.state.zoom;
|
||||||
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||||
|
return (
|
||||||
|
point.x > x1 - threshold &&
|
||||||
|
point.x < x2 + threshold &&
|
||||||
|
point.y > y1 - threshold &&
|
||||||
|
point.y < y2 + threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private handleTextOnPointerDown = (
|
private handleTextOnPointerDown = (
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@ -2852,8 +2943,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
|
||||||
if (hitElement && this.state.selectedElementIds[hitElement.id]) {
|
(element) => this.isASelectedElement(element),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
hasHitASelectedElement ||
|
||||||
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
|
) {
|
||||||
// Marking that click was used for dragging to check
|
// Marking that click was used for dragging to check
|
||||||
// if elements should be deselected on pointerup
|
// if elements should be deselected on pointerup
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
@ -2882,12 +2978,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
const elementsToAppend = [];
|
const elementsToAppend = [];
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const oldIdToDuplicatedId = new Map();
|
const oldIdToDuplicatedId = new Map();
|
||||||
|
const hitElement = pointerDownState.hit.element;
|
||||||
for (const element of this.scene.getElementsIncludingDeleted()) {
|
for (const element of this.scene.getElementsIncludingDeleted()) {
|
||||||
if (
|
if (
|
||||||
this.state.selectedElementIds[element.id] ||
|
this.state.selectedElementIds[element.id] ||
|
||||||
// case: the state.selectedElementIds might not have been
|
// case: the state.selectedElementIds might not have been
|
||||||
// updated yet by the time this mousemove event is fired
|
// updated yet by the time this mousemove event is fired
|
||||||
(element.id === hitElement.id &&
|
(element.id === hitElement?.id &&
|
||||||
pointerDownState.hit.wasAddedToSelection)
|
pointerDownState.hit.wasAddedToSelection)
|
||||||
) {
|
) {
|
||||||
const duplicatedElement = duplicateElement(
|
const duplicatedElement = duplicateElement(
|
||||||
@ -3125,6 +3222,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(draggingElement)) {
|
if (isLinearElement(draggingElement)) {
|
||||||
if (draggingElement!.points.length > 1) {
|
if (draggingElement!.points.length > 1) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
@ -3135,6 +3233,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.canvas,
|
this.canvas,
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
draggingElement &&
|
draggingElement &&
|
||||||
@ -3186,6 +3285,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3230,35 +3330,111 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If click occurred on already selected element
|
// Code below handles selection when element(s) weren't
|
||||||
// it is needed to remove selection from other elements
|
// drag or added to selection on pointer down phase.
|
||||||
// or if SHIFT or META key pressed remove selection
|
|
||||||
// from hitted element
|
|
||||||
//
|
|
||||||
// If click occurred and elements were dragged or some element
|
|
||||||
// was added to selection (on pointerdown phase) we need to keep
|
|
||||||
// selection unchanged
|
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hitElement = pointerDownState.hit.element;
|
||||||
if (
|
if (
|
||||||
getSelectedGroupIds(this.state).length === 0 &&
|
|
||||||
hitElement &&
|
hitElement &&
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
!pointerDownState.hit.wasAddedToSelection
|
!pointerDownState.hit.wasAddedToSelection
|
||||||
) {
|
) {
|
||||||
if (childEvent.shiftKey) {
|
if (childEvent.shiftKey) {
|
||||||
this.setState((prevState) => ({
|
if (this.state.selectedElementIds[hitElement.id]) {
|
||||||
selectedElementIds: {
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
||||||
...prevState.selectedElementIds,
|
// We want to unselect all groups hitElement is part of
|
||||||
[hitElement!.id]: false,
|
// as well as all elements that are part of the groups
|
||||||
},
|
// hitElement is part of
|
||||||
}));
|
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
||||||
|
.flatMap((groupId) =>
|
||||||
|
getElementsInGroup(this.scene.getElements(), groupId),
|
||||||
|
)
|
||||||
|
.map((element) => ({ [element.id]: false }))
|
||||||
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
|
||||||
|
|
||||||
|
this.setState((_prevState) => ({
|
||||||
|
selectedGroupIds: {
|
||||||
|
..._prevState.selectedElementIds,
|
||||||
|
...hitElement.groupIds
|
||||||
|
.map((gId) => ({ [gId]: false }))
|
||||||
|
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
|
||||||
|
},
|
||||||
|
selectedElementIds: {
|
||||||
|
..._prevState.selectedElementIds,
|
||||||
|
...idsOfSelectedElementsThatAreInGroups,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// remove element from selection while
|
||||||
|
// keeping prev elements selected
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
selectedElementIds: {
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[hitElement!.id]: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// add element to selection while
|
||||||
|
// keeping prev elements selected
|
||||||
|
this.setState((_prevState) => ({
|
||||||
|
selectedElementIds: {
|
||||||
|
..._prevState.selectedElementIds,
|
||||||
|
[hitElement!.id]: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState((_prevState) => ({
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
||||||
selectedElementIds: { [hitElement!.id]: true },
|
/*
|
||||||
}));
|
We want to select the group(s) the hit element is in not the particular element.
|
||||||
|
That means we have to deselect elements that are not part of the groups of the
|
||||||
|
hit element, while keeping the elements that are.
|
||||||
|
*/
|
||||||
|
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
||||||
|
.flatMap((groupId) =>
|
||||||
|
getElementsInGroup(this.scene.getElements(), groupId),
|
||||||
|
)
|
||||||
|
.map((element) => ({ [element.id]: true }))
|
||||||
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
|
||||||
|
|
||||||
|
this.setState((_prevState) => ({
|
||||||
|
selectedGroupIds: {
|
||||||
|
...hitElement.groupIds
|
||||||
|
.map((gId) => ({ [gId]: true }))
|
||||||
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {}),
|
||||||
|
},
|
||||||
|
selectedElementIds: { ...idsOfSelectedElementsThatAreInGroups },
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.setState((_prevState) => ({
|
||||||
|
selectedGroupIds: {},
|
||||||
|
selectedElementIds: { [hitElement!.id]: true },
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.state.editingLinearElement &&
|
||||||
|
!pointerDownState.drag.hasOccurred &&
|
||||||
|
!this.state.isResizing &&
|
||||||
|
((hitElement &&
|
||||||
|
isHittingElementBoundingBoxWithoutHittingElement(
|
||||||
|
hitElement,
|
||||||
|
this.state,
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
)) ||
|
||||||
|
(!hitElement &&
|
||||||
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
|
||||||
|
) {
|
||||||
|
// Deselect selected elements
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (draggingElement === null) {
|
if (draggingElement === null) {
|
||||||
// if no element is clicked, clear the selection and redraw
|
// if no element is clicked, clear the selection and redraw
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -3359,17 +3535,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.setState({ suggestedBindings });
|
this.setState({ suggestedBindings });
|
||||||
}
|
}
|
||||||
|
|
||||||
private maybeClearSelectionWhenHittingElement(
|
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
|
||||||
hitElement: ExcalidrawElement | null,
|
|
||||||
): void {
|
|
||||||
const isHittingASelectedElement =
|
|
||||||
hitElement != null && this.state.selectedElementIds[hitElement.id];
|
|
||||||
|
|
||||||
// clear selection if shift is not clicked
|
|
||||||
if (isHittingASelectedElement || event.shiftKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
@ -3713,5 +3879,4 @@ if (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -11,6 +11,8 @@ export const CURSOR_TYPE = {
|
|||||||
CROSSHAIR: "crosshair",
|
CROSSHAIR: "crosshair",
|
||||||
GRABBING: "grabbing",
|
GRABBING: "grabbing",
|
||||||
POINTER: "pointer",
|
POINTER: "pointer",
|
||||||
|
MOVE: "move",
|
||||||
|
AUTO: "",
|
||||||
};
|
};
|
||||||
export const POINTER_BUTTON = {
|
export const POINTER_BUTTON = {
|
||||||
MAIN: 0,
|
MAIN: 0,
|
||||||
|
@ -48,9 +48,33 @@ export const hitTest = (
|
|||||||
const point: Point = [x, y];
|
const point: Point = [x, y];
|
||||||
|
|
||||||
if (isElementSelected(appState, element)) {
|
if (isElementSelected(appState, element)) {
|
||||||
return doesPointHitElementBoundingBox(element, point, threshold);
|
return isPointHittingElementBoundingBox(element, point, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isHittingElementNotConsideringBoundingBox(element, appState, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isHittingElementBoundingBoxWithoutHittingElement = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
appState: AppState,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): boolean => {
|
||||||
|
const threshold = 10 / appState.zoom;
|
||||||
|
|
||||||
|
return (
|
||||||
|
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
|
||||||
|
isPointHittingElementBoundingBox(element, [x, y], threshold)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHittingElementNotConsideringBoundingBox = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
appState: AppState,
|
||||||
|
point: Point,
|
||||||
|
): boolean => {
|
||||||
|
const threshold = 10 / appState.zoom;
|
||||||
|
|
||||||
const check =
|
const check =
|
||||||
element.type === "text"
|
element.type === "text"
|
||||||
? isStrictlyInside
|
? isStrictlyInside
|
||||||
@ -65,7 +89,7 @@ const isElementSelected = (
|
|||||||
element: NonDeleted<ExcalidrawElement>,
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
) => appState.selectedElementIds[element.id];
|
) => appState.selectedElementIds[element.id];
|
||||||
|
|
||||||
const doesPointHitElementBoundingBox = (
|
const isPointHittingElementBoundingBox = (
|
||||||
element: NonDeleted<ExcalidrawElement>,
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
[x, y]: Point,
|
[x, y]: Point,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
|
@ -26,7 +26,10 @@ export {
|
|||||||
getTransformHandlesFromCoords,
|
getTransformHandlesFromCoords,
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
export { hitTest } from "./collision";
|
export {
|
||||||
|
hitTest,
|
||||||
|
isHittingElementBoundingBoxWithoutHittingElement,
|
||||||
|
} from "./collision";
|
||||||
export {
|
export {
|
||||||
resizeTest,
|
resizeTest,
|
||||||
getCursorForResizingElement,
|
getCursorForResizingElement,
|
||||||
|
@ -171,7 +171,7 @@ export class LinearElementEditor {
|
|||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
): {
|
): {
|
||||||
didAddPoint: boolean;
|
didAddPoint: boolean;
|
||||||
hitElement: ExcalidrawElement | null;
|
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||||
} {
|
} {
|
||||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||||
didAddPoint: false,
|
didAddPoint: false,
|
||||||
|
@ -34,6 +34,8 @@ export const getElementAtPosition = (
|
|||||||
) => {
|
) => {
|
||||||
let hitElement = null;
|
let hitElement = null;
|
||||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
|
// because array is ordered from lower z-index to highest and we want element z-index
|
||||||
|
// with higher z-index
|
||||||
for (let i = elements.length - 1; i >= 0; --i) {
|
for (let i = elements.length - 1; i >= 0; --i) {
|
||||||
const element = elements[i];
|
const element = elements[i];
|
||||||
if (element.isDeleted) {
|
if (element.isDeleted) {
|
||||||
@ -48,6 +50,17 @@ export const getElementAtPosition = (
|
|||||||
return hitElement;
|
return hitElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getElementsAtPosition = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||||
|
) => {
|
||||||
|
// The parameter elements comes ordered from lower z-index to higher.
|
||||||
|
// We want to preserve that order on the returned array.
|
||||||
|
return elements.filter(
|
||||||
|
(element) => !element.isDeleted && isAtPositionFn(element),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getElementContainingPosition = (
|
export const getElementContainingPosition = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
x: number,
|
x: number,
|
||||||
|
@ -14,5 +14,6 @@ export {
|
|||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
getElementContainingPosition,
|
getElementContainingPosition,
|
||||||
hasText,
|
hasText,
|
||||||
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
export { getZoomOrigin, getNormalizedZoom } from "./zoom";
|
export { getZoomOrigin, getNormalizedZoom } from "./zoom";
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1213,13 +1213,478 @@ describe("regression tests", () => {
|
|||||||
expect(h.elements[1].groupIds).toHaveLength(0);
|
expect(h.elements[1].groupIds).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps selected element selected when click hits element bounding box but doesn't hit the element", () => {
|
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
|
||||||
clickTool("ellipse");
|
clickTool("ellipse");
|
||||||
mouse.down(0, 0);
|
mouse.down();
|
||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
// click on bounding box but not on element
|
// hits bounding box without hitting element
|
||||||
mouse.click(0, 0);
|
mouse.down();
|
||||||
expect(getSelectedElements().length).toBe(1);
|
expect(getSelectedElements().length).toBe(1);
|
||||||
|
mouse.up();
|
||||||
|
expect(getSelectedElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches selected element on pointer down", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("ellipse");
|
||||||
|
|
||||||
|
// pointer down on rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down();
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("rectangle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can drag element that covers another element, while another elem is selected", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(200, 200);
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(200, 200);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(300, 300);
|
||||||
|
mouse.up(350, 350);
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("ellipse");
|
||||||
|
|
||||||
|
// pointer down on rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(200, 200);
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("rectangle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deselects selected element on pointer down when pointer doesn't hit any element", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(1);
|
||||||
|
|
||||||
|
// pointer down on space without elements
|
||||||
|
mouse.down(100, 100);
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Drags selected element when hitting only bounding box and keeps element selected", () => {
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
const { x: prevX, y: prevY } = getSelectedElement();
|
||||||
|
|
||||||
|
// drag element from point on bounding box that doesn't hit element
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(25, 25);
|
||||||
|
|
||||||
|
expect(getSelectedElement().x).toEqual(prevX + 25);
|
||||||
|
expect(getSelectedElement().y).toEqual(prevY + 25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"given selected element A with lower z-index than unselected element B and given B is partially over A " +
|
||||||
|
"when clicking intersection between A and B " +
|
||||||
|
"B should be selected on pointer up",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
// change background color since default is transparent
|
||||||
|
// and transparent elements can't be selected by clicking inside of them
|
||||||
|
clickLabeledElement("Background");
|
||||||
|
clickLabeledElement("#fa5252");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// draw ellipse partially over rectangle.
|
||||||
|
// since ellipse was created after rectangle it has an higher z-index.
|
||||||
|
// we don't need to change background color again since change above
|
||||||
|
// affects next drawn elements.
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(500, 500);
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// select rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
// pointer down on intersection between ellipse and rectangle
|
||||||
|
mouse.down(900, 900);
|
||||||
|
expect(getSelectedElement().type).toBe("rectangle");
|
||||||
|
|
||||||
|
mouse.up();
|
||||||
|
expect(getSelectedElement().type).toBe("ellipse");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"given selected element A with lower z-index than unselected element B and given B is partially over A " +
|
||||||
|
"when dragging on intersection between A and B " +
|
||||||
|
"A should be dragged and keep being selected",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
// change background color since default is transparent
|
||||||
|
// and transparent elements can't be selected by clicking inside of them
|
||||||
|
clickLabeledElement("Background");
|
||||||
|
clickLabeledElement("#fa5252");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// draw ellipse partially over rectangle.
|
||||||
|
// since ellipse was created after rectangle it has an higher z-index.
|
||||||
|
// we don't need to change background color again since change above
|
||||||
|
// affects next drawn elements.
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(500, 500);
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// select rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const { x: prevX, y: prevY } = getSelectedElement();
|
||||||
|
|
||||||
|
// pointer down on intersection between ellipse and rectangle
|
||||||
|
mouse.down(900, 900);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("rectangle");
|
||||||
|
expect(getSelectedElement().x).toEqual(prevX + 100);
|
||||||
|
expect(getSelectedElement().y).toEqual(prevY + 100);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("deselects group of selected elements on pointer down when pointer doesn't hit any element", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
// Selects first element without deselecting the second element
|
||||||
|
// Second element is already selected because creating it was our last action
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
|
||||||
|
// pointer down on space without elements
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(500, 500);
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches from group of selected elements to another element on pointer down", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("diamond");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
// Selects ellipse without deselecting the diamond
|
||||||
|
// Diamond is already selected because creating it was our last action
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(110, 160);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
|
||||||
|
// select rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down();
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe("rectangle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
// Selects first element without deselecting the second element
|
||||||
|
// Second element is already selected because creating it was our last action
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
// pointer down on common bounding box without hitting any of the elements
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(50, 50);
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
|
||||||
|
mouse.up();
|
||||||
|
expect(getSelectedElements().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"drags selected elements from point inside common bounding box that doesn't hit any element " +
|
||||||
|
"and keeps elements selected after dragging",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
// Selects first element without deselecting the second element
|
||||||
|
// Second element is already selected because creating it was our last action
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: firstElementPrevX,
|
||||||
|
y: firstElementPrevY,
|
||||||
|
} = getSelectedElements()[0];
|
||||||
|
const {
|
||||||
|
x: secondElementPrevX,
|
||||||
|
y: secondElementPrevY,
|
||||||
|
} = getSelectedElements()[1];
|
||||||
|
|
||||||
|
// drag elements from point on common bounding box that doesn't hit any of the elements
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(50, 50);
|
||||||
|
mouse.up(25, 25);
|
||||||
|
|
||||||
|
expect(getSelectedElements()[0].x).toEqual(firstElementPrevX + 25);
|
||||||
|
expect(getSelectedElements()[0].y).toEqual(firstElementPrevY + 25);
|
||||||
|
|
||||||
|
expect(getSelectedElements()[1].x).toEqual(secondElementPrevX + 25);
|
||||||
|
expect(getSelectedElements()[1].y).toEqual(secondElementPrevY + 25);
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"given a group of selected elements with an element that is not selected inside the group common bounding box " +
|
||||||
|
"when element that is not selected is clicked " +
|
||||||
|
"should switch selection to not selected element on pointer up",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("diamond");
|
||||||
|
mouse.down(100, 100);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
// Selects rectangle without deselecting the diamond
|
||||||
|
// Diamond is already selected because creating it was our last action
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// pointer down on ellipse
|
||||||
|
mouse.down(110, 160);
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
|
||||||
|
mouse.up();
|
||||||
|
expect(getSelectedElement().type).toBe("ellipse");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"given a selected element A and a not selected element B with higher z-index than A " +
|
||||||
|
"and given B partialy overlaps A " +
|
||||||
|
"when there's a shift-click on the overlapped section B is added to the selection",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
// change background color since default is transparent
|
||||||
|
// and transparent elements can't be selected by clicking inside of them
|
||||||
|
clickLabeledElement("Background");
|
||||||
|
clickLabeledElement("#fa5252");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// draw ellipse partially over rectangle.
|
||||||
|
// since ellipse was created after rectangle it has an higher z-index.
|
||||||
|
// we don't need to change background color again since change above
|
||||||
|
// affects next drawn elements.
|
||||||
|
clickTool("ellipse");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(500, 500);
|
||||||
|
mouse.up(1000, 1000);
|
||||||
|
|
||||||
|
// select rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
// click on intersection between ellipse and rectangle
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(900, 900);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("shift click on selected element should deselect it on pointer up", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
// Rectangle is already selected since creating
|
||||||
|
// it was our last action
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.down();
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(1);
|
||||||
|
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.up();
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"given element A and group of elements B and given both are selected " +
|
||||||
|
"when user clicks on B, on pointer up " +
|
||||||
|
"only elements from B should be selected",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
// Select first rectangle while keeping third one selected.
|
||||||
|
// Third rectangle is selected because it was the last element
|
||||||
|
// to be created.
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create group with first and third rectangle
|
||||||
|
withModifierKeys({ ctrl: true }, () => {
|
||||||
|
keyPress("g");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
||||||
|
expect(selectedGroupIds.length).toBe(1);
|
||||||
|
|
||||||
|
// Select second rectangle without deselecting group
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(110, 110);
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(3);
|
||||||
|
|
||||||
|
// pointer down on first rectangle that is
|
||||||
|
// part of the group
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down();
|
||||||
|
expect(getSelectedElements().length).toBe(3);
|
||||||
|
|
||||||
|
// should only deselect on pointer up
|
||||||
|
mouse.up();
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
const newSelectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
||||||
|
expect(newSelectedGroupIds.length).toBe(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"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",
|
||||||
|
() => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down();
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(100, 100);
|
||||||
|
|
||||||
|
// Select first rectangle while keeping third one selected.
|
||||||
|
// Third rectangle is selected because it was the last element
|
||||||
|
// to be created.
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create group with first and third rectangle
|
||||||
|
withModifierKeys({ ctrl: true }, () => {
|
||||||
|
keyPress("g");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getSelectedElements().length).toBe(2);
|
||||||
|
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
||||||
|
expect(selectedGroupIds.length).toBe(1);
|
||||||
|
|
||||||
|
// Select second rectangle without deselecting group
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(110, 110);
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(3);
|
||||||
|
|
||||||
|
// pointer down o first rectangle that is
|
||||||
|
// part of the group
|
||||||
|
mouse.reset();
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.down();
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(3);
|
||||||
|
withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.up();
|
||||||
|
});
|
||||||
|
expect(getSelectedElements().length).toBe(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user