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