Extract Sidebar panels into separate components (#230)
* Extract Sidebar panels into separate components * Add Jest TS types
This commit is contained in:
parent
2fb5c4cd13
commit
85365e5bcb
8
package-lock.json
generated
8
package-lock.json
generated
@ -1469,6 +1469,14 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/jest": {
|
||||||
|
"version": "24.0.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.25.tgz",
|
||||||
|
"integrity": "sha512-hnP1WpjN4KbGEK4dLayul6lgtys6FPz0UfxMeMQCv0M+sTnzN3ConfiO72jHgLxl119guHgI8gLqDOrRLsyp2g==",
|
||||||
|
"requires": {
|
||||||
|
"jest-diff": "^24.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"roughjs": "3.1.0"
|
"roughjs": "3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^24.0.25",
|
||||||
"@types/react": "16.9.17",
|
"@types/react": "16.9.17",
|
||||||
"@types/react-color": "^3.0.1",
|
"@types/react-color": "^3.0.1",
|
||||||
"@types/react-dom": "16.9.4",
|
"@types/react-dom": "16.9.4",
|
||||||
|
35
src/components/panels/PanelCanvas.tsx
Normal file
35
src/components/panels/PanelCanvas.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ColorPicker } from "../ColorPicker";
|
||||||
|
|
||||||
|
interface PanelCanvasProps {
|
||||||
|
viewBackgroundColor: string;
|
||||||
|
onViewBackgroundColorChange: (val: string) => void;
|
||||||
|
onClearCanvas: React.MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelCanvas: React.FC<PanelCanvasProps> = ({
|
||||||
|
viewBackgroundColor,
|
||||||
|
onViewBackgroundColorChange,
|
||||||
|
onClearCanvas
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Canvas</h4>
|
||||||
|
<div className="panelColumn">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
24
src/components/panels/PanelColor.tsx
Normal file
24
src/components/panels/PanelColor.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ColorPicker } from "../ColorPicker";
|
||||||
|
|
||||||
|
interface PanelColorProps {
|
||||||
|
title: string;
|
||||||
|
colorValue: string | null;
|
||||||
|
onColorChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelColor: React.FC<PanelColorProps> = ({
|
||||||
|
title,
|
||||||
|
onColorChange,
|
||||||
|
colorValue
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h5>{title}</h5>
|
||||||
|
<ColorPicker
|
||||||
|
color={colorValue}
|
||||||
|
onChange={color => onColorChange(color)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
52
src/components/panels/PanelExport.tsx
Normal file
52
src/components/panels/PanelExport.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { EditableText } from "../EditableText";
|
||||||
|
|
||||||
|
interface PanelExportProps {
|
||||||
|
projectName: string;
|
||||||
|
onProjectNameChange: (name: string) => void;
|
||||||
|
onExportAsPNG: React.MouseEventHandler;
|
||||||
|
exportBackground: boolean;
|
||||||
|
onExportBackgroundChange: (val: boolean) => void;
|
||||||
|
onSaveScene: React.MouseEventHandler;
|
||||||
|
onLoadScene: React.MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelExport: React.FC<PanelExportProps> = ({
|
||||||
|
projectName,
|
||||||
|
exportBackground,
|
||||||
|
onProjectNameChange,
|
||||||
|
onExportBackgroundChange,
|
||||||
|
onSaveScene,
|
||||||
|
onLoadScene,
|
||||||
|
onExportAsPNG
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Export</h4>
|
||||||
|
<div className="panelColumn">
|
||||||
|
<h5>Name</h5>
|
||||||
|
{projectName && (
|
||||||
|
<EditableText
|
||||||
|
value={projectName}
|
||||||
|
onChange={(name: string) => onProjectNameChange(name)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h5>Image</h5>
|
||||||
|
<button onClick={onExportAsPNG}>Export to png</button>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={exportBackground}
|
||||||
|
onChange={e => {
|
||||||
|
onExportBackgroundChange(e.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
background
|
||||||
|
</label>
|
||||||
|
<h5>Scene</h5>
|
||||||
|
<button onClick={onSaveScene}>Save as...</button>
|
||||||
|
<button onClick={onLoadScene}>Load file...</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
35
src/components/panels/PanelSelection.tsx
Normal file
35
src/components/panels/PanelSelection.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PanelSelectionProps {
|
||||||
|
onBringForward: React.MouseEventHandler;
|
||||||
|
onBringToFront: React.MouseEventHandler;
|
||||||
|
onSendBackward: React.MouseEventHandler;
|
||||||
|
onSendToBack: React.MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelSelection: React.FC<PanelSelectionProps> = ({
|
||||||
|
onBringForward,
|
||||||
|
onBringToFront,
|
||||||
|
onSendBackward,
|
||||||
|
onSendToBack
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Selection</h4>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
38
src/components/panels/PanelTools.tsx
Normal file
38
src/components/panels/PanelTools.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { SHAPES } from "../../shapes";
|
||||||
|
import { capitalizeString } from "../../utils";
|
||||||
|
|
||||||
|
interface PanelToolsProps {
|
||||||
|
activeTool: string;
|
||||||
|
onToolChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelTools: React.FC<PanelToolsProps> = ({
|
||||||
|
activeTool,
|
||||||
|
onToolChange
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Shapes</h4>
|
||||||
|
<div className="panelTools">
|
||||||
|
{SHAPES.map(({ value, icon }) => (
|
||||||
|
<label
|
||||||
|
key={value}
|
||||||
|
className="tool"
|
||||||
|
title={`${capitalizeString(value)} - ${capitalizeString(value)[0]}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={activeTool === value}
|
||||||
|
onChange={() => {
|
||||||
|
onToolChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="toolIcon">{icon}</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
136
src/index.tsx
136
src/index.tsx
@ -29,16 +29,19 @@ import {
|
|||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
|
import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
|
||||||
|
|
||||||
import { getDateTime, capitalizeString, isInputLike } from "./utils";
|
import { getDateTime, isInputLike } from "./utils";
|
||||||
|
|
||||||
import { EditableText } from "./components/EditableText";
|
|
||||||
import { ButtonSelect } from "./components/ButtonSelect";
|
import { ButtonSelect } from "./components/ButtonSelect";
|
||||||
import { ColorPicker } from "./components/ColorPicker";
|
import { findShapeByKey, shapesShortcutKeys } from "./shapes";
|
||||||
import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes";
|
|
||||||
import { createHistory } from "./history";
|
import { createHistory } from "./history";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import ContextMenu from "./components/ContextMenu";
|
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";
|
||||||
|
|
||||||
const { elements } = createScene();
|
const { elements } = createScene();
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
@ -368,20 +371,9 @@ class App extends React.Component<{}, AppState> {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="sidePanel">
|
<div className="sidePanel">
|
||||||
<h4>Shapes</h4>
|
<PanelTools
|
||||||
<div className="panelTools">
|
activeTool={this.state.elementType}
|
||||||
{SHAPES.map(({ value, icon }) => (
|
onToolChange={value => {
|
||||||
<label
|
|
||||||
key={value}
|
|
||||||
className="tool"
|
|
||||||
title={`${capitalizeString(value)} - ${
|
|
||||||
capitalizeString(value)[0]
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
checked={this.state.elementType === value}
|
|
||||||
onChange={() => {
|
|
||||||
this.setState({ elementType: value });
|
this.setState({ elementType: value });
|
||||||
clearSelection(elements);
|
clearSelection(elements);
|
||||||
document.documentElement.style.cursor =
|
document.documentElement.style.cursor =
|
||||||
@ -389,38 +381,35 @@ class App extends React.Component<{}, AppState> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="toolIcon">{icon}</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{someElementIsSelected(elements) && (
|
{someElementIsSelected(elements) && (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<h4>Selection</h4>
|
<PanelSelection
|
||||||
<div className="buttonList">
|
onBringForward={this.moveOneRight}
|
||||||
<button onClick={this.moveOneRight}>Bring forward</button>
|
onBringToFront={this.moveAllRight}
|
||||||
<button onClick={this.moveAllRight}>Bring to front</button>
|
onSendBackward={this.moveOneLeft}
|
||||||
<button onClick={this.moveOneLeft}>Send backward</button>
|
onSendToBack={this.moveAllLeft}
|
||||||
<button onClick={this.moveAllLeft}>Send to back</button>
|
/>
|
||||||
</div>
|
|
||||||
<h5>Stroke Color</h5>
|
<PanelColor
|
||||||
<ColorPicker
|
title="Stroke Color"
|
||||||
color={getSelectedAttribute(
|
onColorChange={this.changeStrokeColor}
|
||||||
|
colorValue={getSelectedAttribute(
|
||||||
elements,
|
elements,
|
||||||
element => element.strokeColor
|
element => element.strokeColor
|
||||||
)}
|
)}
|
||||||
onChange={color => this.changeStrokeColor(color)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasBackground(elements) && (
|
{hasBackground(elements) && (
|
||||||
<>
|
<>
|
||||||
<h5>Background Color</h5>
|
<PanelColor
|
||||||
<ColorPicker
|
title="Background Color"
|
||||||
color={getSelectedAttribute(
|
onColorChange={this.changeBackgroundColor}
|
||||||
|
colorValue={getSelectedAttribute(
|
||||||
elements,
|
elements,
|
||||||
element => element.backgroundColor
|
element => element.backgroundColor
|
||||||
)}
|
)}
|
||||||
onChange={color => this.changeBackgroundColor(color)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h5>Fill</h5>
|
<h5>Fill</h5>
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
options={[
|
options={[
|
||||||
@ -498,63 +487,26 @@ class App extends React.Component<{}, AppState> {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h4>Canvas</h4>
|
<PanelCanvas
|
||||||
<div className="panelColumn">
|
onClearCanvas={this.clearCanvas}
|
||||||
<h5>Canvas Background Color</h5>
|
onViewBackgroundColorChange={val =>
|
||||||
<ColorPicker
|
this.setState({ viewBackgroundColor: val })
|
||||||
color={this.state.viewBackgroundColor}
|
}
|
||||||
onChange={color => this.setState({ viewBackgroundColor: color })}
|
viewBackgroundColor={this.state.viewBackgroundColor}
|
||||||
/>
|
/>
|
||||||
<button
|
<PanelExport
|
||||||
onClick={this.clearCanvas}
|
projectName={this.state.name}
|
||||||
title="Clear the canvas & reset background color"
|
onProjectNameChange={this.updateProjectName}
|
||||||
>
|
onExportAsPNG={() => exportAsPNG(elements, canvas, this.state)}
|
||||||
Clear canvas
|
exportBackground={this.state.exportBackground}
|
||||||
</button>
|
onExportBackgroundChange={val =>
|
||||||
</div>
|
this.setState({ exportBackground: val })
|
||||||
<h4>Export</h4>
|
}
|
||||||
<div className="panelColumn">
|
onSaveScene={() => saveAsJSON(elements, this.state.name)}
|
||||||
<h5>Name</h5>
|
onLoadScene={() =>
|
||||||
{this.state.name && (
|
loadFromJSON(elements).then(() => this.forceUpdate())
|
||||||
<EditableText
|
}
|
||||||
value={this.state.name}
|
|
||||||
onChange={(name: string) => this.updateProjectName(name)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<h5>Image</h5>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
exportAsPNG(elements, canvas, this.state);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export to png
|
|
||||||
</button>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.exportBackground}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportBackground: e.target.checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
background
|
|
||||||
</label>
|
|
||||||
<h5>Scene</h5>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
saveAsJSON(elements, this.state.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save as...
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
loadFromJSON(elements).then(() => this.forceUpdate());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Load file...
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
id="canvas"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user