@logicflow/react-node-registry 0.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/model.ts ADDED
@@ -0,0 +1,67 @@
1
+ import LogicFlow, { HtmlNodeModel } from '@logicflow/core'
2
+ import { cloneDeep } from 'lodash-es'
3
+
4
+ export type CustomProperties = {
5
+ // 形状属性
6
+ width?: number
7
+ height?: number
8
+ radius?: number
9
+
10
+ // 文字位置属性
11
+ refX?: number
12
+ refY?: number
13
+
14
+ // 样式属性
15
+ style?: LogicFlow.CommonTheme
16
+ textStyle?: LogicFlow.TextNodeTheme
17
+ }
18
+
19
+ export class ReactNodeModel extends HtmlNodeModel {
20
+ setAttributes() {
21
+ console.log('this.properties', this.properties)
22
+ const { width, height, radius } = this.properties as CustomProperties
23
+ if (width) {
24
+ this.width = width
25
+ }
26
+ if (height) {
27
+ this.height = height
28
+ }
29
+ if (radius) {
30
+ this.radius = radius
31
+ }
32
+ }
33
+
34
+ getTextStyle(): LogicFlow.TextNodeTheme {
35
+ // const { x, y, width, height } = this
36
+ const {
37
+ refX = 0,
38
+ refY = 0,
39
+ textStyle,
40
+ } = this.properties as CustomProperties
41
+ const style = super.getTextStyle()
42
+
43
+ // 通过 transform 重新设置 text 的位置
44
+ return {
45
+ ...style,
46
+ ...(cloneDeep(textStyle) || {}),
47
+ transform: `matrix(1 0 0 1 ${refX} ${refY})`,
48
+ }
49
+ }
50
+
51
+ getNodeStyle(): LogicFlow.CommonTheme {
52
+ const style = super.getNodeStyle()
53
+ const {
54
+ style: customNodeStyle,
55
+ // radius = 0, // 第二种方式,设置圆角
56
+ } = this.properties as CustomProperties
57
+
58
+ return {
59
+ ...style,
60
+ ...(cloneDeep(customNodeStyle) || {}),
61
+ // rx: radius,
62
+ // ry: radius,
63
+ }
64
+ }
65
+ }
66
+
67
+ export default ReactNodeModel
package/src/portal.ts ADDED
@@ -0,0 +1,79 @@
1
+ import React, { useReducer } from 'react'
2
+
3
+ export namespace Portal {
4
+ let active = false
5
+ let dispatch: React.Dispatch<Action>
6
+
7
+ interface Action {
8
+ type: 'add' | 'remove'
9
+ payload: Partial<Payload>
10
+ }
11
+
12
+ interface Payload {
13
+ id: string
14
+ portal: React.ReactPortal
15
+ }
16
+
17
+ const reducer = (state: Payload[], action: Action) => {
18
+ const payload = action.payload as Payload
19
+ switch (action.type) {
20
+ case 'add': {
21
+ const index = state.findIndex((item) => item.id === payload.id)
22
+ if (index >= 0) {
23
+ state[index] = payload
24
+ return [...state]
25
+ }
26
+ return [...state, payload]
27
+ }
28
+ case 'remove': {
29
+ const index = state.findIndex((item) => item.id === payload.id)
30
+ if (index >= 0) {
31
+ const result = [...state]
32
+ result.splice(index, 1)
33
+ return result
34
+ }
35
+ break
36
+ }
37
+ default: {
38
+ break
39
+ }
40
+ }
41
+ return state
42
+ }
43
+
44
+ export function connect(id: string, portal: React.ReactPortal) {
45
+ if (active) {
46
+ dispatch({
47
+ type: 'add',
48
+ payload: {
49
+ id,
50
+ portal,
51
+ },
52
+ })
53
+ }
54
+ }
55
+
56
+ export function disconnect(id: string) {
57
+ if (active) {
58
+ dispatch({
59
+ type: 'remove',
60
+ payload: { id },
61
+ })
62
+ }
63
+ }
64
+
65
+ export function isActive() {
66
+ return active
67
+ }
68
+
69
+ export function getProvider() {
70
+ return () => {
71
+ active = true
72
+ const [items, mutate] = useReducer(reducer, [])
73
+ dispatch = mutate
74
+ return React.createElement(React.Fragment, {
75
+ children: items.map((item) => item.portal),
76
+ })
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,47 @@
1
+ import LogicFlow, { BaseNodeModel, GraphModel } from '@logicflow/core'
2
+ import ReactNodeView from './view'
3
+ import ReactNodeModel from './model'
4
+ import RegisterConfig = LogicFlow.RegisterConfig
5
+
6
+ export type ReactNodeProps = {
7
+ node: BaseNodeModel
8
+ graph: GraphModel
9
+ }
10
+
11
+ export type ReactNodeConfig = {
12
+ type: string
13
+ component: React.ComponentType<ReactNodeProps>
14
+ effect?: (keyof LogicFlow.PropertiesType)[]
15
+ } & Partial<RegisterConfig>
16
+
17
+ export const reactNodesMap: Record<
18
+ string,
19
+ {
20
+ component: React.ComponentType<ReactNodeProps>
21
+ effect?: (keyof LogicFlow.PropertiesType)[]
22
+ }
23
+ > = {}
24
+
25
+ export function register(config: ReactNodeConfig, lf: LogicFlow) {
26
+ const {
27
+ type,
28
+ component,
29
+ effect,
30
+ view: CustomNodeView,
31
+ model: CustomNodeModel,
32
+ } = config
33
+
34
+ if (!type) {
35
+ throw new Error('You should specify type in config')
36
+ }
37
+ reactNodesMap[type] = {
38
+ component,
39
+ effect,
40
+ }
41
+
42
+ lf.register({
43
+ type,
44
+ view: CustomNodeView || ReactNodeView,
45
+ model: CustomNodeModel || ReactNodeModel,
46
+ })
47
+ }
package/src/view.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { createElement, ReactPortal } from 'react'
2
+ import { createRoot, Root } from 'react-dom/client'
3
+ import { HtmlNode } from '@logicflow/core'
4
+ import { Wrapper } from './wrapper'
5
+ import { Portal } from './portal'
6
+ import { createPortal } from 'react-dom'
7
+
8
+ export class ReactNodeView extends HtmlNode {
9
+ root?: Root
10
+
11
+ protected targetId() {
12
+ return `${this.props.graphModel.flowId}:${this.props.model.id}`
13
+ }
14
+
15
+ componentWillUnmount() {
16
+ super.componentWillUnmount()
17
+ this.unmount()
18
+ }
19
+
20
+ setHtml(rootEl: SVGForeignObjectElement) {
21
+ const el = document.createElement('div')
22
+ el.className = 'custom-react-node-content'
23
+
24
+ this.renderReactComponent(el)
25
+ rootEl.appendChild(el)
26
+ }
27
+
28
+ confirmUpdate(_rootEl: SVGForeignObjectElement) {
29
+ // TODO: 如有需要,可以先通过继承的方式,自定义该节点的更新逻辑;我们后续会根据实际需求,丰富该功能
30
+ console.log('_rootEl', _rootEl)
31
+ }
32
+
33
+ protected renderReactComponent(container: HTMLElement) {
34
+ console.log('render render render ===>>>')
35
+ this.unmountReactComponent()
36
+ const { model, graphModel } = this.props
37
+
38
+ if (container) {
39
+ const elem = createElement(Wrapper, {
40
+ node: model,
41
+ graph: graphModel,
42
+ })
43
+
44
+ if (Portal.isActive()) {
45
+ const portal = createPortal(elem, container, model.id) as ReactPortal
46
+ Portal.connect(this.targetId(), portal)
47
+ } else {
48
+ this.root = createRoot(container)
49
+ this.root.render(elem)
50
+ }
51
+ }
52
+ }
53
+
54
+ protected unmountReactComponent() {
55
+ if (this.rootEl && this.root) {
56
+ this.root.unmount()
57
+ this.root = undefined
58
+ this.rootEl.innerHTML = ''
59
+ }
60
+ }
61
+
62
+ // DONE: 是否需要 unmount 或 destroy 方法,在销毁后做一些处理
63
+ unmount() {
64
+ this.unmountReactComponent()
65
+ }
66
+
67
+ // TODO: 确认是否需要重写 onMouseDown 方法
68
+ // handleMouseDown(ev: MouseEvent, x: number, y: number) {
69
+ // const target = ev.target as Element
70
+ // const tagName = target.tagName.toLowerCase()
71
+ // if (tagName === 'input') {
72
+ // const type = target.getAttribute('type')
73
+ // if (
74
+ // type == null ||
75
+ // [
76
+ // 'text',
77
+ // 'password',
78
+ // 'number',
79
+ // 'email',
80
+ // 'search',
81
+ // 'tel',
82
+ // 'url',
83
+ // ].includes(type)
84
+ // ) {
85
+ // return
86
+ // }
87
+ // }
88
+ //
89
+ // console.log('pointer position, x:', x, 'y: ', y)
90
+ // // TODO
91
+ // // super.handleMouseDown(ev)
92
+ // }
93
+ }
94
+
95
+ export default ReactNodeView
@@ -0,0 +1,61 @@
1
+ import React, { PureComponent } from 'react'
2
+ import { BaseNodeModel, EventType, GraphModel } from '@logicflow/core'
3
+ import { reactNodesMap } from './registry'
4
+
5
+ export interface IWrapperProps {
6
+ node: BaseNodeModel
7
+ graph: GraphModel
8
+ }
9
+
10
+ export interface IWrapperState {
11
+ tick: number
12
+ }
13
+
14
+ export class Wrapper extends PureComponent<IWrapperProps, IWrapperState> {
15
+ constructor(props: IWrapperProps) {
16
+ super(props)
17
+ this.state = { tick: 0 }
18
+ }
19
+
20
+ componentDidMount() {
21
+ // TODO: 讨论设计,如果节点有「副作用」属性配置的处理逻辑
22
+ const { node, graph } = this.props
23
+ graph.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData) => {
24
+ const keys = eventData.keys as string[]
25
+ const content = reactNodesMap[node.type]
26
+
27
+ if (content && eventData.id === node.id) {
28
+ const { effect } = content
29
+
30
+ // 如果没有定义 effect,则默认更新;如果定义了 effect,则只有在 effect 中的属性发生变化时才更新
31
+ if (!effect || keys.some((key) => effect.includes(key))) {
32
+ this.setState({ tick: this.state.tick + 1 })
33
+ }
34
+ }
35
+ })
36
+ }
37
+
38
+ clone(elem: React.ReactElement) {
39
+ const { node, graph } = this.props
40
+
41
+ return typeof elem.type === 'string'
42
+ ? React.cloneElement(elem)
43
+ : React.cloneElement(elem, { node, graph })
44
+ }
45
+
46
+ render() {
47
+ const { node } = this.props
48
+ const content = reactNodesMap[node.type]
49
+
50
+ if (!content) return null
51
+
52
+ const { component } = content
53
+ if (React.isValidElement(component)) {
54
+ return this.clone(component)
55
+ }
56
+ const FC = component as React.FC
57
+ return this.clone(<FC />)
58
+ }
59
+ }
60
+
61
+ export default Wrapper
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "moduleResolution": "node",
5
+ "sourceMap": true,
6
+ "declaration": true,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "noImplicitAny": true,
10
+ "noEmitOnError": true,
11
+ "noUnusedLocals": true,
12
+ "strictNullChecks": true,
13
+ "resolveJsonModule": true,
14
+ "experimentalDecorators": true,
15
+ "jsx": "react",
16
+ "target": "es6",
17
+ "lib": ["DOM", "ES2020"]
18
+ },
19
+ "include": ["src/**/*", "**/*.d.ts"],
20
+ "exclude": ["node_modules", "**/*.spec.ts", "es", "lib", "dist"]
21
+ }