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 componentpull/314/head
parent
c253c0b635
commit
f465121f9b
@ -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>
|
||||
)
|
||||
};
|
@ -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>
|
||||
)
|
||||
};
|
@ -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>
|
||||
)
|
||||
};
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
};
|
@ -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"
|
||||
};
|
@ -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
|
||||
};
|
@ -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>
|
||||
)
|
||||
};
|
@ -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";
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue