diff --git a/examples/react/src/examples/EdgeToolbar/CustomEdge.tsx b/examples/react/src/examples/EdgeToolbar/CustomEdge.tsx index c6429f17..2d6b621a 100644 --- a/examples/react/src/examples/EdgeToolbar/CustomEdge.tsx +++ b/examples/react/src/examples/EdgeToolbar/CustomEdge.tsx @@ -1,5 +1,4 @@ import { getBezierPath, BaseEdge, EdgeProps, useReactFlow } from '@xyflow/react'; - import { EdgeToolbar } from '@xyflow/react'; export function CustomEdge({ id, data, ...props }: EdgeProps) { @@ -13,7 +12,7 @@ export function CustomEdge({ id, data, ...props }: EdgeProps) { return ( <> - + diff --git a/examples/react/src/examples/EdgeToolbar/index.tsx b/examples/react/src/examples/EdgeToolbar/index.tsx index 4470fe33..e69ccadb 100644 --- a/examples/react/src/examples/EdgeToolbar/index.tsx +++ b/examples/react/src/examples/EdgeToolbar/index.tsx @@ -1,15 +1,14 @@ import { - ReactFlow, - MiniMap, Background, BackgroundVariant, Controls, - Node, Edge, - NodeTypes, - Position, - NodeOrigin, EdgeTypes, + MiniMap, + Node, + NodeOrigin, + Position, + ReactFlow, } from '@xyflow/react'; import { CustomEdge } from './CustomEdge'; @@ -28,13 +27,13 @@ const initialNodes: Node[] = [ { id: '2', data: { label: 'Node 2', toolbarPosition: Position.Top }, - position: { x: 200, y: 0 }, + position: { x: 100, y: 150 }, className: 'react-flow__node-default', }, { id: '3', data: { label: 'Node 3', toolbarPosition: Position.Top }, - position: { x: 100, y: 150 }, + position: { x: 200, y: 0 }, className: 'react-flow__node-default', }, ]; @@ -47,20 +46,20 @@ const initialEdges: Edge[] = [ type: 'custom', }, { - id: 'e2-3', - source: '2', - target: '3', + id: 'e3-2', + source: '3', + target: '2', type: 'custom', }, { - id: 'e3-1', - source: '3', - target: '1', + id: 'e1-3', + source: '1', + target: '3', type: 'custom', }, ]; -const defaultEdgeOptions = { zIndex: 0 }; +// const defaultEdgeOptions = { zIndex: 0 }; const nodeOrigin: NodeOrigin = [0.5, 0.5]; export default function EdgeToolbarExample() { @@ -72,7 +71,7 @@ export default function EdgeToolbarExample() { minZoom={0.2} maxZoom={4} fitView - defaultEdgeOptions={defaultEdgeOptions} + // defaultEdgeOptions={defaultEdgeOptions} edgeTypes={edgeTypes} nodeOrigin={nodeOrigin} > diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 0ebe84e9..46ea8114 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -13,6 +13,7 @@ 'detached-handle', 'drag-n-drop', 'edges', + 'edge-toolbar', 'figma', 'handle-connect', 'interaction', diff --git a/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte b/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte new file mode 100644 index 00000000..c0591ff8 --- /dev/null +++ b/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte @@ -0,0 +1,62 @@ + + +
+ + + +
diff --git a/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte b/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte new file mode 100644 index 00000000..24876c64 --- /dev/null +++ b/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte @@ -0,0 +1,26 @@ + + + + + + diff --git a/packages/react/src/additional-components/EdgeToolbar/EdgeToolbar.tsx b/packages/react/src/additional-components/EdgeToolbar/EdgeToolbar.tsx index 9228fa3d..b7745b67 100644 --- a/packages/react/src/additional-components/EdgeToolbar/EdgeToolbar.tsx +++ b/packages/react/src/additional-components/EdgeToolbar/EdgeToolbar.tsx @@ -9,11 +9,32 @@ import { Edge, ReactFlowState } from '../../types'; import type { EdgeToolbarProps } from './types'; const storeSelector = (state: ReactFlowState) => ({ - x: state.transform[0], - y: state.transform[1], zoom: state.transform[2], }); +/** + * This component can render a toolbar or tooltip to one side of a custom edge. This + * toolbar doesn't scale with the viewport so that the content is always visible. + * + * @public + * @example + * ```jsx + * import { EdgeToolbar } from "@xyflow/react"; + * + * export function CustomEdge({ id, data, ...props }: EdgeProps) { + * const [edgePath, labelX, labelY] = getBezierPath(props); + * + * return ( + * <> + * + * + * + * + * + * ); + * } + * ``` + */ export function EdgeToolbar({ edgeId, x, @@ -41,9 +62,6 @@ export function EdgeToolbar({ ); const edge = useStore(edgeSelector); - console.log('edge', edge, edgeId); - - const { x, y, zoom } = useStore(storeSelector, shallow); // if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected const isActive = typeof isVisible === 'boolean' ? isVisible : edge?.selected; @@ -52,12 +70,11 @@ export function EdgeToolbar({ return null; } - // TODO: how to get the bounds of an edge? - // const edgeRect = getInternalEdgesBounds(edge.source, edge.target); - // TODO: how to get the z-index of an edge? const zIndex = edge.zIndex ?? 0 + 1; - const transform = getEdgeToolbarTransform(x, y, { x, y, zoom }, 120, offsetY); + const { zoom } = useStore(storeSelector, shallow); + + const transform = getEdgeToolbarTransform(x, y, zoom, offsetX, offsetY); console.log('transform', transform); const wrapperStyle: CSSProperties = { position: 'absolute', diff --git a/packages/react/src/additional-components/EdgeToolbar/types.ts b/packages/react/src/additional-components/EdgeToolbar/types.ts index 651a27c8..b78adb2b 100644 --- a/packages/react/src/additional-components/EdgeToolbar/types.ts +++ b/packages/react/src/additional-components/EdgeToolbar/types.ts @@ -6,10 +6,9 @@ import type { Position, Align } from '@xyflow/system'; */ export type EdgeToolbarProps = HTMLAttributes & { /** - * By passing in an array of edge id's you can render a single tooltip for a group or collection - * of edges. + * An edge toolbar must be attached to an edge. */ - edgeId?: string; + edgeId: string; /** If `true`, edge toolbar is visible even if edge is not selected. */ isVisible?: boolean; /** @@ -41,9 +40,9 @@ export type EdgeToolbarProps = HTMLAttributes & { /** * The `offsetX` position of the edge label relative to the edge in pixels. */ - offsetX: number; + offsetX?: number; /** * The `offsetY` position of the edge label relative to the edge in pixels. */ - offsetY: number; + offsetY?: number; }; diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 981d1442..815803f7 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -22,6 +22,7 @@ export * from '$lib/plugins/Controls'; export * from '$lib/plugins/Background'; export * from '$lib/plugins/Minimap'; export * from '$lib/plugins/NodeToolbar'; +export * from '$lib/plugins/EdgeToolbar'; export * from '$lib/plugins/NodeResizer'; // store diff --git a/packages/svelte/src/lib/plugins/EdgeToolbar/EdgeToolbar.svelte b/packages/svelte/src/lib/plugins/EdgeToolbar/EdgeToolbar.svelte new file mode 100644 index 00000000..23a82a27 --- /dev/null +++ b/packages/svelte/src/lib/plugins/EdgeToolbar/EdgeToolbar.svelte @@ -0,0 +1,52 @@ + + +{#if store.domNode && isActive} + +
+ {@render children?.()} +
+
+{/if} diff --git a/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts b/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts new file mode 100644 index 00000000..602e9a61 --- /dev/null +++ b/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts @@ -0,0 +1,2 @@ +export { default as EdgeToolbar } from './EdgeToolbar.svelte'; +export * from './types'; diff --git a/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts b/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts new file mode 100644 index 00000000..cfbcbff7 --- /dev/null +++ b/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts @@ -0,0 +1,42 @@ +import type { Position, Align } from '@xyflow/system'; +import type { Snippet } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; + +export type EdgeToolbarProps = { + /** + * An edge toolbar must be attached to an edge. + */ + edgeId: string; + /** If `true`, edge toolbar is visible even if edge is not selected. */ + isVisible?: boolean; + /** + * TODO: What and how?? Needed? + * Position of the toolbar relative to the edge. + * @default Position.Top + * @example Position.TopLeft, Position.TopRight, Position.BottomLeft, Position.BottomRight + */ + position?: Position; + /** + * Align the toolbar relative to the edge. + * @default "center" + * @example Align.Start, Align.Center, Align.End + */ + align?: Align; + /** + * The `x` position of the edge label. + */ + x: number; + /** + * The `y` position of the edge label. + */ + y: number; + /** + * The `offsetX` position of the edge label relative to the edge in pixels. + */ + offsetX?: number; + /** + * The `offsetY` position of the edge label relative to the edge in pixels. + */ + offsetY?: number; + children?: Snippet; +} & HTMLAttributes; diff --git a/packages/system/src/utils/edge-toolbar.ts b/packages/system/src/utils/edge-toolbar.ts index 84ef5a5c..75c694aa 100644 --- a/packages/system/src/utils/edge-toolbar.ts +++ b/packages/system/src/utils/edge-toolbar.ts @@ -1,12 +1,10 @@ -import { type Viewport } from '..'; - export function getEdgeToolbarTransform( - labelX: number, - labelY: number, - viewport: Viewport, + x: number, + y: number, + zoom: number, offsetX: number = 0, offsetY: number = 0 ): string { // Position the toolbar at the edge label center (scaling is handled by EdgeLabelRenderer) - return `translate(${labelX + offsetX}px, ${labelY + offsetY}px) translate(-50%, -50%) scale(${1 / viewport.zoom})`; + return `translate(${x + offsetX}px, ${y + offsetY}px) translate(-50%, -50%) scale(${1 / zoom})`; }