Selaa lähdekoodia

Extract Side Panel from App component (#295)

* Extract Side Panel from App component

* Refactor SidePanel component

- Remove unnecessary props (we are already passing appState as a prop)
- Remove unnecessary allback (we are already passing setState)
Gasim Gasimzada 5 vuotta sitten
vanhempi
commit
f2346275ef
4 muutettua tiedostoa jossa 312 lisäystä ja 252 poistoa
  1. 272 0
      src/components/SidePanel.tsx
  2. 1 0
      src/element/index.ts
  3. 9 0
      src/element/textElement.ts
  4. 30 252
      src/index.tsx

+ 272 - 0
src/components/SidePanel.tsx

@@ -0,0 +1,272 @@
+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
+} 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";
+
+interface SidePanelProps {
+  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;
+  appState: AppState;
+  onUpdateElements: (elements: readonly ExcalidrawElement[]) => void;
+  canvas: HTMLCanvasElement;
+}
+
+export const SidePanel: React.FC<SidePanelProps> = ({
+  elements,
+  onToolChange,
+  changeProperty,
+  moveAllLeft,
+  moveOneLeft,
+  moveAllRight,
+  moveOneRight,
+  onClearCanvas,
+  onUpdateAppState,
+  appState,
+  onUpdateElements,
+  canvas
+}) => {
+  return (
+    <div className="sidePanel">
+      <PanelTools
+        activeTool={appState.elementType}
+        onToolChange={value => {
+          onToolChange(value);
+        }}
+      />
+      <Panel title="Selection" hide={!someElementIsSelected(elements)}>
+        <PanelSelection
+          onBringForward={moveOneRight}
+          onBringToFront={moveAllRight}
+          onSendBackward={moveOneLeft}
+          onSendToBack={moveAllLeft}
+        />
+
+        <PanelColor
+          title="Stroke Color"
+          onColorChange={(color: string) => {
+            changeProperty(element => ({
+              ...element,
+              strokeColor: color
+            }));
+            onUpdateAppState("currentItemStrokeColor", color);
+          }}
+          colorValue={getSelectedAttribute(
+            elements,
+            element => element.strokeColor
+          )}
+        />
+
+        {hasBackground(elements) && (
+          <>
+            <PanelColor
+              title="Background Color"
+              onColorChange={(color: string) => {
+                changeProperty(element => ({
+                  ...element,
+                  backgroundColor: color
+                }));
+                onUpdateAppState("currentItemBackgroundColor", color);
+              }}
+              colorValue={getSelectedAttribute(
+                elements,
+                element => element.backgroundColor
+              )}
+            />
+
+            <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
+                }));
+              }}
+            />
+          </>
+        )}
+
+        {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
+                }));
+              }}
+            />
+
+            <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
+                }))
+              }
+            />
+          </>
+        )}
+
+        {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);
+                  }
+
+                  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;
+                })
+              }
+            />
+          </>
+        )}
+
+        <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 */
+          }
+        />
+
+        <button
+          onClick={() => {
+            onUpdateElements(deleteSelectedElements(elements));
+          }}
+        >
+          Delete selected
+        </button>
+      </Panel>
+      <PanelCanvas
+        onClearCanvas={onClearCanvas}
+        onViewBackgroundColorChange={value => {
+          onUpdateAppState("viewBackgroundColor", value);
+        }}
+        viewBackgroundColor={appState.viewBackgroundColor}
+      />
+      <PanelExport
+        projectName={appState.name}
+        onProjectNameChange={name => {
+          onUpdateAppState("name", name);
+        }}
+        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 - 0
src/element/index.ts

@@ -10,3 +10,4 @@ export { hitTest } from "./collision";
 export { resizeTest } from "./resizeTest";
 export { isTextElement } from "./typeChecks";
 export { textWysiwyg } from "./textWysiwyg";
+export { redrawTextBoundingBox } from "./textElement";

+ 9 - 0
src/element/textElement.ts

