import type { dia } from '@joint/core'; import { memo, useContext, useEffect, useSyncExternalStore } from 'react'; import { createPortal } from 'react-dom'; import { useCellId } from '../../hooks'; import { PortGroupContext } from '../../context/port-group-context'; import { useGraphStore } from '../../hooks/use-graph-store'; import { PORTAL_SELECTOR } from '../../data/create-ports-data'; import { jsx } from '../../utils/joint-jsx/jsx-to-markup'; import { createElements } from '../../utils/create'; import { PaperContext } from '../../context'; const elementMarkup = jsx(); export enum Magnet { PASSIVE = 'passive', } export interface PortItemProps { /** * Magnet - define if the port is passive or not. It can be set to any value inside the paper. * @default true */ readonly magnet?: string; /** * The id of the port. It must be unique within the cell. */ readonly id: string; /** * The group id of the port. It must be unique within the cell. */ readonly groupId?: string; /** * The z-index of the port. It must be unique within the cell. */ readonly z?: number | 'auto'; /* * The x position of the port. It can be a number or a string. */ readonly children?: React.ReactNode; /** * The y position of the port. It can be a number or a string. */ readonly x?: number | string; /** * The y position of the port. It can be a number or a string. */ readonly y?: number | string; /** * The x offset of the port. It can be a number or a string. */ readonly dx?: number; /** * The y offset of the port. It can be a number or a string. */ readonly dy?: number; } // eslint-disable-next-line jsdoc/require-jsdoc function Component(props: PortItemProps) { const { magnet, id, children, groupId, z, x, y, dx, dy } = props; const cellId = useCellId(); const paperCtx = useContext(PaperContext); if (!paperCtx) { throw new Error('PortItem must be used within a `PaperProvider` or `Paper` component'); } const { portsStore, paper } = paperCtx; const { graph } = useGraphStore(); const contextGroupId = useContext(PortGroupContext); useEffect(() => { const cell = graph.getCell(cellId); if (!cell) { throw new Error(`Cell with id ${cellId} not found`); } if (!cell.isElement()) { return; } if (!id) { throw new Error(`Port id is required`); } const alreadyExists = cell.getPorts().some((p) => p.id === id); if (alreadyExists) { throw new Error(`Port with id ${id} already exists`); } const port: dia.Element.Port = { group: groupId ?? contextGroupId, z, id, args: { dx, dy, x, y, }, attrs: { [PORTAL_SELECTOR]: { magnet: magnet ?? true, }, }, markup: elementMarkup, }; cell.addPort(port); return () => { cell.removePort(id); }; }, [cellId, contextGroupId, graph, groupId, id, x, y, z, magnet, dx, dy]); const portalNode = useSyncExternalStore( portsStore.subscribe, () => portsStore.getPortElement(cellId, id), () => portsStore.getPortElement(cellId, id) ); useEffect(() => { if (!portalNode) { return; } const elementView = paper.findViewByModel(cellId); elementView.cleanNodesCache(); for (const link of graph.getConnectedLinks(elementView.model)) { const target = link.target(); const source = link.source(); const isElementLink = target.id === cellId || source.id === cellId; if (!isElementLink) { continue; } const isPortLink = target.port === id || source.port === id; if (!isPortLink) { continue; } // @ts-expect-error we use private jointjs api method, it throw error here. link.findView(paper).requestConnectionUpdate({ async: false }); } }, [cellId, graph, id, paper, portalNode]); if (!portalNode) { return null; } return createPortal(children, portalNode); } /** * Create portal based on react component, * @experimental This feature is experimental and may change in the future. * @group Components * @category Port * @returns * @example * With any html element: * ```tsx * import { Port } from '@joint/react'; * * * * ``` * @example * With SVG element: * ```tsx * import { Port } from '@joint/react'; * * * * ``` */ export const PortItem = memo(Component); createElements([ { id: 'port-one', }, ]);