diff --git a/README.md b/README.md index d3708de8..a5fb0dd6 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,79 @@ const Example = ( ### Readonly Mode [stories/ReadonlyMode.tsx](./stories/ReadonlyMode.tsx) +### Tooltips +[stories/Tooltips.tsx](./stories/Tooltips.tsx) + +You can add tooltips by adding `tooltipsGlobal` into the chartState (`IChart`) or `tooltip` to the node objects. +`tooltipsGlobal` will apply for all nodes and `tooltip` for individual nodes. + +#### Example + +```tsx + +export const tooltipChart: IChart = { + tooltipsGlobal: { + showTooltip: true, + toogleOffWhenClicked: 'global', + text: 'This is the global tooltip and will be toggled off, when clicked', + }, + offset: { + x: 0, + y: 0, + }, + scale: 1, + nodes: { + node1: { + tooltip: { + showTooltip: true, + text: 'this is the tooltip for node1', + }, +... + node2: { + tooltip: { + showTooltip: true, + toogleOffWhenClicked: 'node', + text: 'this is the tooltip for node2 and will be toggled off when clicked', + }, + id: 'node2', +... + node3: { + tooltip: { + showTooltip: false, + text: 'this is the tooltip for node3 but its off', + }, + id: 'node3', + type: 'input-output', + position: { + x: 100, + y: 600, + }, +... +``` +You can also customize the tooltipComponent by adding it to Component props of flowChart: + +```tsx +const ExampleToolTipComponent = (props: ITooltipComponentDefaultProps) => { + return ( +
+

{props.tooltip}

+
+ ) +} + +export class Tooltips extends React.Component { +... + return ( + + + ) +} + +``` + ### Other Demos [stories/ExternalReactState.tsx](./stories) diff --git a/package-lock.json b/package-lock.json index daf3120a..088385a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15688,8 +15688,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-json-view": { "version": "1.19.1", @@ -15820,6 +15819,32 @@ "prop-types": "^15.6.0" } }, + "react-tooltip": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.10.tgz", + "integrity": "sha512-D7ZLx6/QwpUl0SZRek3IZy/HWpsEEp0v3562tcT8IwZgu8IgV7hY5ZzniTkHyRcuL+IQnljpjj7A7zCgl2+T3w==", + "requires": { + "prop-types": "^15.7.2", + "uuid": "^7.0.3" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, "react-zoom-pan-pinch": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz", diff --git a/package.json b/package.json index 61cd1374..cb3bc302 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "pathfinding": "^0.4.18", "react-draggable": "^4.4.3", "react-resize-observer": "^1.1.1", + "react-tooltip": "^4.2.10", "react-zoom-pan-pinch": "^1.6.1", "uuid": "^3.3.2" }, diff --git a/src/components/FlowChart/FlowChart.tsx b/src/components/FlowChart/FlowChart.tsx index 16490922..2de0fb02 100644 --- a/src/components/FlowChart/FlowChart.tsx +++ b/src/components/FlowChart/FlowChart.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { - CanvasInnerDefault, CanvasOuterDefault, CanvasWrapper, ICanvasInnerDefaultProps, ICanvasOuterDefaultProps, IChart, IConfig, ILink, - ILinkDefaultProps, INodeDefaultProps, INodeInnerDefaultProps, IOnCanvasClick, IOnCanvasDrop, IOnDeleteKey, IOnDragCanvas, - IOnDragCanvasStop, IOnDragNode, IOnDragNodeStop, IOnLinkCancel, IOnLinkClick, IOnLinkComplete, IOnLinkMouseEnter, - IOnLinkMouseLeave, IOnLinkMove, IOnLinkStart, IOnNodeClick, IOnNodeDoubleClick, IOnNodeMouseEnter, IOnNodeMouseLeave, IOnNodeSizeChange, - IOnPortPositionChange, IOnZoomCanvas, IPortDefaultProps, IPortsDefaultProps, ISelectedOrHovered, LinkDefault, LinkWrapper, NodeDefault, NodeInnerDefault, NodeWrapper, PortDefault, PortsDefault, + CanvasInnerDefault, CanvasOuterDefault, CanvasWrapper, ICanvasInnerDefaultProps, ICanvasOuterDefaultProps, IChart, IConfig, IDeleteTooltip, + ILink, ILinkDefaultProps, INodeDefaultProps, INodeInnerDefaultProps, IOnCanvasClick, IOnCanvasDrop, IOnDeleteKey, IOnDragCanvas, IOnDragCanvasStop, IOnDragNode, IOnDragNodeStop, + IOnLinkCancel, IOnLinkClick, IOnLinkComplete, IOnLinkMouseEnter, IOnLinkMouseLeave, IOnLinkMove, IOnLinkStart, IOnNodeClick, IOnNodeDoubleClick, + IOnNodeMouseEnter, IOnNodeMouseLeave, IOnNodeSizeChange, IOnPortPositionChange, IOnZoomCanvas, IPortDefaultProps, IPortsDefaultProps, ISelectedOrHovered, IToggletooltip, + ITooltipComponentDefaultProps, LinkDefault, LinkWrapper, NodeDefault, NodeInnerDefault, NodeWrapper, PortDefault, PortsDefault, TooltipComponentDefault, } from '../../' import { getMatrix } from './utils/grid' @@ -29,7 +29,9 @@ export interface IFlowChartCallbacks { onNodeMouseEnter: IOnNodeMouseEnter onNodeMouseLeave: IOnNodeMouseLeave onNodeSizeChange: IOnNodeSizeChange - onZoomCanvas: IOnZoomCanvas + onZoomCanvas: IOnZoomCanvas, + deleteTooltip: IDeleteTooltip, + toggleTooltip: IToggletooltip, } export interface IFlowChartComponents { @@ -40,6 +42,7 @@ export interface IFlowChartComponents { Port?: React.FunctionComponent Node?: React.FunctionComponent Link?: React.FunctionComponent + TooltipComponent?: React.FunctionComponent } export interface IFlowChartProps { @@ -90,6 +93,8 @@ export const FlowChart = (props: IFlowChartProps) => { onNodeMouseLeave, onNodeSizeChange, onZoomCanvas, + deleteTooltip, + toggleTooltip, }, Components: { CanvasOuter = CanvasOuterDefault, @@ -99,6 +104,7 @@ export const FlowChart = (props: IFlowChartProps) => { Port = PortDefault, Node = NodeDefault, Link = LinkDefault, + TooltipComponent = TooltipComponentDefault, } = {}, config = {}, } = props @@ -106,7 +112,7 @@ export const FlowChart = (props: IFlowChartProps) => { const canvasCallbacks = { onDragCanvas, onDragCanvasStop, onCanvasClick, onDeleteKey, onCanvasDrop, onZoomCanvas } const linkCallbacks = { onLinkMouseEnter, onLinkMouseLeave, onLinkClick } - const nodeCallbacks = { onDragNode, onNodeClick, onDragNodeStop, onNodeMouseEnter, onNodeMouseLeave, onNodeSizeChange,onNodeDoubleClick } + const nodeCallbacks = { onDragNode, onNodeClick, onDragNodeStop, onNodeMouseEnter, onNodeMouseLeave, onNodeSizeChange,onNodeDoubleClick, deleteTooltip, toggleTooltip } const portCallbacks = { onPortPositionChange, onLinkStart, onLinkMove, onLinkComplete, onLinkCancel } const nodesInView = Object.keys(nodes).filter((nodeId) => { @@ -171,12 +177,18 @@ export const FlowChart = (props: IFlowChartProps) => { const selectedLink = getSelectedLinkForNode(selected, nodeId, links) const hoveredLink = getSelectedLinkForNode(hovered, nodeId, links) + const nodeWithGlobalTooltip = { ...nodes[nodeId] } + if (chart.tooltipsGlobal && chart.tooltipsGlobal.showTooltip) { + nodeWithGlobalTooltip.tooltip = chart.tooltipsGlobal + } + return ( { endPos, ...props, } - return config.showArrowHead ? : diff --git a/src/components/Node/Node.wrapper.tsx b/src/components/Node/Node.wrapper.tsx index 2f3ccb25..552f6828 100644 --- a/src/components/Node/Node.wrapper.tsx +++ b/src/components/Node/Node.wrapper.tsx @@ -1,8 +1,10 @@ import * as React from 'react' import Draggable, { DraggableData } from 'react-draggable' import ResizeObserver from 'react-resize-observer' +import ReactTooltip from 'react-tooltip' import { IConfig, + IDeleteTooltip, ILink, INode, INodeInnerDefaultProps, @@ -23,16 +25,21 @@ import { IPosition, ISelectedOrHovered, ISize, + IToggletooltip, + ITooltipComponentDefaultProps, PortWrapper, + TooltipComponentDefault, } from '../../' import { noop } from '../../utils' import CanvasContext from '../Canvas/CanvasContext' +import { TooltipComponentWrapper } from '../TooltipComponent/TooltipComponent.Wrapper' import { INodeDefaultProps, NodeDefault } from './Node.default' export interface INodeWrapperProps { config: IConfig node: INode Component: React.FunctionComponent + TooltipComponent?: React.FunctionComponent offset: IPosition selected: ISelectedOrHovered | undefined hovered: ISelectedOrHovered | undefined @@ -54,6 +61,8 @@ export interface INodeWrapperProps { onNodeSizeChange: IOnNodeSizeChange onNodeMouseEnter: IOnNodeMouseEnter onNodeMouseLeave: IOnNodeMouseLeave + deleteTooltip: IDeleteTooltip + toggleTooltip: IToggletooltip } export const NodeWrapper = ({ @@ -65,6 +74,7 @@ export const NodeWrapper = ({ onNodeDoubleClick, isSelected, Component = NodeDefault, + TooltipComponent = TooltipComponentDefault, onNodeSizeChange, onNodeMouseEnter, onNodeMouseLeave, @@ -81,6 +91,8 @@ export const NodeWrapper = ({ onLinkMove, onLinkComplete, onLinkCancel, + toggleTooltip, + deleteTooltip, }: INodeWrapperProps) => { const { zoomScale } = React.useContext(CanvasContext) const [size, setSize] = React.useState({ width: 0, height: 0 }) @@ -119,6 +131,12 @@ export const NodeWrapper = ({ onNodeClick({ config, nodeId: node.id }) } } + if (node.tooltip && node.tooltip.showTooltip) { + switch (node.tooltip.toogleOffWhenClicked) { + case 'global': toggleTooltip({ nodeId: 'global' }); break + case 'node' : toggleTooltip({ nodeId: node.id }); break + } + } }, [config, node.id], ) @@ -157,8 +175,16 @@ export const NodeWrapper = ({ } }, [node, compRef.current, size.width, size.height]) + let tooltip = '' + if (node.tooltip && node.tooltip.showTooltip) { + tooltip = node.tooltip.text + } const children = ( -
+
{ const newSize = { width: rect.width, height: rect.height } @@ -189,6 +215,9 @@ export const NodeWrapper = ({ /> ))} + + {tooltip && } +
) diff --git a/src/components/TooltipComponent/TooltipComponent.Wrapper.tsx b/src/components/TooltipComponent/TooltipComponent.Wrapper.tsx new file mode 100644 index 00000000..6dce7763 --- /dev/null +++ b/src/components/TooltipComponent/TooltipComponent.Wrapper.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' +import { ITooltipComponentDefaultProps, TooltipComponentDefault } from './TooltipComponent.default' + +export const TooltipComponentWrapper = + ({ + Component = TooltipComponentDefault, + tooltip, + }: ITooltipComponentWrapperProps) => { + return ( + <> + + + ) + } +export interface ITooltipComponentWrapperProps { + Component: React.FunctionComponent + className?: string + tooltip: string + style?: object +} diff --git a/src/components/TooltipComponent/TooltipComponent.default.tsx b/src/components/TooltipComponent/TooltipComponent.default.tsx new file mode 100644 index 00000000..17c187c5 --- /dev/null +++ b/src/components/TooltipComponent/TooltipComponent.default.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +export const TooltipComponentDefault = ({ tooltip }: ITooltipComponentDefaultProps) => { + return ( + <>{tooltip} + ) +} +export interface ITooltipComponentDefaultProps { + className?: string + tooltip: string + style?: object +} diff --git a/src/components/TooltipComponent/index.ts b/src/components/TooltipComponent/index.ts new file mode 100644 index 00000000..57b328aa --- /dev/null +++ b/src/components/TooltipComponent/index.ts @@ -0,0 +1 @@ +export * from './TooltipComponent.default' diff --git a/src/components/index.ts b/src/components/index.ts index d8886094..43175508 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,3 +6,4 @@ export * from './Ports' export * from './PortsGroup' export * from './Link' export * from './FlowChart' +export * from './TooltipComponent' diff --git a/src/container/actions.ts b/src/container/actions.ts index abb496d0..f86589d3 100644 --- a/src/container/actions.ts +++ b/src/container/actions.ts @@ -1,6 +1,6 @@ import { IChart, - IConfig, + IConfig, IDeleteTooltip, identity, IOnCanvasClick, IOnCanvasDrop, @@ -23,7 +23,7 @@ import { IOnNodeSizeChange, IOnPortPositionChange, IOnZoomCanvas, - IStateCallback, + IStateCallback, IToggletooltip, } from '../' import { rotate } from './utils/rotate' @@ -293,3 +293,40 @@ export const onZoomCanvas: IOnZoomCanvas = ({ config, data }) => (chart: IChart) chart.scale = data.scale return chart } + +export const deleteTooltip: IDeleteTooltip = ({ nodeId }) => (chart: IChart): IChart => { + switch (nodeId) { + case 'global': + delete chart.tooltipsGlobal + break + case undefined: + break + default: + delete chart.nodes[nodeId].tooltip + break + } + + return chart +} + +export const toggleTooltip: IToggletooltip = ({ nodeId }) => (chart: IChart): IChart => { + if (nodeId === 'global') { + if (chart.tooltipsGlobal) { + chart.tooltipsGlobal.showTooltip ? + chart.tooltipsGlobal.showTooltip = false : + chart.tooltipsGlobal.showTooltip = true + } + } + if (nodeId && nodeId !== 'global') { + if (chart.nodes[nodeId] && chart.nodes[nodeId].tooltip) { + // typescript doesn't understand, that there is a check for undefined above. + // @ts-ignore + chart.nodes[nodeId].tooltip.showTooltip ? + // @ts-ignore + chart.nodes[nodeId].tooltip.showTooltip = false : + // @ts-ignore + chart.nodes[nodeId].tooltip.showTooltip = true + } + } + return chart +} diff --git a/src/types/chart.ts b/src/types/chart.ts index 61406a32..cca9e566 100644 --- a/src/types/chart.ts +++ b/src/types/chart.ts @@ -17,6 +17,7 @@ export type IChart< /** System Temp */ selected: ISelectedOrHovered hovered: ISelectedOrHovered, + tooltipsGlobal?: ITooltipConfig, } & (ChartProps extends undefined ? { properties?: any, } : { @@ -29,6 +30,7 @@ export interface ISelectedOrHovered { } export type INode = { + tooltip?: ITooltipConfig, id: string type: string position: IPosition @@ -74,3 +76,9 @@ export type ILink = { } : { properties: LinkProps, }) + +interface ITooltipConfig { + showTooltip: boolean, + toogleOffWhenClicked?: 'global' | 'node' + text: string, +} diff --git a/src/types/functions.ts b/src/types/functions.ts index 1db5461d..743f7351 100644 --- a/src/types/functions.ts +++ b/src/types/functions.ts @@ -116,3 +116,7 @@ export interface IOnCanvasDropInput { export type IOnCanvasDrop = (input: IOnCanvasDropInput) => void export type IOnZoomCanvas = (input: { config?: IConfig; data: any }) => void + +export type IDeleteTooltip = (input: { nodeId?: string }) => void + +export type IToggletooltip = (input: { nodeId?: string }) => void diff --git a/stories/Tooltips.tsx b/stories/Tooltips.tsx new file mode 100644 index 00000000..cf150fdc --- /dev/null +++ b/stories/Tooltips.tsx @@ -0,0 +1,59 @@ +import { cloneDeep, mapValues } from 'lodash' +import * as React from 'react' +import { FlowChart } from '../src' +import { ITooltipComponentDefaultProps } from '../src/components/TooltipComponent/TooltipComponent.default' +import * as actions from '../src/container/actions' +import { Page } from './components' +import { tooltipChart } from './misc/tooltipChartState' + +/** + * State is external to the Element + * + * You could easily move this state to Redux or similar by creating your own callback actions. + */ + +const ExampleToolTipComponent = (props: ITooltipComponentDefaultProps) => { + return ( +
+

{props.tooltip}

+
+ ) +} + +export class Tooltips extends React.Component { + public state = cloneDeep(tooltipChart) + public render () { + const chart = this.state + const stateActions = mapValues(actions, (func: any) => + (...args: any) => this.setState(func(...args))) as typeof actions + + return ( +
+ + + + + + + + + + +
+ ) + } +} diff --git a/stories/index.tsx b/stories/index.tsx index 90061abf..650c4f20 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -12,13 +12,14 @@ import { DragAndDropSidebar } from './DragAndDropSidebar' import { ExternalReactState } from './ExternalReactState' import { InternalReactState } from './InternalReactState' import { LinkColors } from './LinkColors' -import { NodeReadonly } from './NodeReadonly' import { LinkWithArrowHead } from './LinkWithArrowHead' +import { NodeReadonly } from './NodeReadonly' import { ReadonlyMode } from './ReadonlyMode' import { SelectableMode } from './SelectableMode' import { SelectedSidebar } from './SelectedSidebar' import { SmartRouting } from './SmartRouting' import { StressTestDemo } from './StressTest' +import { Tooltips } from './Tooltips' import { Zoom } from './Zoom' storiesOf('State', module) @@ -49,3 +50,4 @@ storiesOf('Other Config', module) .add('Zoom', () => ) .add('Type-safe properties', CustomGraphTypes) .add('Link arrow heads',() => ) + .add('Tooltips',() => ) diff --git a/stories/misc/tooltipChartState.ts b/stories/misc/tooltipChartState.ts new file mode 100644 index 00000000..1b1aa4e8 --- /dev/null +++ b/stories/misc/tooltipChartState.ts @@ -0,0 +1,150 @@ +import { IChart } from '../../src' + +export const tooltipChart: IChart = { + tooltipsGlobal: { + showTooltip: true, + toogleOffWhenClicked: 'global', + text: 'This is the global tooltip and will be toggled off, when clicked', + }, + offset: { + x: 0, + y: 0, + }, + scale: 1, + nodes: { + node1: { + tooltip: { + showTooltip: true, + text: 'this is the tooltip for node1', + }, + id: 'node1', + type: 'output-only', + position: { + x: 300, + y: 100, + }, + ports: { + port1: { + id: 'port1', + type: 'output', + properties: { + value: 'yes', + }, + }, + port2: { + id: 'port2', + type: 'output', + properties: { + value: 'no', + }, + }, + }, + }, + node2: { + tooltip: { + showTooltip: true, + toogleOffWhenClicked: 'node', + text: 'this is the tooltip for node2 and will be toggled off when clicked', + }, + id: 'node2', + type: 'input-output', + position: { + x: 300, + y: 300, + }, + ports: { + port1: { + id: 'port1', + type: 'input', + }, + port2: { + id: 'port2', + type: 'output', + }, + }, + }, + node3: { + tooltip: { + showTooltip: false, + text: 'this is the tooltip for node3 but its off', + }, + id: 'node3', + type: 'input-output', + position: { + x: 100, + y: 600, + }, + ports: { + port1: { + id: 'port1', + type: 'input', + }, + port2: { + id: 'port2', + type: 'output', + }, + }, + }, + node4: { + id: 'node4', + type: 'input-output', + position: { + x: 500, + y: 600, + }, + ports: { + port1: { + id: 'port1', + type: 'input', + }, + port2: { + id: 'port2', + type: 'output', + }, + }, + }, + }, + links: { + link1: { + id: 'link1', + from: { + nodeId: 'node1', + portId: 'port2', + }, + to: { + nodeId: 'node2', + portId: 'port1', + }, + properties: { + label: 'example link label', + }, + }, + link2: { + id: 'link2', + from: { + nodeId: 'node2', + portId: 'port2', + }, + to: { + nodeId: 'node3', + portId: 'port1', + }, + properties: { + label: 'another example link label', + }, + }, + link3: { + id: 'link3', + from: { + nodeId: 'node2', + portId: 'port2', + }, + to: { + nodeId: 'node4', + portId: 'port1', + }, + }, + }, + selected: {}, + hovered: {}, +}