@@ -0,0 +1,9 @@
+import { measureText } from "../utils";
+import { ExcalidrawTextElement } from "./types";
+
+export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
+  const metrics = measureText(element.text, element.font);
+  element.width = metrics.width;
+  element.height = metrics.height;
+  element.baseline = metrics.baseline;
+};

+ 30 - 252
src/index.tsx

@@ -11,7 +11,8 @@ import {
   resizeTest,
   isTextElement,
   textWysiwyg,
-  getElementAbsoluteCoords
+  getElementAbsoluteCoords,
+  redrawTextBoundingBox
 } from "./element";
 import {
   clearSelection,
@@ -20,42 +21,28 @@ import {
   setSelection,
   isOverScrollBars,
   someElementIsSelected,
-  getSelectedAttribute,
-  loadFromJSON,
-  saveAsJSON,
-  exportCanvas,
   restoreFromLocalStorage,
   saveToLocalStorage,
-  hasBackground,
-  hasStroke,
   getElementAtPosition,
   createScene,
-  getElementContainingPosition,
-  hasText
+  getElementContainingPosition
 } from "./scene";
 
 import { renderScene } from "./renderer";
 import { AppState } from "./types";
 import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
-import { ExportType } from "./scene/types";
 
 import { getDateTime, isInputLike, measureText } from "./utils";
 import { KEYS, META_KEY, isArrowKey } from "./keys";
 
-import { ButtonSelect } from "./components/ButtonSelect";
 import { findShapeByKey, shapesShortcutKeys } from "./shapes";
 import { createHistory } from "./history";
 
 import ContextMenu from "./components/ContextMenu";
-import { PanelTools } from "./components/panels/PanelTools";
-import { PanelSelection } from "./components/panels/PanelSelection";
-import { PanelColor } from "./components/panels/PanelColor";
-import { PanelExport } from "./components/panels/PanelExport";
-import { PanelCanvas } from "./components/panels/PanelCanvas";
-import { Panel } from "./components/Panel";
 
 import "./styles.scss";
 import { getElementWithResizeHandler } from "./element/resizeTest";
+import { SidePanel } from "./components/SidePanel";
 
 let { elements } = createScene();
 const { history } = createHistory();
@@ -307,7 +294,7 @@ export class App extends React.Component<{}, AppState> {
         };
         if (isTextElement(newElement)) {
           newElement.font = pastedElement?.font;
-          this.redrawTextBoundingBox(newElement);
+          redrawTextBoundingBox(newElement);
         }
         return newElement;
       }
