mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	Feature: Action System (#298)
* Add Action System - Add keyboard test - Add context menu label - Add PanelComponent * Show context menu items based on actions * Add render action feature - Replace bringForward etc buttons with action manager render functions * Move all property changes and canvas into actions * Remove unnecessary functions and add forgotten force update when elements array change * Extract export operations into actions * Add elements and app state as arguments to `keyTest` function * Add key priorities - Sort actions by key priority when handling key presses * Extract copy/paste styles * Add Context Menu Item order - Sort context menu items based on menu item order parameter * Remove unnecessary functions from App component
This commit is contained in:
		
				
					committed by
					
						
						Christopher Chedeau
					
				
			
			
				
	
			
			
			
						parent
						
							c253c0b635
						
					
				
				
					commit
					f465121f9b
				
			
							
								
								
									
										47
									
								
								src/actions/actionCanvas.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/actions/actionCanvas.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { ColorPicker } from "../components/ColorPicker";
 | 
			
		||||
 | 
			
		||||
export const actionChangeViewBackgroundColor: Action = {
 | 
			
		||||
  name: "changeViewBackgroundColor",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return { appState: { ...appState, viewBackgroundColor: value } };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Canvas Background Color</h5>
 | 
			
		||||
      <ColorPicker
 | 
			
		||||
        color={appState.viewBackgroundColor}
 | 
			
		||||
        onChange={color => updateData(color)}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionClearCanvas: Action = {
 | 
			
		||||
  name: "clearCanvas",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: [],
 | 
			
		||||
      appState: {
 | 
			
		||||
        ...appState,
 | 
			
		||||
        viewBackgroundColor: "#ffffff",
 | 
			
		||||
        scrollX: 0,
 | 
			
		||||
        scrollY: 0
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button
 | 
			
		||||
      type="button"
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        if (window.confirm("This will clear the whole canvas. Are you sure?")) {
 | 
			
		||||
          updateData(null);
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      title="Clear the canvas & reset background color"
 | 
			
		||||
    >
 | 
			
		||||
      Clear canvas
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										19
									
								
								src/actions/actionDeleteSelected.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/actions/actionDeleteSelected.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { deleteSelectedElements } from "../scene";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
 | 
			
		||||
export const actionDeleteSelected: Action = {
 | 
			
		||||
  name: "deleteSelectedElements",
 | 
			
		||||
  perform: elements => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: deleteSelectedElements(elements)
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Delete",
 | 
			
		||||
  contextMenuOrder: 3,
 | 
			
		||||
  keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button onClick={() => updateData(null)}>Delete selected</button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										70
									
								
								src/actions/actionExport.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/actions/actionExport.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { EditableText } from "../components/EditableText";
 | 
			
		||||
import { saveAsJSON, loadFromJSON } from "../scene";
 | 
			
		||||
 | 
			
		||||
export const actionChangeProjectName: Action = {
 | 
			
		||||
  name: "changeProjectName",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return { appState: { ...appState, name: value } };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Name</h5>
 | 
			
		||||
      {appState.name && (
 | 
			
		||||
        <EditableText
 | 
			
		||||
          value={appState.name}
 | 
			
		||||
          onChange={(name: string) => updateData(name)}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeExportBackground: Action = {
 | 
			
		||||
  name: "changeExportBackground",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return { appState: { ...appState, exportBackground: value } };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <label>
 | 
			
		||||
      <input
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        checked={appState.exportBackground}
 | 
			
		||||
        onChange={e => {
 | 
			
		||||
          updateData(e.target.checked);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      background
 | 
			
		||||
    </label>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionSaveScene: Action = {
 | 
			
		||||
  name: "saveScene",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    saveAsJSON(elements, appState.name);
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button onClick={() => updateData(null)}>Save as...</button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionLoadScene: Action = {
 | 
			
		||||
  name: "loadScene",
 | 
			
		||||
  perform: (elements, appState, loadedElements) => {
 | 
			
		||||
    return { elements: loadedElements };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        loadFromJSON().then(({ elements }) => {
 | 
			
		||||
          updateData(elements);
 | 
			
		||||
        });
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      Load file...
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										251
									
								
								src/actions/actionProperties.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/actions/actionProperties.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
 | 
			
		||||
import { getSelectedAttribute } from "../scene";
 | 
			
		||||
import { ButtonSelect } from "../components/ButtonSelect";
 | 
			
		||||
import { PanelColor } from "../components/panels/PanelColor";
 | 
			
		||||
import { isTextElement, redrawTextBoundingBox } from "../element";
 | 
			
		||||
 | 
			
		||||
const changeProperty = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
  callback: (element: ExcalidrawElement) => ExcalidrawElement
 | 
			
		||||
) => {
 | 
			
		||||
  return elements.map(element => {
 | 
			
		||||
    if (element.isSelected) {
 | 
			
		||||
      return callback(element);
 | 
			
		||||
    }
 | 
			
		||||
    return element;
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeStrokeColor: Action = {
 | 
			
		||||
  name: "changeStrokeColor",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        strokeColor: value
 | 
			
		||||
      })),
 | 
			
		||||
      appState: { ...appState, currentItemStrokeColor: value }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => (
 | 
			
		||||
    <PanelColor
 | 
			
		||||
      title="Stroke Color"
 | 
			
		||||
      onColorChange={(color: string) => {
 | 
			
		||||
        updateData(color);
 | 
			
		||||
      }}
 | 
			
		||||
      colorValue={getSelectedAttribute(
 | 
			
		||||
        elements,
 | 
			
		||||
        element => element.strokeColor
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeBackgroundColor: Action = {
 | 
			
		||||
  name: "changeBackgroundColor",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        backgroundColor: value
 | 
			
		||||
      })),
 | 
			
		||||
      appState: { ...appState, currentItemBackgroundColor: value }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, updateData }) => (
 | 
			
		||||
    <PanelColor
 | 
			
		||||
      title="Background Color"
 | 
			
		||||
      onColorChange={(color: string) => {
 | 
			
		||||
        updateData(color);
 | 
			
		||||
      }}
 | 
			
		||||
      colorValue={getSelectedAttribute(
 | 
			
		||||
        elements,
 | 
			
		||||
        element => element.backgroundColor
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeFillStyle: Action = {
 | 
			
		||||
  name: "changeFillStyle",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        fillStyle: value
 | 
			
		||||
      }))
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Fill</h5>
 | 
			
		||||
      <ButtonSelect
 | 
			
		||||
        options={[
 | 
			
		||||
          { value: "solid", text: "Solid" },
 | 
			
		||||
          { value: "hachure", text: "Hachure" },
 | 
			
		||||
          { value: "cross-hatch", text: "Cross-hatch" }
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getSelectedAttribute(elements, element => element.fillStyle)}
 | 
			
		||||
        onChange={value => {
 | 
			
		||||
          updateData(value);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeStrokeWidth: Action = {
 | 
			
		||||
  name: "changeStrokeWidth",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        strokeWidth: value
 | 
			
		||||
      }))
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Stroke Width</h5>
 | 
			
		||||
      <ButtonSelect
 | 
			
		||||
        options={[
 | 
			
		||||
          { value: 1, text: "Thin" },
 | 
			
		||||
          { value: 2, text: "Bold" },
 | 
			
		||||
          { value: 4, text: "Extra Bold" }
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getSelectedAttribute(elements, element => element.strokeWidth)}
 | 
			
		||||
        onChange={value => updateData(value)}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeSloppiness: Action = {
 | 
			
		||||
  name: "changeSloppiness",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        roughness: value
 | 
			
		||||
      }))
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Sloppiness</h5>
 | 
			
		||||
      <ButtonSelect
 | 
			
		||||
        options={[
 | 
			
		||||
          { value: 0, text: "Draftsman" },
 | 
			
		||||
          { value: 1, text: "Artist" },
 | 
			
		||||
          { value: 3, text: "Cartoonist" }
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getSelectedAttribute(elements, element => element.roughness)}
 | 
			
		||||
        onChange={value => updateData(value)}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeOpacity: Action = {
 | 
			
		||||
  name: "changeOpacity",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => ({
 | 
			
		||||
        ...el,
 | 
			
		||||
        opacity: value
 | 
			
		||||
      }))
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Opacity</h5>
 | 
			
		||||
      <input
 | 
			
		||||
        type="range"
 | 
			
		||||
        min="0"
 | 
			
		||||
        max="100"
 | 
			
		||||
        onChange={e => updateData(+e.target.value)}
 | 
			
		||||
        value={
 | 
			
		||||
          getSelectedAttribute(elements, element => element.opacity) ||
 | 
			
		||||
          0 /* Put the opacity at 0 if there are two conflicting ones */
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeFontSize: Action = {
 | 
			
		||||
  name: "changeFontSize",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => {
 | 
			
		||||
        if (isTextElement(el)) {
 | 
			
		||||
          const element: ExcalidrawTextElement = {
 | 
			
		||||
            ...el,
 | 
			
		||||
            font: `${value}px ${el.font.split("px ")[1]}`
 | 
			
		||||
          };
 | 
			
		||||
          redrawTextBoundingBox(element);
 | 
			
		||||
          return element;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return el;
 | 
			
		||||
      })
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Font size</h5>
 | 
			
		||||
      <ButtonSelect
 | 
			
		||||
        options={[
 | 
			
		||||
          { value: 16, text: "Small" },
 | 
			
		||||
          { value: 20, text: "Medium" },
 | 
			
		||||
          { value: 28, text: "Large" },
 | 
			
		||||
          { value: 36, text: "Very Large" }
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getSelectedAttribute(
 | 
			
		||||
          elements,
 | 
			
		||||
          element => isTextElement(element) && +element.font.split("px ")[0]
 | 
			
		||||
        )}
 | 
			
		||||
        onChange={value => updateData(value)}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionChangeFontFamily: Action = {
 | 
			
		||||
  name: "changeFontFamily",
 | 
			
		||||
  perform: (elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: changeProperty(elements, el => {
 | 
			
		||||
        if (isTextElement(el)) {
 | 
			
		||||
          const element: ExcalidrawTextElement = {
 | 
			
		||||
            ...el,
 | 
			
		||||
            font: `${el.font.split("px ")[0]}px ${value}`
 | 
			
		||||
          };
 | 
			
		||||
          redrawTextBoundingBox(element);
 | 
			
		||||
          return element;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return el;
 | 
			
		||||
      })
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, updateData }) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <h5>Font family</h5>
 | 
			
		||||
      <ButtonSelect
 | 
			
		||||
        options={[
 | 
			
		||||
          { value: "Virgil", text: "Virgil" },
 | 
			
		||||
          { value: "Helvetica", text: "Helvetica" },
 | 
			
		||||
          { value: "Courier", text: "Courier" }
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getSelectedAttribute(
 | 
			
		||||
          elements,
 | 
			
		||||
          element => isTextElement(element) && element.font.split("px ")[1]
 | 
			
		||||
        )}
 | 
			
		||||
        onChange={value => updateData(value)}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										13
									
								
								src/actions/actionSelectAll.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/actions/actionSelectAll.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { META_KEY } from "../keys";
 | 
			
		||||
 | 
			
		||||
export const actionSelectAll: Action = {
 | 
			
		||||
  name: "selectAll",
 | 
			
		||||
  perform: elements => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: elements.map(elem => ({ ...elem, isSelected: true }))
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Select All",
 | 
			
		||||
  keyTest: event => event[META_KEY] && event.code === "KeyA"
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										50
									
								
								src/actions/actionStyles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/actions/actionStyles.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import { isTextElement, redrawTextBoundingBox } from "../element";
 | 
			
		||||
import { META_KEY } from "../keys";
 | 
			
		||||
 | 
			
		||||
let copiedStyles: string = "{}";
 | 
			
		||||
 | 
			
		||||
export const actionCopyStyles: Action = {
 | 
			
		||||
  name: "copyStyles",
 | 
			
		||||
  perform: elements => {
 | 
			
		||||
    const element = elements.find(el => el.isSelected);
 | 
			
		||||
    if (element) {
 | 
			
		||||
      copiedStyles = JSON.stringify(element);
 | 
			
		||||
    }
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Copy Styles",
 | 
			
		||||
  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyC",
 | 
			
		||||
  contextMenuOrder: 0
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionPasteStyles: Action = {
 | 
			
		||||
  name: "pasteStyles",
 | 
			
		||||
  perform: elements => {
 | 
			
		||||
    const pastedElement = JSON.parse(copiedStyles);
 | 
			
		||||
    return {
 | 
			
		||||
      elements: elements.map(element => {
 | 
			
		||||
        if (element.isSelected) {
 | 
			
		||||
          const newElement = {
 | 
			
		||||
            ...element,
 | 
			
		||||
            backgroundColor: pastedElement?.backgroundColor,
 | 
			
		||||
            strokeWidth: pastedElement?.strokeWidth,
 | 
			
		||||
            strokeColor: pastedElement?.strokeColor,
 | 
			
		||||
            fillStyle: pastedElement?.fillStyle,
 | 
			
		||||
            opacity: pastedElement?.opacity,
 | 
			
		||||
            roughness: pastedElement?.roughness
 | 
			
		||||
          };
 | 
			
		||||
          if (isTextElement(newElement)) {
 | 
			
		||||
            newElement.font = pastedElement?.font;
 | 
			
		||||
            redrawTextBoundingBox(newElement);
 | 
			
		||||
          }
 | 
			
		||||
          return newElement;
 | 
			
		||||
        }
 | 
			
		||||
        return element;
 | 
			
		||||
      })
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Paste Styles",
 | 
			
		||||
  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyV",
 | 
			
		||||
  contextMenuOrder: 1
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										82
									
								
								src/actions/actionZindex.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/actions/actionZindex.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action } from "./types";
 | 
			
		||||
import {
 | 
			
		||||
  moveOneLeft,
 | 
			
		||||
  moveOneRight,
 | 
			
		||||
  moveAllLeft,
 | 
			
		||||
  moveAllRight
 | 
			
		||||
} from "../zindex";
 | 
			
		||||
import { getSelectedIndices } from "../scene";
 | 
			
		||||
import { META_KEY } from "../keys";
 | 
			
		||||
 | 
			
		||||
export const actionSendBackward: Action = {
 | 
			
		||||
  name: "sendBackward",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: moveOneLeft([...elements], getSelectedIndices(elements)),
 | 
			
		||||
      appState
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Send Backward",
 | 
			
		||||
  keyPriority: 40,
 | 
			
		||||
  keyTest: event =>
 | 
			
		||||
    event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyB",
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button type="button" onClick={e => updateData(null)}>
 | 
			
		||||
      Send backward
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionBringForward: Action = {
 | 
			
		||||
  name: "bringForward",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: moveOneRight([...elements], getSelectedIndices(elements)),
 | 
			
		||||
      appState
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Bring Forward",
 | 
			
		||||
  keyPriority: 40,
 | 
			
		||||
  keyTest: event =>
 | 
			
		||||
    event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyF",
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button type="button" onClick={e => updateData(null)}>
 | 
			
		||||
      Bring Forward
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionSendToBack: Action = {
 | 
			
		||||
  name: "sendToBack",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: moveAllLeft([...elements], getSelectedIndices(elements)),
 | 
			
		||||
      appState
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Send to Back",
 | 
			
		||||
  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyB",
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button type="button" onClick={e => updateData(null)}>
 | 
			
		||||
      Send to Back
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const actionBringToFront: Action = {
 | 
			
		||||
  name: "bringToFront",
 | 
			
		||||
  perform: (elements, appState) => {
 | 
			
		||||
    return {
 | 
			
		||||
      elements: moveAllRight([...elements], getSelectedIndices(elements)),
 | 
			
		||||
      appState
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  contextItemLabel: "Bring to Front",
 | 
			
		||||
  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyF",
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <button type="button" onClick={e => updateData(null)}>
 | 
			
		||||
      Bring to Front
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										33
									
								
								src/actions/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/actions/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
export { ActionManager } from "./manager";
 | 
			
		||||
export { actionDeleteSelected } from "./actionDeleteSelected";
 | 
			
		||||
export {
 | 
			
		||||
  actionBringForward,
 | 
			
		||||
  actionBringToFront,
 | 
			
		||||
  actionSendBackward,
 | 
			
		||||
  actionSendToBack
 | 
			
		||||
} from "./actionZindex";
 | 
			
		||||
export { actionSelectAll } from "./actionSelectAll";
 | 
			
		||||
export {
 | 
			
		||||
  actionChangeStrokeColor,
 | 
			
		||||
  actionChangeBackgroundColor,
 | 
			
		||||
  actionChangeStrokeWidth,
 | 
			
		||||
  actionChangeFillStyle,
 | 
			
		||||
  actionChangeSloppiness,
 | 
			
		||||
  actionChangeOpacity,
 | 
			
		||||
  actionChangeFontSize,
 | 
			
		||||
  actionChangeFontFamily
 | 
			
		||||
} from "./actionProperties";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  actionChangeViewBackgroundColor,
 | 
			
		||||
  actionClearCanvas
 | 
			
		||||
} from "./actionCanvas";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  actionChangeProjectName,
 | 
			
		||||
  actionChangeExportBackground,
 | 
			
		||||
  actionSaveScene,
 | 
			
		||||
  actionLoadScene
 | 
			
		||||
} from "./actionExport";
 | 
			
		||||
 | 
			
		||||
export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
 | 
			
		||||
							
								
								
									
										89
									
								
								src/actions/manager.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/actions/manager.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Action, ActionsManagerInterface, UpdaterFn } from "./types";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
 | 
			
		||||
export class ActionManager implements ActionsManagerInterface {
 | 
			
		||||
  actions: { [keyProp: string]: Action } = {};
 | 
			
		||||
 | 
			
		||||
  updater:
 | 
			
		||||
    | ((elements: ExcalidrawElement[], appState: AppState) => void)
 | 
			
		||||
    | null = null;
 | 
			
		||||
 | 
			
		||||
  setUpdater(
 | 
			
		||||
    updater: (elements: ExcalidrawElement[], appState: AppState) => void
 | 
			
		||||
  ) {
 | 
			
		||||
    this.updater = updater;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerAction(action: Action) {
 | 
			
		||||
    this.actions[action.name] = action;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyDown(
 | 
			
		||||
    event: KeyboardEvent,
 | 
			
		||||
    elements: readonly ExcalidrawElement[],
 | 
			
		||||
    appState: AppState
 | 
			
		||||
  ) {
 | 
			
		||||
    const data = Object.values(this.actions)
 | 
			
		||||
      .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
 | 
			
		||||
      .filter(
 | 
			
		||||
        action => action.keyTest && action.keyTest(event, elements, appState)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    if (data.length === 0) return {};
 | 
			
		||||
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    return data[0].perform(elements, appState, null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getContextMenuItems(
 | 
			
		||||
    elements: readonly ExcalidrawElement[],
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
    updater: UpdaterFn
 | 
			
		||||
  ) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      Object.values(this.actions)
 | 
			
		||||
        .filter(action => "contextItemLabel" in action)
 | 
			
		||||
        .map(a => ({ name: a.name, label: a.contextItemLabel }))
 | 
			
		||||
    );
 | 
			
		||||
    return Object.values(this.actions)
 | 
			
		||||
      .filter(action => "contextItemLabel" in action)
 | 
			
		||||
      .sort(
 | 
			
		||||
        (a, b) =>
 | 
			
		||||
          (a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
 | 
			
		||||
          (b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999)
 | 
			
		||||
      )
 | 
			
		||||
      .map(action => ({
 | 
			
		||||
        label: action.contextItemLabel!,
 | 
			
		||||
        action: () => {
 | 
			
		||||
          updater(action.perform(elements, appState, null));
 | 
			
		||||
        }
 | 
			
		||||
      }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderAction(
 | 
			
		||||
    name: string,
 | 
			
		||||
    elements: readonly ExcalidrawElement[],
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
    updater: UpdaterFn
 | 
			
		||||
  ) {
 | 
			
		||||
    if (this.actions[name] && "PanelComponent" in this.actions[name]) {
 | 
			
		||||
      const action = this.actions[name];
 | 
			
		||||
      const PanelComponent = action.PanelComponent!;
 | 
			
		||||
      const updateData = (formState: any) => {
 | 
			
		||||
        updater(action.perform(elements, appState, formState));
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <PanelComponent
 | 
			
		||||
          elements={elements}
 | 
			
		||||
          appState={appState}
 | 
			
		||||
          updateData={updateData}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								src/actions/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/actions/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
 | 
			
		||||
export type ActionResult = {
 | 
			
		||||
  elements?: ExcalidrawElement[];
 | 
			
		||||
  appState?: AppState;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ActionFn = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
  appState: AppState,
 | 
			
		||||
  formData: any
 | 
			
		||||
) => ActionResult;
 | 
			
		||||
 | 
			
		||||
export type UpdaterFn = (res: ActionResult) => void;
 | 
			
		||||
 | 
			
		||||
export interface Action {
 | 
			
		||||
  name: string;
 | 
			
		||||
  PanelComponent?: React.FC<{
 | 
			
		||||
    elements: readonly ExcalidrawElement[];
 | 
			
		||||
    appState: AppState;
 | 
			
		||||
    updateData: (formData: any) => void;
 | 
			
		||||
  }>;
 | 
			
		||||
  perform: ActionFn;
 | 
			
		||||
  keyPriority?: number;
 | 
			
		||||
  keyTest?: (
 | 
			
		||||
    event: KeyboardEvent,
 | 
			
		||||
    elements?: readonly ExcalidrawElement[],
 | 
			
		||||
    appState?: AppState
 | 
			
		||||
  ) => boolean;
 | 
			
		||||
  contextItemLabel?: string;
 | 
			
		||||
  contextMenuOrder?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ActionsManagerInterface {
 | 
			
		||||
  actions: {
 | 
			
		||||
    [keyProp: string]: Action;
 | 
			
		||||
  };
 | 
			
		||||
  registerAction: (action: Action) => void;
 | 
			
		||||
  handleKeyDown: (
 | 
			
		||||
    event: KeyboardEvent,
 | 
			
		||||
    elements: readonly ExcalidrawElement[],
 | 
			
		||||
    appState: AppState
 | 
			
		||||
  ) => ActionResult | {};
 | 
			
		||||
  getContextMenuItems: (
 | 
			
		||||
    elements: readonly ExcalidrawElement[],
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
    updater: UpdaterFn
 | 
			
		||||
  ) => { label: string; action: () => void }[];
 | 
			
		||||
  renderAction: (
 | 
			
		||||
    name: string,
 | 
			
		||||
    elements: ExcalidrawElement[],
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
    updater: UpdaterFn
 | 
			
		||||
  ) => React.ReactElement | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -2,55 +2,36 @@ import React from "react";
 | 
			
		||||
import { PanelTools } from "./panels/PanelTools";
 | 
			
		||||
import { Panel } from "./Panel";
 | 
			
		||||
import { PanelSelection } from "./panels/PanelSelection";
 | 
			
		||||
import { PanelColor } from "./panels/PanelColor";
 | 
			
		||||
import {
 | 
			
		||||
  hasBackground,
 | 
			
		||||
  someElementIsSelected,
 | 
			
		||||
  getSelectedAttribute,
 | 
			
		||||
  hasStroke,
 | 
			
		||||
  hasText,
 | 
			
		||||
  loadFromJSON,
 | 
			
		||||
  saveAsJSON,
 | 
			
		||||
  exportCanvas,
 | 
			
		||||
  deleteSelectedElements
 | 
			
		||||
  exportCanvas
 | 
			
		||||
} from "../scene";
 | 
			
		||||
import { ButtonSelect } from "./ButtonSelect";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { redrawTextBoundingBox, isTextElement } from "../element";
 | 
			
		||||
import { PanelCanvas } from "./panels/PanelCanvas";
 | 
			
		||||
import { PanelExport } from "./panels/PanelExport";
 | 
			
		||||
import { ExportType } from "../scene/types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import { ActionManager } from "../actions";
 | 
			
		||||
import { UpdaterFn } from "../actions/types";
 | 
			
		||||
 | 
			
		||||
interface SidePanelProps {
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  elements: readonly ExcalidrawElement[];
 | 
			
		||||
  onToolChange: (elementType: string) => void;
 | 
			
		||||
  changeProperty: (
 | 
			
		||||
    callback: (element: ExcalidrawElement) => ExcalidrawElement
 | 
			
		||||
  ) => void;
 | 
			
		||||
  moveAllLeft: () => void;
 | 
			
		||||
  moveOneLeft: () => void;
 | 
			
		||||
  moveAllRight: () => void;
 | 
			
		||||
  moveOneRight: () => void;
 | 
			
		||||
  onClearCanvas: React.MouseEventHandler;
 | 
			
		||||
  onUpdateAppState: (name: string, value: any) => void;
 | 
			
		||||
  syncActionResult: UpdaterFn;
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  onUpdateElements: (elements: readonly ExcalidrawElement[]) => void;
 | 
			
		||||
  onToolChange: (elementType: string) => void;
 | 
			
		||||
  canvas: HTMLCanvasElement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SidePanel: React.FC<SidePanelProps> = ({
 | 
			
		||||
  actionManager,
 | 
			
		||||
  syncActionResult,
 | 
			
		||||
  elements,
 | 
			
		||||
  onToolChange,
 | 
			
		||||
  changeProperty,
 | 
			
		||||
  moveAllLeft,
 | 
			
		||||
  moveOneLeft,
 | 
			
		||||
  moveAllRight,
 | 
			
		||||
  moveOneRight,
 | 
			
		||||
  onClearCanvas,
 | 
			
		||||
  onUpdateAppState,
 | 
			
		||||
  appState,
 | 
			
		||||
  onUpdateElements,
 | 
			
		||||
  canvas
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -63,209 +44,101 @@ export const SidePanel: React.FC<SidePanelProps> = ({
 | 
			
		||||
      />
 | 
			
		||||
      <Panel title="Selection" hide={!someElementIsSelected(elements)}>
 | 
			
		||||
        <PanelSelection
 | 
			
		||||
          onBringForward={moveOneRight}
 | 
			
		||||
          onBringToFront={moveAllRight}
 | 
			
		||||
          onSendBackward={moveOneLeft}
 | 
			
		||||
          onSendToBack={moveAllLeft}
 | 
			
		||||
          actionManager={actionManager}
 | 
			
		||||
          syncActionResult={syncActionResult}
 | 
			
		||||
          elements={elements}
 | 
			
		||||
          appState={appState}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <PanelColor
 | 
			
		||||
          title="Stroke Color"
 | 
			
		||||
          onColorChange={(color: string) => {
 | 
			
		||||
            changeProperty(element => ({
 | 
			
		||||
              ...element,
 | 
			
		||||
              strokeColor: color
 | 
			
		||||
            }));
 | 
			
		||||
            onUpdateAppState("currentItemStrokeColor", color);
 | 
			
		||||
          }}
 | 
			
		||||
          colorValue={getSelectedAttribute(
 | 
			
		||||
            elements,
 | 
			
		||||
            element => element.strokeColor
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "changeStrokeColor",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {hasBackground(elements) && (
 | 
			
		||||
          <>
 | 
			
		||||
            <PanelColor
 | 
			
		||||
              title="Background Color"
 | 
			
		||||
              onColorChange={(color: string) => {
 | 
			
		||||
                changeProperty(element => ({
 | 
			
		||||
                  ...element,
 | 
			
		||||
                  backgroundColor: color
 | 
			
		||||
                }));
 | 
			
		||||
                onUpdateAppState("currentItemBackgroundColor", color);
 | 
			
		||||
              }}
 | 
			
		||||
              colorValue={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element => element.backgroundColor
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeBackgroundColor",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <h5>Fill</h5>
 | 
			
		||||
            <ButtonSelect
 | 
			
		||||
              options={[
 | 
			
		||||
                { value: "solid", text: "Solid" },
 | 
			
		||||
                { value: "hachure", text: "Hachure" },
 | 
			
		||||
                { value: "cross-hatch", text: "Cross-hatch" }
 | 
			
		||||
              ]}
 | 
			
		||||
              value={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element => element.fillStyle
 | 
			
		||||
              )}
 | 
			
		||||
              onChange={value => {
 | 
			
		||||
                changeProperty(element => ({
 | 
			
		||||
                  ...element,
 | 
			
		||||
                  fillStyle: value
 | 
			
		||||
                }));
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeFillStyle",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {hasStroke(elements) && (
 | 
			
		||||
          <>
 | 
			
		||||
            <h5>Stroke Width</h5>
 | 
			
		||||
            <ButtonSelect
 | 
			
		||||
              options={[
 | 
			
		||||
                { value: 1, text: "Thin" },
 | 
			
		||||
                { value: 2, text: "Bold" },
 | 
			
		||||
                { value: 4, text: "Extra Bold" }
 | 
			
		||||
              ]}
 | 
			
		||||
              value={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element => element.strokeWidth
 | 
			
		||||
              )}
 | 
			
		||||
              onChange={value => {
 | 
			
		||||
                changeProperty(element => ({
 | 
			
		||||
                  ...element,
 | 
			
		||||
                  strokeWidth: value
 | 
			
		||||
                }));
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeStrokeWidth",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <h5>Sloppiness</h5>
 | 
			
		||||
            <ButtonSelect
 | 
			
		||||
              options={[
 | 
			
		||||
                { value: 0, text: "Draftsman" },
 | 
			
		||||
                { value: 1, text: "Artist" },
 | 
			
		||||
                { value: 3, text: "Cartoonist" }
 | 
			
		||||
              ]}
 | 
			
		||||
              value={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element => element.roughness
 | 
			
		||||
              )}
 | 
			
		||||
              onChange={value =>
 | 
			
		||||
                changeProperty(element => ({
 | 
			
		||||
                  ...element,
 | 
			
		||||
                  roughness: value
 | 
			
		||||
                }))
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeSloppiness",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {hasText(elements) && (
 | 
			
		||||
          <>
 | 
			
		||||
            <h5>Font size</h5>
 | 
			
		||||
            <ButtonSelect
 | 
			
		||||
              options={[
 | 
			
		||||
                { value: 16, text: "Small" },
 | 
			
		||||
                { value: 20, text: "Medium" },
 | 
			
		||||
                { value: 28, text: "Large" },
 | 
			
		||||
                { value: 36, text: "Very Large" }
 | 
			
		||||
              ]}
 | 
			
		||||
              value={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element =>
 | 
			
		||||
                  isTextElement(element) && +element.font.split("px ")[0]
 | 
			
		||||
              )}
 | 
			
		||||
              onChange={value =>
 | 
			
		||||
                changeProperty(element => {
 | 
			
		||||
                  if (isTextElement(element)) {
 | 
			
		||||
                    element.font = `${value}px ${element.font.split("px ")[1]}`;
 | 
			
		||||
                    redrawTextBoundingBox(element);
 | 
			
		||||
                  }
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeFontSize",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
                  return element;
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
            <h5>Font familly</h5>
 | 
			
		||||
            <ButtonSelect
 | 
			
		||||
              options={[
 | 
			
		||||
                { value: "Virgil", text: "Virgil" },
 | 
			
		||||
                { value: "Helvetica", text: "Helvetica" },
 | 
			
		||||
                { value: "Courier", text: "Courier" }
 | 
			
		||||
              ]}
 | 
			
		||||
              value={getSelectedAttribute(
 | 
			
		||||
                elements,
 | 
			
		||||
                element =>
 | 
			
		||||
                  isTextElement(element) && element.font.split("px ")[1]
 | 
			
		||||
              )}
 | 
			
		||||
              onChange={value =>
 | 
			
		||||
                changeProperty(element => {
 | 
			
		||||
                  if (isTextElement(element)) {
 | 
			
		||||
                    element.font = `${element.font.split("px ")[0]}px ${value}`;
 | 
			
		||||
                    redrawTextBoundingBox(element);
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  return element;
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
            {actionManager.renderAction(
 | 
			
		||||
              "changeFontFamily",
 | 
			
		||||
              elements,
 | 
			
		||||
              appState,
 | 
			
		||||
              syncActionResult
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <h5>Opacity</h5>
 | 
			
		||||
        <input
 | 
			
		||||
          type="range"
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="100"
 | 
			
		||||
          onChange={event => {
 | 
			
		||||
            changeProperty(element => ({
 | 
			
		||||
              ...element,
 | 
			
		||||
              opacity: +event.target.value
 | 
			
		||||
            }));
 | 
			
		||||
          }}
 | 
			
		||||
          value={
 | 
			
		||||
            getSelectedAttribute(elements, element => element.opacity) ||
 | 
			
		||||
            0 /* Put the opacity at 0 if there are two conflicting ones */
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "changeOpacity",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            onUpdateElements(deleteSelectedElements(elements));
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Delete selected
 | 
			
		||||
        </button>
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "deleteSelectedElements",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
      </Panel>
 | 
			
		||||
      <PanelCanvas
 | 
			
		||||
        onClearCanvas={onClearCanvas}
 | 
			
		||||
        onViewBackgroundColorChange={value => {
 | 
			
		||||
          onUpdateAppState("viewBackgroundColor", value);
 | 
			
		||||
        }}
 | 
			
		||||
        viewBackgroundColor={appState.viewBackgroundColor}
 | 
			
		||||
        actionManager={actionManager}
 | 
			
		||||
        syncActionResult={syncActionResult}
 | 
			
		||||
        elements={elements}
 | 
			
		||||
        appState={appState}
 | 
			
		||||
      />
 | 
			
		||||
      <PanelExport
 | 
			
		||||
        projectName={appState.name}
 | 
			
		||||
        onProjectNameChange={name => {
 | 
			
		||||
          onUpdateAppState("name", name);
 | 
			
		||||
        }}
 | 
			
		||||
        actionManager={actionManager}
 | 
			
		||||
        syncActionResult={syncActionResult}
 | 
			
		||||
        elements={elements}
 | 
			
		||||
        appState={appState}
 | 
			
		||||
        onExportCanvas={(type: ExportType) =>
 | 
			
		||||
          exportCanvas(type, elements, canvas, appState)
 | 
			
		||||
        }
 | 
			
		||||
        exportBackground={appState.exportBackground}
 | 
			
		||||
        onExportBackgroundChange={value => {
 | 
			
		||||
          onUpdateAppState("exportBackground", value);
 | 
			
		||||
        }}
 | 
			
		||||
        onSaveScene={() => saveAsJSON(elements, appState.name)}
 | 
			
		||||
        onLoadScene={() =>
 | 
			
		||||
          loadFromJSON().then(({ elements }) => {
 | 
			
		||||
            onUpdateElements(elements);
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,39 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { ColorPicker } from "../ColorPicker";
 | 
			
		||||
import { Panel } from "../Panel";
 | 
			
		||||
import { ActionManager } from "../../actions";
 | 
			
		||||
import { ExcalidrawElement } from "../../element/types";
 | 
			
		||||
import { AppState } from "../../types";
 | 
			
		||||
import { UpdaterFn } from "../../actions/types";
 | 
			
		||||
 | 
			
		||||
interface PanelCanvasProps {
 | 
			
		||||
  viewBackgroundColor: string;
 | 
			
		||||
  onViewBackgroundColorChange: (val: string) => void;
 | 
			
		||||
  onClearCanvas: React.MouseEventHandler;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  elements: readonly ExcalidrawElement[];
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  syncActionResult: UpdaterFn;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const PanelCanvas: React.FC<PanelCanvasProps> = ({
 | 
			
		||||
  viewBackgroundColor,
 | 
			
		||||
  onViewBackgroundColorChange,
 | 
			
		||||
  onClearCanvas
 | 
			
		||||
  actionManager,
 | 
			
		||||
  elements,
 | 
			
		||||
  appState,
 | 
			
		||||
  syncActionResult
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Panel title="Canvas">
 | 
			
		||||
      <h5>Canvas Background Color</h5>
 | 
			
		||||
      <ColorPicker
 | 
			
		||||
        color={viewBackgroundColor}
 | 
			
		||||
        onChange={color => onViewBackgroundColorChange(color)}
 | 
			
		||||
      />
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={onClearCanvas}
 | 
			
		||||
        title="Clear the canvas & reset background color"
 | 
			
		||||
      >
 | 
			
		||||
        Clear canvas
 | 
			
		||||
      </button>
 | 
			
		||||
      {actionManager.renderAction(
 | 
			
		||||
        "changeViewBackgroundColor",
 | 
			
		||||
        elements,
 | 
			
		||||
        appState,
 | 
			
		||||
        syncActionResult
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {actionManager.renderAction(
 | 
			
		||||
        "clearCanvas",
 | 
			
		||||
        elements,
 | 
			
		||||
        appState,
 | 
			
		||||
        syncActionResult
 | 
			
		||||
      )}
 | 
			
		||||
    </Panel>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { EditableText } from "../EditableText";
 | 
			
		||||
import { Panel } from "../Panel";
 | 
			
		||||
import { ExportType } from "../../scene/types";
 | 
			
		||||
 | 
			
		||||
import "./panelExport.scss";
 | 
			
		||||
import { ActionManager } from "../../actions";
 | 
			
		||||
import { ExcalidrawElement } from "../../element/types";
 | 
			
		||||
import { AppState } from "../../types";
 | 
			
		||||
import { UpdaterFn } from "../../actions/types";
 | 
			
		||||
 | 
			
		||||
interface PanelExportProps {
 | 
			
		||||
  projectName: string;
 | 
			
		||||
  onProjectNameChange: (name: string) => void;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  elements: readonly ExcalidrawElement[];
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  syncActionResult: UpdaterFn;
 | 
			
		||||
  onExportCanvas: (type: ExportType) => void;
 | 
			
		||||
  exportBackground: boolean;
 | 
			
		||||
  onExportBackgroundChange: (val: boolean) => void;
 | 
			
		||||
  onSaveScene: React.MouseEventHandler;
 | 
			
		||||
  onLoadScene: React.MouseEventHandler;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fa-clipboard
 | 
			
		||||
@@ -32,23 +33,20 @@ const probablySupportsClipboard =
 | 
			
		||||
  "ClipboardItem" in window;
 | 
			
		||||
 | 
			
		||||
export const PanelExport: React.FC<PanelExportProps> = ({
 | 
			
		||||
  projectName,
 | 
			
		||||
  exportBackground,
 | 
			
		||||
  onProjectNameChange,
 | 
			
		||||
  onExportBackgroundChange,
 | 
			
		||||
  onSaveScene,
 | 
			
		||||
  onLoadScene,
 | 
			
		||||
  actionManager,
 | 
			
		||||
  elements,
 | 
			
		||||
  appState,
 | 
			
		||||
  syncActionResult,
 | 
			
		||||
  onExportCanvas
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Panel title="Export">
 | 
			
		||||
      <div className="panelColumn">
 | 
			
		||||
        <h5>Name</h5>
 | 
			
		||||
        {projectName && (
 | 
			
		||||
          <EditableText
 | 
			
		||||
            value={projectName}
 | 
			
		||||
            onChange={(name: string) => onProjectNameChange(name)}
 | 
			
		||||
          />
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "changeProjectName",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
        <h5>Image</h5>
 | 
			
		||||
        <div className="panelExport-imageButtons">
 | 
			
		||||
@@ -68,19 +66,26 @@ export const PanelExport: React.FC<PanelExportProps> = ({
 | 
			
		||||
            </button>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        <label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            checked={exportBackground}
 | 
			
		||||
            onChange={e => {
 | 
			
		||||
              onExportBackgroundChange(e.target.checked);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          background
 | 
			
		||||
        </label>
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "changeExportBackground",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <h5>Scene</h5>
 | 
			
		||||
        <button onClick={onSaveScene}>Save as...</button>
 | 
			
		||||
        <button onClick={onLoadScene}>Load file...</button>
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "saveScene",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "loadScene",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </Panel>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,49 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { ActionManager } from "../../actions";
 | 
			
		||||
import { ExcalidrawElement } from "../../element/types";
 | 
			
		||||
import { AppState } from "../../types";
 | 
			
		||||
import { UpdaterFn } from "../../actions/types";
 | 
			
		||||
 | 
			
		||||
interface PanelSelectionProps {
 | 
			
		||||
  onBringForward: React.MouseEventHandler;
 | 
			
		||||
  onBringToFront: React.MouseEventHandler;
 | 
			
		||||
  onSendBackward: React.MouseEventHandler;
 | 
			
		||||
  onSendToBack: React.MouseEventHandler;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  elements: readonly ExcalidrawElement[];
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  syncActionResult: UpdaterFn;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const PanelSelection: React.FC<PanelSelectionProps> = ({
 | 
			
		||||
  onBringForward,
 | 
			
		||||
  onBringToFront,
 | 
			
		||||
  onSendBackward,
 | 
			
		||||
  onSendToBack
 | 
			
		||||
  actionManager,
 | 
			
		||||
  elements,
 | 
			
		||||
  appState,
 | 
			
		||||
  syncActionResult
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="buttonList">
 | 
			
		||||
        <button type="button" onClick={onBringForward}>
 | 
			
		||||
          Bring forward
 | 
			
		||||
        </button>
 | 
			
		||||
        <button type="button" onClick={onBringToFront}>
 | 
			
		||||
          Bring to front
 | 
			
		||||
        </button>
 | 
			
		||||
        <button type="button" onClick={onSendBackward}>
 | 
			
		||||
          Send backward
 | 
			
		||||
        </button>
 | 
			
		||||
        <button type="button" onClick={onSendToBack}>
 | 
			
		||||
          Send to back
 | 
			
		||||
        </button>
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "bringForward",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "bringToFront",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "sendBackward",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          "sendToBack",
 | 
			
		||||
          elements,
 | 
			
		||||
          appState,
 | 
			
		||||
          syncActionResult
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										244
									
								
								src/index.tsx
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								src/index.tsx
									
									
									
									
									
								
							@@ -4,19 +4,16 @@ import ReactDOM from "react-dom";
 | 
			
		||||
import rough from "roughjs/bin/wrappers/rough";
 | 
			
		||||
import { RoughCanvas } from "roughjs/bin/canvas";
 | 
			
		||||
 | 
			
		||||
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
 | 
			
		||||
import {
 | 
			
		||||
  newElement,
 | 
			
		||||
  duplicateElement,
 | 
			
		||||
  resizeTest,
 | 
			
		||||
  isTextElement,
 | 
			
		||||
  textWysiwyg,
 | 
			
		||||
  getElementAbsoluteCoords,
 | 
			
		||||
  redrawTextBoundingBox
 | 
			
		||||
  getElementAbsoluteCoords
 | 
			
		||||
} from "./element";
 | 
			
		||||
import {
 | 
			
		||||
  clearSelection,
 | 
			
		||||
  getSelectedIndices,
 | 
			
		||||
  deleteSelectedElements,
 | 
			
		||||
  setSelection,
 | 
			
		||||
  isOverScrollBars,
 | 
			
		||||
@@ -41,7 +38,33 @@ import ContextMenu from "./components/ContextMenu";
 | 
			
		||||
 | 
			
		||||
import "./styles.scss";
 | 
			
		||||
import { getElementWithResizeHandler } from "./element/resizeTest";
 | 
			
		||||
import {
 | 
			
		||||
  ActionManager,
 | 
			
		||||
  actionDeleteSelected,
 | 
			
		||||
  actionSendBackward,
 | 
			
		||||
  actionBringForward,
 | 
			
		||||
  actionSendToBack,
 | 
			
		||||
  actionBringToFront,
 | 
			
		||||
  actionSelectAll,
 | 
			
		||||
  actionChangeStrokeColor,
 | 
			
		||||
  actionChangeBackgroundColor,
 | 
			
		||||
  actionChangeOpacity,
 | 
			
		||||
  actionChangeStrokeWidth,
 | 
			
		||||
  actionChangeFillStyle,
 | 
			
		||||
  actionChangeSloppiness,
 | 
			
		||||
  actionChangeFontSize,
 | 
			
		||||
  actionChangeFontFamily,
 | 
			
		||||
  actionChangeViewBackgroundColor,
 | 
			
		||||
  actionClearCanvas,
 | 
			
		||||
  actionChangeProjectName,
 | 
			
		||||
  actionChangeExportBackground,
 | 
			
		||||
  actionLoadScene,
 | 
			
		||||
  actionSaveScene,
 | 
			
		||||
  actionCopyStyles,
 | 
			
		||||
  actionPasteStyles
 | 
			
		||||
} from "./actions";
 | 
			
		||||
import { SidePanel } from "./components/SidePanel";
 | 
			
		||||
import { ActionResult } from "./actions/types";
 | 
			
		||||
 | 
			
		||||
let { elements } = createScene();
 | 
			
		||||
const { history } = createHistory();
 | 
			
		||||
@@ -50,8 +73,6 @@ const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
 | 
			
		||||
const CANVAS_WINDOW_OFFSET_LEFT = 250;
 | 
			
		||||
const CANVAS_WINDOW_OFFSET_TOP = 0;
 | 
			
		||||
 | 
			
		||||
let copiedStyles: string = "{}";
 | 
			
		||||
 | 
			
		||||
function resetCursor() {
 | 
			
		||||
  document.documentElement.style.cursor = "";
 | 
			
		||||
}
 | 
			
		||||
@@ -101,6 +122,48 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
  canvas: HTMLCanvasElement | null = null;
 | 
			
		||||
  rc: RoughCanvas | null = null;
 | 
			
		||||
 | 
			
		||||
  actionManager: ActionManager = new ActionManager();
 | 
			
		||||
  constructor(props: any) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.actionManager.registerAction(actionDeleteSelected);
 | 
			
		||||
    this.actionManager.registerAction(actionSendToBack);
 | 
			
		||||
    this.actionManager.registerAction(actionBringToFront);
 | 
			
		||||
    this.actionManager.registerAction(actionSendBackward);
 | 
			
		||||
    this.actionManager.registerAction(actionBringForward);
 | 
			
		||||
    this.actionManager.registerAction(actionSelectAll);
 | 
			
		||||
 | 
			
		||||
    this.actionManager.registerAction(actionChangeStrokeColor);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeBackgroundColor);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeFillStyle);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeStrokeWidth);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeOpacity);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeSloppiness);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeFontSize);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeFontFamily);
 | 
			
		||||
 | 
			
		||||
    this.actionManager.registerAction(actionChangeViewBackgroundColor);
 | 
			
		||||
    this.actionManager.registerAction(actionClearCanvas);
 | 
			
		||||
 | 
			
		||||
    this.actionManager.registerAction(actionChangeProjectName);
 | 
			
		||||
    this.actionManager.registerAction(actionChangeExportBackground);
 | 
			
		||||
    this.actionManager.registerAction(actionSaveScene);
 | 
			
		||||
    this.actionManager.registerAction(actionLoadScene);
 | 
			
		||||
 | 
			
		||||
    this.actionManager.registerAction(actionCopyStyles);
 | 
			
		||||
    this.actionManager.registerAction(actionPasteStyles);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private syncActionResult = (res: ActionResult) => {
 | 
			
		||||
    if (res.elements !== undefined) {
 | 
			
		||||
      elements = res.elements;
 | 
			
		||||
      this.forceUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (res.appState !== undefined) {
 | 
			
		||||
      this.setState({ ...res.appState });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  public componentDidMount() {
 | 
			
		||||
    document.addEventListener("keydown", this.onKeyDown, false);
 | 
			
		||||
    document.addEventListener("mousemove", this.getCurrentCursorPosition);
 | 
			
		||||
@@ -166,10 +229,14 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
    }
 | 
			
		||||
    if (isInputLike(event.target)) return;
 | 
			
		||||
 | 
			
		||||
    if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
 | 
			
		||||
      this.deleteSelectedElements();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    } else if (isArrowKey(event.key)) {
 | 
			
		||||
    const data = this.actionManager.handleKeyDown(event, elements, this.state);
 | 
			
		||||
    this.syncActionResult(data);
 | 
			
		||||
 | 
			
		||||
    if (data.elements !== undefined && data.appState !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isArrowKey(event.key)) {
 | 
			
		||||
      const step = event.shiftKey
 | 
			
		||||
        ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
 | 
			
		||||
        : ELEMENT_TRANSLATE_AMOUNT;
 | 
			
		||||
@@ -186,46 +253,6 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
      });
 | 
			
		||||
      this.forceUpdate();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      // Send backward: Cmd-Shift-Alt-B
 | 
			
		||||
    } else if (
 | 
			
		||||
      event[META_KEY] &&
 | 
			
		||||
      event.shiftKey &&
 | 
			
		||||
      event.altKey &&
 | 
			
		||||
      event.code === "KeyB"
 | 
			
		||||
    ) {
 | 
			
		||||
      this.moveOneLeft();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      // Send to back: Cmd-Shift-B
 | 
			
		||||
    } else if (event[META_KEY] && event.shiftKey && event.code === "KeyB") {
 | 
			
		||||
      this.moveAllLeft();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      // Bring forward: Cmd-Shift-Alt-F
 | 
			
		||||
    } else if (
 | 
			
		||||
      event[META_KEY] &&
 | 
			
		||||
      event.shiftKey &&
 | 
			
		||||
      event.altKey &&
 | 
			
		||||
      event.code === "KeyF"
 | 
			
		||||
    ) {
 | 
			
		||||
      this.moveOneRight();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      // Bring to front: Cmd-Shift-F
 | 
			
		||||
    } else if (event[META_KEY] && event.shiftKey && event.code === "KeyF") {
 | 
			
		||||
      this.moveAllRight();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      // Select all: Cmd-A
 | 
			
		||||
    } else if (event[META_KEY] && event.code === "KeyA") {
 | 
			
		||||
      let newElements = [...elements];
 | 
			
		||||
      newElements.forEach(element => {
 | 
			
		||||
        element.isSelected = true;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      elements = newElements;
 | 
			
		||||
      this.forceUpdate();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    } else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
 | 
			
		||||
      this.setState({ elementType: findShapeByKey(event.key) });
 | 
			
		||||
    } else if (event[META_KEY] && event.code === "KeyZ") {
 | 
			
		||||
@@ -244,99 +271,11 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
      }
 | 
			
		||||
      this.forceUpdate();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      // Copy Styles: Cmd-Shift-C
 | 
			
		||||
    } else if (event.metaKey && event.shiftKey && event.code === "KeyC") {
 | 
			
		||||
      this.copyStyles();
 | 
			
		||||
      // Paste Styles: Cmd-Shift-V
 | 
			
		||||
    } else if (event.metaKey && event.shiftKey && event.code === "KeyV") {
 | 
			
		||||
      this.pasteStyles();
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private deleteSelectedElements = () => {
 | 
			
		||||
    elements = deleteSelectedElements(elements);
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private clearCanvas = () => {
 | 
			
		||||
    if (window.confirm("This will clear the whole canvas. Are you sure?")) {
 | 
			
		||||
      elements = [];
 | 
			
		||||
      this.setState({
 | 
			
		||||
        viewBackgroundColor: "#ffffff",
 | 
			
		||||
        scrollX: 0,
 | 
			
		||||
        scrollY: 0
 | 
			
		||||
      });
 | 
			
		||||
      this.forceUpdate();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private copyStyles = () => {
 | 
			
		||||
    const element = elements.find(el => el.isSelected);
 | 
			
		||||
    if (element) {
 | 
			
		||||
      copiedStyles = JSON.stringify(element);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private pasteStyles = () => {
 | 
			
		||||
    const pastedElement = JSON.parse(copiedStyles);
 | 
			
		||||
    elements = elements.map(element => {
 | 
			
		||||
      if (element.isSelected) {
 | 
			
		||||
        const newElement = {
 | 
			
		||||
          ...element,
 | 
			
		||||
          backgroundColor: pastedElement?.backgroundColor,
 | 
			
		||||
          strokeWidth: pastedElement?.strokeWidth,
 | 
			
		||||
          strokeColor: pastedElement?.strokeColor,
 | 
			
		||||
          fillStyle: pastedElement?.fillStyle,
 | 
			
		||||
          opacity: pastedElement?.opacity,
 | 
			
		||||
          roughness: pastedElement?.roughness
 | 
			
		||||
        };
 | 
			
		||||
        if (isTextElement(newElement)) {
 | 
			
		||||
          newElement.font = pastedElement?.font;
 | 
			
		||||
          redrawTextBoundingBox(newElement);
 | 
			
		||||
        }
 | 
			
		||||
        return newElement;
 | 
			
		||||
      }
 | 
			
		||||
      return element;
 | 
			
		||||
    });
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private moveAllLeft = () => {
 | 
			
		||||
    elements = moveAllLeft([...elements], getSelectedIndices(elements));
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private moveOneLeft = () => {
 | 
			
		||||
    elements = moveOneLeft([...elements], getSelectedIndices(elements));
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private moveAllRight = () => {
 | 
			
		||||
    elements = moveAllRight([...elements], getSelectedIndices(elements));
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private moveOneRight = () => {
 | 
			
		||||
    elements = moveOneRight([...elements], getSelectedIndices(elements));
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private removeWheelEventListener: (() => void) | undefined;
 | 
			
		||||
 | 
			
		||||
  private changeProperty = (
 | 
			
		||||
    callback: (element: ExcalidrawElement) => ExcalidrawElement
 | 
			
		||||
  ) => {
 | 
			
		||||
    elements = elements.map(element => {
 | 
			
		||||
      if (element.isSelected) {
 | 
			
		||||
        return callback(element);
 | 
			
		||||
      }
 | 
			
		||||
      return element;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.forceUpdate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private copyToClipboard = () => {
 | 
			
		||||
    if (navigator.clipboard) {
 | 
			
		||||
      const text = JSON.stringify(
 | 
			
		||||
@@ -384,6 +323,9 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <SidePanel
 | 
			
		||||
          actionManager={this.actionManager}
 | 
			
		||||
          syncActionResult={this.syncActionResult}
 | 
			
		||||
          appState={{ ...this.state }}
 | 
			
		||||
          elements={elements}
 | 
			
		||||
          onToolChange={value => {
 | 
			
		||||
            this.setState({ elementType: value });
 | 
			
		||||
@@ -392,20 +334,6 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
              value === "text" ? "text" : "crosshair";
 | 
			
		||||
            this.forceUpdate();
 | 
			
		||||
          }}
 | 
			
		||||
          moveAllLeft={this.moveAllLeft}
 | 
			
		||||
          moveAllRight={this.moveAllRight}
 | 
			
		||||
          moveOneLeft={this.moveOneLeft}
 | 
			
		||||
          moveOneRight={this.moveOneRight}
 | 
			
		||||
          onClearCanvas={this.clearCanvas}
 | 
			
		||||
          changeProperty={this.changeProperty}
 | 
			
		||||
          onUpdateAppState={(name, value) => {
 | 
			
		||||
            this.setState({ [name]: value } as any);
 | 
			
		||||
          }}
 | 
			
		||||
          onUpdateElements={newElements => {
 | 
			
		||||
            elements = newElements;
 | 
			
		||||
            this.forceUpdate();
 | 
			
		||||
          }}
 | 
			
		||||
          appState={{ ...this.state }}
 | 
			
		||||
          canvas={this.canvas!}
 | 
			
		||||
        />
 | 
			
		||||
        <canvas
 | 
			
		||||
@@ -482,13 +410,11 @@ export class App extends React.Component<{}, AppState> {
 | 
			
		||||
                  label: "Paste",
 | 
			
		||||
                  action: () => this.pasteFromClipboard()
 | 
			
		||||
                },
 | 
			
		||||
                { label: "Copy Styles", action: this.copyStyles },
 | 
			
		||||
                { label: "Paste Styles", action: this.pasteStyles },
 | 
			
		||||
                { label: "Delete", action: this.deleteSelectedElements },
 | 
			
		||||
                { label: "Move Forward", action: this.moveOneRight },
 | 
			
		||||
                { label: "Send to Front", action: this.moveAllRight },
 | 
			
		||||
                { label: "Move Backwards", action: this.moveOneLeft },
 | 
			
		||||
                { label: "Send to Back", action: this.moveAllLeft }
 | 
			
		||||
                ...this.actionManager.getContextMenuItems(
 | 
			
		||||
                  elements,
 | 
			
		||||
                  this.state,
 | 
			
		||||
                  this.syncActionResult
 | 
			
		||||
                )
 | 
			
		||||
              ],
 | 
			
		||||
              top: e.clientY,
 | 
			
		||||
              left: e.clientX
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user