├── .vscode └── settings.json ├── .ols.config.ts ├── src ├── styles │ ├── index.less │ └── theme │ │ ├── normal.less │ │ └── primary.less ├── types │ ├── modlue.d.ts │ └── index.ts ├── container │ ├── Background │ │ ├── index.less │ │ ├── Element.tsx │ │ └── index.tsx │ ├── EdgeRenderer │ │ ├── index.less │ │ └── index.tsx │ ├── NodeRenderer │ │ ├── index.less │ │ └── index.tsx │ ├── GraphEditor │ │ ├── index.less │ │ └── index.tsx │ ├── EdgeType │ │ ├── index.less │ │ ├── EdgeLabel.tsx │ │ └── index.tsx │ ├── ZoomWrapper │ │ ├── index.less │ │ └── index.tsx │ ├── MiniMap │ │ ├── index.less │ │ └── index.tsx │ ├── Controls │ │ ├── ControlButton.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── ConnectLineRenderer │ │ └── index.tsx │ ├── GlobalProvider │ │ └── index.tsx │ └── GraphRenderer │ │ └── index.tsx ├── hooks │ ├── useTheme.ts │ ├── usePosition.ts │ ├── useDimension.ts │ ├── useKeyPress.ts │ ├── useDelete.ts │ ├── useGlobal.ts │ ├── useClickPreventionOnDoubleClick.ts │ ├── useNodeResizeObserver.ts │ └── useZoom.ts ├── components │ ├── Node │ │ ├── Default.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── Wrapper.tsx │ ├── Icon │ │ └── index.tsx │ ├── Pointer │ │ ├── index.less │ │ ├── index.tsx │ │ └── Wrapper.tsx │ ├── Edge │ │ ├── Default.tsx │ │ ├── index.tsx │ │ └── Wrapper.tsx │ └── MarkerDefinitions │ │ └── index.tsx ├── utils │ ├── type.ts │ ├── fullscreen.ts │ ├── dom.ts │ └── graph.ts └── index.tsx ├── .prettierignore ├── docs ├── index.md └── demo │ ├── lineType.md │ ├── operation.md │ ├── dynamicNode.md │ ├── theme.md │ ├── orientation.md │ └── event.md ├── __tests__ ├── setup.ts └── button.test.tsx ├── .stylelintrc ├── .editorconfig ├── .prettierrc ├── .gitignore ├── .eslintrc ├── tsconfig.json ├── package.json └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.ols.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: "component" 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './theme/primary.less'; 2 | @import './theme/normal.less'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | .umi-test 8 | -------------------------------------------------------------------------------- /src/types/modlue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | 3 | declare interface Window { 4 | ActiveXObject: any 5 | } 6 | -------------------------------------------------------------------------------- /src/container/Background/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-background { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/container/EdgeRenderer/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-edge-renderer { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/container/NodeRenderer/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-node-renderer { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | order: 1 4 | # toc: menu 5 | --- 6 | 7 | ### Install 8 | 9 | ```bash 10 | npm i @ols-scripts/graph-editor --save 11 | ``` 12 | 13 | ### Usage 14 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tests setup. 3 | */ 4 | 5 | import Enzyme from 'enzyme'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | -------------------------------------------------------------------------------- /src/container/GraphEditor/index.less: -------------------------------------------------------------------------------- 1 | @import 'http://at.alicdn.com/t/font_2532005_6rp5xuiizc6.css'; 2 | 3 | .graph-editor { 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import useGlobal from '@/hooks/useGlobal' 2 | import { Theme } from '@/types' 3 | 4 | export default function useTheme() { 5 | const [global] = useGlobal() 6 | 7 | return global.theme as Theme 8 | } 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@@ols-scripts/eslint-config/style.js"], 3 | "ignoreFiles": ["node_modules/**", "**/*.json"], 4 | "rules": { 5 | "no-descending-specificity": null, 6 | "no-duplicate-selectors": null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Node/Default.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | import { Node } from '@/types' 4 | 5 | const DefaultNode = ({ data }: Node) => {data.label} 6 | 7 | DefaultNode.displayName = 'DefaultNode' 8 | 9 | export default memo(DefaultNode) 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/components/Node/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-node-wrapper { 2 | padding: 10px; 3 | width: 160px; 4 | min-height: 100px; 5 | text-align: center; 6 | position: absolute; 7 | transition: box-shadow 150ms ease-in-out; 8 | user-select: none; 9 | white-space: normal; 10 | 11 | &.draggable { 12 | cursor: grab; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "consistent", 3 | "singleQuote": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "semi": false, 7 | "bracketSpacing": true, 8 | "endOfLine": "lf", 9 | "arrowParens": "always", 10 | "tabWidth": 2, 11 | "useTabs": false, 12 | "printWidth": 106, 13 | "trailingComma": "all" 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import cn from 'classnames' 3 | 4 | export interface IconProps { 5 | className?: string 6 | type: string 7 | } 8 | 9 | const Icon: FC = ({ className = '', type }) => { 10 | return 11 | } 12 | 13 | export default Icon 14 | -------------------------------------------------------------------------------- /src/container/EdgeType/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-edge-wrapper { 2 | &-path { 3 | fill: none; 4 | transition: stroke,stroke-width 150ms ease-in-out; 5 | } 6 | 7 | &.is-selected { 8 | } 9 | } 10 | 11 | .graph-editor-edge-label { 12 | &-rect { 13 | fill: #fff; 14 | } 15 | &-text { 16 | font-size: 10px; 17 | user-select: none; 18 | pointer-events: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/container/ZoomWrapper/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-zoom-wrapper { 2 | width: 100%; 3 | height: 100%; 4 | z-index: 3; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | pointer-events: none; 9 | 10 | &-wheel { 11 | width: 100%; 12 | height: 100%; 13 | z-index: 2; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /package-lock.json 8 | 9 | # production 10 | /dist 11 | /docs-dist 12 | 13 | # misc 14 | .DS_Store 15 | .idea 16 | 17 | # umi 18 | .umi 19 | .umi-production 20 | .umi-test 21 | .env.local 22 | 23 | /coverage 24 | 25 | .ols/themes -------------------------------------------------------------------------------- /src/hooks/usePosition.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import useGlobal from '@/hooks/useGlobal' 3 | import { ElementId, Node } from '@/types' 4 | 5 | export default function usePosition(id: ElementId) { 6 | const [global] = useGlobal() 7 | 8 | return useMemo(() => { 9 | const sourceNode = global.nodes.find((item: Node) => item.id === id) as Node 10 | return sourceNode ? sourceNode.position : {} 11 | }, [id, global.nodes]) 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import Component from '../src'; 4 | 5 | describe('Test component', () => { 6 | 7 | it('should be called on click', () => { 8 | const clickFunc = jest.fn(() => { console.log(1); }); 9 | 10 | const calculator = mount(); 11 | 12 | calculator.simulate('click'); 13 | expect(clickFunc).toHaveBeenCalled(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/container/MiniMap/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-minimap { 2 | overflow: hidden; 3 | position: absolute; 4 | bottom: 10px; 5 | right: 10px; 6 | z-index: 5; 7 | background-color: rgba(240, 242, 243, 0.7); 8 | // border: 1px solid #b1b7ff; 9 | border-radius: 4px; 10 | box-sizing: content-box; 11 | 12 | svg { 13 | position: absolute; 14 | bottom: 0; 15 | right: 0; 16 | pointer-events: none; 17 | } 18 | 19 | &-operate { 20 | position: absolute; 21 | background: rgba(255, 255, 255, 0.8); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/@ols-scripts/eslint-config", "plugin:prettier/recommended"], 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "globals": { 8 | "document": true, 9 | "localStorage": true 10 | }, 11 | "env": { 12 | "browser": true, 13 | "node": true, 14 | "es6": true 15 | }, 16 | "rules": { 17 | "prettier/prettier": 1, 18 | "@typescript-eslint/prefer-for-of": 1, 19 | "react/jsx-curly-newline": 0, 20 | "no-shadow": "off", 21 | "no-underscore-dangle": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/container/Controls/ControlButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLAttributes, memo } from 'react' 2 | import cn from 'classnames' 3 | 4 | export interface ControlButtonProps extends HTMLAttributes { 5 | prefix?: string 6 | className?: string 7 | onClick: () => void 8 | } 9 | 10 | const ControlButton: FC = ({ 11 | prefix = 'graph-editor-controls-button', 12 | className = '', 13 | children, 14 | ...rest 15 | }) => ( 16 |
17 | {children} 18 |
19 | ) 20 | 21 | export default memo(ControlButton) 22 | -------------------------------------------------------------------------------- /src/container/Controls/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-controls { 2 | box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1); 3 | position: absolute; 4 | z-index: 5; 5 | bottom: 10px; 6 | left: 10px; 7 | 8 | &-button { 9 | background: #fefefe; 10 | border-bottom: 1px solid #eee; 11 | box-sizing: content-box; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | width: 16px; 16 | height: 16px; 17 | cursor: pointer; 18 | user-select: none; 19 | padding: 5px; 20 | 21 | &:hover { 22 | background: #f4f4f4; 23 | } 24 | 25 | .iconfont { 26 | color: #222; 27 | font-size: 18px; 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/utils/type.ts: -------------------------------------------------------------------------------- 1 | // import { MouseEvent } from 'react' 2 | 3 | // export const createEvent = (p: Type) => (event: MouseEvent, node: Type): void => {} 4 | 5 | // export function omit, K extends keyof T>(obj: T, fields: K[]): Omit { 6 | // const clone = { ...obj } 7 | 8 | // if (Array.isArray(fields)) { 9 | // fields.forEach((key) => { 10 | // delete clone[key] 11 | // }) 12 | // } 13 | 14 | // return clone 15 | // } 16 | 17 | export const tuple = (...args: T) => args 18 | 19 | export const tupleNum = (...args: T) => args 20 | 21 | export type Omit = Pick> 22 | -------------------------------------------------------------------------------- /src/components/Pointer/index.less: -------------------------------------------------------------------------------- 1 | .graph-editor-pointer-wrapper { 2 | background: #1a192b; 3 | pointer-events: all; 4 | position: absolute; 5 | border: 1px solid #fff; 6 | border-radius: 100%; 7 | 8 | &.connectable { 9 | cursor: crosshair; 10 | } 11 | 12 | &-top { 13 | left: 50%; 14 | top: 0; 15 | transform: translate(-50%, -50%); 16 | } 17 | 18 | &-bottom { 19 | left: 50%; 20 | bottom: 0; 21 | transform: translate(-50%, 50%); 22 | } 23 | 24 | &-left { 25 | left: 0; 26 | top: 50%; 27 | transform: translate(-50%, -50%); 28 | } 29 | 30 | &-right { 31 | right: 0; 32 | top: 50%; 33 | transform: translate(50%, -50%); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Edge/Default.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { ConnectionLineType } from '@/types' 3 | import { EdgeBezier, EdgeStraight, EdgeSmooth } from '@/container/EdgeType' 4 | 5 | const DefaultEdge = ({ lineType = ConnectionLineType.Bezier, source, target, ...rest }) => { 6 | if (lineType === ConnectionLineType.Bezier) { 7 | return 8 | } 9 | 10 | if (lineType === ConnectionLineType.Straight) { 11 | return 12 | } 13 | 14 | if (lineType === ConnectionLineType.Smooth) { 15 | return 16 | } 17 | 18 | return null 19 | } 20 | 21 | export default memo(DefaultEdge) 22 | -------------------------------------------------------------------------------- /src/container/Background/Element.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /** 4 | * element line type 5 | * @param size 6 | * @param strokeWidth 7 | * @param stroke 8 | * @returns React.ReactElement 9 | */ 10 | export const createGridLinesPath = ( 11 | size: number, 12 | strokeWidth: number, 13 | stroke: string, 14 | ): React.ReactElement => { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | /** 25 | * element dot type 26 | * @param size 27 | * @param fill 28 | * @returns React.ReactElement 29 | */ 30 | export const createGridDotsPath = (size: number, fill: string): React.ReactElement => { 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "target": "es6", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "allowJs": true, 8 | "checkJs": true, 9 | "jsx": "react", 10 | "declaration": true, 11 | "sourceMap": true, 12 | "noImplicitThis": true, 13 | "importHelpers": true, 14 | "noImplicitAny": false, 15 | "strictNullChecks": false, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "baseUrl": "./", 19 | "moduleResolution": "node", 20 | "strict": true, 21 | "paths": { 22 | "*": ["*"], 23 | "@/*": ["./src/*"], 24 | }, 25 | "esModuleInterop": true, 26 | "experimentalDecorators": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": ["src", "src/types"], 30 | } 31 | -------------------------------------------------------------------------------- /src/container/ConnectLineRenderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react' 2 | import useGlobal from '@/hooks/useGlobal' 3 | import useTheme from '@/hooks/useTheme' 4 | import cn from 'classnames' 5 | import DefaultEdge from '@/components/Edge/Default' 6 | import { ConnectLine, Orientation } from '@/types' 7 | 8 | export type ConnectLineProps = { 9 | connectLine: ConnectLine 10 | orientation: Orientation 11 | } 12 | 13 | const ConnectLineRenderer: FC = ({ connectLine = {}, orientation }) => { 14 | const [global] = useGlobal() 15 | const theme = useTheme() 16 | const { isConnecting, source, target } = global.connectLine 17 | 18 | if (!isConnecting) { 19 | return null 20 | } 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default memo(ConnectLineRenderer) 30 | -------------------------------------------------------------------------------- /src/container/ZoomWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, ReactNode, useCallback, useRef } from 'react' 2 | import useZoom, { UseZoomProps } from '@/hooks/useZoom' 3 | import useGlobal from '@/hooks/useGlobal' 4 | 5 | import './index.less' 6 | 7 | export type ZoomWrapperProps = { 8 | prefix: string 9 | children: (zoomInfo: UseZoomProps) => ReactNode 10 | } 11 | 12 | const ZoomWrapper: FC = ({ prefix, children }) => { 13 | const zoomRef = useRef() 14 | const [, setGlobal] = useGlobal() 15 | 16 | const zoomInfo = useZoom({ 17 | trigger: zoomRef, 18 | }) 19 | 20 | const onClick = useCallback(() => { 21 | setGlobal({ 22 | selectNode: {}, 23 | selectEdge: {}, 24 | }) 25 | }, [setGlobal]) 26 | 27 | return ( 28 | <> 29 |
{children(zoomInfo)}
30 |
31 | 32 | ) 33 | } 34 | 35 | export default memo(ZoomWrapper) 36 | -------------------------------------------------------------------------------- /src/styles/theme/normal.less: -------------------------------------------------------------------------------- 1 | .graph-editor-marker-definitions-container.normal { 2 | #graph-editor-marker-definitions__arrowclosed polyline { 3 | stroke: #b1b1b7; 4 | fill: #b1b1b7; 5 | } 6 | #graph-editor-marker-definitions__arrowclosed__selected polyline { 7 | stroke: #1a192b; 8 | fill: #1a192b; 9 | } 10 | #graph-editor-marker-definitions__arrow polyline { 11 | stroke: #b1b1b7; 12 | } 13 | #graph-editor-marker-definitions__arrow__selected polyline { 14 | stroke: #b1b1b7; 15 | } 16 | } 17 | 18 | 19 | .graph-editor-node-wrapper.normal { 20 | background: #fff; 21 | border-radius: 3px; 22 | font-size: 12px; 23 | border: 1px solid #1a192b; 24 | color: #222; 25 | &.is-selected { 26 | box-shadow: 0 0 0 0.5px #1a192b; 27 | } 28 | } 29 | 30 | 31 | .graph-editor-edge-wrapper.normal { 32 | .graph-editor-edge-wrapper-path { 33 | stroke: #b1b1b7; 34 | stroke-width: 1; 35 | } 36 | &.is-selected { 37 | .graph-editor-edge-wrapper-path { 38 | stroke: rgba(78, 88, 191, 1); 39 | stroke-width: 6; 40 | } 41 | } 42 | } 43 | 44 | 45 | .graph-editor-pointer-wrapper.normal { 46 | width: 8px; 47 | height: 8px; 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Pointer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react' 2 | import cn from 'classnames' 3 | import { PointerPosition, PointerType, XYPosition, Node, ConnectEventHandler, Orientation } from '@/types' 4 | import Wrapper from './Wrapper' 5 | 6 | import './index.less' 7 | 8 | export type PointerProps = { 9 | prefix?: string 10 | position: PointerPosition 11 | type: PointerType 12 | nodePosition: XYPosition 13 | node: Node 14 | onConnect?: ConnectEventHandler 15 | onConnectStart?: ConnectEventHandler 16 | onConnectStop?: ConnectEventHandler 17 | onConnectEnd?: ConnectEventHandler 18 | connectable?: boolean 19 | orientation?: Orientation 20 | } 21 | 22 | const Pointer: FC = ({ 23 | prefix, 24 | type, 25 | position, 26 | node, 27 | nodePosition, 28 | connectable, 29 | orientation, 30 | }) => { 31 | return ( 32 | 40 |
41 | 42 | ) 43 | } 44 | 45 | export default memo(Pointer) 46 | -------------------------------------------------------------------------------- /src/components/Node/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ComponentType, memo } from 'react' 2 | import { NodeRendererProps } from '@/container/NodeRenderer' 3 | import { Node } from '@/types' 4 | import Wrapper from './Wrapper' 5 | 6 | import './index.less' 7 | 8 | export type NodeComponentProps = Omit< 9 | NodeRendererProps, 10 | 'nodeTypes' | 'nodes' | 'transform' | 'transformStyle' 11 | > & 12 | Pick< 13 | Node, 14 | | 'className' 15 | | 'style' 16 | | 'id' 17 | | 'position' 18 | | 'data' 19 | | 'visible' 20 | | 'connectable' 21 | | 'draggable' 22 | | 'selectable' 23 | | 'deletable' 24 | | 'cancel' 25 | > & { 26 | node: Node 27 | nodeType: string 28 | Component: ComponentType 29 | observer: (element: HTMLElement) => void 30 | onChange?: (data: any, force?: boolean) => void 31 | } 32 | 33 | const NodeComponent: FC = ({ id, node, Component, ...rest }) => { 34 | return ( 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | NodeComponent.displayName = 'NodeComponent' 42 | 43 | export default memo(NodeComponent) 44 | -------------------------------------------------------------------------------- /src/components/Edge/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, ComponentType } from 'react' 2 | import { EdgeRendererProps } from '@/container/EdgeRenderer' 3 | import { Edge } from '@/types' 4 | import Wrapper from './Wrapper' 5 | 6 | export type EdgeComponentProps = Omit< 7 | EdgeRendererProps, 8 | | 'edgeTypes' 9 | | 'edges' 10 | | 'transform' 11 | | 'transformStyle' 12 | | 'onConnectStart' 13 | | 'onConnectStop' 14 | | 'onConnectEnd' 15 | > & 16 | Pick< 17 | Edge, 18 | | 'style' 19 | | 'className' 20 | | 'id' 21 | | 'data' 22 | | 'label' 23 | | 'lineType' 24 | | 'source' 25 | | 'target' 26 | | 'selectable' 27 | | 'arrowType' 28 | > & { 29 | edge: Edge 30 | Component: ComponentType 31 | } 32 | 33 | const EdgeComponent: FC = ({ 34 | id, 35 | Component, 36 | connectLine, 37 | lineType, 38 | arrowType, 39 | ...rest 40 | }) => { 41 | return ( 42 | 43 | 50 | 51 | ) 52 | } 53 | 54 | EdgeComponent.displayName = 'EdgeComponent' 55 | 56 | export default memo(EdgeComponent) 57 | -------------------------------------------------------------------------------- /src/hooks/useDimension.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useMemo } from 'react' 2 | import useGlobal from '@/hooks/useGlobal' 3 | import get from 'lodash/get' 4 | 5 | export default function useDimension(id) { 6 | const [global] = useGlobal() 7 | 8 | const dimension = useMemo(() => { 9 | const nodeRect = global.nodeRects.find((item) => item.id === id) 10 | 11 | return { 12 | width: get(nodeRect, 'width'), 13 | height: get(nodeRect, 'height'), 14 | } 15 | }, [global.nodeRects, id]) 16 | 17 | return dimension 18 | } 19 | 20 | export function useBatchUpdateDimension(graphRef?: RefObject) { 21 | const [, setGlobal] = useGlobal() 22 | 23 | useEffect(() => { 24 | const graphElement = graphRef.current 25 | ? graphRef.current.closest('.graph-editor') 26 | : document.querySelector('.graph-editor') 27 | const container = graphElement?.getBoundingClientRect() 28 | 29 | // const nodeElements = graphElement.querySelectorAll('.graph-editor-node-wrapper') 30 | // const nextNodes = cloneDeep(global.nodes) 31 | 32 | // nodeElements.forEach((nodeElement, index) => { 33 | // const { width, height } = getDimensions(nodeElement as HTMLDivElement) 34 | 35 | // nextNodes[index].__extra = { 36 | // ...(nextNodes[index].__extra || {}), 37 | // width, 38 | // height, 39 | // } 40 | // }) 41 | 42 | setGlobal({ container, graphElement }) 43 | }, []) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/fullscreen.ts: -------------------------------------------------------------------------------- 1 | export function cancelFullScreen() { 2 | if (document.exitFullscreen) { 3 | return document.exitFullscreen() 4 | } 5 | 6 | if (typeof window.ActiveXObject !== 'undefined') { 7 | // Older IE. 8 | const wscript = new window.ActiveXObject('WScript.Shell') 9 | if (wscript !== null) { 10 | wscript.SendKeys('{F11}') 11 | } 12 | } 13 | } 14 | 15 | export function requestFullScreen(el) { 16 | // Supports most browsers and their versions. 17 | const requestMethod = 18 | el.requestFullScreen || 19 | el.webkitRequestFullScreen || 20 | el.mozRequestFullScreen || 21 | el.msRequestFullscreen 22 | 23 | if (requestMethod) { 24 | // Native full screen. 25 | requestMethod.call(el) 26 | } else if (typeof window.ActiveXObject !== 'undefined') { 27 | // Older IE. 28 | const wscript = new window.ActiveXObject('WScript.Shell') 29 | if (wscript !== null) { 30 | wscript.SendKeys('{F11}') 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * 37 | * @param {HTMLElement} el 38 | * @returns {boolean} isInFullScreen 39 | */ 40 | export default function fullScreen(el: HTMLElement) { 41 | if (!el) { 42 | el = document.body // Make the body go full screen. 43 | } 44 | const isInFullScreen = document.fullscreenElement && document.fullscreenElement !== null 45 | 46 | if (isInFullScreen) { 47 | cancelFullScreen() 48 | } else { 49 | requestFullScreen(el) 50 | } 51 | 52 | return isInFullScreen 53 | } 54 | -------------------------------------------------------------------------------- /src/styles/theme/primary.less: -------------------------------------------------------------------------------- 1 | .primary { 2 | &.graph-editor-marker-definitions-container { 3 | #graph-editor-marker-definitions__arrowclosed polyline { 4 | stroke: rgb(106, 113, 197); 5 | fill: rgb(106, 113, 197); 6 | } 7 | #graph-editor-marker-definitions__arrowclosed__selected polyline { 8 | stroke: #ffd92c; 9 | fill: #ffd92c; 10 | } 11 | #graph-editor-marker-definitions__arrow polyline { 12 | stroke: rgb(106, 113, 197); 13 | } 14 | #graph-editor-marker-definitions__arrow__selected polyline { 15 | stroke: #ffd92c; 16 | } 17 | } 18 | 19 | 20 | &.graph-editor-node-wrapper { 21 | background: rgba(110, 136, 255, 0.8); 22 | border-radius: 10px; 23 | font-size: 16px; 24 | border: 2px solid #4e58bf; 25 | color: #fff; 26 | &:hover { 27 | background-color: rgba(130, 153, 255, 0.8); 28 | } 29 | &.is-selected { 30 | background-color: #ffd92c; 31 | border-color: #e3c000; 32 | } 33 | } 34 | 35 | 36 | &.graph-editor-edge-wrapper { 37 | .graph-editor-edge-wrapper-path { 38 | stroke: rgba(78, 88, 191, 0.8); 39 | stroke-width: 4; 40 | } 41 | &.is-selected { 42 | .graph-editor-edge-wrapper-path { 43 | stroke: #ffd92c; 44 | } 45 | } 46 | } 47 | 48 | 49 | &.graph-editor-pointer-wrapper { 50 | width: 24px; 51 | height: 24px; 52 | background-color: #96b38a; 53 | &:hover { 54 | border-width: 4px; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/demo/lineType.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LineType 3 | order: 6 4 | # toc: menu 5 | --- 6 | 7 | ## Operation 8 | 9 | ```tsx live 10 | import React from 'react'; 11 | import GraphEditor, { Controls, Background, MiniMap } from '@ols-scripts/graph-editor'; 12 | 13 | const initElements = { 14 | nodes: [{ 15 | id: '1', 16 | data: { 17 | label: ( 18 | <> 19 | Welcome to React Flow! 20 | 21 | ), 22 | }, 23 | position: { x: 250, y: 0 }, 24 | deletable: false 25 | }, 26 | { 27 | id: '2', 28 | data: { 29 | label: ( 30 | <> 31 | This is a default node 32 | 33 | ), 34 | }, 35 | position: { x: 100, y: 150 }, 36 | }, 37 | { 38 | id: '3', 39 | data: { 40 | label: ( 41 | <> 42 | This is a default node 43 | 44 | ), 45 | }, 46 | position: { x: 200, y: 300 }, 47 | }], 48 | edges: [ 49 | { 50 | source: '1', 51 | target: '2', 52 | } 53 | ] 54 | } 55 | 56 | const Demo = () => { 57 | return ( 58 |
59 | 68 | 69 | 70 | 71 | 72 |
73 | ); 74 | }; 75 | 76 | ReactDOM.render(, mountNode); 77 | ``` 78 | -------------------------------------------------------------------------------- /src/container/EdgeType/EdgeLabel.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode, useEffect, useRef, useState } from 'react' 2 | 3 | import './index.less' 4 | 5 | export type EdgeLabelProps = { 6 | prefix?: string 7 | label?: string | ReactNode 8 | x: number 9 | y: number 10 | } 11 | 12 | const LABEL_PADDING = [2, 4] 13 | 14 | const EdgeLabel: FC = ({ label, x, y }) => { 15 | const labelRef = useRef() 16 | const [labelRect, setLabelRect] = useState({ x: 0, y: 0, width: 0, height: 0 }) 17 | 18 | useEffect(() => { 19 | if (labelRef.current) { 20 | const labelBox = labelRef.current.getBBox() 21 | 22 | setLabelRect({ 23 | x: labelBox.x, 24 | y: labelBox.y, 25 | width: labelBox.width, 26 | height: labelBox.height, 27 | }) 28 | } 29 | }, []) 30 | 31 | if (typeof label === 'undefined' || !label) { 32 | return null 33 | } 34 | 35 | return ( 36 | 40 | 47 | 48 | {label} 49 | 50 | 51 | ) 52 | } 53 | 54 | EdgeLabel.displayName = 'EdgeLabel' 55 | 56 | export default EdgeLabel 57 | -------------------------------------------------------------------------------- /docs/demo/operation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Operation 3 | order: 3 4 | # toc: menu 5 | --- 6 | 7 | ## Operation 8 | 9 | ```tsx live 10 | import React from 'react'; 11 | import GraphEditor, { Controls, Background, MiniMap } from '@ols-scripts/graph-editor'; 12 | 13 | const initElements = { 14 | nodes: [{ 15 | id: '1', 16 | data: { 17 | label: ( 18 | <> 19 | Welcome to React Flow! 20 | 21 | ), 22 | }, 23 | position: { x: 250, y: 0 }, 24 | deletable: false 25 | }, 26 | { 27 | id: '2', 28 | data: { 29 | label: ( 30 | <> 31 | This is a default node 32 | 33 | ), 34 | }, 35 | position: { x: 100, y: 150 }, 36 | }, 37 | { 38 | id: '3', 39 | data: { 40 | label: ( 41 | <> 42 | This is a default node 43 | 44 | ), 45 | }, 46 | position: { x: 200, y: 300 }, 47 | connectable: false 48 | }], 49 | edges: [ 50 | { 51 | source: '1', 52 | target: '2', 53 | } 54 | ] 55 | } 56 | 57 | const Demo = () => { 58 | return ( 59 |
60 | { 64 | console.log('node delete', e, node) 65 | }} 66 | onEdgeDelete={(e, node) => { 67 | console.log('edge delete', e, node) 68 | }} 69 | > 70 | 71 | 72 | 73 | 74 |
75 | ); 76 | }; 77 | 78 | ReactDOM.render(, mountNode); 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/demo/dynamicNode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dynamic Node 3 | order: 4 4 | # toc: menu 5 | --- 6 | 7 | ## Operation 8 | 9 | ```tsx live 10 | import React, { useState, useCallback } from 'react'; 11 | import GraphEditor, { Controls, Background, MiniMap } from '@ols-scripts/graph-editor'; 12 | 13 | const initElements = { 14 | nodes: [{ 15 | id: '1', 16 | type: 'CustomNode', 17 | data: { 18 | content: 'This is a Custom Node!' 19 | }, 20 | cancel: ['textarea'], 21 | style: { 22 | width: '400px' 23 | }, 24 | position: { x: 250, y: 20 }, 25 | }, 26 | { 27 | id: '2', 28 | data: { 29 | label: ( 30 | <> 31 | This is a default node 32 | 33 | ), 34 | }, 35 | position: { x: 200, y: 400 }, 36 | }], 37 | edges: [ 38 | { 39 | source: '1', 40 | target: '2', 41 | } 42 | ] 43 | } 44 | 45 | const CustomNode = ({ data, onChange }) => { 46 | const onInputChange = useCallback((e) => { 47 | onChange({ 48 | content: e.target.value 49 | }) 50 | }, []) 51 | 52 | return ( 53 |
54 |