Добавление нового функционала
This commit is contained in:
+127
-16
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
|
||||
import { createContext, useContext, type CSSProperties } from 'react';
|
||||
import { Handle, NodeResizer, Position, type Node, type NodeProps } from '@xyflow/react';
|
||||
import type { DiagramNodeData, NodeKind } from './types';
|
||||
|
||||
type ShapeNode = Node<DiagramNodeData, NodeKind>;
|
||||
@@ -7,11 +7,64 @@ type ShapeNode = Node<DiagramNodeData, NodeKind>;
|
||||
/** Ids of nodes that currently have children (i.e. render as a drill-into-able sub-process). */
|
||||
export const NodesWithChildrenContext = createContext<Set<string>>(new Set());
|
||||
|
||||
/** Current editor mode — resize handles and connection points only show in 'edit'. */
|
||||
export const ModeContext = createContext<'edit' | 'view'>('edit');
|
||||
|
||||
export const DEFAULT_NODE_COLOR: Record<NodeKind, string> = {
|
||||
task: '#eef2ff',
|
||||
startEvent: '#f0fdf4',
|
||||
intermediateEvent: '#fefce8',
|
||||
endEvent: '#fef2f2',
|
||||
exclusiveGateway: '#fff7ed',
|
||||
parallelGateway: '#faf5ff',
|
||||
inclusiveGateway: '#f0fdfa',
|
||||
};
|
||||
|
||||
const HANDLE_POSITIONS: { id: string; position: Position }[] = [
|
||||
{ id: 'top', position: Position.Top },
|
||||
{ id: 'right', position: Position.Right },
|
||||
{ id: 'bottom', position: Position.Bottom },
|
||||
{ id: 'left', position: Position.Left },
|
||||
];
|
||||
|
||||
/**
|
||||
* A single handle per side acts as both connection start and end
|
||||
* (React Flow handles are connectable both ways by default) — paired with
|
||||
* connectionMode="loose" on <ReactFlow> so any point can link to any point.
|
||||
*/
|
||||
function ShapeHandles() {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
{HANDLE_POSITIONS.map(({ id, position }) => (
|
||||
<Handle key={id} id={id} type="source" position={position} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The diamond div is a rotated square (`transform: rotate(45deg)`). Handles
|
||||
* placed at the square's own corners land on the diamond's tips once rotated
|
||||
* — corners, not `Position.Top/Right/Bottom/Left` (those are edge-midpoints,
|
||||
* which rotate onto the diamond's flat sides instead).
|
||||
*/
|
||||
const DIAMOND_HANDLES: { id: string; position: Position; style: CSSProperties }[] = [
|
||||
{ id: 'top', position: Position.Top, style: { top: 0, left: 0, transform: 'translate(-50%, -50%)' } },
|
||||
{ id: 'right', position: Position.Right, style: { top: 0, left: '100%', transform: 'translate(-50%, -50%)' } },
|
||||
{
|
||||
id: 'bottom',
|
||||
position: Position.Bottom,
|
||||
style: { top: '100%', left: '100%', transform: 'translate(-50%, -50%)' },
|
||||
},
|
||||
{ id: 'left', position: Position.Left, style: { top: '100%', left: 0, transform: 'translate(-50%, -50%)' } },
|
||||
];
|
||||
|
||||
function DiamondHandles() {
|
||||
return (
|
||||
<>
|
||||
{DIAMOND_HANDLES.map(({ id, position, style }) => (
|
||||
<Handle key={id} id={id} type="source" position={position} style={style} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,38 +79,96 @@ function SubProcessBadge({ id }: { id: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TaskNode({ id, data }: NodeProps<ShapeNode>) {
|
||||
function ShapeResizer({
|
||||
selected,
|
||||
minWidth,
|
||||
minHeight,
|
||||
keepAspectRatio = true,
|
||||
}: {
|
||||
selected: boolean;
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
keepAspectRatio?: boolean;
|
||||
}) {
|
||||
const mode = useContext(ModeContext);
|
||||
return (
|
||||
<div className="diagram-node diagram-node--task">
|
||||
<NodeResizer
|
||||
isVisible={selected && mode === 'edit'}
|
||||
minWidth={minWidth}
|
||||
minHeight={minHeight}
|
||||
keepAspectRatio={keepAspectRatio}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
|
||||
return (
|
||||
<div className="diagram-node diagram-node--task" style={{ background: data.color ?? DEFAULT_NODE_COLOR.task }}>
|
||||
<ShapeResizer selected={selected} minWidth={70} minHeight={32} keepAspectRatio={false} />
|
||||
<ShapeHandles />
|
||||
<SubProcessBadge id={id} />
|
||||
<span>{data.label}</span>
|
||||
<span className="diagram-node__label">{data.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GatewayNode({ id, data }: NodeProps<ShapeNode>) {
|
||||
const EVENT_BORDER_CLASS: Record<'startEvent' | 'intermediateEvent' | 'endEvent', string> = {
|
||||
startEvent: 'diagram-node--startEvent',
|
||||
intermediateEvent: 'diagram-node--intermediateEvent',
|
||||
endEvent: 'diagram-node--endEvent',
|
||||
};
|
||||
|
||||
function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
||||
const kind = (type as 'startEvent' | 'intermediateEvent' | 'endEvent') ?? 'startEvent';
|
||||
return (
|
||||
<div className="diagram-node diagram-node--gateway">
|
||||
<div
|
||||
className={`diagram-node diagram-node--circle ${EVENT_BORDER_CLASS[kind]}`}
|
||||
style={{ background: data.color ?? DEFAULT_NODE_COLOR[kind] }}
|
||||
>
|
||||
<ShapeResizer selected={selected} minWidth={24} minHeight={24} />
|
||||
<ShapeHandles />
|
||||
<SubProcessBadge id={id} />
|
||||
<span>{data.label}</span>
|
||||
<span className="diagram-node__caption">{data.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventNode({ id, data }: NodeProps<ShapeNode>) {
|
||||
const GATEWAY_ICON: Record<'exclusiveGateway' | 'parallelGateway' | 'inclusiveGateway', string> = {
|
||||
exclusiveGateway: '✕',
|
||||
parallelGateway: '+',
|
||||
inclusiveGateway: '○',
|
||||
};
|
||||
|
||||
function GatewayNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
||||
const kind = (type as 'exclusiveGateway' | 'parallelGateway' | 'inclusiveGateway') ?? 'exclusiveGateway';
|
||||
return (
|
||||
<div className="diagram-node diagram-node--event">
|
||||
<ShapeHandles />
|
||||
<div className="diagram-node--gateway-outer">
|
||||
<ShapeResizer selected={selected} minWidth={32} minHeight={32} />
|
||||
{/*
|
||||
The diamond is an inner square inset to 1/sqrt(2) of the outer box and
|
||||
rotated 45deg, so its tips exactly reach the outer box's edges without
|
||||
clipping. The caption sits on the (unrotated) outer box instead of the
|
||||
diamond itself, so it renders upright below the shape.
|
||||
*/}
|
||||
<div
|
||||
className="diagram-node diagram-node--diamond"
|
||||
style={{ background: data.color ?? DEFAULT_NODE_COLOR[kind] }}
|
||||
>
|
||||
<DiamondHandles />
|
||||
<span className="diagram-node__icon">{GATEWAY_ICON[kind]}</span>
|
||||
</div>
|
||||
<SubProcessBadge id={id} />
|
||||
<span>{data.label}</span>
|
||||
<span className="diagram-node__caption">{data.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const nodeTypes = {
|
||||
task: TaskNode,
|
||||
gateway: GatewayNode,
|
||||
event: EventNode,
|
||||
startEvent: EventNode,
|
||||
intermediateEvent: EventNode,
|
||||
endEvent: EventNode,
|
||||
exclusiveGateway: GatewayNode,
|
||||
parallelGateway: GatewayNode,
|
||||
inclusiveGateway: GatewayNode,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user