diff --git a/README.md b/README.md index 8bd0e870..3b068061 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ const BasicFlow = () => ; - `defaultZoom`: default: `1` - `defaultPosition`: default: `[0, 0]` - `snapToGrid`: default: `false` -- `snapGrid`: [x, y] array - default: `[16, 16]` +- `snapGrid`: [x, y] array - default: `[15, 15]` - `onlyRenderVisibleNodes`: default: `true` - `translateExtent`: [default `[[-∞, -∞], [+∞, +∞]]`](https://github.com/d3/d3-zoom#zoom_translateExtent) @@ -118,7 +118,7 @@ const BasicFlow = () => ; #### Element Customization - `nodeTypes`: object with [node types](#node-types--custom-nodes) - `edgeTypes`: object with [edge types](#edge-types--custom-edges) -- `arrowHeadColor`: default: `#bbb` +- `arrowHeadColor`: default: `#b1b1b7` #### Connection Line Options - `connectionLineType`: connection line type = `default` (bezier), `straight`, `step`, `smoothstep` @@ -405,9 +405,9 @@ const FlowWithBackground = () => ( ### Prop Types - `variant`: string - has to be 'dots' or 'lines' - default: `dots` -- `gap`: number - the gap between the dots or lines - default: `16` +- `gap`: number - the gap between the dots or lines - default: `15` - `size`: number - the radius of the dots or the stroke width of the lines - default: `0.5` -- `color`: string - the color of the dots or lines - default: `#999` for dots, `#eee` for lines +- `color`: string - the color of the dots or lines - default: `#81818a` for dots, `#eee` for lines - `style`: css properties - `className`: additional class name @@ -437,6 +437,7 @@ const FlowWithMiniMap = () => ( ### Prop Types - `nodeColor`: string or function - If you pass a color as a string all nodes will get that color. If you pass a function you can return a color depending on the passed node. +- `nodeStrokeColor`: string or function - If you pass a color as a string all nodes will get that stroke color. If you pass a function you can return a color depending on the passed node. - `nodeBorderRadius`: number - `nodeClassName`: string or function for adding an additional class to the nodes inside the mini map - `maskColor`: string @@ -543,7 +544,7 @@ The React Flow wrapper has the className `react-flow`. If you want to change the You could achieve the same effect by passing a style prop to the `ReactFlow` component: ```javascript -const style = { background: 'red', width: '100%' height: '300px' }; +const style = { background: 'red', height: '300px' }; const FlowWithRedBg = ( { }); it('selects an edge by click', () => { - cy.get('.react-flow__edge:first').click().should('have.class', 'selected'); + cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected'); }); it('deselects edge', () => { @@ -71,7 +71,7 @@ describe('Basic Flow Rendering', () => { }); it('selects an edge', () => { - cy.get('.react-flow__edge:first').click().should('have.class', 'selected'); + cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected'); }); it('drags a node', () => { diff --git a/cypress/integration/flow/interaction.spec.js b/cypress/integration/flow/interaction.spec.js index 08236192..8846aade 100644 --- a/cypress/integration/flow/interaction.spec.js +++ b/cypress/integration/flow/interaction.spec.js @@ -93,7 +93,7 @@ describe('Interaction Flow Rendering', () => { }); it('selects an edge by click', () => { - cy.get('.react-flow__edge:first').click().should('have.class', 'selected'); + cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected'); }); it('toggles connectable mode', () => { diff --git a/example/src/CustomNode/ColorSelectorNode.js b/example/src/CustomNode/ColorSelectorNode.js index 54ffdacf..1b6381f3 100644 --- a/example/src/CustomNode/ColorSelectorNode.js +++ b/example/src/CustomNode/ColorSelectorNode.js @@ -8,15 +8,15 @@ export default memo(({ data }) => { console.log('handle onConnect', params)} />
Custom Color Picker Node: {data.color}
- - + + ); }); diff --git a/example/src/CustomNode/index.js b/example/src/CustomNode/index.js index b633608d..6fa29443 100644 --- a/example/src/CustomNode/index.js +++ b/example/src/CustomNode/index.js @@ -8,7 +8,7 @@ const onLoad = (reactFlowInstance) => console.log('flow loaded:', reactFlowInsta const onNodeDragStop = (event, node) => console.log('drag stop', node); const onElementClick = (event, element) => console.log('click', element); -const initBgColor = '#f0e742'; +const initBgColor = '#1A192B'; const connectionLineStyle = { stroke: '#fff' }; const snapGrid = [16, 16]; @@ -78,12 +78,17 @@ const CustomNodeFlow = () => { connectionLineStyle={connectionLineStyle} snapToGrid={true} snapGrid={snapGrid} + defaultZoom={1.5} > { - if (n.type === 'input') return 'blue'; + nodeStrokeColor={(n) => { + if (n.type === 'input') return '#0041d0'; if (n.type === 'selectorNode') return bgColor; - if (n.type === 'output') return 'green'; + if (n.type === 'output') return '#ff0072'; + }} + nodeColor={(n) => { + if (n.type === 'selectorNode') return bgColor; + return '#fff'; }} /> diff --git a/example/src/Horizontal/index.js b/example/src/Horizontal/index.js index e66e5c8a..3b9ce9fb 100644 --- a/example/src/Horizontal/index.js +++ b/example/src/Horizontal/index.js @@ -26,8 +26,22 @@ const initialElements = [ { id: '4', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 4' }, position: { x: 500, y: 0 } }, { id: '5', sourcePosition: 'top', targetPosition: 'bottom', data: { label: 'Node 5' }, position: { x: 500, y: 100 } }, { id: '6', sourcePosition: 'bottom', targetPosition: 'top', data: { label: 'Node 6' }, position: { x: 500, y: 230 } }, - { id: '7', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 7' }, position: { x: 750, y: 50 } }, - { id: '8', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 8' }, position: { x: 750, y: 300 } }, + { + id: '7', + type: 'output', + sourcePosition: 'right', + targetPosition: 'left', + data: { label: 'Node 7' }, + position: { x: 750, y: 50 }, + }, + { + id: '8', + type: 'output', + sourcePosition: 'right', + targetPosition: 'left', + data: { label: 'Node 8' }, + position: { x: 750, y: 300 }, + }, { id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }, { id: 'e1-3', source: '1', type: 'smoothstep', target: '3', animated: true }, diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js index 20246191..d5703ead 100644 --- a/example/src/Overview/index.js +++ b/example/src/Overview/index.js @@ -54,7 +54,7 @@ const initialElements = [ ), }, position: { x: 400, y: 100 }, - style: { background: '#eee', color: '#222', border: '1px solid #bbb', width: 180 }, + style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, }, { id: '4', @@ -139,14 +139,20 @@ const OverviewFlow = () => { snapGrid={snapGrid} > { + nodeStrokeColor={(n) => { if (n.style?.background) return n.style.background; - if (n.type === 'input') return '#9999ff'; - if (n.type === 'output') return '#79c9b7'; - if (n.type === 'default') return '#ff6060'; + if (n.type === 'input') return '#0041d0'; + if (n.type === 'output') return '#ff0072'; + if (n.type === 'default') return '#1a192b'; return '#eee'; }} + nodeColor={(n) => { + if (n.style?.background) return n.style.background; + + return '#fff'; + }} + borderRadius={2} /> diff --git a/example/src/index.css b/example/src/index.css index c9324205..8ad19a95 100644 --- a/example/src/index.css +++ b/example/src/index.css @@ -138,18 +138,22 @@ nav a.active:before { } .react-flow__node.dark-node { - background: #333; + background: #0041d0; color: #f8f8f8; } .react-flow__node-selectorNode { font-size: 12px; - background: #eee; - border: 1px solid #555; + background: #f0f2f3; + border: 1px solid 555; border-radius: 5px; text-align: center; } +.react-flow__node-selectorNode .react-flow__handle { + border-color: #f0f2f3; +} + @media screen and (min-width: 768px) { nav { position: relative; @@ -176,4 +180,4 @@ nav a.active:before { .overview-example__add { display: block; } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 629c8feb..cf0ae1bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11241,9 +11241,9 @@ } }, "rollup": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.27.1.tgz", - "integrity": "sha512-GiWHQvnmMgBktSpY/1+nrGpwPsTw4b9P28og2uedfeq4JZ16rzAmnQ5Pm/E0/BEmDNia1ZbY7+qu3nBgNa19Hg==", + "version": "2.28.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.2.tgz", + "integrity": "sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -11570,9 +11570,9 @@ } }, "rollup-plugin-typescript2": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.2.tgz", - "integrity": "sha512-zarMH2F8oT/NO6p20gl/jkts+WxyzOlhOIUwUU/EDx5e6ewdDPS/flwLj5XFuijUCr64bZwqKuRVwCPdXXYefQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz", + "integrity": "sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg==", "dev": true, "requires": { "@rollup/pluginutils": "^3.1.0", diff --git a/package.json b/package.json index 1edbcf07..1cf2987b 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "prop-types": "^15.7.2", "react": "^16.13.1", "release-it": "^14.0.3", - "rollup": "^2.27.1", + "rollup": "^2.28.2", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-commonjs": "^10.1.0", @@ -63,7 +63,7 @@ "rollup-plugin-replace": "^2.2.0", "rollup-plugin-serve": "^1.0.4", "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.27.2", + "rollup-plugin-typescript2": "^0.27.3", "rollup-plugin-uglify": "^6.0.4", "start-server-and-test": "^1.11.4", "typescript": "^4.0.3" diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 2007702a..4f1530a1 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -15,13 +15,13 @@ interface BackgroundProps extends HTMLAttributes { } const defaultColors = { - [BackgroundVariant.Dots]: '#999', + [BackgroundVariant.Dots]: '#81818a', [BackgroundVariant.Lines]: '#eee', }; const Background = ({ variant = BackgroundVariant.Dots, - gap = 24, + gap = 15, size = 0.5, color, style, diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx index a3ab7a43..618c6135 100644 --- a/src/additional-components/MiniMap/MiniMapNode.tsx +++ b/src/additional-components/MiniMap/MiniMapNode.tsx @@ -6,13 +6,14 @@ interface MiniMapNodeProps { y: number; width: number; height: number; - color: string; - className: string; borderRadius: number; + className: string; + color: string; + strokeColor: string; style?: CSSProperties; } -const MiniMapNode = ({ x, y, width, height, style, color, className, borderRadius }: MiniMapNodeProps) => { +const MiniMapNode = ({ x, y, width, height, style, color, strokeColor, className, borderRadius }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; @@ -26,6 +27,8 @@ const MiniMapNode = ({ x, y, width, height, style, color, className, borderRadiu width={width} height={height} fill={fill} + stroke={strokeColor} + strokeWidth={2} /> ); }; diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index f01671c6..e1670738 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -12,6 +12,7 @@ type StringFunc = (node: Node) => string; interface MiniMapProps extends React.HTMLAttributes { nodeColor?: string | StringFunc; + nodeStrokeColor?: string | StringFunc; nodeClassName?: string | StringFunc; nodeBorderRadius?: number; maskColor?: string; @@ -21,12 +22,13 @@ const defaultWidth = 200; const defaultHeight = 150; const MiniMap = ({ - style = { backgroundColor: '#f8f8f8' }, + style = { backgroundColor: '#fff' }, className, - nodeColor = '#ddd', + nodeStrokeColor = '#555', + nodeColor = '#fff', nodeClassName = '', nodeBorderRadius = 5, - maskColor = 'rgba(10, 10, 10, .25)', + maskColor = '#F0F2F3', }: MiniMapProps) => { const containerWidth = useStoreState((s) => s.width); const containerHeight = useStoreState((s) => s.height); @@ -37,6 +39,9 @@ const MiniMap = ({ const elementWidth = (style.width || defaultWidth)! as number; const elementHeight = (style.height || defaultHeight)! as number; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; + const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function + ? nodeStrokeColor + : () => nodeStrokeColor) as StringFunc; const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; const hasNodes = nodes && nodes.length; const bb = getRectOfNodes(nodes); @@ -79,6 +84,7 @@ const MiniMap = ({ className={nodeClassNameFunc(node)} color={nodeColorFunc(node)} borderRadius={nodeBorderRadius} + strokeColor={nodeStrokeColorFunc(node)} /> ))} { @@ -205,16 +208,6 @@ const GraphView = ({ }); } - const initialTransform = { - x: defaultPosition[0], - y: defaultPosition[1], - k: defaultZoom, - }; - - if (initialTransform.x !== 0 || initialTransform.y !== 0 || initialTransform.k !== 1) { - setInitTransform(initialTransform); - } - isInitialised.current = true; } }, [d3Initialised, onLoad]); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 18674df8..bf99ac32 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -83,6 +83,7 @@ const NodeRenderer = ({ onlyRenderVisibleNodes = true, ...props }: NodeRendererP const nodesDraggable = useStoreState((s) => s.nodesDraggable); const nodesConnectable = useStoreState((s) => s.nodesConnectable); const elementsSelectable = useStoreState((s) => s.elementsSelectable); + const transformStyle = { transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, }; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index aee9fa52..e92fa364 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -142,8 +142,8 @@ const ReactFlow = ({ onPaneScroll, onPaneContextMenu, }: ReactFlowProps) => { - const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []); - const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []); + const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypes]); + const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypes]); const reactFlowClasses = cc(['react-flow', className]); return ( @@ -224,7 +224,7 @@ ReactFlow.defaultProps = { deleteKeyCode: 8, selectionKeyCode: 16, snapToGrid: false, - snapGrid: [16, 16], + snapGrid: [15, 15], onlyRenderVisibleNodes: true, nodesDraggable: true, nodesConnectable: true, @@ -235,7 +235,7 @@ ReactFlow.defaultProps = { maxZoom: 2, defaultZoom: 1, defaultPosition: [0, 0], - arrowHeadColor: '#bbb', + arrowHeadColor: '#b1b1b7', zoomOnScroll: true, zoomOnDoubleClick: true, }; diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts index 4fbbef6c..60d430c7 100644 --- a/src/hooks/useD3Zoom.ts +++ b/src/hooks/useD3Zoom.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, MutableRefObject } from 'react'; import { useStoreState, useStoreActions } from '../store/hooks'; -import { FlowTransform } from '../types'; +import { FlowTransform, TranslateExtent } from '../types'; interface UseD3ZoomParams { zoomPane: MutableRefObject; @@ -9,6 +9,9 @@ interface UseD3ZoomParams { zoomOnScroll: boolean; zoomOnDoubleClick: boolean; paneMoveable: boolean; + defaultPosition: [number, number]; + defaultZoom: number; + translateExtent?: TranslateExtent; onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; @@ -34,6 +37,9 @@ export default ({ zoomOnDoubleClick, selectionKeyPressed, paneMoveable, + defaultPosition, + defaultZoom, + translateExtent, }: UseD3ZoomParams): void => { const prevTransform = useRef({ x: 0, y: 0, zoom: 0 }); const d3Zoom = useStoreState((s) => s.d3Zoom); @@ -43,7 +49,7 @@ export default ({ useEffect(() => { if (zoomPane.current) { - initD3(zoomPane.current); + initD3({ zoomPane: zoomPane.current, defaultPosition, defaultZoom, translateExtent }); } }, []); diff --git a/src/store/index.ts b/src/store/index.ts index 85e08fba..1e3e743a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -51,6 +51,12 @@ type SetSnapGrid = { snapGrid: [number, number]; }; +type InitD3 = { + zoomPane: Element; + defaultPosition: [number, number]; + defaultZoom: number; + translateExtent?: TranslateExtent; +}; export interface StoreModel { width: number; height: number; @@ -116,7 +122,7 @@ export interface StoreModel { updateSize: Action; - initD3: Action; + initD3: Action; setMinMaxZoom: Action; @@ -369,11 +375,21 @@ export const storeModel: StoreModel = { state.height = size.height || 500; }), - initD3: action((state, zoomPaneNode) => { - const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(state.translateExtent); + initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => { + const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; + const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); + const selection = select(zoomPane).call(d3ZoomInstance); - const selection = select(zoomPaneNode).call(d3ZoomInstance); + const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]); + const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]); + const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom); + const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); + selection.property('__zoom', updatedTransform); + + state.transform[0] = clampedX; + state.transform[1] = clampedY; + state.transform[2] = clampedZoom; state.d3Zoom = d3ZoomInstance; state.d3Selection = selection; state.d3Initialised = true; diff --git a/src/style.css b/src/style.css index 47c9aa0c..00a965d2 100644 --- a/src/style.css +++ b/src/style.css @@ -62,12 +62,12 @@ .react-flow__edge-path { fill: none; - stroke: #bbb; - stroke-width: 1.5; + stroke: #b1b1b7; + stroke-width: 1; } .react-flow__edge-text { - font-size: 12px; + font-size: 10px; pointer-events: none; user-select: none; } @@ -87,8 +87,8 @@ .react-flow__connection-path { fill: none; - stroke: #ddd; - stroke-width: 1.5; + stroke: #b1b1b7; + stroke-width: 1; } .react-flow__nodes { @@ -117,32 +117,58 @@ font-size: 12px; color: #222; text-align: center; + border-width: 1px; + border-style: solid; } .react-flow__node-default.selectable, .react-flow__node-input.selectable, .react-flow__node-output.selectable { - - &.selected, - &.selected:hover { - box-shadow: 0 0 0 1px #333; - } - &:hover { - box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08); } } .react-flow__node-input { - background: #9999ff; + background: #fff; + border-color: #0041d0; + + &.selected, + &.selected:hover { + box-shadow: 0 0 0 0.5px #0041d0; + } + + .react-flow__handle { + background: #0041d0; + } } .react-flow__node-default { - background: #ff6060; + background: #fff; + border-color: #1a192b; + + &.selected, + &.selected:hover { + box-shadow: 0 0 0 0.5px #1a192b; + } + + .react-flow__handle { + background: #1a192b; + } } .react-flow__node-output { - background: #79c9b7; + background: #fff; + border-color: #ff0072; + + &.selected, + &.selected:hover { + box-shadow: 0 0 0 0.5px #ff0072; + } + + .react-flow__handle { + background: #ff0072; + } } .react-flow__nodesselection { @@ -165,10 +191,12 @@ .react-flow__handle { position: absolute; - width: 10px; - height: 8px; - background: rgba(255, 255, 255, 0.4); + width: 6px; + height: 6px; + background: #555; + border: 1px solid white; pointer-events: none; + border-radius: 100%; &.connectable { pointer-events: all; @@ -179,24 +207,24 @@ .react-flow__handle-bottom { top: auto; left: 50%; - bottom: 0; + bottom: -4px; transform: translate(-50%, 0); } .react-flow__handle-top { left: 50%; - top: 0; + top: -4px; transform: translate(-50%, 0); } .react-flow__handle-left { top: 50%; - left: 0; + left: -4px; transform: translate(0, -50%); } .react-flow__handle-right { - right: 0; + right: -4px; top: 50%; transform: translate(0, -50%); -} \ No newline at end of file +}