mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Implement node dragging behavior
This commit is contained in:
parent
0b8d66a172
commit
5e6b6fff5b
|
@ -3,11 +3,14 @@
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import { OssFlow } from './oss-flow';
|
import { OssFlow } from './oss-flow';
|
||||||
|
import { OssFlowState } from './oss-flow-state';
|
||||||
|
|
||||||
export function EditorOssGraph() {
|
export function EditorOssGraph() {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<OssFlow />
|
<OssFlowState>
|
||||||
|
<OssFlow />
|
||||||
|
</OssFlowState>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,14 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
||||||
import { useOssEdit } from '../../oss-edit-context';
|
import { useOssEdit } from '../../oss-edit-context';
|
||||||
|
import { useOssFlow } from '../oss-flow-context';
|
||||||
|
|
||||||
export const BLOCK_NODE_MIN_WIDTH = 160;
|
export const BLOCK_NODE_MIN_WIDTH = 160;
|
||||||
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
||||||
|
|
||||||
export function BlockNode(node: BlockInternalNode) {
|
export function BlockNode(node: BlockInternalNode) {
|
||||||
const { selected, schema } = useOssEdit();
|
const { selected, schema } = useOssEdit();
|
||||||
|
const { dropTarget } = useOssFlow();
|
||||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
||||||
|
|
||||||
|
@ -42,7 +44,8 @@ export function BlockNode(node: BlockInternalNode) {
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-node-block h-full w-full',
|
'cc-node-block h-full w-full',
|
||||||
isParent && 'border-primary',
|
dropTarget && isParent && dropTarget !== node.data.block.id && 'border-destructive',
|
||||||
|
((isParent && !dropTarget) || dropTarget === node.data.block.id) && 'border-primary',
|
||||||
isChild && 'border-accent-orange50'
|
isChild && 'border-accent-orange50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, use } from 'react';
|
||||||
|
|
||||||
|
interface IOssFlowContext {
|
||||||
|
dropTarget: number | null;
|
||||||
|
setDropTarget: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
containMovement: boolean;
|
||||||
|
setContainMovement: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OssFlowContext = createContext<IOssFlowContext | null>(null);
|
||||||
|
export const useOssFlow = () => {
|
||||||
|
const context = use(OssFlowContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('useOssFlow has to be used within <OssFlowState>');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { OssFlowContext } from './oss-flow-context';
|
||||||
|
|
||||||
|
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const [dropTarget, setDropTarget] = useState<number | null>(null);
|
||||||
|
const [containMovement, setContainMovement] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OssFlowContext
|
||||||
|
value={{
|
||||||
|
dropTarget,
|
||||||
|
setDropTarget,
|
||||||
|
|
||||||
|
containMovement,
|
||||||
|
setContainMovement
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OssFlowContext>
|
||||||
|
);
|
||||||
|
};
|
|
@ -11,6 +11,7 @@ import {
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
useStoreApi
|
useStoreApi
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
|
import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
|
||||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
|
@ -30,6 +31,7 @@ import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { ContextMenu, type ContextMenuData } from './context-menu/context-menu';
|
import { ContextMenu, type ContextMenuData } from './context-menu/context-menu';
|
||||||
import { OssNodeTypes } from './graph/oss-node-types';
|
import { OssNodeTypes } from './graph/oss-node-types';
|
||||||
|
import { useOssFlow } from './oss-flow-context';
|
||||||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
|
@ -51,7 +53,8 @@ export function OssFlow() {
|
||||||
isMutable,
|
isMutable,
|
||||||
canDeleteOperation: canDelete
|
canDeleteOperation: canDelete
|
||||||
} = useOssEdit();
|
} = useOssEdit();
|
||||||
const { fitView, screenToFlowPosition } = useReactFlow();
|
const { fitView, screenToFlowPosition, getIntersectingNodes } = useReactFlow();
|
||||||
|
const { setDropTarget, setContainMovement, containMovement } = useOssFlow();
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { resetSelectedElements, addSelectedNodes } = store.getState();
|
const { resetSelectedElements, addSelectedNodes } = store.getState();
|
||||||
|
|
||||||
|
@ -109,8 +112,6 @@ export function OssFlow() {
|
||||||
height: block.height
|
height: block.height
|
||||||
},
|
},
|
||||||
parentId: block.parent ? `-${block.parent}` : undefined,
|
parentId: block.parent ? `-${block.parent}` : undefined,
|
||||||
expandParent: true,
|
|
||||||
extent: 'parent' as const,
|
|
||||||
zIndex: Z_BLOCK
|
zIndex: Z_BLOCK
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -120,8 +121,6 @@ export function OssFlow() {
|
||||||
data: { label: operation.alias, operation: operation },
|
data: { label: operation.alias, operation: operation },
|
||||||
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
||||||
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
||||||
expandParent: true,
|
|
||||||
extent: 'parent' as const,
|
|
||||||
zIndex: Z_SCHEMA
|
zIndex: Z_SCHEMA
|
||||||
}))
|
}))
|
||||||
]);
|
]);
|
||||||
|
@ -266,6 +265,81 @@ export function OssFlow() {
|
||||||
setMouseCoords(targetPosition);
|
setMouseCoords(targetPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: React.MouseEvent, target: Node) {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
setContainMovement(true);
|
||||||
|
setNodes(prev =>
|
||||||
|
prev.map(node =>
|
||||||
|
node.id === target.id || selected.includes(Number(node.id))
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
extent: 'parent',
|
||||||
|
expandParent: true
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setContainMovement(false);
|
||||||
|
}
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrag(event: React.MouseEvent) {
|
||||||
|
if (containMovement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mousePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
const blocks = getIntersectingNodes({
|
||||||
|
x: mousePosition.x,
|
||||||
|
y: mousePosition.y,
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
})
|
||||||
|
.map(node => Number(node.id))
|
||||||
|
.filter(id => id < 0)
|
||||||
|
.map(id => schema.blockByID.get(-id))
|
||||||
|
.filter(block => !!block);
|
||||||
|
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
setDropTarget(null);
|
||||||
|
return;
|
||||||
|
} else if (blocks.length === 1) {
|
||||||
|
setDropTarget(blocks[0].id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parents = blocks.map(block => block.parent).filter(id => !!id);
|
||||||
|
const potentialTargets = blocks.map(block => block.id).filter(id => !parents.includes(id));
|
||||||
|
if (potentialTargets.length === 0) {
|
||||||
|
setDropTarget(null);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setDropTarget(potentialTargets[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStop(_: React.MouseEvent, target: Node) {
|
||||||
|
if (containMovement) {
|
||||||
|
setNodes(prev =>
|
||||||
|
prev.map(node =>
|
||||||
|
node.id === target.id || selected.includes(Number(node.id))
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
extent: undefined,
|
||||||
|
expandParent: undefined
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO: process drop event
|
||||||
|
}
|
||||||
|
setContainMovement(false);
|
||||||
|
setDropTarget(null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -288,7 +362,10 @@ export function OssFlow() {
|
||||||
|
|
||||||
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||||
|
|
||||||
<div className='cc-fade-in relative w-[100vw] cc-mask-sides' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
<div
|
||||||
|
className={clsx('cc-fade-in relative w-[100vw] cc-mask-sides', !containMovement && 'cursor-relocate')}
|
||||||
|
style={{ height: mainHeight, fontFamily: 'Rubik' }}
|
||||||
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
|
@ -306,7 +383,9 @@ export function OssFlow() {
|
||||||
onClick={() => setIsContextMenuOpen(false)}
|
onClick={() => setIsContextMenuOpen(false)}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onNodeDragStart={() => setIsContextMenuOpen(false)}
|
onNodeDragStart={handleDragStart}
|
||||||
|
onNodeDrag={handleDrag}
|
||||||
|
onNodeDragStop={handleDragStop}
|
||||||
>
|
>
|
||||||
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
|
@ -304,6 +304,10 @@
|
||||||
outline-color: var(--color-graph-selected);
|
outline-color: var(--color-graph-selected);
|
||||||
border-color: var(--color-foreground);
|
border-color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-relocate .dragging & {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility cc-node-block {
|
@utility cc-node-block {
|
||||||
|
@ -326,4 +330,8 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-relocate .dragging & {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user