mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Refactor ExcalidrawElement (#874)
* Get rid of isSelected, canvas, canvasZoom, canvasOffsetX and canvasOffsetY on ExcalidrawElement. * Fix most unit tests. Fix cmd a. Fix alt drag * Focus on paste * shift select should include previously selected items * Fix last test * Move this.shape out of ExcalidrawElement and into a WeakMap
This commit is contained in:
parent
8ecb4201db
commit
ccbbdb75a6
39 changed files with 416 additions and 306 deletions
|
@ -19,7 +19,6 @@ import {
|
|||
normalizeDimensions,
|
||||
} from "../element";
|
||||
import {
|
||||
clearSelection,
|
||||
deleteSelectedElements,
|
||||
getElementsWithinSelection,
|
||||
isOverScrollBars,
|
||||
|
@ -77,6 +76,7 @@ import {
|
|||
} from "../constants";
|
||||
import { LayerUI } from "./LayerUI";
|
||||
import { ScrollBars } from "../scene/types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TEST HOOKS
|
||||
|
@ -179,8 +179,8 @@ export class App extends React.Component<any, AppState> {
|
|||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements);
|
||||
elements = deleteSelectedElements(elements);
|
||||
copyToAppClipboard(elements, this.state);
|
||||
elements = deleteSelectedElements(elements, this.state);
|
||||
history.resumeRecording();
|
||||
this.setState({});
|
||||
event.preventDefault();
|
||||
|
@ -189,7 +189,7 @@ export class App extends React.Component<any, AppState> {
|
|||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements);
|
||||
copyToAppClipboard(elements, this.state);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
|
@ -296,7 +296,7 @@ export class App extends React.Component<any, AppState> {
|
|||
public state: AppState = getDefaultAppState();
|
||||
|
||||
private onResize = () => {
|
||||
elements = elements.map(el => ({ ...el, shape: null }));
|
||||
elements.forEach(element => invalidateShapeForElement(element));
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
|
@ -325,7 +325,7 @@ export class App extends React.Component<any, AppState> {
|
|||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||
: ELEMENT_TRANSLATE_AMOUNT;
|
||||
elements = elements.map(el => {
|
||||
if (el.isSelected) {
|
||||
if (this.state.selectedElementIds[el.id]) {
|
||||
const element = { ...el };
|
||||
if (event.key === KEYS.ARROW_LEFT) {
|
||||
element.x -= step;
|
||||
|
@ -361,19 +361,18 @@ export class App extends React.Component<any, AppState> {
|
|||
if (this.state.elementType === "selection") {
|
||||
resetCursor();
|
||||
} else {
|
||||
elements = clearSelection(elements);
|
||||
document.documentElement.style.cursor =
|
||||
this.state.elementType === "text"
|
||||
? CURSOR_TYPE.TEXT
|
||||
: CURSOR_TYPE.CROSSHAIR;
|
||||
this.setState({});
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
isHoldingSpace = false;
|
||||
}
|
||||
};
|
||||
|
||||
private copyToAppClipboard = () => {
|
||||
copyToAppClipboard(elements);
|
||||
copyToAppClipboard(elements, this.state);
|
||||
};
|
||||
|
||||
private pasteFromClipboard = async (event: ClipboardEvent | null) => {
|
||||
|
@ -413,9 +412,8 @@ export class App extends React.Component<any, AppState> {
|
|||
this.state.currentItemFont,
|
||||
);
|
||||
|
||||
element.isSelected = true;
|
||||
|
||||
elements = [...clearSelection(elements), element];
|
||||
elements = [...elements, element];
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
history.resumeRecording();
|
||||
}
|
||||
this.selectShapeTool("selection");
|
||||
|
@ -431,9 +429,10 @@ export class App extends React.Component<any, AppState> {
|
|||
document.activeElement.blur();
|
||||
}
|
||||
if (elementType !== "selection") {
|
||||
elements = clearSelection(elements);
|
||||
this.setState({ elementType, selectedElementIds: {} });
|
||||
} else {
|
||||
this.setState({ elementType });
|
||||
}
|
||||
this.setState({ elementType });
|
||||
}
|
||||
|
||||
private onGestureStart = (event: GestureEvent) => {
|
||||
|
@ -524,6 +523,7 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
const element = getElementAtPosition(
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
this.state.zoom,
|
||||
|
@ -545,10 +545,8 @@ export class App extends React.Component<any, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!element.isSelected) {
|
||||
elements = clearSelection(elements);
|
||||
element.isSelected = true;
|
||||
this.setState({});
|
||||
if (!this.state.selectedElementIds[element.id]) {
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
}
|
||||
|
||||
ContextMenu.push({
|
||||
|
@ -760,12 +758,16 @@ export class App extends React.Component<any, AppState> {
|
|||
if (this.state.elementType === "selection") {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
elements,
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
);
|
||||
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
const selectedElements = getSelectedElements(
|
||||
elements,
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && resizeElement) {
|
||||
this.setState({
|
||||
resizingElement: resizeElement
|
||||
|
@ -781,13 +783,19 @@ export class App extends React.Component<any, AppState> {
|
|||
} else {
|
||||
hitElement = getElementAtPosition(
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
this.state.zoom,
|
||||
);
|
||||
// clear selection if shift is not clicked
|
||||
if (!hitElement?.isSelected && !event.shiftKey) {
|
||||
elements = clearSelection(elements);
|
||||
if (
|
||||
!(
|
||||
hitElement && this.state.selectedElementIds[hitElement.id]
|
||||
) &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
|
||||
// If we click on something
|
||||
|
@ -796,30 +804,37 @@ export class App extends React.Component<any, AppState> {
|
|||
// if shift is not clicked, this will always return true
|
||||
// otherwise, it will trigger selection based on current
|
||||
// state of the box
|
||||
if (!hitElement.isSelected) {
|
||||
hitElement.isSelected = true;
|
||||
if (!this.state.selectedElementIds[hitElement.id]) {
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement!.id]: true,
|
||||
},
|
||||
}));
|
||||
elements = elements.slice();
|
||||
elementIsAddedToSelection = true;
|
||||
}
|
||||
|
||||
// We duplicate the selected element if alt is pressed on pointer down
|
||||
if (event.altKey) {
|
||||
elements = [
|
||||
...elements.map(element => ({
|
||||
...element,
|
||||
isSelected: false,
|
||||
})),
|
||||
...getSelectedElements(elements).map(element => {
|
||||
const newElement = duplicateElement(element);
|
||||
newElement.isSelected = true;
|
||||
return newElement;
|
||||
}),
|
||||
];
|
||||
// Move the currently selected elements to the top of the z index stack, and
|
||||
// put the duplicates where the selected elements used to be.
|
||||
const nextElements = [];
|
||||
const elementsToAppend = [];
|
||||
for (const element of elements) {
|
||||
if (this.state.selectedElementIds[element.id]) {
|
||||
nextElements.push(duplicateElement(element));
|
||||
elementsToAppend.push(element);
|
||||
} else {
|
||||
nextElements.push(element);
|
||||
}
|
||||
}
|
||||
elements = [...nextElements, ...elementsToAppend];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
elements = clearSelection(elements);
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
|
@ -872,10 +887,15 @@ export class App extends React.Component<any, AppState> {
|
|||
text,
|
||||
this.state.currentItemFont,
|
||||
),
|
||||
isSelected: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[element.id]: true,
|
||||
},
|
||||
}));
|
||||
if (this.state.elementLocked) {
|
||||
setCursorForShape(this.state.elementType);
|
||||
}
|
||||
|
@ -905,13 +925,23 @@ export class App extends React.Component<any, AppState> {
|
|||
if (this.state.multiElement) {
|
||||
const { multiElement } = this.state;
|
||||
const { x: rx, y: ry } = multiElement;
|
||||
multiElement.isSelected = true;
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[multiElement.id]: true,
|
||||
},
|
||||
}));
|
||||
multiElement.points.push([x - rx, y - ry]);
|
||||
multiElement.shape = null;
|
||||
invalidateShapeForElement(multiElement);
|
||||
} else {
|
||||
element.isSelected = false;
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[element.id]: false,
|
||||
},
|
||||
}));
|
||||
element.points.push([0, 0]);
|
||||
element.shape = null;
|
||||
invalidateShapeForElement(element);
|
||||
elements = [...elements, element];
|
||||
this.setState({
|
||||
draggingElement: element,
|
||||
|
@ -1047,7 +1077,10 @@ export class App extends React.Component<any, AppState> {
|
|||
if (isResizingElements && this.state.resizingElement) {
|
||||
this.setState({ isResizing: true });
|
||||
const el = this.state.resizingElement;
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
const selectedElements = getSelectedElements(
|
||||
elements,
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
|
@ -1261,7 +1294,7 @@ export class App extends React.Component<any, AppState> {
|
|||
);
|
||||
el.x = element.x;
|
||||
el.y = element.y;
|
||||
el.shape = null;
|
||||
invalidateShapeForElement(el);
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
|
@ -1270,11 +1303,17 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (hitElement?.isSelected) {
|
||||
if (
|
||||
hitElement &&
|
||||
this.state.selectedElementIds[hitElement.id]
|
||||
) {
|
||||
// Marking that click was used for dragging to check
|
||||
// if elements should be deselected on pointerup
|
||||
draggingOccurred = true;
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
const selectedElements = getSelectedElements(
|
||||
elements,
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length > 0) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
|
@ -1354,19 +1393,30 @@ export class App extends React.Component<any, AppState> {
|
|||
draggingElement.height = height;
|
||||
}
|
||||
|
||||
draggingElement.shape = null;
|
||||
invalidateShapeForElement(draggingElement);
|
||||
|
||||
if (this.state.elementType === "selection") {
|
||||
if (!event.shiftKey && isSomeElementSelected(elements)) {
|
||||
elements = clearSelection(elements);
|
||||
if (
|
||||
!event.shiftKey &&
|
||||
isSomeElementSelected(elements, this.state)
|
||||
) {
|
||||
this.setState({ selectedElementIds: {} });
|
||||
}
|
||||
const elementsWithinSelection = getElementsWithinSelection(
|
||||
elements,
|
||||
draggingElement,
|
||||
);
|
||||
elementsWithinSelection.forEach(element => {
|
||||
element.isSelected = true;
|
||||
});
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
...Object.fromEntries(
|
||||
elementsWithinSelection.map(element => [
|
||||
element.id,
|
||||
true,
|
||||
]),
|
||||
),
|
||||
},
|
||||
}));
|
||||
}
|
||||
this.setState({});
|
||||
};
|
||||
|
@ -1406,20 +1456,27 @@ export class App extends React.Component<any, AppState> {
|
|||
x - draggingElement.x,
|
||||
y - draggingElement.y,
|
||||
]);
|
||||
draggingElement.shape = null;
|
||||
invalidateShapeForElement(draggingElement);
|
||||
this.setState({ multiElement: this.state.draggingElement });
|
||||
} else if (draggingOccurred && !multiElement) {
|
||||
this.state.draggingElement!.isSelected = true;
|
||||
if (!elementLocked) {
|
||||
resetCursor();
|
||||
this.setState({
|
||||
this.setState(prevState => ({
|
||||
draggingElement: null,
|
||||
elementType: "selection",
|
||||
});
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[this.state.draggingElement!.id]: true,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
this.setState({
|
||||
this.setState(prevState => ({
|
||||
draggingElement: null,
|
||||
});
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[this.state.draggingElement!.id]: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
@ -1470,27 +1527,37 @@ export class App extends React.Component<any, AppState> {
|
|||
!elementIsAddedToSelection
|
||||
) {
|
||||
if (event.shiftKey) {
|
||||
hitElement.isSelected = false;
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement!.id]: false,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
elements = clearSelection(elements);
|
||||
hitElement.isSelected = true;
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: { [hitElement!.id]: true },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (draggingElement === null) {
|
||||
// if no element is clicked, clear the selection and redraw
|
||||
elements = clearSelection(elements);
|
||||
this.setState({});
|
||||
this.setState({ selectedElementIds: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elementLocked) {
|
||||
draggingElement.isSelected = true;
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[draggingElement.id]: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
elementType !== "selection" ||
|
||||
isSomeElementSelected(elements)
|
||||
isSomeElementSelected(elements, this.state)
|
||||
) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
|
@ -1524,6 +1591,7 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
const elementAtPosition = getElementAtPosition(
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
this.state.zoom,
|
||||
|
@ -1616,10 +1684,15 @@ export class App extends React.Component<any, AppState> {
|
|||
// we need to recreate the element to update dimensions &
|
||||
// position
|
||||
...newTextElement(element, text, element.font),
|
||||
isSelected: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[element.id]: true,
|
||||
},
|
||||
}));
|
||||
history.resumeRecording();
|
||||
resetSelection();
|
||||
},
|
||||
|
@ -1695,7 +1768,7 @@ export class App extends React.Component<any, AppState> {
|
|||
const pnt = points[points.length - 1];
|
||||
pnt[0] = x - originX;
|
||||
pnt[1] = y - originY;
|
||||
multiElement.shape = null;
|
||||
invalidateShapeForElement(multiElement);
|
||||
this.setState({});
|
||||
return;
|
||||
}
|
||||
|
@ -1708,10 +1781,14 @@ export class App extends React.Component<any, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
const selectedElements = getSelectedElements(
|
||||
elements,
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1 && !isOverScrollBar) {
|
||||
const resizeElement = getElementWithResizeHandler(
|
||||
elements,
|
||||
this.state,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
|
@ -1725,6 +1802,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
const hitElement = getElementAtPosition(
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
this.state.zoom,
|
||||
|
@ -1782,8 +1860,6 @@ export class App extends React.Component<any, AppState> {
|
|||
private addElementsFromPaste = (
|
||||
clipboardElements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
elements = clearSelection(elements);
|
||||
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
|
||||
|
||||
const elementsCenterX = distance(minX, maxX) / 2;
|
||||
|
@ -1798,17 +1874,20 @@ export class App extends React.Component<any, AppState> {
|
|||
const dx = x - elementsCenterX;
|
||||
const dy = y - elementsCenterY;
|
||||
|
||||
elements = [
|
||||
...elements,
|
||||
...clipboardElements.map(clipboardElements => {
|
||||
const duplicate = duplicateElement(clipboardElements);
|
||||
duplicate.x += dx - minX;
|
||||
duplicate.y += dy - minY;
|
||||
return duplicate;
|
||||
}),
|
||||
];
|
||||
const newElements = clipboardElements.map(clipboardElements => {
|
||||
const duplicate = duplicateElement(clipboardElements);
|
||||
duplicate.x += dx - minX;
|
||||
duplicate.y += dy - minY;
|
||||
return duplicate;
|
||||
});
|
||||
|
||||
elements = [...elements, ...newElements];
|
||||
history.resumeRecording();
|
||||
this.setState({});
|
||||
this.setState({
|
||||
selectedElementIds: Object.fromEntries(
|
||||
newElements.map(element => [element.id, true]),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
|
||||
|
@ -1845,6 +1924,7 @@ export class App extends React.Component<any, AppState> {
|
|||
componentDidUpdate() {
|
||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
||||
elements,
|
||||
this.state,
|
||||
this.state.selectionElement,
|
||||
this.rc!,
|
||||
this.canvas!,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue