@@ -15,6 +15,19 @@ describe('Basic Flow Rendering', () => {
|
|||||||
cy.get('.react-flow__background');
|
cy.get('.react-flow__background');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('selects two nodes by clicks', () => {
|
||||||
|
cy.get('body').type('{cmd}', { release: false });
|
||||||
|
cy.get('.react-flow__node:first')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'selected')
|
||||||
|
.get('.react-flow__node:last')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'selected')
|
||||||
|
.get('.react-flow__node:first')
|
||||||
|
.should('have.class', 'selected');
|
||||||
|
cy.get('body').type('{cmd}', { release: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('selects a node by click', () => {
|
it('selects a node by click', () => {
|
||||||
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
|
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ describe('Interaction Flow Rendering', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('zooms by double click', () => {
|
it('zooms by double click', () => {
|
||||||
|
cy.get('.react-flow__controls-zoomout').click();
|
||||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||||
|
|
||||||
cy.get('.react-flow__renderer')
|
cy.get('.react-flow__renderer')
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
markerEndId,
|
markerEndId,
|
||||||
isHidden,
|
isHidden,
|
||||||
}: WrapEdgeProps) => {
|
}: WrapEdgeProps) => {
|
||||||
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements);
|
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||||
|
|
||||||
const inactive = !elementsSelectable && !onClick;
|
const inactive = !elementsSelectable && !onClick;
|
||||||
const edgeClasses = cc([
|
const edgeClasses = cc([
|
||||||
@@ -46,7 +46,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
const onEdgeClick = useCallback(
|
const onEdgeClick = useCallback(
|
||||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||||
if (elementsSelectable) {
|
if (elementsSelectable) {
|
||||||
setSelectedElements({ id, source, target });
|
addSelectedElements({ id, source, target });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
isDragging,
|
isDragging,
|
||||||
}: WrapNodeProps) => {
|
}: WrapNodeProps) => {
|
||||||
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
|
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
|
||||||
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements);
|
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||||
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
||||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
|
|
||||||
@@ -94,13 +94,16 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
if (!isDraggable) {
|
if (!isDraggable) {
|
||||||
if (isSelectable) {
|
if (isSelectable) {
|
||||||
unsetNodesSelection();
|
unsetNodesSelection();
|
||||||
setSelectedElements({ id: node.id, type: node.type } as Node);
|
|
||||||
|
if (!selected) {
|
||||||
|
addSelectedElements({ id: node.id, type: node.type } as Node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick?.(event, node);
|
onClick?.(event, node);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSelectable, isDraggable, onClick, node]
|
[isSelectable, selected, isDraggable, onClick, node]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragStart = useCallback(
|
const onDragStart = useCallback(
|
||||||
@@ -109,10 +112,13 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
|
|
||||||
if (selectNodesOnDrag && isSelectable) {
|
if (selectNodesOnDrag && isSelectable) {
|
||||||
unsetNodesSelection();
|
unsetNodesSelection();
|
||||||
setSelectedElements({ id: node.id, type: node.type } as Node);
|
|
||||||
|
if (!selected) {
|
||||||
|
addSelectedElements({ id: node.id, type: node.type } as Node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[node, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrag = useCallback(
|
const onDrag = useCallback(
|
||||||
@@ -133,8 +139,8 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
// onDragStop also gets called when user just clicks on a node.
|
// onDragStop also gets called when user just clicks on a node.
|
||||||
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
if (isSelectable && !selectNodesOnDrag) {
|
if (isSelectable && !selectNodesOnDrag && !selected) {
|
||||||
setSelectedElements({ id: node.id, type: node.type } as Node);
|
addSelectedElements({ id: node.id, type: node.type } as Node);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick?.(event as MouseEvent, node);
|
onClick?.(event as MouseEvent, node);
|
||||||
@@ -143,13 +149,13 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNodePosDiff({
|
updateNodePosDiff({
|
||||||
id,
|
id: node.id,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
onNodeDragStop?.(event as MouseEvent, node);
|
onNodeDragStop?.(event as MouseEvent, node);
|
||||||
},
|
},
|
||||||
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging]
|
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -77,14 +77,11 @@ export default ({
|
|||||||
onSelectionDrag(event, selectedNodes);
|
onSelectionDrag(event, selectedNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNodes?.forEach((node) => {
|
updateNodePosDiff({
|
||||||
updateNodePosDiff({
|
diff: {
|
||||||
id: node.id,
|
x: data.deltaX,
|
||||||
diff: {
|
y: data.deltaY,
|
||||||
x: data.deltaX,
|
},
|
||||||
y: data.deltaY,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onSelectionDrag, selectedNodes, updateNodePosDiff]
|
[onSelectionDrag, selectedNodes, updateNodePosDiff]
|
||||||
@@ -92,11 +89,8 @@ export default ({
|
|||||||
|
|
||||||
const onStop = useCallback(
|
const onStop = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
selectedNodes?.forEach((node) => {
|
updateNodePosDiff({
|
||||||
updateNodePosDiff({
|
isDragging: false,
|
||||||
id: node.id,
|
|
||||||
isDragging: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onSelectionDragStop?.(event, selectedNodes);
|
onSelectionDragStop?.(event, selectedNodes);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const FlowRenderer = ({
|
|||||||
onMoveStart,
|
onMoveStart,
|
||||||
onMoveEnd,
|
onMoveEnd,
|
||||||
selectionKeyCode,
|
selectionKeyCode,
|
||||||
|
multiSelectionKeyCode,
|
||||||
elementsSelectable,
|
elementsSelectable,
|
||||||
zoomOnScroll,
|
zoomOnScroll,
|
||||||
panOnScroll,
|
panOnScroll,
|
||||||
@@ -50,15 +51,18 @@ const FlowRenderer = ({
|
|||||||
onSelectionContextMenu,
|
onSelectionContextMenu,
|
||||||
}: FlowRendererProps) => {
|
}: FlowRendererProps) => {
|
||||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
|
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
|
||||||
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
|
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
|
||||||
|
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||||
|
|
||||||
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode });
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
onPaneClick?.(event);
|
onPaneClick?.(event);
|
||||||
unsetNodesSelection();
|
unsetNodesSelection();
|
||||||
|
resetSelectedElements();
|
||||||
},
|
},
|
||||||
[onPaneClick]
|
[onPaneClick]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface GraphViewProps {
|
|||||||
connectionLineStyle?: CSSProperties;
|
connectionLineStyle?: CSSProperties;
|
||||||
connectionLineComponent?: ConnectionLineComponent;
|
connectionLineComponent?: ConnectionLineComponent;
|
||||||
deleteKeyCode: number;
|
deleteKeyCode: number;
|
||||||
|
multiSelectionKeyCode: number;
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
snapGrid: [number, number];
|
snapGrid: [number, number];
|
||||||
onlyRenderVisibleNodes: boolean;
|
onlyRenderVisibleNodes: boolean;
|
||||||
@@ -98,6 +99,7 @@ const GraphView = ({
|
|||||||
connectionLineStyle,
|
connectionLineStyle,
|
||||||
connectionLineComponent,
|
connectionLineComponent,
|
||||||
selectionKeyCode,
|
selectionKeyCode,
|
||||||
|
multiSelectionKeyCode,
|
||||||
onElementsRemove,
|
onElementsRemove,
|
||||||
deleteKeyCode,
|
deleteKeyCode,
|
||||||
elements,
|
elements,
|
||||||
@@ -249,6 +251,7 @@ const GraphView = ({
|
|||||||
onElementsRemove={onElementsRemove}
|
onElementsRemove={onElementsRemove}
|
||||||
deleteKeyCode={deleteKeyCode}
|
deleteKeyCode={deleteKeyCode}
|
||||||
selectionKeyCode={selectionKeyCode}
|
selectionKeyCode={selectionKeyCode}
|
||||||
|
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||||
elementsSelectable={elementsSelectable}
|
elementsSelectable={elementsSelectable}
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
onMoveStart={onMoveStart}
|
onMoveStart={onMoveStart}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
|||||||
connectionLineComponent?: ConnectionLineComponent;
|
connectionLineComponent?: ConnectionLineComponent;
|
||||||
deleteKeyCode?: number;
|
deleteKeyCode?: number;
|
||||||
selectionKeyCode?: number;
|
selectionKeyCode?: number;
|
||||||
|
multiSelectionKeyCode?: number;
|
||||||
snapToGrid?: boolean;
|
snapToGrid?: boolean;
|
||||||
snapGrid?: [number, number];
|
snapGrid?: [number, number];
|
||||||
onlyRenderVisibleNodes?: boolean;
|
onlyRenderVisibleNodes?: boolean;
|
||||||
@@ -134,6 +135,7 @@ const ReactFlow = ({
|
|||||||
connectionLineComponent,
|
connectionLineComponent,
|
||||||
deleteKeyCode = 8,
|
deleteKeyCode = 8,
|
||||||
selectionKeyCode = 16,
|
selectionKeyCode = 16,
|
||||||
|
multiSelectionKeyCode = 91,
|
||||||
snapToGrid = false,
|
snapToGrid = false,
|
||||||
snapGrid = [15, 15],
|
snapGrid = [15, 15],
|
||||||
onlyRenderVisibleNodes = true,
|
onlyRenderVisibleNodes = true,
|
||||||
@@ -186,6 +188,7 @@ const ReactFlow = ({
|
|||||||
selectionKeyCode={selectionKeyCode}
|
selectionKeyCode={selectionKeyCode}
|
||||||
onElementsRemove={onElementsRemove}
|
onElementsRemove={onElementsRemove}
|
||||||
deleteKeyCode={deleteKeyCode}
|
deleteKeyCode={deleteKeyCode}
|
||||||
|
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onConnectStart={onConnectStart}
|
onConnectStart={onConnectStart}
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ import { Elements } from '../types';
|
|||||||
|
|
||||||
interface HookParams {
|
interface HookParams {
|
||||||
deleteKeyCode: number;
|
deleteKeyCode: number;
|
||||||
|
multiSelectionKeyCode: number;
|
||||||
onElementsRemove?: (elements: Elements) => void;
|
onElementsRemove?: (elements: Elements) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
||||||
const selectedElements = useStoreState((state) => state.selectedElements);
|
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||||
const edges = useStoreState((state) => state.edges);
|
const edges = useStoreState((state) => state.edges);
|
||||||
|
|
||||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||||
|
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
|
||||||
|
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
|
||||||
|
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
|
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onElementsRemove && deleteKeyPressed && selectedElements) {
|
if (onElementsRemove && deleteKeyPressed && selectedElements) {
|
||||||
@@ -30,6 +35,11 @@ export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
|||||||
|
|
||||||
onElementsRemove(elementsToRemove);
|
onElementsRemove(elementsToRemove);
|
||||||
unsetNodesSelection();
|
unsetNodesSelection();
|
||||||
|
resetSelectedElements();
|
||||||
}
|
}
|
||||||
}, [deleteKeyPressed]);
|
}, [deleteKeyPressed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMultiSelectionActive(multiSelectionKeyPressed);
|
||||||
|
}, [multiSelectionKeyPressed]);
|
||||||
};
|
};
|
||||||
|
|||||||
+31
-2
@@ -84,6 +84,8 @@ export interface StoreModel {
|
|||||||
nodesConnectable: boolean;
|
nodesConnectable: boolean;
|
||||||
elementsSelectable: boolean;
|
elementsSelectable: boolean;
|
||||||
|
|
||||||
|
multiSelectionActive: boolean;
|
||||||
|
|
||||||
reactFlowVersion: string;
|
reactFlowVersion: string;
|
||||||
|
|
||||||
onConnect?: OnConnectFunc;
|
onConnect?: OnConnectFunc;
|
||||||
@@ -106,8 +108,10 @@ export interface StoreModel {
|
|||||||
setSelection: Action<StoreModel, boolean>;
|
setSelection: Action<StoreModel, boolean>;
|
||||||
|
|
||||||
unsetNodesSelection: Action<StoreModel>;
|
unsetNodesSelection: Action<StoreModel>;
|
||||||
|
resetSelectedElements: Action<StoreModel>;
|
||||||
|
|
||||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
||||||
|
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
||||||
|
|
||||||
updateTransform: Action<StoreModel, TransformXYK>;
|
updateTransform: Action<StoreModel, TransformXYK>;
|
||||||
|
|
||||||
@@ -143,6 +147,8 @@ export interface StoreModel {
|
|||||||
zoom: Thunk<StoreModel, number, any, StoreModel>;
|
zoom: Thunk<StoreModel, number, any, StoreModel>;
|
||||||
zoomIn: Thunk<StoreModel>;
|
zoomIn: Thunk<StoreModel>;
|
||||||
zoomOut: Thunk<StoreModel>;
|
zoomOut: Thunk<StoreModel>;
|
||||||
|
|
||||||
|
setMultiSelectionActive: Action<StoreModel, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storeModel: StoreModel = {
|
export const storeModel: StoreModel = {
|
||||||
@@ -191,6 +197,8 @@ export const storeModel: StoreModel = {
|
|||||||
nodesConnectable: true,
|
nodesConnectable: true,
|
||||||
elementsSelectable: true,
|
elementsSelectable: true,
|
||||||
|
|
||||||
|
multiSelectionActive: false,
|
||||||
|
|
||||||
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
|
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
|
||||||
|
|
||||||
setOnConnect: action((state, onConnect) => {
|
setOnConnect: action((state, onConnect) => {
|
||||||
@@ -255,9 +263,9 @@ export const storeModel: StoreModel = {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateNodePosDiff: action((state, { id, diff = null, isDragging = true }) => {
|
updateNodePosDiff: action((state, { id = null, diff = null, isDragging = true }) => {
|
||||||
state.elements.forEach((n) => {
|
state.elements.forEach((n) => {
|
||||||
if (n.id === id && isNode(n)) {
|
if (isNode(n) && (id === n.id || state.selectedElements?.find((sNode) => sNode.id === n.id))) {
|
||||||
if (diff) {
|
if (diff) {
|
||||||
n.__rf.position = {
|
n.__rf.position = {
|
||||||
x: n.__rf.position.x + diff.x,
|
x: n.__rf.position.x + diff.x,
|
||||||
@@ -336,6 +344,9 @@ export const storeModel: StoreModel = {
|
|||||||
|
|
||||||
unsetNodesSelection: action((state) => {
|
unsetNodesSelection: action((state) => {
|
||||||
state.nodesSelectionActive = false;
|
state.nodesSelectionActive = false;
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetSelectedElements: action((state) => {
|
||||||
state.selectedElements = null;
|
state.selectedElements = null;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -347,6 +358,20 @@ export const storeModel: StoreModel = {
|
|||||||
state.selectedElements = selectedElements;
|
state.selectedElements = selectedElements;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
addSelectedElements: thunk((actions, elements, helpers) => {
|
||||||
|
const { multiSelectionActive, selectedElements } = helpers.getState();
|
||||||
|
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
||||||
|
|
||||||
|
if (multiSelectionActive) {
|
||||||
|
const nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
|
||||||
|
actions.setSelectedElements(nextElements);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.setSelectedElements(elements);
|
||||||
|
}),
|
||||||
|
|
||||||
updateTransform: action((state, transform) => {
|
updateTransform: action((state, transform) => {
|
||||||
state.transform[0] = transform.x;
|
state.transform[0] = transform.x;
|
||||||
state.transform[1] = transform.y;
|
state.transform[1] = transform.y;
|
||||||
@@ -517,6 +542,10 @@ export const storeModel: StoreModel = {
|
|||||||
zoomOut: thunk((actions) => {
|
zoomOut: thunk((actions) => {
|
||||||
actions.zoom(-0.2);
|
actions.zoom(-0.2);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setMultiSelectionActive: action((state, isActive) => {
|
||||||
|
state.multiSelectionActive = isActive;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;
|
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;
|
||||||
|
|||||||
+1
-1
@@ -308,7 +308,7 @@ export type NodePosUpdate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NodeDiffUpdate = {
|
export type NodeDiffUpdate = {
|
||||||
id: ElementId;
|
id?: ElementId;
|
||||||
diff?: XYPosition;
|
diff?: XYPosition;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user