@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/.turbo/turbo-build.log +37 -0
- package/LICENSE +203 -0
- package/README.md +35 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/es/index.d.ts +5 -0
- package/es/index.js +6 -0
- package/es/index.js.map +1 -0
- package/es/model.d.ts +16 -0
- package/es/model.js +63 -0
- package/es/model.js.map +1 -0
- package/es/portal.d.ts +9 -0
- package/es/portal.js +78 -0
- package/es/portal.js.map +1 -0
- package/es/registry.d.ts +17 -0
- package/es/registry.js +19 -0
- package/es/registry.js.map +1 -0
- package/es/view.d.ts +13 -0
- package/es/view.js +78 -0
- package/es/view.js.map +1 -0
- package/es/wrapper.d.ts +16 -0
- package/es/wrapper.js +64 -0
- package/es/wrapper.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +22 -0
- package/lib/index.js.map +1 -0
- package/lib/model.d.ts +16 -0
- package/lib/model.js +66 -0
- package/lib/model.js.map +1 -0
- package/lib/portal.d.ts +9 -0
- package/lib/portal.js +104 -0
- package/lib/portal.js.map +1 -0
- package/lib/registry.d.ts +17 -0
- package/lib/registry.js +26 -0
- package/lib/registry.js.map +1 -0
- package/lib/view.d.ts +13 -0
- package/lib/view.js +81 -0
- package/lib/view.js.map +1 -0
- package/lib/wrapper.d.ts +16 -0
- package/lib/wrapper.js +90 -0
- package/lib/wrapper.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +5 -0
- package/src/model.ts +67 -0
- package/src/portal.ts +79 -0
- package/src/registry.ts +47 -0
- package/src/view.ts +95 -0
- package/src/wrapper.tsx +61 -0
- package/tsconfig.json +21 -0
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
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -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
|
package/src/wrapper.tsx
ADDED
|
@@ -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
|
+
}
|