@logicflow/react-node-registry 1.2.0-alpha.7 → 1.2.1

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 DELETED
@@ -1,140 +0,0 @@
1
- import LogicFlow, {
2
- HtmlNodeModel,
3
- IHtmlNodeProperties,
4
- BaseNodeModel,
5
- GraphModel,
6
- } from '@logicflow/core'
7
- import { cloneDeep, isNil, isArray, isEmpty } from 'lodash-es'
8
-
9
- import NodeConfig = LogicFlow.NodeConfig
10
-
11
- export type NodeAction = {
12
- name: string
13
- callback?: (node: BaseNodeModel, graph: GraphModel) => void
14
- }
15
-
16
- export interface ReactCustomProperties extends IHtmlNodeProperties {
17
- // 形状属性
18
- width?: number
19
- height?: number
20
- radius?: number
21
-
22
- // 文字位置属性
23
- refX?: number
24
- refY?: number
25
-
26
- // 样式属性
27
- style?: LogicFlow.CommonTheme
28
- textStyle?: LogicFlow.TextNodeTheme
29
- // 标题配置
30
- _showTitle?: boolean
31
- _title?: string
32
- _icon?: string
33
- _titleHeight?: number
34
- _expanded?: boolean
35
- }
36
-
37
- export class ReactNodeModel<
38
- P extends ReactCustomProperties = ReactCustomProperties,
39
- > extends HtmlNodeModel<P> {
40
- private __baseHeight?: number
41
- public __actions?: {
42
- name: string
43
- callback?: (node: BaseNodeModel, graph: GraphModel) => void
44
- }[]
45
- constructor(data: NodeConfig<P>, graphModel: GraphModel) {
46
- super(data, graphModel)
47
- const { properties } = data
48
- if (properties) {
49
- const { _showTitle = false, style = {} } = properties
50
- if (_showTitle) {
51
- this.minWidth = 160
52
- this.minHeight = 80
53
- this.text.editable = false
54
- // 判断当前节点宽高是否小于最小宽高,如果是,强制设置为最小宽高
55
- const newWidth = this.width < this.minWidth ? this.minWidth : this.width
56
- const newHeight =
57
- this.height < this.minHeight ? this.minHeight : this.height
58
-
59
- this.setProperties({
60
- _expanded: false,
61
- ...properties,
62
- style: {
63
- overflow: 'visible',
64
- ...cloneDeep(style),
65
- },
66
- width: newWidth,
67
- height: newHeight,
68
- })
69
- this.setNodeActions([
70
- {
71
- name: '复制',
72
- callback: (nodeModel: BaseNodeModel, graphModel: GraphModel) => {
73
- graphModel.cloneNode(nodeModel.id)
74
- },
75
- },
76
- {
77
- name: '删除',
78
- callback: (nodeModel: BaseNodeModel, graphModel: GraphModel) => {
79
- graphModel.deleteNode(nodeModel.id)
80
- },
81
- },
82
- ])
83
- }
84
- }
85
- }
86
-
87
- setAttributes() {
88
- const { width, height, radius, _showTitle, _titleHeight } = this.properties
89
- if (!isNil(width)) {
90
- this.width = width
91
- }
92
- if (!isNil(height)) {
93
- this.height = height
94
- }
95
- if (!isNil(radius)) {
96
- this.radius = radius
97
- }
98
- if (this.__baseHeight === undefined) {
99
- this.__baseHeight = isNil(height) ? this.height : height
100
- }
101
- const extra = _showTitle ? (_titleHeight ?? 28) : 0
102
- const base = isNil(height) ? this.__baseHeight : height
103
- this.height = base + extra
104
- }
105
-
106
- getTextStyle(): LogicFlow.TextNodeTheme {
107
- // const { x, y, width, height } = this
108
- const { refX = 0, refY = 0, textStyle } = this.properties
109
- const style = super.getTextStyle()
110
-
111
- // 通过 transform 重新设置 text 的位置
112
- return {
113
- ...style,
114
- ...(cloneDeep(textStyle) || {}),
115
- transform: `matrix(1 0 0 1 ${refX} ${refY})`,
116
- }
117
- }
118
-
119
- getNodeStyle(): LogicFlow.CommonTheme {
120
- const style = super.getNodeStyle()
121
- const {
122
- style: customNodeStyle,
123
- // radius = 0, // 第二种方式,设置圆角
124
- } = this.properties
125
-
126
- return {
127
- ...style,
128
- ...(cloneDeep(customNodeStyle) || {}),
129
- // rx: radius,
130
- // ry: radius,
131
- }
132
- }
133
-
134
- setNodeActions(actions: NodeAction[]) {
135
- this.__actions =
136
- isArray(actions) && !isEmpty(actions) ? cloneDeep(actions) : []
137
- }
138
- }
139
-
140
- export default ReactNodeModel
package/src/portal.ts DELETED
@@ -1,79 +0,0 @@
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 DELETED
@@ -1,47 +0,0 @@
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
- }
@@ -1,140 +0,0 @@
1
- .lf-vue-node-container {
2
- position: relative;
3
- display: flex;
4
- flex-direction: column;
5
- box-sizing: border-box;
6
- padding: 6px;
7
- color: #474747;
8
- border-radius: 12px;
9
- box-shadow: 0 0 10px #cad2e15f;
10
- }
11
-
12
- .lf-vue-node-content-wrap {
13
- display: flex;
14
- flex: 1 1 auto;
15
- justify-content: center;
16
- }
17
-
18
- .lf-vue-node-title {
19
- display: flex;
20
- align-items: flex-start;
21
- justify-content: space-between;
22
- box-sizing: border-box;
23
- margin-bottom: 4px;
24
- padding: 0 8px;
25
- backdrop-filter: saturate(180%) blur(4px);
26
-
27
- &-expanded {
28
- margin-bottom: 6px;
29
- padding-bottom: 8px;
30
- border-bottom: 1px solid #eaeaea;
31
- }
32
- }
33
-
34
- @supports not (backdrop-filter: blur(1px)) {
35
- .lf-vue-node-title {
36
- backdrop-filter: none;
37
- }
38
- }
39
-
40
- .lf-vue-node-title-left {
41
- display: flex;
42
- gap: 6px;
43
- align-items: center;
44
- min-width: 0;
45
- }
46
-
47
- .lf-vue-node-title-icon {
48
- display: inline-block;
49
- width: 16px;
50
- height: 16px;
51
- color: #666;
52
- font-style: normal;
53
- line-height: 16px;
54
- text-align: center;
55
- }
56
-
57
- .lf-vue-node-title-text {
58
- overflow: hidden;
59
- color: #333;
60
- font-weight: 500;
61
- font-size: 14px;
62
- white-space: nowrap;
63
- text-overflow: ellipsis;
64
- }
65
-
66
- .lf-vue-node-title-actions {
67
- display: flex;
68
- gap: 6px;
69
- align-items: center;
70
- }
71
-
72
- .lf-vue-node-title-expand,
73
- .lf-vue-node-title-more {
74
- display: inline-flex;
75
- align-items: center;
76
- justify-content: center;
77
- width: 20px;
78
- height: 20px;
79
- padding: 2px;
80
- background: transparent;
81
- border: none;
82
- border-radius: 4px;
83
- cursor: pointer;
84
- transition: background 0.15s ease;
85
- appearance: none;
86
- }
87
-
88
- .lf-vue-node-title-expand:hover,
89
- .lf-vue-node-title-more:hover {
90
- background: rgb(0 0 0 / 6%);
91
- }
92
-
93
- .lf-vue-node-title-expand-icon {
94
- color: #666;
95
- font-style: normal;
96
- transition: transform 0.3s ease;
97
- }
98
-
99
- .lf-vue-node-title-more-icon {
100
- color: #666;
101
- font-style: normal;
102
- }
103
-
104
- .lf-vue-node-title-tooltip {
105
- position: absolute;
106
- top: -50px;
107
- right: -135px;
108
- min-width: 120px;
109
- max-width: 240px;
110
- padding: 6px 8px;
111
- background: #fff;
112
- border: 1px solid rgb(0 0 0 / 10%);
113
- border-radius: 6px;
114
- box-shadow: 0 6px 20px rgb(0 0 0 / 12%);
115
- transform: translateY(calc(100% + 4px));
116
- transition:
117
- opacity 0.15s ease,
118
- transform 0.15s ease;
119
- }
120
-
121
- .lf-vue-node-title-tooltip-list {
122
- display: flex;
123
- flex-direction: column;
124
- gap: 4px;
125
- }
126
-
127
- .lf-vue-node-title-tooltip-item {
128
- display: flex;
129
- align-items: center;
130
- justify-content: flex-start;
131
- padding: 6px;
132
- color: #333;
133
- font-size: 12px;
134
- border-radius: 4px;
135
- cursor: pointer;
136
- }
137
-
138
- .lf-vue-node-title-tooltip-item:hover {
139
- background: rgb(0 0 0 / 5%);
140
- }
package/src/style/raw.ts DELETED
@@ -1,129 +0,0 @@
1
- /* eslint-disable */
2
-
3
- /**
4
- * Auto generated file, do not modify it!
5
- */
6
-
7
- export const content = `.lf-vue-node-container {
8
- position: relative;
9
- display: flex;
10
- flex-direction: column;
11
- box-sizing: border-box;
12
- padding: 6px;
13
- color: #474747;
14
- border-radius: 12px;
15
- box-shadow: 0 0 10px #cad2e15f;
16
- }
17
- .lf-vue-node-content-wrap {
18
- display: flex;
19
- flex: 1 1 auto;
20
- justify-content: center;
21
- }
22
- .lf-vue-node-title {
23
- display: flex;
24
- align-items: flex-start;
25
- justify-content: space-between;
26
- box-sizing: border-box;
27
- margin-bottom: 4px;
28
- padding: 0 8px;
29
- backdrop-filter: saturate(180%) blur(4px);
30
- }
31
- .lf-vue-node-title-expanded {
32
- margin-bottom: 6px;
33
- padding-bottom: 8px;
34
- border-bottom: 1px solid #eaeaea;
35
- }
36
- @supports not (backdrop-filter: blur(1px)) {
37
- .lf-vue-node-title {
38
- backdrop-filter: none;
39
- }
40
- }
41
- .lf-vue-node-title-left {
42
- display: flex;
43
- gap: 6px;
44
- align-items: center;
45
- min-width: 0;
46
- }
47
- .lf-vue-node-title-icon {
48
- display: inline-block;
49
- width: 16px;
50
- height: 16px;
51
- color: #666;
52
- font-style: normal;
53
- line-height: 16px;
54
- text-align: center;
55
- }
56
- .lf-vue-node-title-text {
57
- overflow: hidden;
58
- color: #333;
59
- font-weight: 500;
60
- font-size: 14px;
61
- white-space: nowrap;
62
- text-overflow: ellipsis;
63
- }
64
- .lf-vue-node-title-actions {
65
- display: flex;
66
- gap: 6px;
67
- align-items: center;
68
- }
69
- .lf-vue-node-title-expand,
70
- .lf-vue-node-title-more {
71
- display: inline-flex;
72
- align-items: center;
73
- justify-content: center;
74
- width: 20px;
75
- height: 20px;
76
- padding: 2px;
77
- background: transparent;
78
- border: none;
79
- border-radius: 4px;
80
- cursor: pointer;
81
- transition: background 0.15s ease;
82
- appearance: none;
83
- }
84
- .lf-vue-node-title-expand:hover,
85
- .lf-vue-node-title-more:hover {
86
- background: rgba(0, 0, 0, 0.06);
87
- }
88
- .lf-vue-node-title-expand-icon {
89
- color: #666;
90
- font-style: normal;
91
- transition: transform 0.3s ease;
92
- }
93
- .lf-vue-node-title-more-icon {
94
- color: #666;
95
- font-style: normal;
96
- }
97
- .lf-vue-node-title-tooltip {
98
- position: absolute;
99
- top: -50px;
100
- right: -135px;
101
- min-width: 120px;
102
- max-width: 240px;
103
- padding: 6px 8px;
104
- background: #fff;
105
- border: 1px solid rgba(0, 0, 0, 0.1);
106
- border-radius: 6px;
107
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
108
- transform: translateY(calc(100% + 4px));
109
- transition: opacity 0.15s ease, transform 0.15s ease;
110
- }
111
- .lf-vue-node-title-tooltip-list {
112
- display: flex;
113
- flex-direction: column;
114
- gap: 4px;
115
- }
116
- .lf-vue-node-title-tooltip-item {
117
- display: flex;
118
- align-items: center;
119
- justify-content: flex-start;
120
- padding: 6px;
121
- color: #333;
122
- font-size: 12px;
123
- border-radius: 4px;
124
- cursor: pointer;
125
- }
126
- .lf-vue-node-title-tooltip-item:hover {
127
- background: rgba(0, 0, 0, 0.05);
128
- }
129
- `
package/src/view.ts DELETED
@@ -1,190 +0,0 @@
1
- import { createElement, ReactPortal } from 'react'
2
- import { createRoot, Root } from 'react-dom/client'
3
- import { HtmlNode } from '@logicflow/core'
4
- import {
5
- throttle,
6
- round,
7
- get,
8
- isFunction,
9
- isArray,
10
- clamp,
11
- isNumber,
12
- } from 'lodash-es'
13
- import { Wrapper } from './wrapper'
14
- import { Portal } from './portal'
15
- import { createPortal } from 'react-dom'
16
-
17
- export class ReactNodeView extends HtmlNode {
18
- root?: Root
19
- private containerEl?: HTMLElement
20
- private __resizeObserver?: ResizeObserver
21
- private __resizeRafId?: number
22
- private __lastWidth?: number
23
- private __lastHeight?: number
24
- private __fallbackUnlisten?: () => void
25
- private __throttledUpdate = throttle(() => this.measureAndUpdate(), 80)
26
-
27
- protected targetId() {
28
- return `${this.props.graphModel.flowId}:${this.props.model.id}`
29
- }
30
-
31
- componentWillUnmount() {
32
- super.componentWillUnmount()
33
- this.unmount()
34
- }
35
-
36
- setHtml(rootEl: SVGForeignObjectElement) {
37
- const existed = rootEl.querySelector(
38
- '.custom-react-node-content',
39
- ) as HTMLElement | null
40
- if (existed) {
41
- this.containerEl = existed
42
- } else {
43
- const el = document.createElement('div')
44
- el.className = 'custom-react-node-content'
45
- this.containerEl = el
46
- this.renderReactComponent(el)
47
- rootEl.appendChild(el)
48
- }
49
- this.startResizeObserver()
50
- }
51
-
52
- // confirmUpdate(_rootEl: SVGForeignObjectElement) {
53
- // // TODO: 如有需要,可以先通过继承的方式,自定义该节点的更新逻辑;我们后续会根据实际需求,丰富该功能
54
- // console.log('_rootEl', _rootEl)
55
- // }
56
-
57
- protected renderReactComponent(container: HTMLElement) {
58
- this.unmountReactComponent()
59
- const { model, graphModel } = this.props
60
-
61
- if (container) {
62
- // 基于自定义节点新建 React 元素
63
- const elem = createElement(Wrapper, {
64
- node: model,
65
- graph: graphModel,
66
- })
67
-
68
- if (Portal.isActive()) {
69
- // 使用 Portal
70
- const portal = createPortal(elem, container, model.id) as ReactPortal
71
- Portal.connect(this.targetId(), portal)
72
- } else {
73
- // 创建 Root 元素
74
- this.root = createRoot(container)
75
- this.root.render(elem)
76
- }
77
- }
78
- }
79
-
80
- protected unmountReactComponent() {
81
- if (this.rootEl && this.root) {
82
- this.stopResizeObserver()
83
- this.root.unmount()
84
- this.root = undefined
85
- this.rootEl.innerHTML = ''
86
- }
87
- }
88
-
89
- // DONE: 是否需要 unmount 或 destroy 方法,在销毁后做一些处理
90
- unmount() {
91
- this.unmountReactComponent()
92
- }
93
-
94
- private measureAndUpdate = () => {
95
- try {
96
- const root = this.containerEl as HTMLElement
97
- if (!root) return
98
- const target = (root.firstElementChild as HTMLElement) || root
99
- const rect = target.getBoundingClientRect()
100
- const width = round(rect.width)
101
- const height = round(rect.height)
102
- if (width <= 0 || height <= 0) return
103
- if (width === this.__lastWidth && height === this.__lastHeight) return
104
- this.__lastWidth = width
105
- this.__lastHeight = height
106
- const props = this.props.model.properties as any
107
- const extra = get(props, '_showTitle')
108
- ? isNumber(get(props, '_titleHeight'))
109
- ? get(props, '_titleHeight')
110
- : 28
111
- : 0
112
- const baseHeight = clamp(height - extra, 1, Number.MAX_SAFE_INTEGER)
113
- this.props.model.setProperties({ width, height: baseHeight })
114
- } catch (err) {
115
- console.error('measureAndUpdate error', err)
116
- }
117
- }
118
-
119
- private startResizeObserver() {
120
- const root = this.containerEl as HTMLElement
121
- if (!root) return
122
- try {
123
- if (isFunction((window as any).ResizeObserver)) {
124
- this.__resizeObserver = new (window as any).ResizeObserver(
125
- (entries: any[]) => {
126
- if (!isArray(entries) || !entries.length) return
127
- if (this.__resizeRafId) cancelAnimationFrame(this.__resizeRafId)
128
- this.__resizeRafId = requestAnimationFrame(this.__throttledUpdate)
129
- },
130
- )
131
- const target = (root.firstElementChild as HTMLElement) || root
132
- this.__resizeObserver?.observe(target)
133
- } else {
134
- window.addEventListener('resize', () => this.__throttledUpdate())
135
- this.__fallbackUnlisten = () =>
136
- window.removeEventListener('resize', () => this.__throttledUpdate())
137
- }
138
- } catch (err) {
139
- console.error('startResizeObserver error', err)
140
- }
141
- }
142
-
143
- private stopResizeObserver() {
144
- try {
145
- if (this.__resizeObserver) {
146
- this.__resizeObserver.disconnect()
147
- this.__resizeObserver = undefined
148
- }
149
- if (this.__resizeRafId) {
150
- cancelAnimationFrame(this.__resizeRafId)
151
- this.__resizeRafId = undefined
152
- }
153
- if (this.__fallbackUnlisten) {
154
- this.__fallbackUnlisten()
155
- this.__fallbackUnlisten = undefined
156
- }
157
- } catch (err) {
158
- console.error('stopResizeObserver error', err)
159
- }
160
- }
161
-
162
- // TODO: 确认是否需要重写 onMouseDown 方法
163
- // handleMouseDown(ev: MouseEvent, x: number, y: number) {
164
- // const target = ev.target as Element
165
- // const tagName = target.tagName.toLowerCase()
166
- // if (tagName === 'input') {
167
- // const type = target.getAttribute('type')
168
- // if (
169
- // type == null ||
170
- // [
171
- // 'text',
172
- // 'password',
173
- // 'number',
174
- // 'email',
175
- // 'search',
176
- // 'tel',
177
- // 'url',
178
- // ].includes(type)
179
- // ) {
180
- // return
181
- // }
182
- // }
183
- //
184
- // console.log('pointer position, x:', x, 'y: ', y)
185
- // // TODO
186
- // // super.handleMouseDown(ev)
187
- // }
188
- }
189
-
190
- export default ReactNodeView