|
@@ -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!,
|