This is an automated email from the ASF dual-hosted git repository.
marat pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
The following commit(s) were added to refs/heads/main by this push:
new 41d6643d Apply last changes to space
41d6643d is described below
commit 41d6643dd6a4d8c4bd272df70e554be2e98d6e08
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Mon Jan 23 09:49:14 2023 -0500
Apply last changes to space
---
karavan-space/src/designer/rest/RestDesigner.tsx | 1 -
.../src/designer/route/DslConnections.tsx | 8 +-
karavan-space/src/designer/route/DslElement.tsx | 23 ++-
karavan-space/src/designer/route/DslProperties.tsx | 19 --
karavan-space/src/designer/route/RouteDesigner.tsx | 201 +++++++++++++++------
karavan-space/src/designer/utils/EventBus.ts | 6 +-
6 files changed, 170 insertions(+), 88 deletions(-)
diff --git a/karavan-space/src/designer/rest/RestDesigner.tsx
b/karavan-space/src/designer/rest/RestDesigner.tsx
index f91c5129..d13d07e6 100644
--- a/karavan-space/src/designer/rest/RestDesigner.tsx
+++ b/karavan-space/src/designer/rest/RestDesigner.tsx
@@ -240,7 +240,6 @@ export class RestDesigner extends React.Component<Props,
State> {
step={this.state.selectedStep}
onIntegrationUpdate={this.onIntegrationUpdate}
onPropertyUpdate={this.onPropertyUpdate}
- clipboardStep={undefined}
isRouteDesigner={false}
onClone={this.cloneRest}
dark={this.props.dark}/>
diff --git a/karavan-space/src/designer/route/DslConnections.tsx
b/karavan-space/src/designer/route/DslConnections.tsx
index 0806761e..6dc1ced3 100644
--- a/karavan-space/src/designer/route/DslConnections.tsx
+++ b/karavan-space/src/designer/route/DslConnections.tsx
@@ -57,12 +57,18 @@ export class DslConnections extends React.Component<Props,
State> {
}
setPosition(evt: DslPosition) {
- if (evt.command === "add") this.setState(prevState => ({steps:
prevState.steps.set(evt.step.uuid, evt)}));
+ if (evt.command === "add") {
+ this.setState(prevState => ({steps:
prevState.steps.set(evt.step.uuid, evt)}));
+ }
else if (evt.command === "delete") this.setState(prevState => {
// prevState.steps.clear();
prevState.steps.delete(evt.step.uuid);
return {steps: prevState.steps};
});
+ else if (evt.command === "clean") this.setState(prevState => {
+ prevState.steps.clear();
+ return {steps: prevState.steps};
+ });
}
getIncomings() {
diff --git a/karavan-space/src/designer/route/DslElement.tsx
b/karavan-space/src/designer/route/DslElement.tsx
index 868f3e2c..f9cd9498 100644
--- a/karavan-space/src/designer/route/DslElement.tsx
+++ b/karavan-space/src/designer/route/DslElement.tsx
@@ -41,7 +41,7 @@ interface Props {
selectElement: any
openSelector: (parentId: string | undefined, parentDsl: string |
undefined, showSteps: boolean, position?: number | undefined) => void
moveElement: (source: string, target: string, asChild: boolean) => void
- selectedUuid: string
+ selectedUuid: string []
inSteps: boolean
position: number
}
@@ -51,7 +51,6 @@ interface State {
showMoveConfirmation: boolean
moveElements: [string | undefined, string | undefined]
tabIndex: string | number
- selectedUuid: string
isDragging: boolean
isDraggedOver: boolean
}
@@ -63,16 +62,16 @@ export class DslElement extends React.Component<Props,
State> {
showMoveConfirmation: false,
moveElements: [undefined, undefined],
tabIndex: 0,
- selectedUuid: this.props.selectedUuid,
isDragging: false,
- isDraggedOver: false
+ isDraggedOver: false,
};
- componentDidUpdate = (prevProps: Readonly<Props>, prevState:
Readonly<State>, snapshot?: any) => {
- if (prevState.selectedUuid !== this.props.selectedUuid) {
- this.setState({selectedUuid: this.props.selectedUuid});
- }
- }
+ //
+ // componentDidUpdate = (prevProps: Readonly<Props>, prevState:
Readonly<State>, snapshot?: any) => {
+ // if (prevState.selectedUuid !== this.props.selectedUuid) {
+ // this.setState({selectedUuid: this.props.selectedUuid});
+ // }
+ // }
openSelector = (evt: React.MouseEvent, showSteps: boolean = true,
isInsert: boolean = false) => {
evt.stopPropagation();
@@ -126,7 +125,7 @@ export class DslElement extends React.Component<Props,
State> {
}
isSelected = (): boolean => {
- return this.state.selectedUuid === this.props.step.uuid
+ return this.props.selectedUuid.includes(this.props.step.uuid);
}
hasBorder = (): boolean => {
@@ -352,7 +351,7 @@ export class DslElement extends React.Component<Props,
State> {
deleteElement={this.props.deleteElement}
selectElement={this.props.selectElement}
moveElement={this.props.moveElement}
- selectedUuid={this.state.selectedUuid}
+ selectedUuid={this.props.selectedUuid}
inSteps={child.name === 'steps'}
position={index}
step={element}
@@ -373,7 +372,7 @@ export class DslElement extends React.Component<Props,
State> {
getAddStepButton() {
const {integration, step, selectedUuid} = this.props;
- const hideAddButton = step.dslName === 'StepDefinition' &&
!CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid,
selectedUuid);
+ const hideAddButton = step.dslName === 'StepDefinition' &&
!CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid,
selectedUuid.at(0));
if (hideAddButton) return (<></>)
else return (
<Tooltip position={"bottom"}
diff --git a/karavan-space/src/designer/route/DslProperties.tsx
b/karavan-space/src/designer/route/DslProperties.tsx
index 0359bb36..f0000bc1 100644
--- a/karavan-space/src/designer/route/DslProperties.tsx
+++ b/karavan-space/src/designer/route/DslProperties.tsx
@@ -36,8 +36,6 @@ import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
import {CamelUi, RouteToCreate} from "../utils/CamelUi";
import {CamelMetadataApi, PropertyMeta} from
"karavan-core/lib/model/CamelMetadata";
import {IntegrationHeader} from "../utils/KaravanComponents";
-import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon'
-import PasteIcon from '@patternfly/react-icons/dist/esm/icons/paste-icon'
import CloneIcon from "@patternfly/react-icons/dist/esm/icons/clone-icon";
interface Props {
@@ -45,8 +43,6 @@ interface Props {
step?: CamelElement,
onIntegrationUpdate?: any,
onPropertyUpdate?: (element: CamelElement, newRoute?: RouteToCreate) =>
void
- clipboardStep?: CamelElement
- onSaveClipboardStep?: (element?: CamelElement) => void
onClone?: (element: CamelElement) => void
isRouteDesigner: boolean
dark: boolean
@@ -76,15 +72,6 @@ export class DslProperties extends React.Component<Props,
State> {
}
}
- pasteClipboardStep = () => {
- if (this.props.clipboardStep && this.state.step) {
- const clone = CamelUtil.cloneStep(this.props.clipboardStep);
- clone.uuid = this.state.step.uuid;
- this.setStep(clone)
- this.props.onPropertyUpdate?.call(this, clone);
- }
- }
-
dataFormatChanged = (value: DataFormatDefinition) => {
value.uuid = this.state.step?.uuid ? this.state.step?.uuid :
value.uuid;
this.setStep(value);
@@ -144,12 +131,6 @@ export class DslProperties extends React.Component<Props,
State> {
<div className="headers">
<div className="top">
<Title headingLevel="h1" size="md">{title}</Title>
- <Tooltip content="Copy step" position="bottom">
- <Button variant="link" onClick={() =>
this.props.onSaveClipboardStep?.call(this, this.state.step)}
icon={<CopyIcon/>}/>
- </Tooltip>
- <Tooltip content="Paste step" position="bottom">
- <Button variant="link" onClick={() =>
this.pasteClipboardStep()} icon={<PasteIcon/>}/>
- </Tooltip>
</div>
<Text
component={TextVariants.p}>{descriptionLines.at(0)}</Text>
{descriptionLines.length > 1 && <ExpandableSection
toggleText={isDescriptionExpanded ? 'Show less' : 'Show more'}
diff --git a/karavan-space/src/designer/route/RouteDesigner.tsx
b/karavan-space/src/designer/route/RouteDesigner.tsx
index 949295ea..67779963 100644
--- a/karavan-space/src/designer/route/RouteDesigner.tsx
+++ b/karavan-space/src/designer/route/RouteDesigner.tsx
@@ -57,13 +57,14 @@ interface State {
parentDsl?: string
selectedPosition?: number
showSteps: boolean
- selectedUuid: string
+ selectedUuids: string []
key: string
width: number
height: number
top: number
left: number
- clipboardStep?: CamelElement
+ clipboardSteps: CamelElement[]
+ shiftKeyPressed?: boolean
ref?: any
printerRef?: any
propertyOnly: boolean
@@ -79,7 +80,8 @@ export class RouteDesigner extends React.Component<Props,
State> {
deleteMessage: '',
parentId: '',
showSteps: true,
- selectedUuid: '',
+ selectedUuids: [],
+ clipboardSteps: [],
key: "",
width: 1000,
height: 1000,
@@ -92,6 +94,8 @@ export class RouteDesigner extends React.Component<Props,
State> {
componentDidMount() {
window.addEventListener('resize', this.handleResize);
+ window.addEventListener('keydown', this.handleKeyDown);
+ window.addEventListener('keyup', this.handleKeyUp);
const element =
findDOMNode(this.state.ref.current)?.parentElement?.parentElement;
const checkResize = (mutations: any) => {
const el = mutations[0].target;
@@ -107,20 +111,74 @@ export class RouteDesigner extends React.Component<Props,
State> {
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ window.removeEventListener('keydown', this.handleKeyDown);
+ window.removeEventListener('keyup', this.handleKeyUp);
}
handleResize = (event: any) => {
this.setState({key: Math.random().toString()});
}
+ handleKeyDown = (event: KeyboardEvent) => {
+ const {integration, selectedUuids, clipboardSteps} = this.state;
+ if ((event.shiftKey)) {
+ this.setState({shiftKeyPressed: true});
+ }
+ if (window.document.hasFocus() && window.document.activeElement) {
+ if (['BODY',
'MAIN'].includes(window.document.activeElement.tagName)) {
+ let charCode = String.fromCharCode(event.which).toLowerCase();
+ if ((event.ctrlKey || event.metaKey) && charCode === 'c') {
+ const steps: CamelElement[] = []
+ selectedUuids.forEach(selectedUuid => {
+ const selectedElement =
CamelDefinitionApiExt.findElementInIntegration(integration, selectedUuid);
+ if (selectedElement) {
+ steps.push(selectedElement);
+ }
+ })
+ this.saveToClipboard(steps);
+ } else if ((event.ctrlKey || event.metaKey) && charCode ===
'v') {
+ if (clipboardSteps.length === 1 &&
clipboardSteps[0]?.dslName === 'FromDefinition') {
+ const clone = CamelUtil.cloneStep(clipboardSteps[0],
true);
+ const route =
CamelDefinitionApi.createRouteDefinition({from: clone});
+ this.addStep(route, '', 0)
+ } else if (selectedUuids.length === 1) {
+ const targetMeta =
CamelDefinitionApiExt.findElementMetaInIntegration(integration,
selectedUuids[0]);
+ clipboardSteps.reverse().forEach(clipboardStep => {
+ if (clipboardStep && targetMeta.parentUuid) {
+ const clone =
CamelUtil.cloneStep(clipboardStep, true);
+ this.addStep(clone, targetMeta.parentUuid,
targetMeta.position);
+ }
+ })
+ }
+ }
+ }
+ } else {
+ if (event.repeat) {
+ window.dispatchEvent(event);
+ }
+ }
+ }
+
+ handleKeyUp = (event: KeyboardEvent) => {
+ this.setState({shiftKeyPressed: false});
+ if (event.repeat) {
+ window.dispatchEvent(event);
+ }
+ }
+
componentDidUpdate = (prevProps: Readonly<Props>, prevState:
Readonly<State>, snapshot?: any) => {
if (prevState.key !== this.state.key) {
this.props.onSave?.call(this, this.state.integration,
this.state.propertyOnly);
}
}
- saveToClipboard = (step?: CamelElement): void => {
- this.setState({clipboardStep: step, key: Math.random().toString()});
+ saveToClipboard = (steps: CamelElement[]): void => {
+ if (steps.length >0) {
+ this.setState(prevState => ({
+ key: Math.random().toString(),
+ clipboardSteps: [...steps]
+ }));
+ }
}
onPropertyUpdate = (element: CamelElement, newRoute?: RouteToCreate) => {
@@ -130,14 +188,14 @@ export class RouteDesigner extends React.Component<Props,
State> {
const r = CamelDefinitionApi.createRouteDefinition({from: f, id:
newRoute.name})
i = CamelDefinitionApiExt.addStepToIntegration(i, r, '');
const clone = CamelUtil.cloneIntegration(i);
- this.setState({
+ this.setState(prevState => ({
integration: clone,
key: Math.random().toString(),
showSelector: false,
selectedStep: element,
- selectedUuid: element.uuid,
- propertyOnly: false
- });
+ propertyOnly: false,
+ selectedUuids: [element.uuid]
+ }));
} else {
const clone = CamelUtil.cloneIntegration(this.state.integration);
const i =
CamelDefinitionApiExt.updateIntegrationRouteElement(clone, element);
@@ -170,37 +228,73 @@ export class RouteDesigner extends React.Component<Props,
State> {
} else {
message = 'Delete element from route?';
}
- this.setState({selectedUuid: id, showSelector: false,
showDeleteConfirmation: true, deleteMessage: message});
+ this.setState(prevState => ({
+ showSelector: false,
+ showDeleteConfirmation: true,
+ deleteMessage: message,
+ selectedUuids: [id],
+ }));
}
deleteElement = () => {
- const id = this.state.selectedUuid;
- const i =
CamelDefinitionApiExt.deleteStepFromIntegration(this.state.integration, id);
- this.setState({
- integration: i,
- showSelector: false,
- showDeleteConfirmation: false,
- deleteMessage: '',
- key: Math.random().toString(),
- selectedStep: undefined,
- selectedUuid: '',
- propertyOnly: false
- });
- const el = new CamelElement("");
- el.uuid = id;
- EventBus.sendPosition("delete", el, undefined, new DOMRect(), new
DOMRect(), 0);
+ const id = this.state.selectedUuids.at(0);
+ if (id) {
+ const i =
CamelDefinitionApiExt.deleteStepFromIntegration(this.state.integration, id);
+ this.setState(prevState => ({
+ integration: i,
+ showSelector: false,
+ showDeleteConfirmation: false,
+ deleteMessage: '',
+ key: Math.random().toString(),
+ selectedStep: undefined,
+ propertyOnly: false,
+ selectedUuids: [id],
+ }));
+ const el = new CamelElement("");
+ el.uuid = id;
+ EventBus.sendPosition("delete", el, undefined, new DOMRect(), new
DOMRect(), 0);
+ }
}
selectElement = (element: CamelElement) => {
+ const {shiftKeyPressed, selectedUuids, integration} = this.state;
+ let canNotAdd: boolean = false;
+ if (shiftKeyPressed) {
+ const hasFrom = selectedUuids.map(e =>
CamelDefinitionApiExt.findElementInIntegration(integration, e)?.dslName ===
'FromDefinition').filter(r => r).length > 0;
+ canNotAdd = hasFrom || (selectedUuids.length > 0 &&
element.dslName === 'FromDefinition');
+ }
+ const add = shiftKeyPressed && !selectedUuids.includes(element.uuid);
+ const remove = shiftKeyPressed && selectedUuids.includes(element.uuid);
const i =
CamelDisplayUtil.setIntegrationVisibility(this.state.integration, element.uuid);
- this.setState({integration: i, selectedStep: element, selectedUuid:
element.uuid, showSelector: false})
+ this.setState((prevState: State) => {
+ if (remove) {
+ const index = prevState.selectedUuids.indexOf(element.uuid);
+ prevState.selectedUuids.splice(index, 1);
+ } else if (add && !canNotAdd) {
+ prevState.selectedUuids.push(element.uuid);
+ }
+ const uuid: string =
prevState.selectedUuids.includes(element.uuid) ? element.uuid :
prevState.selectedUuids.at(0) || '';
+ const selectedElement = shiftKeyPressed ?
CamelDefinitionApiExt.findElementInIntegration(integration, uuid) : element;
+ return {
+ integration: i,
+ selectedStep: selectedElement,
+ showSelector: false,
+ selectedUuids: shiftKeyPressed ? [...prevState.selectedUuids]
: [element.uuid],
+ }
+ });
}
unselectElement = (evt: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if ((evt.target as any).dataset.click === 'FLOWS') {
evt.stopPropagation()
const i =
CamelDisplayUtil.setIntegrationVisibility(this.state.integration, undefined);
- this.setState({integration: i, selectedStep: undefined,
selectedUuid: '', showSelector: false, selectedPosition: undefined})
+ this.setState(prevState => ({
+ integration: i,
+ selectedStep: undefined,
+ showSelector: false,
+ selectedPosition: undefined,
+ selectedUuids: [],
+ }));
}
}
@@ -222,8 +316,8 @@ export class RouteDesigner extends React.Component<Props,
State> {
onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number |
undefined) => {
switch (dsl.dsl) {
case 'FromDefinition' :
- const from = CamelDefinitionApi.createRouteDefinition({from:
new FromDefinition({uri: dsl.uri})});
- this.addStep(from, parentId, position)
+ const route = CamelDefinitionApi.createRouteDefinition({from:
new FromDefinition({uri: dsl.uri})});
+ this.addStep(route, parentId, position)
break;
case 'ToDefinition' :
const to = CamelDefinitionApi.createStep(dsl.dsl, {uri:
dsl.uri});
@@ -248,26 +342,27 @@ export class RouteDesigner extends React.Component<Props,
State> {
const clone = CamelUtil.cloneIntegration(this.state.integration);
const routeConfiguration = new RouteConfigurationDefinition();
const i =
CamelDefinitionApiExt.addRouteConfigurationToIntegration(clone,
routeConfiguration);
- this.setState({
+ this.setState(prevState => ({
integration: i,
propertyOnly: false,
key: Math.random().toString(),
selectedStep: routeConfiguration,
- selectedUuid: routeConfiguration.uuid,
- });
+ selectedUuids: [routeConfiguration.uuid],
+ }));
}
addStep = (step: CamelElement, parentId: string, position?: number |
undefined) => {
const i =
CamelDefinitionApiExt.addStepToIntegration(this.state.integration, step,
parentId, position);
const clone = CamelUtil.cloneIntegration(i);
- this.setState({
+ EventBus.sendPosition("clean", step, undefined, new DOMRect(), new
DOMRect(), 0);
+ this.setState(prevState => ({
integration: clone,
key: Math.random().toString(),
showSelector: false,
selectedStep: step,
- selectedUuid: step.uuid,
- propertyOnly: false
- });
+ propertyOnly: false,
+ selectedUuids: [step.uuid],
+ }));
}
onIntegrationUpdate = (i: Integration) => {
@@ -278,14 +373,14 @@ export class RouteDesigner extends React.Component<Props,
State> {
const i =
CamelDefinitionApiExt.moveRouteElement(this.state.integration, source, target,
asChild);
const clone = CamelUtil.cloneIntegration(i);
const selectedStep =
CamelDefinitionApiExt.findElementInIntegration(clone, source);
- this.setState({
+ this.setState(prevState => ({
integration: clone,
key: Math.random().toString(),
showSelector: false,
selectedStep: selectedStep,
- selectedUuid: source,
- propertyOnly: false
- });
+ propertyOnly: false,
+ selectedUuids: [source],
+ }));
}
onResizePage(el: HTMLDivElement | null) {
@@ -338,9 +433,7 @@ export class RouteDesigner extends React.Component<Props,
State> {
step={this.state.selectedStep}
onIntegrationUpdate={this.onIntegrationUpdate}
onPropertyUpdate={this.onPropertyUpdate}
- clipboardStep={this.state.clipboardStep}
isRouteDesigner={true}
- onSaveClipboardStep={this.saveToClipboard}
dark={this.props.dark}/>
</DrawerPanelContent>
)
@@ -356,7 +449,7 @@ export class RouteDesigner extends React.Component<Props,
State> {
integrationImageDownloadFilter = (node: HTMLElement) => {
const exclusionClasses = ['add-flow'];
return !exclusionClasses.some(classname => {
- return node.classList === undefined ? false:
node.classList.contains(classname);
+ return node.classList === undefined ? false :
node.classList.contains(classname);
});
}
@@ -364,15 +457,19 @@ export class RouteDesigner extends React.Component<Props,
State> {
if (this.state.printerRef.current === null) {
return
}
- toPng(this.state.printerRef.current, { style:{overflow:'hidden'},
cacheBust: true, filter: this.integrationImageDownloadFilter,
- height:this.state.height,width:this.state.width,
backgroundColor: this.props.dark?"black":"white" }).then(v => {
- toPng(this.state.printerRef.current, {
style:{overflow:'hidden'}, cacheBust: true, filter:
this.integrationImageDownloadFilter,
- height:this.state.height,width:this.state.width,
backgroundColor: this.props.dark?"black":"white"
}).then(this.downloadIntegrationImage);
- })
+ toPng(this.state.printerRef.current, {
+ style: {overflow: 'hidden'}, cacheBust: true, filter:
this.integrationImageDownloadFilter,
+ height: this.state.height, width: this.state.width,
backgroundColor: this.props.dark ? "black" : "white"
+ }).then(v => {
+ toPng(this.state.printerRef.current, {
+ style: {overflow: 'hidden'}, cacheBust: true, filter:
this.integrationImageDownloadFilter,
+ height: this.state.height, width: this.state.width,
backgroundColor: this.props.dark ? "black" : "white"
+ }).then(this.downloadIntegrationImage);
+ })
}
getGraph() {
- const {selectedUuid, integration, key, width, height, top, left} =
this.state;
+ const {selectedUuids, integration, key, width, height, top, left} =
this.state;
const routes = CamelUi.getRoutes(integration);
const routeConfigurations =
CamelUi.getRouteConfigurations(integration);
return (
@@ -380,14 +477,14 @@ export class RouteDesigner extends React.Component<Props,
State> {
<DslConnections height={height} width={width} top={top}
left={left} integration={integration}/>
<div className="flows" data-click="FLOWS" onClick={event =>
this.unselectElement(event)}
ref={el => this.onResizePage(el)}>
- {routeConfigurations?.map((routeConfiguration , index:
number) => (
+ {routeConfigurations?.map((routeConfiguration, index:
number) => (
<DslElement key={routeConfiguration.uuid + key}
integration={integration}
openSelector={this.openSelector}
deleteElement={this.showDeleteConfirmation}
selectElement={this.selectElement}
moveElement={this.moveElement}
- selectedUuid={selectedUuid}
+ selectedUuid={selectedUuids}
inSteps={false}
position={index}
step={routeConfiguration}
@@ -400,7 +497,7 @@ export class RouteDesigner extends React.Component<Props,
State> {
deleteElement={this.showDeleteConfirmation}
selectElement={this.selectElement}
moveElement={this.moveElement}
- selectedUuid={selectedUuid}
+ selectedUuid={selectedUuids}
inSteps={false}
position={index}
step={route}
diff --git a/karavan-space/src/designer/utils/EventBus.ts
b/karavan-space/src/designer/utils/EventBus.ts
index 98e573d7..2df867f4 100644
--- a/karavan-space/src/designer/utils/EventBus.ts
+++ b/karavan-space/src/designer/utils/EventBus.ts
@@ -27,9 +27,9 @@ export class DslPosition {
position: number = 0;
rect: DOMRect = new DOMRect();
headerRect: DOMRect = new DOMRect();
- command: "add" | "delete" = "add";
+ command: "add" | "delete" | "clean" = "add";
- constructor(command: "add" | "delete",
+ constructor(command: "add" | "delete" | "clean",
step: CamelElement,
parent:CamelElement | undefined,
rect: DOMRect,
@@ -49,7 +49,7 @@ export class DslPosition {
}
export const EventBus = {
- sendPosition: (command: "add" | "delete",
+ sendPosition: (command: "add" | "delete" | "clean",
step: CamelElement,
parent: CamelElement | undefined,
rect: DOMRect,