@@ -338,10 +325,6 @@ export class App extends React.Component<{}, AppState> {
 
   private removeWheelEventListener: (() => void) | undefined;
 
-  private updateProjectName(name: string): void {
-    this.setState({ name });
-  }
-
   private changeProperty = (
     callback: (element: ExcalidrawElement) => ExcalidrawElement
   ) => {
@@ -355,29 +338,6 @@ export class App extends React.Component<{}, AppState> {
     this.forceUpdate();
   };
 
-  private changeOpacity = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.changeProperty(element => ({
-      ...element,
-      opacity: +event.target.value
-    }));
-  };
-
-  private changeStrokeColor = (color: string) => {
-    this.changeProperty(element => ({
-      ...element,
-      strokeColor: color
-    }));
-    this.setState({ currentItemStrokeColor: color });
-  };
-
-  private changeBackgroundColor = (color: string) => {
-    this.changeProperty(element => ({
-      ...element,
-      backgroundColor: color
-    }));
-    this.setState({ currentItemBackgroundColor: color });
-  };
-
   private copyToClipboard = () => {
     if (navigator.clipboard) {
       const text = JSON.stringify(
@@ -395,13 +355,6 @@ export class App extends React.Component<{}, AppState> {
     }
   };
 
-  private redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
-    const metrics = measureText(element.text, element.font);
-    element.width = metrics.width;
-    element.height = metrics.height;
-    element.baseline = metrics.baseline;
-  };
-
   public render() {
     const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT;
     const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP;
@@ -431,206 +384,31 @@ export class App extends React.Component<{}, AppState> {
           e.preventDefault();
         }}
       >
-        <div className="sidePanel">
-          <PanelTools
-            activeTool={this.state.elementType}
-            onToolChange={value => {
-              this.setState({ elementType: value });
-              elements = clearSelection(elements);
-              document.documentElement.style.cursor =
-                value === "text" ? "text" : "crosshair";
-              this.forceUpdate();
-            }}
-          />
-          <Panel title="Selection" hide={!someElementIsSelected(elements)}>
-            <PanelSelection
-              onBringForward={this.moveOneRight}
-              onBringToFront={this.moveAllRight}
-              onSendBackward={this.moveOneLeft}
-              onSendToBack={this.moveAllLeft}
-            />
-
-            <PanelColor
-              title="Stroke Color"
-              onColorChange={this.changeStrokeColor}
-              colorValue={getSelectedAttribute(
-                elements,
-                element => element.strokeColor
-              )}
-            />
-
-            {hasBackground(elements) && (
-              <>
-                <PanelColor
-                  title="Background Color"
-                  onColorChange={this.changeBackgroundColor}
-                  colorValue={getSelectedAttribute(
-                    elements,
-                    element => element.backgroundColor
-                  )}
-                />
-
-                <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 => {
-                    this.changeProperty(element => ({
-                      ...element,
-                      fillStyle: value
-                    }));
-                  }}
-                />
-              </>
-            )}
-
-            {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 => {
-                    this.changeProperty(element => ({
-                      ...element,
-                      strokeWidth: value
-                    }));
-                  }}
-                />
-
-                <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 =>
-                    this.changeProperty(element => ({
-                      ...element,
-                      roughness: value
-                    }))
-                  }
-                />
-              </>
-            )}
-
-            {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 =>
-                    this.changeProperty(element => {
-                      if (isTextElement(element)) {
-                        element.font = `${value}px ${
-                          element.font.split("px ")[1]
-                        }`;
-                        this.redrawTextBoundingBox(element);
-                      }
-
-                      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 =>
-                    this.changeProperty(element => {
-                      if (isTextElement(element)) {
-                        element.font = `${
-                          element.font.split("px ")[0]
-                        }px ${value}`;
-                        this.redrawTextBoundingBox(element);
-                      }
-
-                      return element;
-                    })
-                  }
-                />
-              </>
-            )}
-
-            <h5>Opacity</h5>
-            <input
-              type="range"
-              min="0"
-              max="100"
-              onChange={this.changeOpacity}
-              value={
-                getSelectedAttribute(elements, element => element.opacity) ||
-                0 /* Put the opacity at 0 if there are two conflicting ones */
-              }
-            />
-
-            <button onClick={this.deleteSelectedElements}>
-              Delete selected
-            </button>
-          </Panel>
-          <PanelCanvas
-            onClearCanvas={this.clearCanvas}
-            onViewBackgroundColorChange={val =>
-              this.setState({ viewBackgroundColor: val })
-            }
-            viewBackgroundColor={this.state.viewBackgroundColor}
-          />
-          <PanelExport
-            projectName={this.state.name}
-            onProjectNameChange={this.updateProjectName}
-            onExportCanvas={(type: ExportType) =>
-              exportCanvas(type, elements, this.canvas!, this.state)
-            }
-            exportBackground={this.state.exportBackground}
-            onExportBackgroundChange={val =>
-              this.setState({ exportBackground: val })
-            }
-            onSaveScene={() => saveAsJSON(elements, this.state.name)}
-            onLoadScene={() =>
-              loadFromJSON().then(({ elements: newElements }) => {
-                elements = newElements;
-                this.forceUpdate();
-              })
-            }
-          />
-        </div>
+        <SidePanel
+          elements={elements}
+          onToolChange={value => {
+            this.setState({ elementType: value });
+            elements = clearSelection(elements);
+            document.documentElement.style.cursor =
+              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
           id="canvas"
           style={{