@nocobase/flow-engine 2.1.0-beta.10 → 2.1.0-beta.12
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/lib/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +332 -296
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +272 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
- package/lib/components/subModel/AddSubModelButton.js +11 -0
- package/lib/flowContext.js +27 -0
- package/lib/runjs-context/setup.js +1 -0
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/package.json +5 -4
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/runjsContext.test.ts +3 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/components/FlowModelRenderer.tsx +3 -1
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +17 -7
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +457 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +547 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
- package/src/components/subModel/AddSubModelButton.tsx +15 -1
- package/src/flowContext.ts +30 -0
- package/src/runjs-context/setup.ts +1 -0
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
11
12
|
import { Alert, Space } from 'antd';
|
|
12
13
|
import { css } from '@emotion/css';
|
|
13
14
|
import { FlowModel } from '../../../../models';
|
|
@@ -18,16 +19,255 @@ import { FlowEngine } from '../../../../flowEngine';
|
|
|
18
19
|
import { getT } from '../../../../utils';
|
|
19
20
|
import { useFlowContext } from '../../../..';
|
|
20
21
|
import { observer } from '../../../../reactive';
|
|
22
|
+
import {
|
|
23
|
+
omitToolbarPortalInsetStyle,
|
|
24
|
+
ToolbarPortalRect,
|
|
25
|
+
ToolbarPortalRenderSnapshot,
|
|
26
|
+
useFloatToolbarPortal,
|
|
27
|
+
} from './useFloatToolbarPortal';
|
|
28
|
+
import { useFloatToolbarVisibility } from './useFloatToolbarVisibility';
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
const TOOLBAR_Z_INDEX = 999;
|
|
31
|
+
|
|
32
|
+
type ToolbarPosition = 'inside' | 'above' | 'below';
|
|
33
|
+
|
|
34
|
+
interface BaseFloatContextMenuProps {
|
|
35
|
+
children?: React.ReactNode;
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
showDeleteButton?: boolean;
|
|
38
|
+
showCopyUidButton?: boolean;
|
|
39
|
+
containerStyle?: React.CSSProperties;
|
|
40
|
+
/** 自定义工具栏样式,`top/left/right/bottom` 会作为 portal overlay 的 inset 使用。 */
|
|
41
|
+
toolbarStyle?: React.CSSProperties;
|
|
42
|
+
className?: string;
|
|
43
|
+
/**
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
showBorder?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
50
|
+
showBackground?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* @default false
|
|
53
|
+
*/
|
|
54
|
+
showTitle?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
showDragHandle?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Settings menu levels: 1=current model only (default), 2=include sub-models
|
|
61
|
+
*/
|
|
62
|
+
settingsMenuLevel?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Extra toolbar items to add to this context menu instance
|
|
65
|
+
*/
|
|
66
|
+
extraToolbarItems?: ToolbarItemConfig[];
|
|
67
|
+
/**
|
|
68
|
+
* @default 'inside'
|
|
69
|
+
*/
|
|
70
|
+
toolbarPosition?: ToolbarPosition;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hostContainerStyles = css`
|
|
74
|
+
position: relative;
|
|
75
|
+
|
|
76
|
+
&.has-button-child {
|
|
77
|
+
display: inline-block;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const toolbarPositionClassNames: Record<ToolbarPosition, string> = {
|
|
82
|
+
inside: 'nb-toolbar-position-inside',
|
|
83
|
+
above: 'nb-toolbar-position-above',
|
|
84
|
+
below: 'nb-toolbar-position-below',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const toolbarContainerStyles = ({
|
|
88
|
+
showBackground,
|
|
89
|
+
showBorder,
|
|
90
|
+
ctx,
|
|
91
|
+
}: {
|
|
92
|
+
showBackground: boolean;
|
|
93
|
+
showBorder: boolean;
|
|
94
|
+
ctx: any;
|
|
95
|
+
}) => css`
|
|
96
|
+
z-index: ${TOOLBAR_Z_INDEX};
|
|
97
|
+
opacity: 0;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
overflow: visible;
|
|
100
|
+
transition: opacity 0.12s ease;
|
|
101
|
+
background: ${showBackground ? 'var(--colorBgSettingsHover)' : 'transparent'};
|
|
102
|
+
border: ${showBorder ? '2px solid var(--colorBorderSettingsHover)' : 'none'};
|
|
103
|
+
border-radius: ${ctx.themeToken.borderRadiusLG}px;
|
|
104
|
+
|
|
105
|
+
&.nb-toolbar-visible {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
transition-delay: 0.1s;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&.nb-toolbar-portal {
|
|
111
|
+
top: 0;
|
|
112
|
+
left: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&.nb-toolbar-portal-fixed {
|
|
116
|
+
position: fixed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&.nb-toolbar-portal-absolute {
|
|
120
|
+
position: absolute;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&.nb-in-template {
|
|
124
|
+
background: var(--colorTemplateBgSettingsHover);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
> .nb-toolbar-container-title {
|
|
128
|
+
pointer-events: none;
|
|
129
|
+
position: absolute;
|
|
130
|
+
top: 2px;
|
|
131
|
+
left: 2px;
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 4px;
|
|
135
|
+
height: 16px;
|
|
136
|
+
padding: 0;
|
|
137
|
+
font-size: 12px;
|
|
138
|
+
line-height: 16px;
|
|
139
|
+
border-bottom-right-radius: 2px;
|
|
140
|
+
border-radius: 2px;
|
|
141
|
+
|
|
142
|
+
.title-tag {
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
padding: 0 3px;
|
|
145
|
+
border-radius: 2px;
|
|
146
|
+
background: var(--colorSettings);
|
|
147
|
+
color: #fff;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
> .nb-toolbar-container-icons {
|
|
152
|
+
display: none;
|
|
153
|
+
position: absolute;
|
|
154
|
+
right: 2px;
|
|
155
|
+
line-height: 16px;
|
|
156
|
+
pointer-events: all;
|
|
157
|
+
|
|
158
|
+
&.nb-toolbar-position-inside {
|
|
159
|
+
top: 2px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&.nb-toolbar-position-above {
|
|
163
|
+
top: 0;
|
|
164
|
+
transform: translateY(-100%);
|
|
165
|
+
padding-bottom: 0;
|
|
166
|
+
margin-bottom: -2px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
&.nb-toolbar-position-below {
|
|
170
|
+
top: 0;
|
|
171
|
+
transform: translateY(100%);
|
|
172
|
+
padding-top: 2px;
|
|
173
|
+
margin-top: -2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ant-space-item {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
width: 16px;
|
|
181
|
+
height: 16px;
|
|
182
|
+
padding: 2px;
|
|
183
|
+
line-height: 16px;
|
|
184
|
+
background-color: var(--colorSettings);
|
|
185
|
+
color: #fff;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
&.nb-toolbar-visible > .nb-toolbar-container-icons {
|
|
190
|
+
display: block;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
> .resize-handle {
|
|
194
|
+
position: absolute;
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
pointer-events: all;
|
|
199
|
+
opacity: 0.6;
|
|
200
|
+
border-radius: 4px;
|
|
201
|
+
background: var(--colorSettings);
|
|
202
|
+
|
|
203
|
+
&:hover {
|
|
204
|
+
opacity: 0.9;
|
|
205
|
+
background: var(--colorSettingsHover, var(--colorSettings));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&::before {
|
|
209
|
+
content: '';
|
|
210
|
+
position: absolute;
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
background: rgba(255, 255, 255, 0.9);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
&::after {
|
|
216
|
+
content: '';
|
|
217
|
+
position: absolute;
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
background: rgba(255, 255, 255, 0.9);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
> .resize-handle-left {
|
|
224
|
+
top: 50%;
|
|
225
|
+
left: -4px;
|
|
226
|
+
width: 6px;
|
|
227
|
+
height: 20px;
|
|
228
|
+
cursor: ew-resize;
|
|
229
|
+
transform: translateY(-50%);
|
|
230
|
+
|
|
231
|
+
&::before {
|
|
232
|
+
top: 6px;
|
|
233
|
+
left: 50%;
|
|
234
|
+
width: 2px;
|
|
235
|
+
height: 2px;
|
|
236
|
+
transform: translateX(-50%);
|
|
237
|
+
box-shadow:
|
|
238
|
+
0 4px 0 rgba(255, 255, 255, 0.9),
|
|
239
|
+
0 8px 0 rgba(255, 255, 255, 0.9);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
> .resize-handle-right {
|
|
244
|
+
top: 50%;
|
|
245
|
+
right: -4px;
|
|
246
|
+
width: 6px;
|
|
247
|
+
height: 20px;
|
|
248
|
+
cursor: ew-resize;
|
|
249
|
+
transform: translateY(-50%);
|
|
250
|
+
|
|
251
|
+
&::before {
|
|
252
|
+
top: 6px;
|
|
253
|
+
left: 50%;
|
|
254
|
+
width: 2px;
|
|
255
|
+
height: 2px;
|
|
256
|
+
transform: translateX(-50%);
|
|
257
|
+
box-shadow:
|
|
258
|
+
0 4px 0 rgba(255, 255, 255, 0.9),
|
|
259
|
+
0 8px 0 rgba(255, 255, 255, 0.9);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
// 检测直接子节点里是否有按钮,保留原来的 inline-block 兼容行为。
|
|
23
265
|
const detectButtonInDOM = (container: HTMLElement): boolean => {
|
|
24
266
|
if (!container) return false;
|
|
25
267
|
|
|
26
|
-
// 只检测直接子元素中的button
|
|
27
268
|
const directChildren = container.children;
|
|
28
269
|
for (let i = 0; i < directChildren.length; i++) {
|
|
29
270
|
const child = directChildren[i];
|
|
30
|
-
// 检查是否是button元素或具有button特征的元素
|
|
31
271
|
if (child.tagName === 'BUTTON' || child.getAttribute('role') === 'button' || child.classList.contains('ant-btn')) {
|
|
32
272
|
return true;
|
|
33
273
|
}
|
|
@@ -36,7 +276,7 @@ const detectButtonInDOM = (container: HTMLElement): boolean => {
|
|
|
36
276
|
return false;
|
|
37
277
|
};
|
|
38
278
|
|
|
39
|
-
//
|
|
279
|
+
// 渲染工具栏项目,并让设置菜单与工具栏共享同一个 popup 容器。
|
|
40
280
|
const renderToolbarItems = (
|
|
41
281
|
model: FlowModel,
|
|
42
282
|
showDeleteButton: boolean,
|
|
@@ -44,428 +284,175 @@ const renderToolbarItems = (
|
|
|
44
284
|
flowEngine: FlowEngine,
|
|
45
285
|
settingsMenuLevel?: number,
|
|
46
286
|
extraToolbarItems?: ToolbarItemConfig[],
|
|
287
|
+
onSettingsMenuOpenChange?: (open: boolean) => void,
|
|
288
|
+
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement,
|
|
47
289
|
) => {
|
|
48
290
|
const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
|
|
49
|
-
|
|
50
|
-
// 合并额外的工具栏项目
|
|
51
291
|
const allToolbarItems = [...toolbarItems, ...(extraToolbarItems || [])];
|
|
52
292
|
|
|
53
|
-
// 按 sort 字段排序
|
|
54
293
|
allToolbarItems.sort((a, b) => (a.sort || 0) - (b.sort || 0)).reverse();
|
|
55
294
|
|
|
56
295
|
return allToolbarItems
|
|
57
296
|
.filter((itemConfig: ToolbarItemConfig) => {
|
|
58
|
-
// 检查项目是否应该显示
|
|
59
297
|
return itemConfig.visible ? itemConfig.visible(model) : true;
|
|
60
298
|
})
|
|
61
299
|
.map((itemConfig: ToolbarItemConfig) => {
|
|
62
|
-
// 渲染项目组件
|
|
63
300
|
const ItemComponent = itemConfig.component;
|
|
64
301
|
|
|
65
|
-
// 对于默认设置项目,传递额外的 props
|
|
66
302
|
if (itemConfig.key === 'settings-menu') {
|
|
67
303
|
return (
|
|
68
304
|
<ItemComponent
|
|
69
305
|
key={itemConfig.key}
|
|
70
306
|
model={model}
|
|
71
|
-
id={model.uid}
|
|
307
|
+
id={model.uid}
|
|
72
308
|
showDeleteButton={showDeleteButton}
|
|
73
309
|
showCopyUidButton={showCopyUidButton}
|
|
74
310
|
menuLevels={settingsMenuLevel}
|
|
311
|
+
onDropdownVisibleChange={onSettingsMenuOpenChange}
|
|
312
|
+
getPopupContainer={getPopupContainer}
|
|
75
313
|
/>
|
|
76
314
|
);
|
|
77
315
|
}
|
|
78
316
|
|
|
79
|
-
// 其他项目只传递 model
|
|
80
317
|
return <ItemComponent key={itemConfig.key} model={model} />;
|
|
81
318
|
});
|
|
82
319
|
};
|
|
83
320
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.nb-toolbar-container-icons {
|
|
120
|
-
display: block;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* 当有.hide-parent-menu类时隐藏菜单 */
|
|
125
|
-
&.hide-parent-menu > .nb-toolbar-container {
|
|
126
|
-
opacity: 0 !important;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
> .nb-toolbar-container {
|
|
130
|
-
position: absolute;
|
|
131
|
-
top: 0;
|
|
132
|
-
bottom: 0;
|
|
133
|
-
left: 0;
|
|
134
|
-
right: 0;
|
|
135
|
-
z-index: 999;
|
|
136
|
-
opacity: 0;
|
|
137
|
-
background: ${showBackground ? 'var(--colorBgSettingsHover)' : ''};
|
|
138
|
-
border: ${showBorder ? '2px solid var(--colorBorderSettingsHover)' : ''};
|
|
139
|
-
border-radius: ${ctx.themeToken.borderRadiusLG}px;
|
|
140
|
-
pointer-events: none;
|
|
141
|
-
min-width: ${TOOLBAR_ITEM_WIDTH * toolbarCount}px;
|
|
142
|
-
|
|
143
|
-
&.nb-in-template {
|
|
144
|
-
background: var(--colorTemplateBgSettingsHover);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
> .nb-toolbar-container-title {
|
|
148
|
-
pointer-events: none;
|
|
149
|
-
position: absolute;
|
|
150
|
-
font-size: 12px;
|
|
151
|
-
padding: 0;
|
|
152
|
-
line-height: 16px;
|
|
153
|
-
height: 16px;
|
|
154
|
-
border-bottom-right-radius: 2px;
|
|
155
|
-
border-radius: 2px;
|
|
156
|
-
top: 2px;
|
|
157
|
-
left: 2px;
|
|
158
|
-
display: flex;
|
|
159
|
-
align-items: center;
|
|
160
|
-
gap: 4px;
|
|
161
|
-
|
|
162
|
-
.title-tag {
|
|
163
|
-
padding: 0 3px;
|
|
164
|
-
border-radius: 2px;
|
|
165
|
-
background: var(--colorSettings);
|
|
166
|
-
color: #fff;
|
|
167
|
-
display: inline-flex;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
> .nb-toolbar-container-icons {
|
|
172
|
-
display: none; // 防止遮挡其它 icons
|
|
173
|
-
position: absolute;
|
|
174
|
-
right: 2px;
|
|
175
|
-
${toolbarPositionToCSS[toolbarPosition] || ''}
|
|
176
|
-
line-height: 16px;
|
|
177
|
-
pointer-events: all;
|
|
178
|
-
|
|
179
|
-
.ant-space-item {
|
|
180
|
-
background-color: var(--colorSettings);
|
|
181
|
-
color: #fff;
|
|
182
|
-
line-height: 16px;
|
|
183
|
-
width: 16px;
|
|
184
|
-
height: 16px;
|
|
185
|
-
padding: 2px;
|
|
186
|
-
display: flex;
|
|
187
|
-
align-items: center;
|
|
188
|
-
justify-content: center;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/* 拖拽把手样式 - 参考 AirTable 样式 */
|
|
193
|
-
> .resize-handle {
|
|
194
|
-
position: absolute;
|
|
195
|
-
pointer-events: all;
|
|
196
|
-
background: var(--colorSettings);
|
|
197
|
-
opacity: 0.6;
|
|
198
|
-
border-radius: 4px;
|
|
199
|
-
display: flex;
|
|
200
|
-
align-items: center;
|
|
201
|
-
justify-content: center;
|
|
202
|
-
|
|
203
|
-
&:hover {
|
|
204
|
-
opacity: 0.9;
|
|
205
|
-
background: var(--colorSettingsHover, var(--colorSettings));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
&::before {
|
|
209
|
-
content: '';
|
|
210
|
-
position: absolute;
|
|
211
|
-
background: rgba(255, 255, 255, 0.9);
|
|
212
|
-
border-radius: 50%;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
&::after {
|
|
216
|
-
content: '';
|
|
217
|
-
position: absolute;
|
|
218
|
-
background: rgba(255, 255, 255, 0.9);
|
|
219
|
-
border-radius: 50%;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
> .resize-handle-left {
|
|
224
|
-
left: -4px;
|
|
225
|
-
top: 50%;
|
|
226
|
-
transform: translateY(-50%);
|
|
227
|
-
width: 6px;
|
|
228
|
-
height: 20px;
|
|
229
|
-
cursor: ew-resize;
|
|
230
|
-
|
|
231
|
-
&::before {
|
|
232
|
-
width: 2px;
|
|
233
|
-
height: 2px;
|
|
234
|
-
top: 6px;
|
|
235
|
-
left: 50%;
|
|
236
|
-
transform: translateX(-50%);
|
|
237
|
-
box-shadow:
|
|
238
|
-
0 4px 0 rgba(255, 255, 255, 0.9),
|
|
239
|
-
0 8px 0 rgba(255, 255, 255, 0.9);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
> .resize-handle-right {
|
|
244
|
-
right: -4px;
|
|
245
|
-
top: 50%;
|
|
246
|
-
transform: translateY(-50%);
|
|
247
|
-
width: 6px;
|
|
248
|
-
height: 20px;
|
|
249
|
-
cursor: ew-resize;
|
|
250
|
-
|
|
251
|
-
&::before {
|
|
252
|
-
width: 2px;
|
|
253
|
-
height: 2px;
|
|
254
|
-
top: 6px;
|
|
255
|
-
left: 50%;
|
|
256
|
-
transform: translateX(-50%);
|
|
257
|
-
box-shadow:
|
|
258
|
-
0 4px 0 rgba(255, 255, 255, 0.9),
|
|
259
|
-
0 8px 0 rgba(255, 255, 255, 0.9);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
> .resize-handle-bottom {
|
|
264
|
-
bottom: -4px;
|
|
265
|
-
left: 50%;
|
|
266
|
-
transform: translateX(-50%);
|
|
267
|
-
width: 20px;
|
|
268
|
-
height: 6px;
|
|
269
|
-
cursor: ns-resize;
|
|
270
|
-
|
|
271
|
-
&::before {
|
|
272
|
-
width: 2px;
|
|
273
|
-
height: 2px;
|
|
274
|
-
left: 6px;
|
|
275
|
-
top: 50%;
|
|
276
|
-
transform: translateY(-50%);
|
|
277
|
-
box-shadow:
|
|
278
|
-
4px 0 0 rgba(255, 255, 255, 0.9),
|
|
279
|
-
8px 0 0 rgba(255, 255, 255, 0.9);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
`;
|
|
321
|
+
const buildToolbarContainerClassName = ({
|
|
322
|
+
showBackground,
|
|
323
|
+
showBorder,
|
|
324
|
+
ctx,
|
|
325
|
+
portalRenderSnapshot,
|
|
326
|
+
isToolbarVisible,
|
|
327
|
+
className,
|
|
328
|
+
}: {
|
|
329
|
+
showBackground: boolean;
|
|
330
|
+
showBorder: boolean;
|
|
331
|
+
ctx: any;
|
|
332
|
+
portalRenderSnapshot: ToolbarPortalRenderSnapshot | null;
|
|
333
|
+
isToolbarVisible: boolean;
|
|
334
|
+
className?: string;
|
|
335
|
+
}) =>
|
|
336
|
+
[
|
|
337
|
+
toolbarContainerStyles({ showBackground, showBorder, ctx }),
|
|
338
|
+
'nb-toolbar-portal',
|
|
339
|
+
portalRenderSnapshot?.positioningMode === 'absolute' ? 'nb-toolbar-portal-absolute' : 'nb-toolbar-portal-fixed',
|
|
340
|
+
isToolbarVisible ? 'nb-toolbar-visible' : '',
|
|
341
|
+
className?.includes('nb-in-template') ? 'nb-in-template' : '',
|
|
342
|
+
]
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
.join(' ');
|
|
345
|
+
|
|
346
|
+
const buildToolbarContainerStyle = (
|
|
347
|
+
portalRect: ToolbarPortalRect,
|
|
348
|
+
toolbarStyle?: React.CSSProperties,
|
|
349
|
+
): React.CSSProperties => ({
|
|
350
|
+
top: `${portalRect.top}px`,
|
|
351
|
+
left: `${portalRect.left}px`,
|
|
352
|
+
width: `${portalRect.width}px`,
|
|
353
|
+
height: `${portalRect.height}px`,
|
|
354
|
+
...omitToolbarPortalInsetStyle(toolbarStyle),
|
|
355
|
+
});
|
|
284
356
|
|
|
285
|
-
|
|
286
|
-
interface ModelProvidedProps {
|
|
357
|
+
interface ModelProvidedProps extends BaseFloatContextMenuProps {
|
|
287
358
|
model: FlowModel<any>;
|
|
288
|
-
children?: React.ReactNode;
|
|
289
|
-
enabled?: boolean;
|
|
290
|
-
showDeleteButton?: boolean;
|
|
291
|
-
showCopyUidButton?: boolean;
|
|
292
|
-
containerStyle?: React.CSSProperties;
|
|
293
|
-
toolbarStyle?: React.CSSProperties;
|
|
294
|
-
className?: string;
|
|
295
|
-
/**
|
|
296
|
-
* @default true
|
|
297
|
-
*/
|
|
298
|
-
showBorder?: boolean;
|
|
299
|
-
/**
|
|
300
|
-
* @default true
|
|
301
|
-
*/
|
|
302
|
-
showBackground?: boolean;
|
|
303
|
-
/**
|
|
304
|
-
* @default false
|
|
305
|
-
*/
|
|
306
|
-
showTitle?: boolean;
|
|
307
|
-
/**
|
|
308
|
-
* @default false
|
|
309
|
-
*/
|
|
310
|
-
showDragHandle?: boolean;
|
|
311
|
-
/**
|
|
312
|
-
* Settings menu levels: 1=current model only (default), 2=include sub-models
|
|
313
|
-
*/
|
|
314
|
-
settingsMenuLevel?: number;
|
|
315
|
-
/**
|
|
316
|
-
* Extra toolbar items to add to this context menu instance
|
|
317
|
-
*/
|
|
318
|
-
extraToolbarItems?: ToolbarItemConfig[];
|
|
319
|
-
/**
|
|
320
|
-
* @default 'inside'
|
|
321
|
-
*/
|
|
322
|
-
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
323
359
|
}
|
|
324
360
|
|
|
325
|
-
interface ModelByIdProps {
|
|
361
|
+
interface ModelByIdProps extends BaseFloatContextMenuProps {
|
|
326
362
|
uid: string;
|
|
327
363
|
modelClassName: string;
|
|
328
|
-
children?: React.ReactNode;
|
|
329
|
-
enabled?: boolean;
|
|
330
|
-
showDeleteButton?: boolean;
|
|
331
|
-
showCopyUidButton?: boolean;
|
|
332
|
-
containerStyle?: React.CSSProperties;
|
|
333
|
-
className?: string;
|
|
334
|
-
/**
|
|
335
|
-
* @default true
|
|
336
|
-
*/
|
|
337
|
-
showBorder?: boolean;
|
|
338
|
-
/**
|
|
339
|
-
* @default true
|
|
340
|
-
*/
|
|
341
|
-
showBackground?: boolean;
|
|
342
|
-
/**
|
|
343
|
-
* @default false
|
|
344
|
-
*/
|
|
345
|
-
showTitle?: boolean;
|
|
346
|
-
/**
|
|
347
|
-
* Settings menu levels: 1=current model only (default), 2=include sub-models
|
|
348
|
-
*/
|
|
349
|
-
settingsMenuLevel?: number;
|
|
350
|
-
/**
|
|
351
|
-
* Extra toolbar items to add to this context menu instance
|
|
352
|
-
*/
|
|
353
|
-
extraToolbarItems?: ToolbarItemConfig[];
|
|
354
|
-
/**
|
|
355
|
-
* @default 'inside'
|
|
356
|
-
*/
|
|
357
|
-
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
type FlowsFloatContextMenuProps = ModelProvidedProps | ModelByIdProps;
|
|
361
367
|
|
|
362
|
-
// 判断是否是通过ID获取模型的props
|
|
368
|
+
// 判断是否是通过ID获取模型的 props
|
|
363
369
|
const isModelByIdProps = (props: FlowsFloatContextMenuProps): props is ModelByIdProps => {
|
|
364
370
|
return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName);
|
|
365
371
|
};
|
|
366
372
|
|
|
367
373
|
/**
|
|
368
|
-
* FlowsFloatContextMenu组件 -
|
|
374
|
+
* FlowsFloatContextMenu组件 - 悬浮配置工具栏组件
|
|
369
375
|
*
|
|
370
376
|
* 功能特性:
|
|
371
377
|
* - 鼠标悬浮显示右上角配置图标
|
|
372
378
|
* - 点击图标显示配置菜单
|
|
373
379
|
* - 支持删除功能
|
|
374
380
|
* - Wrapper 模式支持
|
|
375
|
-
* -
|
|
376
|
-
* -
|
|
381
|
+
* - 使用 portal overlay 避免被宿主或祖先裁剪
|
|
382
|
+
* - 设置菜单与工具栏共享同一个 popup 容器
|
|
377
383
|
*
|
|
378
384
|
* 支持两种使用方式:
|
|
379
|
-
* 1. 直接提供model:
|
|
380
|
-
* 2. 通过uid和modelClassName获取model:
|
|
385
|
+
* 1. 直接提供 model: `<FlowsFloatContextMenu model={myModel}>{children}</FlowsFloatContextMenu>`
|
|
386
|
+
* 2. 通过 uid 和 modelClassName 获取 model:
|
|
387
|
+
* `<FlowsFloatContextMenu uid="model1" modelClassName="MyModel">{children}</FlowsFloatContextMenu>`
|
|
381
388
|
*
|
|
382
389
|
* @param props.children 子组件,必须提供
|
|
383
|
-
* @param props.enabled 是否启用悬浮菜单,默认为true
|
|
384
|
-
* @param props.showDeleteButton 是否显示删除按钮,默认为true
|
|
385
|
-
* @param props.showCopyUidButton 是否显示复制UID按钮,默认为true
|
|
390
|
+
* @param props.enabled 是否启用悬浮菜单,默认为 true
|
|
391
|
+
* @param props.showDeleteButton 是否显示删除按钮,默认为 true
|
|
392
|
+
* @param props.showCopyUidButton 是否显示复制 UID 按钮,默认为 true
|
|
386
393
|
* @param props.containerStyle 容器自定义样式
|
|
394
|
+
* @param props.toolbarStyle 工具栏自定义样式;`top/left/right/bottom` 会作为 portal overlay 的 inset 使用
|
|
387
395
|
* @param props.className 容器自定义类名
|
|
388
|
-
* @param props.showTitle 是否在边框左上角显示模型title,默认为false
|
|
396
|
+
* @param props.showTitle 是否在边框左上角显示模型 title,默认为 false
|
|
389
397
|
* @param props.settingsMenuLevel 设置菜单层级:1=仅当前模型(默认),2=包含子模型
|
|
390
398
|
* @param props.extraToolbarItems 额外的工具栏项目,仅应用于此实例
|
|
391
399
|
*/
|
|
392
400
|
const FlowsFloatContextMenu: React.FC<FlowsFloatContextMenuProps> = observer((props) => {
|
|
393
401
|
const ctx = useFlowContext();
|
|
394
|
-
// Only render if flowSettings is enabled
|
|
395
402
|
if (!ctx.flowSettingsEnabled) {
|
|
396
403
|
return <>{props.children}</>;
|
|
397
404
|
}
|
|
398
405
|
if (isModelByIdProps(props)) {
|
|
399
406
|
return <FlowsFloatContextMenuWithModelById {...props} />;
|
|
400
|
-
} else {
|
|
401
|
-
return <FlowsFloatContextMenuWithModel {...props} />;
|
|
402
407
|
}
|
|
408
|
+
return <FlowsFloatContextMenuWithModel {...props} />;
|
|
403
409
|
});
|
|
404
410
|
|
|
405
|
-
const ResizeHandles: React.FC<{
|
|
411
|
+
const ResizeHandles: React.FC<{
|
|
412
|
+
model: FlowModel;
|
|
413
|
+
onDragStart: () => void;
|
|
414
|
+
onDragEnd: () => void;
|
|
415
|
+
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
|
416
|
+
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
|
|
417
|
+
}> = (props) => {
|
|
406
418
|
const isDraggingRef = useRef<boolean>(false);
|
|
407
|
-
const dragTypeRef = useRef<'left' | 'right' |
|
|
419
|
+
const dragTypeRef = useRef<'left' | 'right' | null>(null);
|
|
408
420
|
const dragStartPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
409
421
|
const { onDragStart, onDragEnd } = props;
|
|
410
422
|
|
|
411
|
-
//
|
|
423
|
+
// 把拖拽位移转成上层已约定的 resize 事件。
|
|
412
424
|
const handleDragMove = useCallback(
|
|
413
425
|
(e: MouseEvent) => {
|
|
414
426
|
if (!isDraggingRef.current || !dragTypeRef.current) return;
|
|
415
427
|
|
|
416
428
|
const deltaX = e.clientX - dragStartPosRef.current.x;
|
|
417
|
-
const deltaY = e.clientY - dragStartPosRef.current.y;
|
|
418
|
-
|
|
419
|
-
let resizeDistance = 0;
|
|
420
429
|
|
|
421
430
|
switch (dragTypeRef.current) {
|
|
422
431
|
case 'left':
|
|
423
|
-
|
|
424
|
-
resizeDistance = -deltaX;
|
|
425
|
-
props.model.parent.emitter.emit('onResizeLeft', { resizeDistance, model: props.model });
|
|
432
|
+
props.model.parent.emitter.emit('onResizeLeft', { resizeDistance: -deltaX, model: props.model });
|
|
426
433
|
break;
|
|
427
|
-
|
|
428
434
|
case 'right':
|
|
429
|
-
|
|
430
|
-
resizeDistance = deltaX;
|
|
431
|
-
props.model.parent.emitter.emit('onResizeRight', { resizeDistance, model: props.model });
|
|
432
|
-
break;
|
|
433
|
-
|
|
434
|
-
case 'bottom':
|
|
435
|
-
// 底部把手:向下拖为正数,向上拖为负数
|
|
436
|
-
resizeDistance = deltaY;
|
|
437
|
-
props.model.parent.emitter.emit('onResizeBottom', { resizeDistance, model: props.model });
|
|
438
|
-
break;
|
|
439
|
-
|
|
440
|
-
case 'corner': {
|
|
441
|
-
// 右下角把手:同时计算宽度和高度变化
|
|
442
|
-
const widthDelta = deltaX;
|
|
443
|
-
const heightDelta = deltaY;
|
|
444
|
-
props.model.parent.emitter.emit('onResizeCorner', { widthDelta, heightDelta, model: props.model });
|
|
435
|
+
props.model.parent.emitter.emit('onResizeRight', { resizeDistance: deltaX, model: props.model });
|
|
445
436
|
break;
|
|
446
|
-
}
|
|
447
437
|
}
|
|
448
438
|
},
|
|
449
439
|
[props.model],
|
|
450
440
|
);
|
|
451
441
|
|
|
452
|
-
// 拖拽结束处理函数
|
|
453
442
|
const handleDragEnd = useCallback(() => {
|
|
454
443
|
isDraggingRef.current = false;
|
|
455
444
|
dragTypeRef.current = null;
|
|
456
445
|
dragStartPosRef.current = { x: 0, y: 0 };
|
|
457
446
|
|
|
458
|
-
// 移除全局事件监听
|
|
459
447
|
document.removeEventListener('mousemove', handleDragMove);
|
|
460
448
|
document.removeEventListener('mouseup', handleDragEnd);
|
|
461
449
|
|
|
462
450
|
props.model.parent.emitter.emit('onResizeEnd');
|
|
463
451
|
onDragEnd?.();
|
|
464
|
-
}, [handleDragMove, props.model
|
|
452
|
+
}, [handleDragMove, onDragEnd, props.model]);
|
|
465
453
|
|
|
466
|
-
// 拖拽开始处理函数
|
|
467
454
|
const handleDragStart = useCallback(
|
|
468
|
-
(e: React.MouseEvent, type: 'left' | 'right'
|
|
455
|
+
(e: React.MouseEvent, type: 'left' | 'right') => {
|
|
469
456
|
e.preventDefault();
|
|
470
457
|
e.stopPropagation();
|
|
471
458
|
|
|
@@ -473,7 +460,6 @@ const ResizeHandles: React.FC<{ model: FlowModel; onDragStart: () => void; onDra
|
|
|
473
460
|
dragTypeRef.current = type;
|
|
474
461
|
dragStartPosRef.current = { x: e.clientX, y: e.clientY };
|
|
475
462
|
|
|
476
|
-
// 添加全局事件监听
|
|
477
463
|
document.addEventListener('mousemove', handleDragMove);
|
|
478
464
|
document.addEventListener('mouseup', handleDragEnd);
|
|
479
465
|
|
|
@@ -484,27 +470,25 @@ const ResizeHandles: React.FC<{ model: FlowModel; onDragStart: () => void; onDra
|
|
|
484
470
|
|
|
485
471
|
return (
|
|
486
472
|
<>
|
|
487
|
-
{/* 拖拽把手 */}
|
|
488
473
|
<div
|
|
489
474
|
className="resize-handle resize-handle-left"
|
|
490
475
|
title="拖拽调节宽度"
|
|
476
|
+
onMouseEnter={props.onMouseEnter}
|
|
477
|
+
onMouseLeave={props.onMouseLeave}
|
|
491
478
|
onMouseDown={(e) => handleDragStart(e, 'left')}
|
|
492
|
-
|
|
479
|
+
/>
|
|
493
480
|
<div
|
|
494
481
|
className="resize-handle resize-handle-right"
|
|
495
482
|
title="拖拽调节宽度"
|
|
483
|
+
onMouseEnter={props.onMouseEnter}
|
|
484
|
+
onMouseLeave={props.onMouseLeave}
|
|
496
485
|
onMouseDown={(e) => handleDragStart(e, 'right')}
|
|
497
|
-
|
|
498
|
-
{/* <div
|
|
499
|
-
className="resize-handle resize-handle-bottom"
|
|
500
|
-
title="拖拽调节高度"
|
|
501
|
-
onMouseDown={(e) => handleDragStart(e, 'bottom')}
|
|
502
|
-
></div> */}
|
|
486
|
+
/>
|
|
503
487
|
</>
|
|
504
488
|
);
|
|
505
489
|
};
|
|
506
490
|
|
|
507
|
-
//
|
|
491
|
+
// 使用直接传入的 model 渲染工具栏。
|
|
508
492
|
const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
509
493
|
({
|
|
510
494
|
model,
|
|
@@ -523,34 +507,93 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
523
507
|
toolbarStyle,
|
|
524
508
|
toolbarPosition = 'inside',
|
|
525
509
|
}: ModelProvidedProps) => {
|
|
526
|
-
const [
|
|
527
|
-
const [hasButton, setHasButton] = useState<boolean>(false);
|
|
510
|
+
const [hasButton, setHasButton] = useState(false);
|
|
528
511
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
529
|
-
const flowEngine = useFlowEngine();
|
|
530
|
-
const [style, setStyle] = useState<React.CSSProperties>({});
|
|
531
512
|
const toolbarContainerRef = useRef<HTMLDivElement>(null);
|
|
532
|
-
const
|
|
513
|
+
const portalActionsRef = useRef<{
|
|
514
|
+
updatePortalRect: () => void;
|
|
515
|
+
schedulePortalRectUpdate: () => void;
|
|
516
|
+
}>({
|
|
517
|
+
updatePortalRect: () => {},
|
|
518
|
+
schedulePortalRectUpdate: () => {},
|
|
519
|
+
});
|
|
520
|
+
const modelUid = model?.uid || '';
|
|
521
|
+
const flowEngine = useFlowEngine();
|
|
522
|
+
const updatePortalRectProxy = useCallback(() => {
|
|
523
|
+
portalActionsRef.current.updatePortalRect();
|
|
524
|
+
}, []);
|
|
525
|
+
const schedulePortalRectUpdateProxy = useCallback(() => {
|
|
526
|
+
portalActionsRef.current.schedulePortalRectUpdate();
|
|
527
|
+
}, []);
|
|
528
|
+
const {
|
|
529
|
+
isToolbarVisible,
|
|
530
|
+
shouldRenderToolbar,
|
|
531
|
+
handleSettingsMenuOpenChange,
|
|
532
|
+
handleChildHover,
|
|
533
|
+
handleHostMouseEnter,
|
|
534
|
+
handleHostMouseLeave,
|
|
535
|
+
handleToolbarMouseEnter,
|
|
536
|
+
handleToolbarMouseLeave,
|
|
537
|
+
handleResizeDragStart,
|
|
538
|
+
handleResizeDragEnd,
|
|
539
|
+
} = useFloatToolbarVisibility({
|
|
540
|
+
modelUid,
|
|
541
|
+
containerRef,
|
|
542
|
+
toolbarContainerRef,
|
|
543
|
+
updatePortalRect: updatePortalRectProxy,
|
|
544
|
+
schedulePortalRectUpdate: schedulePortalRectUpdateProxy,
|
|
545
|
+
});
|
|
546
|
+
const { portalRect, portalRenderSnapshot, getPopupContainer, updatePortalRect, schedulePortalRectUpdate } =
|
|
547
|
+
useFloatToolbarPortal({
|
|
548
|
+
active: shouldRenderToolbar,
|
|
549
|
+
containerRef,
|
|
550
|
+
toolbarContainerRef,
|
|
551
|
+
toolbarStyle,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
portalActionsRef.current.updatePortalRect = updatePortalRect;
|
|
555
|
+
portalActionsRef.current.schedulePortalRectUpdate = schedulePortalRectUpdate;
|
|
556
|
+
|
|
557
|
+
const toolbarItems = useMemo(
|
|
558
|
+
() =>
|
|
559
|
+
model
|
|
560
|
+
? renderToolbarItems(
|
|
561
|
+
model,
|
|
562
|
+
showDeleteButton,
|
|
563
|
+
showCopyUidButton,
|
|
564
|
+
flowEngine,
|
|
565
|
+
settingsMenuLevel,
|
|
566
|
+
extraToolbarItems,
|
|
567
|
+
handleSettingsMenuOpenChange,
|
|
568
|
+
getPopupContainer,
|
|
569
|
+
)
|
|
570
|
+
: [],
|
|
571
|
+
[
|
|
572
|
+
extraToolbarItems,
|
|
573
|
+
flowEngine,
|
|
574
|
+
getPopupContainer,
|
|
575
|
+
handleSettingsMenuOpenChange,
|
|
576
|
+
model,
|
|
577
|
+
settingsMenuLevel,
|
|
578
|
+
showCopyUidButton,
|
|
579
|
+
showDeleteButton,
|
|
580
|
+
],
|
|
581
|
+
);
|
|
533
582
|
|
|
534
|
-
// 检测DOM中是否包含button元素
|
|
535
583
|
useEffect(() => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
584
|
+
const container = containerRef.current;
|
|
585
|
+
if (!container) {
|
|
586
|
+
return;
|
|
539
587
|
}
|
|
540
|
-
}, [children]); // 当children变化时重新检测
|
|
541
588
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
589
|
+
const syncHasButton = () => {
|
|
590
|
+
setHasButton(detectButtonInDOM(container));
|
|
591
|
+
};
|
|
545
592
|
|
|
546
|
-
|
|
547
|
-
if (containerRef.current) {
|
|
548
|
-
const hasButtonElement = detectButtonInDOM(containerRef.current);
|
|
549
|
-
setHasButton(hasButtonElement);
|
|
550
|
-
}
|
|
551
|
-
});
|
|
593
|
+
syncHasButton();
|
|
552
594
|
|
|
553
|
-
observer
|
|
595
|
+
const observer = new MutationObserver(syncHasButton);
|
|
596
|
+
observer.observe(container, {
|
|
554
597
|
childList: true,
|
|
555
598
|
subtree: true,
|
|
556
599
|
attributes: true,
|
|
@@ -562,75 +605,79 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
562
605
|
};
|
|
563
606
|
}, []);
|
|
564
607
|
|
|
565
|
-
const handleChildHover = useCallback((e: React.MouseEvent) => {
|
|
566
|
-
const target = e.target as HTMLElement;
|
|
567
|
-
const childWithMenu = target.closest('[data-has-float-menu]');
|
|
568
|
-
|
|
569
|
-
// 如果悬浮的是子元素(且不是当前容器),则隐藏当前菜单
|
|
570
|
-
if (childWithMenu && childWithMenu !== containerRef.current) {
|
|
571
|
-
setHideMenu(true);
|
|
572
|
-
} else {
|
|
573
|
-
setHideMenu(false);
|
|
574
|
-
}
|
|
575
|
-
}, []);
|
|
576
|
-
|
|
577
608
|
if (!model) {
|
|
578
609
|
const t = getT(model || ({} as FlowModel));
|
|
579
610
|
return <Alert message={t('Invalid model provided')} type="error" />;
|
|
580
611
|
}
|
|
581
612
|
|
|
582
|
-
// 如果未启用或没有children,直接返回children
|
|
583
613
|
if (!enabled || !children) {
|
|
584
614
|
return <>{children}</>;
|
|
585
615
|
}
|
|
586
616
|
|
|
617
|
+
const toolbarContainerClassName = buildToolbarContainerClassName({
|
|
618
|
+
showBackground,
|
|
619
|
+
showBorder,
|
|
620
|
+
ctx: model.context,
|
|
621
|
+
portalRenderSnapshot,
|
|
622
|
+
isToolbarVisible,
|
|
623
|
+
className,
|
|
624
|
+
});
|
|
625
|
+
const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle);
|
|
626
|
+
|
|
627
|
+
const toolbarNode = shouldRenderToolbar ? (
|
|
628
|
+
<div
|
|
629
|
+
ref={toolbarContainerRef}
|
|
630
|
+
className={`nb-toolbar-container ${toolbarContainerClassName}`}
|
|
631
|
+
style={toolbarContainerStyle}
|
|
632
|
+
data-model-uid={model.uid}
|
|
633
|
+
>
|
|
634
|
+
{showTitle && (model.title || model.extraTitle) && (
|
|
635
|
+
<div className="nb-toolbar-container-title">
|
|
636
|
+
{model.title && <span className="title-tag">{model.title}</span>}
|
|
637
|
+
{model.extraTitle && <span className="title-tag">{model.extraTitle}</span>}
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
640
|
+
<div
|
|
641
|
+
className={`nb-toolbar-container-icons ${toolbarPositionClassNames[toolbarPosition]}`}
|
|
642
|
+
onClick={(e) => e.stopPropagation()}
|
|
643
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
644
|
+
onMouseMove={(e) => e.stopPropagation()}
|
|
645
|
+
onMouseEnter={handleToolbarMouseEnter}
|
|
646
|
+
onMouseLeave={handleToolbarMouseLeave}
|
|
647
|
+
>
|
|
648
|
+
<Space size={3} align="center">
|
|
649
|
+
{toolbarItems}
|
|
650
|
+
</Space>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
{showDragHandle && (
|
|
654
|
+
<ResizeHandles
|
|
655
|
+
model={model}
|
|
656
|
+
onMouseEnter={handleToolbarMouseEnter}
|
|
657
|
+
onMouseLeave={handleToolbarMouseLeave}
|
|
658
|
+
onDragStart={handleResizeDragStart}
|
|
659
|
+
onDragEnd={handleResizeDragEnd}
|
|
660
|
+
/>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
) : null;
|
|
664
|
+
|
|
587
665
|
return (
|
|
588
666
|
<div
|
|
589
667
|
ref={containerRef}
|
|
590
|
-
className={`${
|
|
591
|
-
showBackground,
|
|
592
|
-
showBorder,
|
|
593
|
-
ctx: model.context,
|
|
594
|
-
toolbarPosition,
|
|
595
|
-
toolbarCount: getToolbarCount(flowEngine, extraToolbarItems),
|
|
596
|
-
})} ${hideMenu ? 'hide-parent-menu' : ''} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
|
|
668
|
+
className={`${hostContainerStyles} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
|
|
597
669
|
style={containerStyle}
|
|
598
670
|
data-has-float-menu="true"
|
|
671
|
+
data-float-menu-model-uid={model.uid}
|
|
599
672
|
onMouseMove={handleChildHover}
|
|
673
|
+
onMouseEnter={handleHostMouseEnter}
|
|
674
|
+
onMouseLeave={handleHostMouseLeave}
|
|
600
675
|
>
|
|
601
676
|
{children}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
<div className="nb-toolbar-container-title">
|
|
607
|
-
{model.title && <span className="title-tag">{model.title}</span>}
|
|
608
|
-
{model.extraTitle && <span className="title-tag">{model.extraTitle}</span>}
|
|
609
|
-
</div>
|
|
610
|
-
)}
|
|
611
|
-
<div
|
|
612
|
-
className="nb-toolbar-container-icons"
|
|
613
|
-
onClick={(e) => e.stopPropagation()}
|
|
614
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
615
|
-
onMouseMove={(e) => e.stopPropagation()}
|
|
616
|
-
>
|
|
617
|
-
<Space size={3} align="center">
|
|
618
|
-
{renderToolbarItems(
|
|
619
|
-
model,
|
|
620
|
-
showDeleteButton,
|
|
621
|
-
showCopyUidButton,
|
|
622
|
-
flowEngine,
|
|
623
|
-
settingsMenuLevel,
|
|
624
|
-
extraToolbarItems,
|
|
625
|
-
)}
|
|
626
|
-
</Space>
|
|
627
|
-
</div>
|
|
628
|
-
|
|
629
|
-
{/* 拖拽把手 */}
|
|
630
|
-
{showDragHandle && (
|
|
631
|
-
<ResizeHandles model={model} onDragStart={() => setStyle({ opacity: 1 })} onDragEnd={() => setStyle({})} />
|
|
632
|
-
)}
|
|
633
|
-
</div>
|
|
677
|
+
{toolbarNode &&
|
|
678
|
+
(portalRenderSnapshot?.mountElement
|
|
679
|
+
? createPortal(toolbarNode, portalRenderSnapshot.mountElement)
|
|
680
|
+
: toolbarNode)}
|
|
634
681
|
</div>
|
|
635
682
|
);
|
|
636
683
|
},
|
|
@@ -639,22 +686,9 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
639
686
|
},
|
|
640
687
|
);
|
|
641
688
|
|
|
642
|
-
// 通过
|
|
689
|
+
// 通过 uid + modelClassName 解析 model,再复用主实现。
|
|
643
690
|
const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
|
|
644
|
-
({
|
|
645
|
-
uid,
|
|
646
|
-
modelClassName,
|
|
647
|
-
children,
|
|
648
|
-
enabled = true,
|
|
649
|
-
showDeleteButton = true,
|
|
650
|
-
showCopyUidButton = true,
|
|
651
|
-
containerStyle,
|
|
652
|
-
className,
|
|
653
|
-
showTitle = false,
|
|
654
|
-
settingsMenuLevel,
|
|
655
|
-
extraToolbarItems: extraToolbarItems,
|
|
656
|
-
toolbarPosition,
|
|
657
|
-
}) => {
|
|
691
|
+
({ uid, modelClassName, children, ...restProps }) => {
|
|
658
692
|
const model = useFlowModelById(uid, modelClassName);
|
|
659
693
|
const flowEngine = useFlowEngine();
|
|
660
694
|
|
|
@@ -663,18 +697,7 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
|
|
|
663
697
|
}
|
|
664
698
|
|
|
665
699
|
return (
|
|
666
|
-
<FlowsFloatContextMenuWithModel
|
|
667
|
-
model={model}
|
|
668
|
-
enabled={enabled}
|
|
669
|
-
showDeleteButton={showDeleteButton}
|
|
670
|
-
showCopyUidButton={showCopyUidButton}
|
|
671
|
-
containerStyle={containerStyle}
|
|
672
|
-
className={className}
|
|
673
|
-
showTitle={showTitle}
|
|
674
|
-
settingsMenuLevel={settingsMenuLevel}
|
|
675
|
-
extraToolbarItems={extraToolbarItems}
|
|
676
|
-
toolbarPosition={toolbarPosition}
|
|
677
|
-
>
|
|
700
|
+
<FlowsFloatContextMenuWithModel model={model} {...restProps}>
|
|
678
701
|
{children}
|
|
679
702
|
</FlowsFloatContextMenuWithModel>
|
|
680
703
|
);
|
|
@@ -685,9 +708,3 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
|
|
|
685
708
|
);
|
|
686
709
|
|
|
687
710
|
export { FlowsFloatContextMenu };
|
|
688
|
-
|
|
689
|
-
function getToolbarCount(flowEngine, extraToolbarItems) {
|
|
690
|
-
const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
|
|
691
|
-
const allToolbarItems = [...toolbarItems, ...(extraToolbarItems || [])];
|
|
692
|
-
return allToolbarItems.length;
|
|
693
|
-
}
|