@xbeeant/form-engine-designer 0.0.3 → 0.0.4
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/dist/es/Canvas.js +42 -47
- package/dist/es/Canvas.js.map +1 -1
- package/dist/es/Designer.js +121 -98
- package/dist/es/Designer.js.map +1 -1
- package/dist/es/DesignerContext.js +86 -73
- package/dist/es/DesignerContext.js.map +1 -1
- package/dist/es/Palette.js +3 -3
- package/dist/es/Palette.js.map +1 -1
- package/dist/es/PropertyPanel.js +99 -100
- package/dist/es/PropertyPanel.js.map +1 -1
- package/dist/es/catalog.js +17 -10
- package/dist/es/catalog.js.map +1 -1
- package/dist/es/designer-context.js +110 -0
- package/dist/es/designer-context.js.map +1 -0
- package/dist/es/history.js +73 -0
- package/dist/es/history.js.map +1 -0
- package/dist/es/index.css +1 -1
- package/dist/es/index.js +3 -3
- package/dist/es/property/basic-property.js +6 -0
- package/dist/es/property/basic-property.js.map +1 -1
- package/dist/es/property-panel.js +294 -0
- package/dist/es/property-panel.js.map +1 -0
- package/dist/es/resizable-panel.js +103 -0
- package/dist/es/resizable-panel.js.map +1 -0
- package/dist/es/schema-utils.js +286 -0
- package/dist/es/schema-utils.js.map +1 -0
- package/dist/es/widgets/BindEditor.js +60 -49
- package/dist/es/widgets/BindEditor.js.map +1 -1
- package/dist/es/widgets/CodeEditor.js +41 -49
- package/dist/es/widgets/CodeEditor.js.map +1 -1
- package/dist/es/widgets/DefaultEditor.js +25 -30
- package/dist/es/widgets/DefaultEditor.js.map +1 -1
- package/dist/es/widgets/DependenciesEditor.js +21 -21
- package/dist/es/widgets/DependenciesEditor.js.map +1 -1
- package/dist/es/widgets/EnumEditor.js +25 -31
- package/dist/es/widgets/EnumEditor.js.map +1 -1
- package/dist/es/widgets/ExpressionSwitch.js +32 -30
- package/dist/es/widgets/ExpressionSwitch.js.map +1 -1
- package/dist/es/widgets/ReactionsEditor.js +103 -103
- package/dist/es/widgets/ReactionsEditor.js.map +1 -1
- package/dist/es/widgets/RemoteDataEditor.js +63 -63
- package/dist/es/widgets/RemoteDataEditor.js.map +1 -1
- package/dist/es/widgets/ValidateEditor.js +49 -49
- package/dist/es/widgets/ValidateEditor.js.map +1 -1
- package/dist/es/widgets/bind-editor.js +78 -0
- package/dist/es/widgets/bind-editor.js.map +1 -0
- package/dist/es/widgets/code-editor.js +76 -0
- package/dist/es/widgets/code-editor.js.map +1 -0
- package/dist/es/widgets/default-editor.js +50 -0
- package/dist/es/widgets/default-editor.js.map +1 -0
- package/dist/es/widgets/dependencies-editor.js +36 -0
- package/dist/es/widgets/dependencies-editor.js.map +1 -0
- package/dist/es/widgets/enum-editor.js +43 -0
- package/dist/es/widgets/enum-editor.js.map +1 -0
- package/dist/es/widgets/expression-builder.js +367 -0
- package/dist/es/widgets/expression-builder.js.map +1 -0
- package/dist/es/widgets/expression-model.js +340 -0
- package/dist/es/widgets/expression-model.js.map +1 -0
- package/dist/es/widgets/expression-switch.js +49 -0
- package/dist/es/widgets/expression-switch.js.map +1 -0
- package/dist/es/widgets/index.js +13 -13
- package/dist/es/widgets/index.js.map +1 -1
- package/dist/es/widgets/options-editor.js +102 -0
- package/dist/es/widgets/options-editor.js.map +1 -0
- package/dist/es/widgets/reactions-editor.js +401 -0
- package/dist/es/widgets/reactions-editor.js.map +1 -0
- package/dist/es/widgets/reactions-model.js +107 -0
- package/dist/es/widgets/reactions-model.js.map +1 -0
- package/dist/es/widgets/remote-data-editor.js +314 -0
- package/dist/es/widgets/remote-data-editor.js.map +1 -0
- package/dist/es/widgets/use-form-data-fields.js +16 -0
- package/dist/es/widgets/use-form-data-fields.js.map +1 -0
- package/dist/es/widgets/validate-editor.js +118 -0
- package/dist/es/widgets/validate-editor.js.map +1 -0
- package/package.json +4 -5
- package/dist/es/react/src/components/FormController.js +0 -160
- package/dist/es/react/src/components/FormController.js.map +0 -1
- package/dist/es/react/src/components/NexusField.js +0 -98
- package/dist/es/react/src/components/NexusField.js.map +0 -1
- package/dist/es/react/src/components/NexusForm.js +0 -128
- package/dist/es/react/src/components/NexusForm.js.map +0 -1
- package/dist/es/react/src/components/NexusFormProvider.js +0 -19
- package/dist/es/react/src/components/NexusFormProvider.js.map +0 -1
- package/dist/es/react/src/components/NexusLayout.js +0 -49
- package/dist/es/react/src/components/NexusLayout.js.map +0 -1
- package/dist/es/react/src/components/NexusObject.js +0 -65
- package/dist/es/react/src/components/NexusObject.js.map +0 -1
- package/dist/es/react/src/contexts/FieldInheritContext.js +0 -6
- package/dist/es/react/src/contexts/FieldInheritContext.js.map +0 -1
- package/dist/es/react/src/contexts/GridContext.js +0 -6
- package/dist/es/react/src/contexts/GridContext.js.map +0 -1
- package/dist/es/react/src/contexts/LayoutConfigContext.js +0 -6
- package/dist/es/react/src/contexts/LayoutConfigContext.js.map +0 -1
- package/dist/es/react/src/contexts/NexusContext.js +0 -13
- package/dist/es/react/src/contexts/NexusContext.js.map +0 -1
- package/dist/es/react/src/hooks/useForm.js +0 -13
- package/dist/es/react/src/hooks/useForm.js.map +0 -1
- package/dist/es/react/src/utils/renderTreeNode.js +0 -18
- package/dist/es/react/src/utils/renderTreeNode.js.map +0 -1
- package/dist/es/react/src/utils/resolveColSpan.js +0 -10
- package/dist/es/react/src/utils/resolveColSpan.js.map +0 -1
- package/src/Canvas.tsx +0 -1111
- package/src/Designer.css +0 -206
- package/src/Designer.tsx +0 -254
- package/src/DesignerContext.tsx +0 -192
- package/src/Palette.tsx +0 -261
- package/src/PropertyPanel.tsx +0 -486
- package/src/ResizablePanel.tsx +0 -210
- package/src/catalog.ts +0 -527
- package/src/index.ts +0 -25
- package/src/property/basic-property.ts +0 -126
- package/src/schemaUtils.test.ts +0 -261
- package/src/schemaUtils.ts +0 -627
- package/src/types.ts +0 -38
- package/src/vite-env.d.ts +0 -1
- package/src/widgets/BindEditor.tsx +0 -105
- package/src/widgets/CodeEditor.tsx +0 -114
- package/src/widgets/DefaultEditor.tsx +0 -73
- package/src/widgets/DependenciesEditor.tsx +0 -56
- package/src/widgets/EnumEditor.tsx +0 -66
- package/src/widgets/ExpressionBuilder.tsx +0 -509
- package/src/widgets/ExpressionSwitch.tsx +0 -62
- package/src/widgets/OptionsEditor.tsx +0 -141
- package/src/widgets/ReactionsEditor.tsx +0 -532
- package/src/widgets/RemoteDataEditor.tsx +0 -382
- package/src/widgets/ValidateEditor.tsx +0 -162
- package/src/widgets/expressionModel.test.ts +0 -262
- package/src/widgets/expressionModel.ts +0 -573
- package/src/widgets/index.ts +0 -45
- package/src/widgets/reactionsModel.ts +0 -190
- package/src/widgets/useFormDataFields.ts +0 -21
package/src/schemaUtils.ts
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// @xbeeant/form-engine-designer — Schema 树操作工具(不可变)
|
|
3
|
-
// ============================================================================
|
|
4
|
-
// 路径 path 为 property key 的数组,例如 ['card1', 'username']
|
|
5
|
-
// 表示 schema.properties.card1.properties.username
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
isDataArray,
|
|
9
|
-
isDataField,
|
|
10
|
-
isDataObject,
|
|
11
|
-
isDeepEqual,
|
|
12
|
-
isLayoutNode,
|
|
13
|
-
type NexusSchema,
|
|
14
|
-
type SchemaNode,
|
|
15
|
-
} from '@xbeeant/form-engine';
|
|
16
|
-
|
|
17
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
// 深拷贝
|
|
19
|
-
// Schema 通常是纯 JSON,但 rules.pattern 可能为 RegExp(JSON 序列化会丢失),
|
|
20
|
-
// 因此优先使用 structuredClone(保留 RegExp/Date/Map/Set),环境不支持时回退到序列化。
|
|
21
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
const _hasStructuredClone = typeof structuredClone === 'function';
|
|
24
|
-
|
|
25
|
-
function clone<T>(value: T): T {
|
|
26
|
-
if (_hasStructuredClone) {
|
|
27
|
-
return structuredClone(value) as T;
|
|
28
|
-
}
|
|
29
|
-
return JSON.parse(JSON.stringify(value)) as T;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
// 节点访问
|
|
34
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 从一个 properties 记录按路径段遍历,返回路径终点的节点
|
|
38
|
-
*/
|
|
39
|
-
export function getNodeAtProperties(
|
|
40
|
-
props: Record<string, SchemaNode>,
|
|
41
|
-
path: string[],
|
|
42
|
-
): SchemaNode | undefined {
|
|
43
|
-
if (path.length === 0) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
let current: SchemaNode | undefined = props[path[0]];
|
|
47
|
-
for (let i = 1; i < path.length; i++) {
|
|
48
|
-
if (!current) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
const childProps = getPropertiesOf(current);
|
|
52
|
-
if (!childProps) {
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
current = childProps[path[i]];
|
|
56
|
-
}
|
|
57
|
-
return current;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 获取一个节点的子 properties:
|
|
62
|
-
* - object / layout / pane:返回 node.properties
|
|
63
|
-
* - array:items 为 object 时返回 items.properties
|
|
64
|
-
*/
|
|
65
|
-
export function getPropertiesOf(
|
|
66
|
-
node: SchemaNode,
|
|
67
|
-
): Record<string, SchemaNode> | undefined {
|
|
68
|
-
if ('properties' in node) {
|
|
69
|
-
return node.properties;
|
|
70
|
-
}
|
|
71
|
-
if ('items' in node && node.items.type === 'object') {
|
|
72
|
-
return node.items.properties;
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 是否为容器节点(拥有 properties 或 items)
|
|
79
|
-
*/
|
|
80
|
-
export function isContainerNode(node: SchemaNode): boolean {
|
|
81
|
-
return 'properties' in node || 'items' in node;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 收集 schema 中所有会进入 formData 数据路径的字段路径(供变量选择器 / 依赖选择使用)
|
|
86
|
-
* 遵循 §2 路径计算规则:
|
|
87
|
-
* - 数据节点:Key 进入路径(对象/数组透传子字段,数组项子字段以 `key[0].sub` 表示)
|
|
88
|
-
* - 布局节点:Key 被丢弃,路径透传父级
|
|
89
|
-
* - 带 widget 的 object(子表单)视为叶子字段
|
|
90
|
-
*/
|
|
91
|
-
export function collectDataFieldPaths(schema: NexusSchema): string[] {
|
|
92
|
-
return collectDataFieldOptions(schema).map((o) => o.value);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 收集 schema 中所有会进入 formData 数据路径的字段(供变量选择器 / 依赖选择使用)
|
|
97
|
-
* 返回「路径 + 字段标题」对:依赖字段(dependencies)等选择器显示字段名称
|
|
98
|
-
* (title || key),提交/存储时使用路径 key
|
|
99
|
-
*/
|
|
100
|
-
export function collectDataFieldOptions(
|
|
101
|
-
schema: NexusSchema,
|
|
102
|
-
): Array<{ value: string; label: string }> {
|
|
103
|
-
const result: Array<{ value: string; label: string }> = [];
|
|
104
|
-
const walk = (node: SchemaNode, parentPath: string, key: string): void => {
|
|
105
|
-
// 数据节点路径 = 父路径 + 自身 key;布局节点不计算(Key 被丢弃)
|
|
106
|
-
const ownPath = parentPath ? `${parentPath}.${key}` : key;
|
|
107
|
-
const title = node.title || key;
|
|
108
|
-
if (isDataArray(node)) {
|
|
109
|
-
result.push({ value: ownPath, label: title });
|
|
110
|
-
const items = node.items;
|
|
111
|
-
if (items && typeof items === 'object') {
|
|
112
|
-
if (isDataObject(items)) {
|
|
113
|
-
walkProperties(items.properties, `${ownPath}[0]`);
|
|
114
|
-
} else if (isDataField(items)) {
|
|
115
|
-
result.push({ value: `${ownPath}[0]`, label: items.title || '项' });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (isDataField(node)) {
|
|
121
|
-
result.push({ value: ownPath, label: title });
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (isDataObject(node)) {
|
|
125
|
-
walkProperties(node.properties, ownPath);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (isLayoutNode(node)) {
|
|
129
|
-
// 布局节点 Key 不进入路径:子节点沿用父路径
|
|
130
|
-
walkProperties(node.properties, parentPath);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const walkProperties = (
|
|
135
|
-
props: Record<string, SchemaNode> | undefined,
|
|
136
|
-
parentPath: string,
|
|
137
|
-
): void => {
|
|
138
|
-
if (!props) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
for (const [key, child] of Object.entries(props)) {
|
|
142
|
-
walk(child, parentPath, key);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
walkProperties(schema.properties, '');
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 获取指定父路径下的 properties 记录(用于增删改)
|
|
151
|
-
function getPropertiesAt(
|
|
152
|
-
schema: NexusSchema,
|
|
153
|
-
parentPath: string[],
|
|
154
|
-
): Record<string, SchemaNode> | undefined {
|
|
155
|
-
if (parentPath.length === 0) {
|
|
156
|
-
return schema.properties;
|
|
157
|
-
}
|
|
158
|
-
const parent = getNodeAtProperties(schema.properties, parentPath);
|
|
159
|
-
if (!parent) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
return getPropertiesOf(parent);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
166
|
-
// 增删改
|
|
167
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 在 parentPath 下新增一个子节点(深拷贝后写入)
|
|
171
|
-
* parentPath 为空时添加到根 properties
|
|
172
|
-
*/
|
|
173
|
-
export function addChildToSchema(
|
|
174
|
-
schema: NexusSchema,
|
|
175
|
-
parentPath: string[],
|
|
176
|
-
key: string,
|
|
177
|
-
node: SchemaNode,
|
|
178
|
-
): NexusSchema {
|
|
179
|
-
const next = clone(schema);
|
|
180
|
-
const props = getPropertiesAt(next, parentPath);
|
|
181
|
-
if (props) {
|
|
182
|
-
props[key] = node;
|
|
183
|
-
}
|
|
184
|
-
return next;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 移除路径终点节点
|
|
189
|
-
*/
|
|
190
|
-
export function removeNodeFromSchema(
|
|
191
|
-
schema: NexusSchema,
|
|
192
|
-
path: string[],
|
|
193
|
-
): NexusSchema {
|
|
194
|
-
if (path.length === 0) {
|
|
195
|
-
return schema;
|
|
196
|
-
}
|
|
197
|
-
const next = clone(schema);
|
|
198
|
-
const parentPath = path.slice(0, -1);
|
|
199
|
-
const key = path[path.length - 1];
|
|
200
|
-
const props = getPropertiesAt(next, parentPath);
|
|
201
|
-
if (props) {
|
|
202
|
-
delete props[key];
|
|
203
|
-
}
|
|
204
|
-
return next;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* 将 patch 浅合并到路径终点节点
|
|
209
|
-
*/
|
|
210
|
-
export function updateNodeInSchema(
|
|
211
|
-
schema: NexusSchema,
|
|
212
|
-
path: string[],
|
|
213
|
-
patch: Record<string, unknown>,
|
|
214
|
-
): NexusSchema {
|
|
215
|
-
if (path.length === 0) {
|
|
216
|
-
return schema;
|
|
217
|
-
}
|
|
218
|
-
const next = clone(schema);
|
|
219
|
-
const node = getNodeAtProperties(next.properties, path);
|
|
220
|
-
if (node) {
|
|
221
|
-
Object.assign(node, patch);
|
|
222
|
-
}
|
|
223
|
-
return next;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 将 fromPath 节点移动到 toParentPath 下
|
|
228
|
-
* insertIndex 指定在目标父级中的最终位置(基于最终顺序),省略则追加到末尾
|
|
229
|
-
*/
|
|
230
|
-
export function moveNodeInSchema(
|
|
231
|
-
schema: NexusSchema,
|
|
232
|
-
fromPath: string[],
|
|
233
|
-
toParentPath: string[],
|
|
234
|
-
insertIndex?: number,
|
|
235
|
-
): NexusSchema {
|
|
236
|
-
if (fromPath.length === 0) {
|
|
237
|
-
return schema;
|
|
238
|
-
}
|
|
239
|
-
const next = clone(schema);
|
|
240
|
-
const fromKey = fromPath[fromPath.length - 1];
|
|
241
|
-
const fromParentPath = fromPath.slice(0, -1);
|
|
242
|
-
|
|
243
|
-
const fromProps = getPropertiesAt(next, fromParentPath);
|
|
244
|
-
if (!fromProps || !(fromKey in fromProps)) {
|
|
245
|
-
return next;
|
|
246
|
-
}
|
|
247
|
-
const node = fromProps[fromKey];
|
|
248
|
-
|
|
249
|
-
const toProps = getPropertiesAt(next, toParentPath);
|
|
250
|
-
if (!toProps) {
|
|
251
|
-
return next;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const sameParent = fromProps === toProps;
|
|
255
|
-
// 目标父级删除前的快照
|
|
256
|
-
const toEntries = Object.entries(toProps);
|
|
257
|
-
|
|
258
|
-
// 从原父级移除
|
|
259
|
-
delete fromProps[fromKey];
|
|
260
|
-
|
|
261
|
-
let finalIndex = insertIndex;
|
|
262
|
-
if (finalIndex === undefined) {
|
|
263
|
-
finalIndex = toEntries.filter(([k]) => k !== fromKey).length;
|
|
264
|
-
} else if (sameParent) {
|
|
265
|
-
// 同一父级内移动:删除会让 fromKey 之后项索引前移
|
|
266
|
-
const fromIndexInTo = toEntries.findIndex(([k]) => k === fromKey);
|
|
267
|
-
if (fromIndexInTo !== -1 && fromIndexInTo < finalIndex) {
|
|
268
|
-
finalIndex -= 1;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const updatedEntries = toEntries.filter(([k]) => k !== fromKey);
|
|
273
|
-
const clamped =
|
|
274
|
-
finalIndex < 0
|
|
275
|
-
? 0
|
|
276
|
-
: finalIndex > updatedEntries.length
|
|
277
|
-
? updatedEntries.length
|
|
278
|
-
: finalIndex;
|
|
279
|
-
updatedEntries.splice(clamped, 0, [fromKey, node]);
|
|
280
|
-
|
|
281
|
-
// 重建目标 properties,保留插入顺序
|
|
282
|
-
for (const k of Object.keys(toProps)) {
|
|
283
|
-
delete toProps[k];
|
|
284
|
-
}
|
|
285
|
-
for (const [k, v] of updatedEntries) {
|
|
286
|
-
toProps[k] = v;
|
|
287
|
-
}
|
|
288
|
-
return next;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* 重命名节点的 key(schema 结构级操作:删除旧 key + 添加新 key)
|
|
293
|
-
* - newKey 已存在时返回原 schema,不覆盖
|
|
294
|
-
* - 重命名后自动更新选中路径
|
|
295
|
-
*/
|
|
296
|
-
export function renameNodeInSchema(
|
|
297
|
-
schema: NexusSchema,
|
|
298
|
-
path: string[],
|
|
299
|
-
newKey: string,
|
|
300
|
-
): { schema: NexusSchema; newPath: string[] } {
|
|
301
|
-
if (path.length === 0) {
|
|
302
|
-
return { schema, newPath: path };
|
|
303
|
-
}
|
|
304
|
-
const oldKey = path[path.length - 1];
|
|
305
|
-
if (oldKey === newKey) {
|
|
306
|
-
return { schema, newPath: path };
|
|
307
|
-
}
|
|
308
|
-
const parentPath = path.slice(0, -1);
|
|
309
|
-
const next = clone(schema);
|
|
310
|
-
const props = getPropertiesAt(next, parentPath);
|
|
311
|
-
if (!props || !(oldKey in props)) {
|
|
312
|
-
return { schema, newPath: path };
|
|
313
|
-
}
|
|
314
|
-
// 新 key 已存在则放弃,不覆盖
|
|
315
|
-
if (newKey in props) {
|
|
316
|
-
return { schema, newPath: path };
|
|
317
|
-
}
|
|
318
|
-
const entries = Object.entries(props);
|
|
319
|
-
const oldIndex = entries.findIndex(([k]) => k === oldKey);
|
|
320
|
-
|
|
321
|
-
// 重建:删除旧 key,在原位置插入新 key
|
|
322
|
-
for (const k of Object.keys(props)) {
|
|
323
|
-
delete props[k];
|
|
324
|
-
}
|
|
325
|
-
const updated: Array<[string, SchemaNode]> = [];
|
|
326
|
-
for (let i = 0; i < entries.length; i++) {
|
|
327
|
-
const [k, v] = entries[i];
|
|
328
|
-
if (i === oldIndex) {
|
|
329
|
-
updated.push([newKey, v]);
|
|
330
|
-
} else {
|
|
331
|
-
updated.push([k, v]);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
for (const [k, v] of updated) {
|
|
335
|
-
props[k] = v;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const newPath = [...parentPath, newKey];
|
|
339
|
-
return { schema: next, newPath };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
343
|
-
// 工具
|
|
344
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Schema 节点的顶层关键字段(非 UI 组件属性)。
|
|
348
|
-
* 这些属性直接写在 schema node 上,而非嵌套在 props 对象内。
|
|
349
|
-
*/
|
|
350
|
-
const SCHEMA_LEVEL_KEYS = new Set([
|
|
351
|
-
// BaseSchemaNode
|
|
352
|
-
'title',
|
|
353
|
-
'description',
|
|
354
|
-
'required',
|
|
355
|
-
'rules',
|
|
356
|
-
'reactions',
|
|
357
|
-
'bind',
|
|
358
|
-
'validate',
|
|
359
|
-
'dependencies',
|
|
360
|
-
'default',
|
|
361
|
-
'props',
|
|
362
|
-
'className',
|
|
363
|
-
'style',
|
|
364
|
-
'disabled',
|
|
365
|
-
'readOnly',
|
|
366
|
-
'readOnlyWidget',
|
|
367
|
-
'hidden',
|
|
368
|
-
'width',
|
|
369
|
-
'order',
|
|
370
|
-
'extra',
|
|
371
|
-
'colSpan',
|
|
372
|
-
'displayType',
|
|
373
|
-
'labelWidth',
|
|
374
|
-
'column',
|
|
375
|
-
// DataFieldSchema
|
|
376
|
-
'type',
|
|
377
|
-
'widget',
|
|
378
|
-
'enum',
|
|
379
|
-
'enumNames',
|
|
380
|
-
'placeholder',
|
|
381
|
-
// 远程数据配置(远程数据组件的顶层扩展属性)
|
|
382
|
-
'remoteData',
|
|
383
|
-
// 注意:'format' 不在 schema 级列表中——
|
|
384
|
-
// 属性面板编辑的 format 是 antd 控件的「显示格式」(date/time 系列),
|
|
385
|
-
// 必须写入 node.props 才能经 state.props 透传到 widget;
|
|
386
|
-
// JSON Schema 的校验语义(format: 'email' | 'url')由 Parser 直接读 node.format,
|
|
387
|
-
// 与属性面板写回路径互不干扰。
|
|
388
|
-
// 字段级校验约束
|
|
389
|
-
'pattern',
|
|
390
|
-
'min',
|
|
391
|
-
'max',
|
|
392
|
-
// DataObjectSchema
|
|
393
|
-
'properties',
|
|
394
|
-
// DataArraySchema
|
|
395
|
-
'items',
|
|
396
|
-
// LayoutBaseProps extras
|
|
397
|
-
'gap',
|
|
398
|
-
'bordered',
|
|
399
|
-
'span',
|
|
400
|
-
'direction',
|
|
401
|
-
'align',
|
|
402
|
-
'justify',
|
|
403
|
-
'wrap',
|
|
404
|
-
// Designer internals
|
|
405
|
-
'_lockedKey',
|
|
406
|
-
]);
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* 读取节点用于属性面板的初始值:
|
|
410
|
-
* - 直接保留 schema 级属性
|
|
411
|
-
* - 扁平化 props 与 schema 级属性同一平面暴露给表单,
|
|
412
|
-
* - 当节点使用 enum/enumNames(canonical 选项格式)而未显式配置 props.options 时,
|
|
413
|
-
* 合成 options 供 value/label 编辑器回显
|
|
414
|
-
*/
|
|
415
|
-
export function flattenNodeForPropertyEditor(
|
|
416
|
-
node: Record<string, unknown>,
|
|
417
|
-
): Record<string, unknown> {
|
|
418
|
-
const result: Record<string, unknown> = { ...node };
|
|
419
|
-
// 再展开 props(新统一格式)高优先级
|
|
420
|
-
if (node.props && typeof node.props === 'object') {
|
|
421
|
-
Object.assign(result, node.props as Record<string, unknown>);
|
|
422
|
-
}
|
|
423
|
-
if (
|
|
424
|
-
result.options === undefined &&
|
|
425
|
-
Array.isArray(node.enum) &&
|
|
426
|
-
node.enum.length > 0
|
|
427
|
-
) {
|
|
428
|
-
const names = Array.isArray(node.enumNames)
|
|
429
|
-
? (node.enumNames as unknown[])
|
|
430
|
-
: [];
|
|
431
|
-
result.options = (node.enum as unknown[]).map((value, index) => ({
|
|
432
|
-
value,
|
|
433
|
-
label: names[index] == null ? String(value) : String(names[index]),
|
|
434
|
-
}));
|
|
435
|
-
}
|
|
436
|
-
return result;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* 将属性面板返回的 flat patch 拆分:
|
|
441
|
-
* - schema 级属性 → 直接写在节点上
|
|
442
|
-
* - 其余(UI 组件属性)→ 合并写入 node.props 对象(统一入口)
|
|
443
|
-
*/
|
|
444
|
-
export function nestPatchForNode(
|
|
445
|
-
patch: Record<string, unknown>,
|
|
446
|
-
): Record<string, unknown> {
|
|
447
|
-
const schemaPatch: Record<string, unknown> = {};
|
|
448
|
-
let propsPatch: Record<string, unknown> | null = null;
|
|
449
|
-
|
|
450
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
451
|
-
if (SCHEMA_LEVEL_KEYS.has(key)) {
|
|
452
|
-
schemaPatch[key] = value;
|
|
453
|
-
} else {
|
|
454
|
-
if (!propsPatch) {
|
|
455
|
-
propsPatch = {};
|
|
456
|
-
}
|
|
457
|
-
propsPatch[key] = value;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (propsPatch) {
|
|
462
|
-
// UI 组件属性统一写入 props
|
|
463
|
-
schemaPatch.props = propsPatch;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return schemaPatch;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* 从 NexusSchema 顶层提取表单级配置(供属性面板 form-level 编辑器作为 initialValues)
|
|
471
|
-
*
|
|
472
|
-
* 只提取 schema 顶层可配置键(displayType/labelWidth/colon/label/readOnly/column 等),
|
|
473
|
-
* 而非整个 schema 对象——整个 schema 含 properties 与嵌套字段定义,
|
|
474
|
-
* 不属于表单级配置的取值来源,直接作为 initialValues 是错误的。
|
|
475
|
-
*
|
|
476
|
-
* @param schema 被编辑的 NexusSchema
|
|
477
|
-
* @param keys 表单级可编辑键集合(对应属性面板 formLevelProps 的 key)
|
|
478
|
-
* @returns 仅包含 schema 中存在的表单级配置
|
|
479
|
-
*/
|
|
480
|
-
export function extractFormLevelConfig(
|
|
481
|
-
schema: Record<string, unknown>,
|
|
482
|
-
keys: readonly string[],
|
|
483
|
-
): Record<string, unknown> {
|
|
484
|
-
const result: Record<string, unknown> = {};
|
|
485
|
-
for (const key of keys) {
|
|
486
|
-
if (key in schema) {
|
|
487
|
-
result[key] = schema[key];
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return result;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* 计算属性表单 allValues → 节点 patch 的差异:
|
|
495
|
-
* - 空字符串(undefined / null / '')默认视为「未赋值」,不写回 Schema——
|
|
496
|
-
* 属性表单会把未触碰的字符串字段初始化为 '',若写回会污染 Schema
|
|
497
|
-
* (如 bind:'' 被引擎当作 bind 路径导致数据 key 丢失、hidden:''/required:''
|
|
498
|
-
* 被当作空表达式联动),因此空值统一跳过
|
|
499
|
-
* - 但当字段被用户实际改动过(changedFields 包含该 key,即用户主动清空)时,
|
|
500
|
-
* 空值以 undefined 写回,由 updateNodeWithNesting 执行删除(清空属性)
|
|
501
|
-
* - 跳过与初始值深相等的字段(未触碰的字段不重复写回)
|
|
502
|
-
*
|
|
503
|
-
* @param initialValues 节点当前的扁平值(flattenNodeForPropertyEditor 输出)
|
|
504
|
-
* @param allValues 属性表单当前全部值
|
|
505
|
-
* @param changedFields 本表单实例中用户实际改动过的字段集合('#' watcher 第三参累计)
|
|
506
|
-
* @returns 需要写回节点的 patch(未改动字段不含任何空值;主动清空的字段值为 undefined)
|
|
507
|
-
*/
|
|
508
|
-
export function diffPropertyPatch(
|
|
509
|
-
initialValues: Record<string, unknown>,
|
|
510
|
-
allValues: Record<string, unknown>,
|
|
511
|
-
changedFields?: ReadonlySet<string>,
|
|
512
|
-
): Record<string, unknown> {
|
|
513
|
-
const patch: Record<string, unknown> = {};
|
|
514
|
-
for (const [key, value] of Object.entries(allValues)) {
|
|
515
|
-
if (value === undefined || value === null || value === '') {
|
|
516
|
-
if (changedFields?.has(key)) {
|
|
517
|
-
patch[key] = undefined;
|
|
518
|
-
}
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (isDeepEqual(initialValues[key], value)) {
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
patch[key] = value;
|
|
525
|
-
}
|
|
526
|
-
return patch;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* 合并 props patch 到已存在的 props 中(UI 组件属性统一入口),
|
|
531
|
-
* 而不是整体覆盖,避免修改一个属性时丢失其它属性。
|
|
532
|
-
*/
|
|
533
|
-
export function updateNodeWithNesting(
|
|
534
|
-
schema: NexusSchema,
|
|
535
|
-
path: string[],
|
|
536
|
-
flatPatch: Record<string, unknown>,
|
|
537
|
-
): NexusSchema {
|
|
538
|
-
if (path.length === 0) {
|
|
539
|
-
return schema;
|
|
540
|
-
}
|
|
541
|
-
const next = clone(schema);
|
|
542
|
-
const node = getNodeAtProperties(next.properties, path);
|
|
543
|
-
if (!node) {
|
|
544
|
-
return next;
|
|
545
|
-
}
|
|
546
|
-
const nodeRec = node as unknown as Record<string, unknown>;
|
|
547
|
-
|
|
548
|
-
// value/label 选项编辑器:同步写出 canonical 的 enum + enumNames,
|
|
549
|
-
// 使选项同时作用于控件展示、校验(enum 规则)与只读回显
|
|
550
|
-
if (Array.isArray(flatPatch.options)) {
|
|
551
|
-
const enumValues: unknown[] = [];
|
|
552
|
-
const enumLabels: string[] = [];
|
|
553
|
-
for (const item of flatPatch.options as unknown[]) {
|
|
554
|
-
if (item && typeof item === 'object') {
|
|
555
|
-
const rec = item as { value?: unknown; label?: unknown };
|
|
556
|
-
enumValues.push(rec.value);
|
|
557
|
-
enumLabels.push(
|
|
558
|
-
rec.label == null ? String(rec.value ?? '') : String(rec.label),
|
|
559
|
-
);
|
|
560
|
-
} else {
|
|
561
|
-
enumValues.push(item);
|
|
562
|
-
enumLabels.push(String(item));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
nodeRec.enum = enumValues;
|
|
566
|
-
nodeRec.enumNames = enumLabels;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// schema 级属性直接覆盖(undefined 视为清空,删除该 key)
|
|
570
|
-
for (const [key, value] of Object.entries(flatPatch)) {
|
|
571
|
-
if (SCHEMA_LEVEL_KEYS.has(key)) {
|
|
572
|
-
if (value === undefined) {
|
|
573
|
-
delete (nodeRec as Record<string, unknown>)[key];
|
|
574
|
-
} else {
|
|
575
|
-
(nodeRec as Record<string, unknown>)[key] = value;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// UI 组件属性:只合并进 props(统一入口);
|
|
581
|
-
const uiProps: Record<string, unknown> = {};
|
|
582
|
-
for (const [key, value] of Object.entries(flatPatch)) {
|
|
583
|
-
if (!SCHEMA_LEVEL_KEYS.has(key)) {
|
|
584
|
-
uiProps[key] = value;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (Object.keys(uiProps).length > 0) {
|
|
588
|
-
const oldProps =
|
|
589
|
-
nodeRec.props && typeof nodeRec.props === 'object'
|
|
590
|
-
? (nodeRec.props as Record<string, unknown>)
|
|
591
|
-
: {};
|
|
592
|
-
const mergedProps = { ...oldProps, ...uiProps };
|
|
593
|
-
// undefined 视为清空:从 props 中删除该属性,而非写入 undefined 残留
|
|
594
|
-
for (const key of Object.keys(uiProps)) {
|
|
595
|
-
if (uiProps[key] === undefined) {
|
|
596
|
-
delete mergedProps[key];
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
nodeRec.props = mergedProps;
|
|
600
|
-
}
|
|
601
|
-
return next;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* 生成唯一 property key:8 位随机字符 [a-z0-9],确保不与已有 key 冲突
|
|
606
|
-
*/
|
|
607
|
-
export function generateKey(
|
|
608
|
-
_props: Record<string, SchemaNode>,
|
|
609
|
-
_base: string,
|
|
610
|
-
): string {
|
|
611
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
612
|
-
let key = '';
|
|
613
|
-
do {
|
|
614
|
-
key = '';
|
|
615
|
-
for (let i = 0; i < 8; i++) {
|
|
616
|
-
key += chars[Math.floor(Math.random() * chars.length)];
|
|
617
|
-
}
|
|
618
|
-
} while (key in _props);
|
|
619
|
-
return key;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* 节点显示标签:title || key || type
|
|
624
|
-
*/
|
|
625
|
-
export function getNodeLabel(node: SchemaNode, key: string): string {
|
|
626
|
-
return node.title || key || node.type || '';
|
|
627
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// @xbeeant/form-engine-designer — 设计器类型定义
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
/** 设计器模式:设计 / 预览 / Schema */
|
|
6
|
-
export type DesignerMode = 'design' | 'preview' | 'schema';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 路径段:通过 property key 序列在 schema 树中定位一个节点
|
|
10
|
-
* 例如 ['card1', 'username'] 表示 schema.properties.card1.properties.username
|
|
11
|
-
*/
|
|
12
|
-
export type SchemaPath = string[];
|
|
13
|
-
|
|
14
|
-
/** 组件目录项 */
|
|
15
|
-
export interface CatalogItem {
|
|
16
|
-
label: string;
|
|
17
|
-
/** emoji 图标 */
|
|
18
|
-
icon: string;
|
|
19
|
-
category: 'widget' | 'layout';
|
|
20
|
-
/** 数据字段 widget 名(category === 'widget' 时必填) */
|
|
21
|
-
widget?: string;
|
|
22
|
-
/** 布局容器类型(category === 'layout' 时必填) */
|
|
23
|
-
layoutType?: string;
|
|
24
|
-
/** 创建一个新的 SchemaNode(每次返回新对象) */
|
|
25
|
-
createNode: () => Record<string, unknown>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 外部字段定义:由使用方传入,可在设计器左侧「字段列表」中展示并拖入画布
|
|
30
|
-
* - id:作为表单项的 key(不可修改)
|
|
31
|
-
* - name:作为表单项的 title(不可修改)
|
|
32
|
-
* - widget:使用的 widget 名
|
|
33
|
-
*/
|
|
34
|
-
export interface FieldDef {
|
|
35
|
-
id: string;
|
|
36
|
-
name: string;
|
|
37
|
-
widget: string;
|
|
38
|
-
}
|
package/src/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|