@xbeeant/form-engine-designer 0.0.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/README.md +69 -0
- package/dist/es/Canvas.js +636 -0
- package/dist/es/Canvas.js.map +1 -0
- package/dist/es/Designer.js +162 -0
- package/dist/es/Designer.js.map +1 -0
- package/dist/es/DesignerContext.js +97 -0
- package/dist/es/DesignerContext.js.map +1 -0
- package/dist/es/Palette.js +148 -0
- package/dist/es/Palette.js.map +1 -0
- package/dist/es/PropertyPanel.js +294 -0
- package/dist/es/PropertyPanel.js.map +1 -0
- package/dist/es/ResizablePanel.js +103 -0
- package/dist/es/ResizablePanel.js.map +1 -0
- package/dist/es/catalog.js +514 -0
- package/dist/es/catalog.js.map +1 -0
- package/dist/es/form-engine-designer.css +1 -0
- package/dist/es/index.js +20 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/property/basic-property.js +115 -0
- package/dist/es/property/basic-property.js.map +1 -0
- package/dist/es/react/src/components/FormController.js +154 -0
- package/dist/es/react/src/components/FormController.js.map +1 -0
- package/dist/es/react/src/components/NexusField.js +99 -0
- package/dist/es/react/src/components/NexusField.js.map +1 -0
- package/dist/es/react/src/components/NexusForm.js +121 -0
- package/dist/es/react/src/components/NexusForm.js.map +1 -0
- package/dist/es/react/src/components/NexusFormProvider.js +19 -0
- package/dist/es/react/src/components/NexusFormProvider.js.map +1 -0
- package/dist/es/react/src/components/NexusLayout.js +49 -0
- package/dist/es/react/src/components/NexusLayout.js.map +1 -0
- package/dist/es/react/src/components/NexusObject.js +65 -0
- package/dist/es/react/src/components/NexusObject.js.map +1 -0
- package/dist/es/react/src/contexts/FieldInheritContext.js +6 -0
- package/dist/es/react/src/contexts/FieldInheritContext.js.map +1 -0
- package/dist/es/react/src/contexts/GridContext.js +6 -0
- package/dist/es/react/src/contexts/GridContext.js.map +1 -0
- package/dist/es/react/src/contexts/LayoutConfigContext.js +6 -0
- package/dist/es/react/src/contexts/LayoutConfigContext.js.map +1 -0
- package/dist/es/react/src/contexts/NexusContext.js +13 -0
- package/dist/es/react/src/contexts/NexusContext.js.map +1 -0
- package/dist/es/react/src/hooks/useForm.js +10 -0
- package/dist/es/react/src/hooks/useForm.js.map +1 -0
- package/dist/es/react/src/utils/renderTreeNode.js +18 -0
- package/dist/es/react/src/utils/renderTreeNode.js.map +1 -0
- package/dist/es/react/src/utils/resolveColSpan.js +10 -0
- package/dist/es/react/src/utils/resolveColSpan.js.map +1 -0
- package/dist/es/schemaUtils.js +286 -0
- package/dist/es/schemaUtils.js.map +1 -0
- package/dist/es/schemaUtils.test.js +188 -0
- package/dist/es/schemaUtils.test.js.map +1 -0
- package/dist/es/types.js +2 -0
- package/dist/es/types.js.map +1 -0
- package/dist/es/vite-env.d.js +2 -0
- package/dist/es/vite-env.d.js.map +1 -0
- package/dist/es/widgets/BindEditor.js +67 -0
- package/dist/es/widgets/BindEditor.js.map +1 -0
- package/dist/es/widgets/CodeEditor.js +84 -0
- package/dist/es/widgets/CodeEditor.js.map +1 -0
- package/dist/es/widgets/DefaultEditor.js +55 -0
- package/dist/es/widgets/DefaultEditor.js.map +1 -0
- package/dist/es/widgets/DependenciesEditor.js +36 -0
- package/dist/es/widgets/DependenciesEditor.js.map +1 -0
- package/dist/es/widgets/EnumEditor.js +49 -0
- package/dist/es/widgets/EnumEditor.js.map +1 -0
- package/dist/es/widgets/ExpressionBuilder.js +367 -0
- package/dist/es/widgets/ExpressionBuilder.js.map +1 -0
- package/dist/es/widgets/ExpressionSwitch.js +47 -0
- package/dist/es/widgets/ExpressionSwitch.js.map +1 -0
- package/dist/es/widgets/OptionsEditor.js +102 -0
- package/dist/es/widgets/OptionsEditor.js.map +1 -0
- package/dist/es/widgets/ReactionsEditor.js +401 -0
- package/dist/es/widgets/ReactionsEditor.js.map +1 -0
- package/dist/es/widgets/RemoteDataEditor.js +314 -0
- package/dist/es/widgets/RemoteDataEditor.js.map +1 -0
- package/dist/es/widgets/ValidateEditor.js +118 -0
- package/dist/es/widgets/ValidateEditor.js.map +1 -0
- package/dist/es/widgets/expressionModel.js +340 -0
- package/dist/es/widgets/expressionModel.js.map +1 -0
- package/dist/es/widgets/expressionModel.test.js +193 -0
- package/dist/es/widgets/expressionModel.test.js.map +1 -0
- package/dist/es/widgets/index.js +57 -0
- package/dist/es/widgets/index.js.map +1 -0
- package/dist/es/widgets/reactionsModel.js +107 -0
- package/dist/es/widgets/reactionsModel.js.map +1 -0
- package/dist/es/widgets/useFormDataFields.js +16 -0
- package/dist/es/widgets/useFormDataFields.js.map +1 -0
- package/package.json +62 -0
- package/src/Canvas.tsx +1111 -0
- package/src/Designer.css +206 -0
- package/src/Designer.tsx +254 -0
- package/src/DesignerContext.tsx +192 -0
- package/src/Palette.tsx +261 -0
- package/src/PropertyPanel.tsx +486 -0
- package/src/ResizablePanel.tsx +210 -0
- package/src/catalog.ts +526 -0
- package/src/index.ts +25 -0
- package/src/property/basic-property.ts +126 -0
- package/src/schemaUtils.test.ts +261 -0
- package/src/schemaUtils.ts +627 -0
- package/src/types.ts +38 -0
- package/src/vite-env.d.ts +1 -0
- package/src/widgets/BindEditor.tsx +105 -0
- package/src/widgets/CodeEditor.tsx +114 -0
- package/src/widgets/DefaultEditor.tsx +73 -0
- package/src/widgets/DependenciesEditor.tsx +56 -0
- package/src/widgets/EnumEditor.tsx +66 -0
- package/src/widgets/ExpressionBuilder.tsx +509 -0
- package/src/widgets/ExpressionSwitch.tsx +62 -0
- package/src/widgets/OptionsEditor.tsx +141 -0
- package/src/widgets/ReactionsEditor.tsx +532 -0
- package/src/widgets/RemoteDataEditor.tsx +382 -0
- package/src/widgets/ValidateEditor.tsx +162 -0
- package/src/widgets/expressionModel.test.ts +262 -0
- package/src/widgets/expressionModel.ts +573 -0
- package/src/widgets/index.ts +45 -0
- package/src/widgets/reactionsModel.ts +190 -0
- package/src/widgets/useFormDataFields.ts +21 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const t = {
|
|
2
|
+
title: {
|
|
3
|
+
widget: "input",
|
|
4
|
+
type: "string",
|
|
5
|
+
title: "标题(title)"
|
|
6
|
+
},
|
|
7
|
+
description: {
|
|
8
|
+
widget: "textarea",
|
|
9
|
+
type: "string",
|
|
10
|
+
title: "描述(description)"
|
|
11
|
+
},
|
|
12
|
+
placeholder: {
|
|
13
|
+
widget: "input",
|
|
14
|
+
type: "string",
|
|
15
|
+
title: "占位提示(placeholder)"
|
|
16
|
+
},
|
|
17
|
+
default: {
|
|
18
|
+
widget: "input",
|
|
19
|
+
type: "string",
|
|
20
|
+
title: "默认值(default)"
|
|
21
|
+
},
|
|
22
|
+
extra: {
|
|
23
|
+
widget: "input",
|
|
24
|
+
type: "string",
|
|
25
|
+
title: "额外说明(extra)"
|
|
26
|
+
},
|
|
27
|
+
// ── 状态控制 ────────────────────────────────────────────────────────────
|
|
28
|
+
disabled: {
|
|
29
|
+
widget: "propertyExpr",
|
|
30
|
+
type: "string",
|
|
31
|
+
title: "禁用(disabled)"
|
|
32
|
+
},
|
|
33
|
+
readOnly: {
|
|
34
|
+
widget: "propertyExpr",
|
|
35
|
+
type: "string",
|
|
36
|
+
title: "只读(readOnly)"
|
|
37
|
+
},
|
|
38
|
+
readOnlyWidget: {
|
|
39
|
+
widget: "input",
|
|
40
|
+
type: "string",
|
|
41
|
+
title: "只读渲染组件(readOnlyWidget)",
|
|
42
|
+
placeholder: "如 html,未配置时沿用只读文本渲染"
|
|
43
|
+
},
|
|
44
|
+
hidden: {
|
|
45
|
+
widget: "propertyExpr",
|
|
46
|
+
type: "string",
|
|
47
|
+
title: "隐藏(hidden)"
|
|
48
|
+
},
|
|
49
|
+
// ── 布局尺寸(x-render 对齐)──────────────────────────────────────────
|
|
50
|
+
width: {
|
|
51
|
+
widget: "input",
|
|
52
|
+
type: "string",
|
|
53
|
+
title: "单元素宽度(width)",
|
|
54
|
+
placeholder: "如 20%"
|
|
55
|
+
},
|
|
56
|
+
colSpan: {
|
|
57
|
+
widget: "number",
|
|
58
|
+
type: "number",
|
|
59
|
+
title: "栅格跨度(colSpan)",
|
|
60
|
+
placeholder: "grid 列数的整数倍"
|
|
61
|
+
}
|
|
62
|
+
}, e = {
|
|
63
|
+
required: {
|
|
64
|
+
widget: "propertyExpr",
|
|
65
|
+
type: "string",
|
|
66
|
+
title: "必填(required)"
|
|
67
|
+
},
|
|
68
|
+
pattern: {
|
|
69
|
+
widget: "input",
|
|
70
|
+
type: "string",
|
|
71
|
+
title: "正则校验(pattern)",
|
|
72
|
+
placeholder: "如 ^1[3-9]\\d{9}$"
|
|
73
|
+
},
|
|
74
|
+
min: {
|
|
75
|
+
widget: "number",
|
|
76
|
+
type: "number",
|
|
77
|
+
title: "最小值/最小长度(min)",
|
|
78
|
+
placeholder: "number→数值 / string→长度 / array→项数"
|
|
79
|
+
},
|
|
80
|
+
max: {
|
|
81
|
+
widget: "number",
|
|
82
|
+
type: "number",
|
|
83
|
+
title: "最大值/最大长度(max)",
|
|
84
|
+
placeholder: "number→数值 / string→长度 / array→项数"
|
|
85
|
+
},
|
|
86
|
+
validate: {
|
|
87
|
+
widget: "propertyValidate",
|
|
88
|
+
type: "string",
|
|
89
|
+
title: "跨字段校验(validate)"
|
|
90
|
+
}
|
|
91
|
+
}, i = {
|
|
92
|
+
bind: {
|
|
93
|
+
widget: "propertyBind",
|
|
94
|
+
type: "string",
|
|
95
|
+
title: "数据绑定(bind)"
|
|
96
|
+
},
|
|
97
|
+
dependencies: {
|
|
98
|
+
widget: "propertyDependencies",
|
|
99
|
+
type: "string",
|
|
100
|
+
title: "依赖字段(dependencies)"
|
|
101
|
+
}
|
|
102
|
+
}, r = {
|
|
103
|
+
reactions: {
|
|
104
|
+
widget: "propertyReactions",
|
|
105
|
+
type: "string",
|
|
106
|
+
title: "联动规则(reactions)"
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
export {
|
|
110
|
+
t as commonPropertyFields,
|
|
111
|
+
i as dependencyPropertyFields,
|
|
112
|
+
r as reactionPropertyFields,
|
|
113
|
+
e as validationPropertyFields
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=basic-property.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"basic-property.js","sources":["../../../src/property/basic-property.ts"],"sourcesContent":["import type { SchemaNode } from '@xbeeant/form-engine';\n\n// ============================================================================\n// 属性面板各分组的字段定义(扁平结构,由 PropertyPanel 包裹进 collapse 分组)\n// ============================================================================\n\n// ── 通用属性 ──────────────────────────────────────────────────────────────\nexport const commonPropertyFields: Record<string, SchemaNode> = {\n title: {\n widget: 'input',\n type: 'string',\n title: '标题(title)',\n },\n description: {\n widget: 'textarea',\n type: 'string',\n title: '描述(description)',\n },\n placeholder: {\n widget: 'input',\n type: 'string',\n title: '占位提示(placeholder)',\n },\n default: {\n widget: 'input',\n type: 'string',\n title: '默认值(default)',\n },\n extra: {\n widget: 'input',\n type: 'string',\n title: '额外说明(extra)',\n },\n // ── 状态控制 ────────────────────────────────────────────────────────────\n disabled: {\n widget: 'propertyExpr',\n type: 'string',\n title: '禁用(disabled)',\n },\n readOnly: {\n widget: 'propertyExpr',\n type: 'string',\n title: '只读(readOnly)',\n },\n readOnlyWidget: {\n widget: 'input',\n type: 'string',\n title: '只读渲染组件(readOnlyWidget)',\n placeholder: '如 html,未配置时沿用只读文本渲染',\n },\n hidden: {\n widget: 'propertyExpr',\n type: 'string',\n title: '隐藏(hidden)',\n },\n // ── 布局尺寸(x-render 对齐)──────────────────────────────────────────\n width: {\n widget: 'input',\n type: 'string',\n title: '单元素宽度(width)',\n placeholder: '如 20%',\n },\n colSpan: {\n widget: 'number',\n type: 'number',\n title: '栅格跨度(colSpan)',\n placeholder: 'grid 列数的整数倍',\n },\n};\n\n// ── 校验配置 ──────────────────────────────────────────────────────────────\n// 字段级约束(pattern/min/max)由 SchemaParser 自动转换为 ValidationRule;\n// 完整的 rules 数组可通过「Schema」标签页直接编辑 JSON。\nexport const validationPropertyFields: Record<string, SchemaNode> = {\n required: {\n widget: 'propertyExpr',\n type: 'string',\n title: '必填(required)',\n },\n pattern: {\n widget: 'input',\n type: 'string',\n title: '正则校验(pattern)',\n placeholder: '如 ^1[3-9]\\\\d{9}$',\n },\n min: {\n widget: 'number',\n type: 'number',\n title: '最小值/最小长度(min)',\n placeholder: 'number→数值 / string→长度 / array→项数',\n },\n max: {\n widget: 'number',\n type: 'number',\n title: '最大值/最大长度(max)',\n placeholder: 'number→数值 / string→长度 / array→项数',\n },\n validate: {\n widget: 'propertyValidate',\n type: 'string',\n title: '跨字段校验(validate)',\n },\n};\n\n// ── 依赖配置 ──────────────────────────────────────────────────────────────\nexport const dependencyPropertyFields: Record<string, SchemaNode> = {\n bind: {\n widget: 'propertyBind',\n type: 'string',\n title: '数据绑定(bind)',\n },\n dependencies: {\n widget: 'propertyDependencies',\n type: 'string',\n title: '依赖字段(dependencies)',\n },\n};\n\n// ── 联动配置 ──────────────────────────────────────────────────────────────\nexport const reactionPropertyFields: Record<string, SchemaNode> = {\n reactions: {\n widget: 'propertyReactions',\n type: 'string',\n title: '联动规则(reactions)',\n },\n};\n"],"names":["commonPropertyFields","validationPropertyFields","dependencyPropertyFields","reactionPropertyFields"],"mappings":"AAOO,MAAMA,IAAmD;AAAA,EAC9D,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA;AAAA,EAGT,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,gBAAgB;AAAA,IACd,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAAA,EAEf,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA;AAAA,EAGT,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAAA,EAEf,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAEjB,GAKaC,IAAuD;AAAA,EAClE,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAAA,EAEf,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAAA,EAEf,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,EAAA;AAAA,EAEf,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAEX,GAGaC,IAAuD;AAAA,EAClE,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAAA,EAET,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAEX,GAGaC,IAAqD;AAAA,EAChE,WAAW;AAAA,IACT,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EAAA;AAEX;"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { NexusEngine as l, AsyncValidatorPlugin as o } from "@xbeeant/form-engine";
|
|
2
|
+
class c {
|
|
3
|
+
engine;
|
|
4
|
+
formElementRef;
|
|
5
|
+
/** 稳定的 getter:每次 submit 执行时从外部 ref 读取最新的 onFinish/onFinishFailed 回调 */
|
|
6
|
+
getOnFinish;
|
|
7
|
+
getOnFinishFailed;
|
|
8
|
+
removeHiddenData = !0;
|
|
9
|
+
watchers = /* @__PURE__ */ new Map();
|
|
10
|
+
globalWatcher = null;
|
|
11
|
+
constructor(e) {
|
|
12
|
+
this.engine = e ?? new l(), this.engine.hasPlugin("async-validator") || this.engine.use(new o(this.engine)), this.formElementRef = {
|
|
13
|
+
current: null
|
|
14
|
+
}, this.getOnFinish = () => () => {
|
|
15
|
+
}, this.getOnFinishFailed = () => () => {
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** 内部:首次绑定表单 DOM + 回调 getter(由 NexusForm 在挂载时调用一次) */
|
|
19
|
+
_bind(e, t, i) {
|
|
20
|
+
this.formElementRef.current = e, this.getOnFinish = t, this.getOnFinishFailed = i, this.engine.registerOnFieldValueChange(
|
|
21
|
+
(n, r) => this._onFieldValueChange(n, r)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
/** 内部:同步 watch / removeHiddenData 配置(由 NexusForm 在它们变化时调用) */
|
|
25
|
+
_syncConfig(e) {
|
|
26
|
+
if (e.removeHiddenData !== void 0 && (this.removeHiddenData = e.removeHiddenData), e.watch) {
|
|
27
|
+
this.watchers.clear(), this.globalWatcher = null;
|
|
28
|
+
for (const [t, i] of Object.entries(e.watch))
|
|
29
|
+
t === "#" ? this.globalWatcher = i : this.watchers.set(t, i);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** 内部:值变更时调用(由 Engine 通知) */
|
|
33
|
+
_onFieldValueChange(e, t) {
|
|
34
|
+
const i = this.engine.getFormData(), n = this.removeHiddenData ? i : this.engine.getAllFormData();
|
|
35
|
+
this.globalWatcher && this.globalWatcher(n, n, e);
|
|
36
|
+
const r = this.watchers.get(e);
|
|
37
|
+
r && r(t, n);
|
|
38
|
+
}
|
|
39
|
+
/** 内部:获取 Engine 实例 */
|
|
40
|
+
_getEngine() {
|
|
41
|
+
return this.engine;
|
|
42
|
+
}
|
|
43
|
+
async submit() {
|
|
44
|
+
const e = await this.engine.validate();
|
|
45
|
+
if (e.size > 0) {
|
|
46
|
+
this.focusFirstError(e), this.getOnFinishFailed()?.(e);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const t = this.removeHiddenData ? this.engine.getFormData() : this.engine.getAllFormData();
|
|
50
|
+
await this.getOnFinish()?.(t);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 定位到第一个校验失败的字段:按 DOM 渲染顺序查找(保证视觉上的"第一个"),
|
|
54
|
+
* 滚动入视并聚焦其内部可交互控件。
|
|
55
|
+
*/
|
|
56
|
+
focusFirstError(e) {
|
|
57
|
+
const t = this.formElementRef.current;
|
|
58
|
+
if (!t || e.size === 0)
|
|
59
|
+
return;
|
|
60
|
+
const i = new Set(e.keys());
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
const r = t.querySelectorAll("[data-nexus-field]");
|
|
63
|
+
for (const s of Array.from(r)) {
|
|
64
|
+
const a = s.getAttribute("data-nexus-field");
|
|
65
|
+
if (!a || !i.has(a))
|
|
66
|
+
continue;
|
|
67
|
+
s.scrollIntoView({ behavior: "smooth", block: "center" }), s.querySelector(
|
|
68
|
+
'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
69
|
+
)?.focus();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
resetFields() {
|
|
75
|
+
this.engine.reset();
|
|
76
|
+
}
|
|
77
|
+
setErrorFields(e) {
|
|
78
|
+
this.engine.setErrorFields(e);
|
|
79
|
+
}
|
|
80
|
+
setValues(e) {
|
|
81
|
+
this.engine.setFieldValues(e);
|
|
82
|
+
}
|
|
83
|
+
setValueByPath(e, t) {
|
|
84
|
+
this.engine.setFieldValue(e, t);
|
|
85
|
+
}
|
|
86
|
+
setSchemaByPath(e, t) {
|
|
87
|
+
this.engine.setSchemaByPath(e, t);
|
|
88
|
+
}
|
|
89
|
+
setSchema(e) {
|
|
90
|
+
this.engine.setSchema(e);
|
|
91
|
+
}
|
|
92
|
+
getValues(e) {
|
|
93
|
+
return this.engine.getFormData(e);
|
|
94
|
+
}
|
|
95
|
+
getHiddenValues() {
|
|
96
|
+
return this.engine.getHiddenValues();
|
|
97
|
+
}
|
|
98
|
+
/** 获取所有字段值(含 hidden) */
|
|
99
|
+
getAllValues() {
|
|
100
|
+
return this.engine.getAllFormData();
|
|
101
|
+
}
|
|
102
|
+
getValueByPath(e) {
|
|
103
|
+
return this.engine.getFieldValue(e);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 注册字段校验逻辑
|
|
107
|
+
* @param path 字段路径(如 'username')
|
|
108
|
+
* @param validator 校验函数,返回错误消息数组(空数组表示通过)
|
|
109
|
+
*/
|
|
110
|
+
registerValidator(e, t) {
|
|
111
|
+
this.engine.registerFieldValidator(e, t);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 注销字段校验逻辑(按函数引用移除)
|
|
115
|
+
* 与 registerValidator 配对;widget 组件卸载时清理,避免校验器累积
|
|
116
|
+
*/
|
|
117
|
+
unregisterValidator(e, t) {
|
|
118
|
+
this.engine.unregisterFieldValidator(e, t);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 实时重校验指定字段(同步)
|
|
122
|
+
* 供 widget 组件内部状态变化(非字段值变化)时主动刷新错误态
|
|
123
|
+
*/
|
|
124
|
+
revalidateField(e) {
|
|
125
|
+
this.engine.validateField(e, { trigger: "change" });
|
|
126
|
+
}
|
|
127
|
+
getSchema() {
|
|
128
|
+
return this.engine.getSchema();
|
|
129
|
+
}
|
|
130
|
+
removeErrorField(e) {
|
|
131
|
+
this.engine.removeErrorField(e);
|
|
132
|
+
}
|
|
133
|
+
scrollToPath(e) {
|
|
134
|
+
this.formElementRef.current?.querySelector(
|
|
135
|
+
`[data-nexus-field="${e}"]`
|
|
136
|
+
)?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
137
|
+
}
|
|
138
|
+
getFieldError(e) {
|
|
139
|
+
return this.engine.getFieldError(e);
|
|
140
|
+
}
|
|
141
|
+
getFieldsError() {
|
|
142
|
+
return this.engine.getFieldsError();
|
|
143
|
+
}
|
|
144
|
+
validateFields(e) {
|
|
145
|
+
return this.engine.validate(e);
|
|
146
|
+
}
|
|
147
|
+
getFieldState(e) {
|
|
148
|
+
return this.engine.getFieldState(e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
c as FormController
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=FormController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormController.js","sources":["../../../../../../react/src/components/FormController.ts"],"sourcesContent":["import type {\n FieldState,\n NexusFormInstance,\n NexusSchema,\n} from '@xbeeant/form-engine';\nimport { AsyncValidatorPlugin, NexusEngine } from '@xbeeant/form-engine';\nimport type { RefObject } from 'react';\n\n/**\n * FormController — 包裹 Engine,暴露 Form 实例 API\n */\nexport class FormController implements NexusFormInstance {\n private engine: NexusEngine;\n private formElementRef: RefObject<HTMLFormElement | null>;\n /** 稳定的 getter:每次 submit 执行时从外部 ref 读取最新的 onFinish/onFinishFailed 回调 */\n private getOnFinish: () => (\n data: Record<string, unknown>,\n ) => void | Promise<void>;\n private getOnFinishFailed: () => (errors: Map<string, string[]>) => void;\n private removeHiddenData: boolean = true;\n private watchers: Map<\n string,\n (value: unknown, allValues: Record<string, unknown>) => void\n > = new Map();\n private globalWatcher:\n | ((\n value: Record<string, unknown>,\n allValues: Record<string, unknown>,\n changedPath?: string,\n ) => void)\n | null = null;\n\n constructor(engine?: NexusEngine) {\n this.engine = engine ?? new NexusEngine();\n // 默认注入异步校验器插件:useFieldValidator / registerFieldValidator\n // 注册的异步校验器即可在字段值变化时被触发(防抖调度,与默认 'change' trigger 对齐)。\n // 外部已注入同名插件时不重复注入(hasPlugin 幂等)。\n if (!this.engine.hasPlugin('async-validator')) {\n this.engine.use(new AsyncValidatorPlugin(this.engine));\n }\n this.formElementRef = {\n current: null,\n } as RefObject<HTMLFormElement | null>;\n this.getOnFinish = () => () => {};\n this.getOnFinishFailed = () => () => {};\n }\n\n /** 内部:首次绑定表单 DOM + 回调 getter(由 NexusForm 在挂载时调用一次) */\n _bind(\n formEl: HTMLFormElement | null,\n getOnFinish: () => (data: Record<string, unknown>) => void | Promise<void>,\n getOnFinishFailed: () => (errors: Map<string, string[]>) => void,\n ): void {\n (this.formElementRef as { current: HTMLFormElement | null }).current =\n formEl;\n this.getOnFinish = getOnFinish;\n this.getOnFinishFailed = getOnFinishFailed;\n // 注册值变更回调到 Engine,只需一次(回调闭包引用了稳定的实例)\n this.engine.registerOnFieldValueChange((path, value) =>\n this._onFieldValueChange(path, value),\n );\n }\n\n /** 内部:同步 watch / removeHiddenData 配置(由 NexusForm 在它们变化时调用) */\n _syncConfig(config: {\n removeHiddenData?: boolean;\n watch?: {\n [path: string]: (\n value: unknown,\n allValues: Record<string, unknown>,\n changedPath?: string,\n ) => void;\n };\n }): void {\n if (config.removeHiddenData !== undefined) {\n this.removeHiddenData = config.removeHiddenData;\n }\n if (config.watch) {\n this.watchers.clear();\n this.globalWatcher = null;\n for (const [path, fn] of Object.entries(config.watch)) {\n if (path === '#') {\n this.globalWatcher = fn;\n } else {\n this.watchers.set(path, fn);\n }\n }\n }\n }\n\n /** 内部:值变更时调用(由 Engine 通知) */\n _onFieldValueChange(path: string, value: unknown): void {\n const allValues = this.engine.getFormData();\n const globalData = this.removeHiddenData\n ? allValues\n : this.engine.getAllFormData();\n\n // 全局 watcher(# 监听所有字段变化,value 即为全部表单值;\n // 第三参携带本次实际变更的字段路径,供清空值场景区分「未赋值默认值」与「用户主动清空」)\n if (this.globalWatcher) {\n this.globalWatcher(globalData, globalData, path);\n }\n // 路径匹配的 watcher\n const fn = this.watchers.get(path);\n if (fn) {\n fn(value, globalData);\n }\n }\n\n /** 内部:获取 Engine 实例 */\n _getEngine(): NexusEngine {\n return this.engine;\n }\n\n async submit(): Promise<void> {\n const errors = await this.engine.validate();\n if (errors.size > 0) {\n this.focusFirstError(errors);\n this.getOnFinishFailed()?.(errors);\n return;\n }\n const formData = this.removeHiddenData\n ? this.engine.getFormData()\n : this.engine.getAllFormData();\n await this.getOnFinish()?.(formData);\n }\n\n /**\n * 定位到第一个校验失败的字段:按 DOM 渲染顺序查找(保证视觉上的\"第一个\"),\n * 滚动入视并聚焦其内部可交互控件。\n */\n private focusFirstError(errors: Map<string, string[]>): void {\n const formEl = this.formElementRef.current;\n if (!formEl || errors.size === 0) {\n return;\n }\n\n const errorPaths = new Set(errors.keys());\n\n const focus = () => {\n const fieldEls =\n formEl.querySelectorAll<HTMLElement>('[data-nexus-field]');\n for (const el of Array.from(fieldEls)) {\n const path = el.getAttribute('data-nexus-field');\n if (!path || !errorPaths.has(path)) {\n continue;\n }\n\n el.scrollIntoView({ behavior: 'smooth', block: 'center' });\n const focusable = el.querySelector<HTMLElement>(\n 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n );\n focusable?.focus();\n return;\n }\n };\n\n // 等待 React 将错误态刷到 DOM 后再定位,保证错误提示已可见\n requestAnimationFrame(focus);\n }\n\n resetFields(): void {\n this.engine.reset();\n }\n\n setErrorFields(errors: Array<{ path: string; errors: string[] }>): void {\n this.engine.setErrorFields(errors);\n }\n\n setValues(values: Record<string, unknown>): void {\n this.engine.setFieldValues(values);\n }\n\n setValueByPath(path: string, value: unknown): void {\n this.engine.setFieldValue(path, value);\n }\n\n setSchemaByPath(path: string, patch: Record<string, unknown>): void {\n this.engine.setSchemaByPath(path, patch);\n }\n\n setSchema(schema: NexusSchema): void {\n this.engine.setSchema(schema);\n }\n\n getValues(paths?: string[]): Record<string, unknown> {\n return this.engine.getFormData(paths);\n }\n\n getHiddenValues(): Record<string, unknown> {\n return this.engine.getHiddenValues();\n }\n\n /** 获取所有字段值(含 hidden) */\n getAllValues(): Record<string, unknown> {\n return this.engine.getAllFormData();\n }\n\n getValueByPath(path: string): unknown {\n return this.engine.getFieldValue(path);\n }\n\n /**\n * 注册字段校验逻辑\n * @param path 字段路径(如 'username')\n * @param validator 校验函数,返回错误消息数组(空数组表示通过)\n */\n registerValidator(\n path: string,\n validator: (\n value: unknown,\n formData: Record<string, unknown>,\n ) => string[] | Promise<string[]>,\n ): void {\n // 注册到 Engine(validate 与 实时校验 统一由 Engine 执行)\n this.engine.registerFieldValidator(path, validator);\n }\n\n /**\n * 注销字段校验逻辑(按函数引用移除)\n * 与 registerValidator 配对;widget 组件卸载时清理,避免校验器累积\n */\n unregisterValidator(\n path: string,\n validator: (\n value: unknown,\n formData: Record<string, unknown>,\n ) => string[] | Promise<string[]>,\n ): void {\n this.engine.unregisterFieldValidator(path, validator);\n }\n\n /**\n * 实时重校验指定字段(同步)\n * 供 widget 组件内部状态变化(非字段值变化)时主动刷新错误态\n */\n revalidateField(path: string): void {\n this.engine.validateField(path, { trigger: 'change' });\n }\n\n getSchema(): NexusSchema | null {\n return this.engine.getSchema();\n }\n\n removeErrorField(path: string): void {\n this.engine.removeErrorField(path);\n }\n\n scrollToPath(path: string): void {\n const el = this.formElementRef.current?.querySelector(\n `[data-nexus-field=\"${path}\"]`,\n );\n el?.scrollIntoView({ behavior: 'smooth', block: 'center' });\n }\n\n getFieldError(path: string): string[] {\n return this.engine.getFieldError(path);\n }\n\n getFieldsError(): Map<string, string[]> {\n return this.engine.getFieldsError();\n }\n\n validateFields(paths?: string[]): Promise<Map<string, string[]>> {\n return this.engine.validate(paths);\n }\n\n getFieldState(path: string): FieldState | undefined {\n return this.engine.getFieldState(path);\n }\n}\n"],"names":["FormController","engine","NexusEngine","AsyncValidatorPlugin","formEl","getOnFinish","getOnFinishFailed","path","value","config","fn","allValues","globalData","errors","formData","errorPaths","fieldEls","el","values","patch","schema","paths","validator"],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as d, jsxs as j } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore as k, useContext as a, useCallback as g, useMemo as x } from "react";
|
|
3
|
+
import { FieldInheritContext as q } from "../contexts/FieldInheritContext.js";
|
|
4
|
+
import { GridContext as B } from "../contexts/GridContext.js";
|
|
5
|
+
import { LayoutConfigContext as L } from "../contexts/LayoutConfigContext.js";
|
|
6
|
+
import { useNexusContext as $ } from "../contexts/NexusContext.js";
|
|
7
|
+
import { resolveColSpan as D } from "../utils/resolveColSpan.js";
|
|
8
|
+
function A({ dataPath: t, layoutKey: y }) {
|
|
9
|
+
const { engine: i, config: o, form: b } = $();
|
|
10
|
+
k(
|
|
11
|
+
(n) => i.subscribeField(t, n),
|
|
12
|
+
() => i.getFieldVersion(t),
|
|
13
|
+
() => i.getFieldVersion(t)
|
|
14
|
+
);
|
|
15
|
+
const e = i.getFieldState(t), C = a(B), v = a(L), s = a(q), F = g(
|
|
16
|
+
(n) => {
|
|
17
|
+
i.setFieldValue(t, n);
|
|
18
|
+
},
|
|
19
|
+
[i, t]
|
|
20
|
+
), W = g(
|
|
21
|
+
(n) => {
|
|
22
|
+
n.currentTarget.contains(n.relatedTarget) || i.validateField(t, { trigger: "blur" });
|
|
23
|
+
},
|
|
24
|
+
[i, t]
|
|
25
|
+
), h = x(
|
|
26
|
+
() => e?.meta.enum ? e.meta.enum.map((n, l) => ({
|
|
27
|
+
value: n,
|
|
28
|
+
label: e.meta.enumNames?.[l] ?? String(n)
|
|
29
|
+
})) : e?.props.options,
|
|
30
|
+
[e?.meta.enum, e?.meta.enumNames, e?.props.options]
|
|
31
|
+
), w = x(() => {
|
|
32
|
+
const n = {};
|
|
33
|
+
if (e?.reactions) {
|
|
34
|
+
for (const l of e.reactions)
|
|
35
|
+
if (l.dependencies)
|
|
36
|
+
for (const f of l.dependencies)
|
|
37
|
+
n[f] = i.getFieldValue(f);
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
}, [e?.reactions, i]);
|
|
41
|
+
if (!e)
|
|
42
|
+
return i.getSnapshot() > 0 && console.warn(`[NexusField] Field not found: ${t}`), null;
|
|
43
|
+
if (s.visible === !1 || !e.visible)
|
|
44
|
+
return v.removeHidden === !0 ? null : /* @__PURE__ */ d("div", { className: "hidden", "data-nexus-hidden": t });
|
|
45
|
+
const m = o.readOnly || s.readOnly === !0 || e.readOnly, S = s.disabled === !0 || e.disabled, c = m && !!e.meta.readOnlyWidget, N = c ? e.meta.readOnlyWidget : e.meta.widget;
|
|
46
|
+
let r = i.getWidget(N);
|
|
47
|
+
if (!r && c && (r = i.getWidget(e.meta.widget)), !r)
|
|
48
|
+
return /* @__PURE__ */ j("div", { className: "text-xs text-red-500", "data-nexus-field": t, children: [
|
|
49
|
+
'⚠️ Widget "',
|
|
50
|
+
e.meta.widget,
|
|
51
|
+
'" 未注册 (path: ',
|
|
52
|
+
t,
|
|
53
|
+
")"
|
|
54
|
+
] });
|
|
55
|
+
const O = e.meta.displayType ?? o.displayType, T = e.meta.labelWidth ?? o.labelWidth, V = e.meta.column ?? o.column, u = D(e.meta.colSpan, C), p = {
|
|
56
|
+
...e.meta.width ? { width: e.meta.width, flexShrink: 0 } : {},
|
|
57
|
+
...u ? { gridColumn: `span ${u}` } : {}
|
|
58
|
+
};
|
|
59
|
+
return /* @__PURE__ */ d(
|
|
60
|
+
"div",
|
|
61
|
+
{
|
|
62
|
+
"data-nexus-field": t,
|
|
63
|
+
onBlur: W,
|
|
64
|
+
style: Object.keys(p).length > 0 ? p : void 0,
|
|
65
|
+
children: /* @__PURE__ */ d(
|
|
66
|
+
r,
|
|
67
|
+
{
|
|
68
|
+
dataPath: t,
|
|
69
|
+
path: t,
|
|
70
|
+
value: e.value,
|
|
71
|
+
onChange: F,
|
|
72
|
+
disabled: S,
|
|
73
|
+
readOnly: m,
|
|
74
|
+
loading: e.loading,
|
|
75
|
+
required: e.required,
|
|
76
|
+
title: e.meta.title,
|
|
77
|
+
description: e.meta.description,
|
|
78
|
+
placeholder: e.meta.placeholder,
|
|
79
|
+
label: e.meta.label,
|
|
80
|
+
options: h,
|
|
81
|
+
errors: e.errors,
|
|
82
|
+
extra: e.meta.extra,
|
|
83
|
+
displayType: O,
|
|
84
|
+
labelWidth: T,
|
|
85
|
+
column: V,
|
|
86
|
+
form: b,
|
|
87
|
+
dependValues: w,
|
|
88
|
+
items: e.meta.items,
|
|
89
|
+
...e.props
|
|
90
|
+
},
|
|
91
|
+
y
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
A as NexusField
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=NexusField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NexusField.js","sources":["../../../../../../react/src/components/NexusField.tsx"],"sourcesContent":["import type { CSSProperties, FocusEvent } from 'react';\nimport { useCallback, useContext, useMemo, useSyncExternalStore } from 'react';\nimport { FieldInheritContext } from '../contexts/FieldInheritContext';\nimport { GridContext } from '../contexts/GridContext';\nimport { LayoutConfigContext } from '../contexts/LayoutConfigContext';\nimport { useNexusContext } from '../contexts/NexusContext';\nimport { resolveColSpan } from '../utils/resolveColSpan';\n\ninterface NexusFieldProps {\n dataPath: string;\n layoutKey: string;\n}\n\n/**\n * NexusField — 单个字段渲染器\n */\nexport function NexusField({ dataPath, layoutKey }: NexusFieldProps) {\n const { engine, config, form } = useNexusContext();\n // 按路径精准订阅:仅该字段版本变化时重渲染(reaction 影响其他字段不会触发本组件)\n // 第三个参数 getServerSnapshot 与 getSnapshot 一致(引擎状态同步,SSR 必需)\n useSyncExternalStore(\n (onStoreChange) => engine.subscribeField(dataPath, onStoreChange),\n () => engine.getFieldVersion(dataPath),\n () => engine.getFieldVersion(dataPath),\n );\n const state = engine.getFieldState(dataPath);\n // GridContext 必须在所有 early return 之前调用,否则会破坏 Hooks 调用顺序\n const gridCtx = useContext(GridContext);\n const layoutConfig = useContext(LayoutConfigContext);\n // 祖先对象容器(NexusObject)下发的继承属性:visible=false 时子树整体隐藏\n const inherit = useContext(FieldInheritContext);\n\n const handleChange = useCallback(\n (value: unknown) => {\n engine.setFieldValue(dataPath, value);\n },\n [engine, dataPath],\n );\n\n // 失焦触发 blur 规则校验(trigger: 'blur'):\n // React onBlur 冒泡(focusout 语义),包裹层统一处理内部控件失焦;\n // 焦点仍在字段内部(如 dateRange 双输入框间切换)时跳过。\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLDivElement>) => {\n if (e.currentTarget.contains(e.relatedTarget as Node)) {\n return;\n }\n engine.validateField(dataPath, { trigger: 'blur' });\n },\n [engine, dataPath],\n );\n\n // 从 enum + enumNames 构建选项(x-render 对齐)。\n // meta/props 引用在状态更新时保持稳定,useMemo 避免每次渲染重建数组(破坏子组件 memo)。\n // 必须在所有 early return 之前调用(Hooks 顺序规则),state 未定义时安全降级\n const options = useMemo(\n () =>\n state?.meta.enum\n ? state.meta.enum.map((value: any, index: number) => ({\n value,\n label: state.meta.enumNames?.[index] ?? String(value),\n }))\n : (state?.props.options as\n | Array<{ label: string; value: unknown } | string | number>\n | undefined),\n [state?.meta.enum, state?.meta.enumNames, state?.props.options],\n );\n\n // 从 reactions 依赖构建 dependValues,供 widget 获取关联字段值。\n // reactions 引用稳定,值在 memo 执行时读取;避免每次渲染新建对象\n const dependValues = useMemo(() => {\n const values: Record<string, unknown> = {};\n if (state?.reactions) {\n for (const reaction of state.reactions) {\n if (reaction.dependencies) {\n for (const dep of reaction.dependencies) {\n values[dep] = engine.getFieldValue(dep);\n }\n }\n }\n }\n return values;\n }, [state?.reactions, engine]);\n\n if (!state) {\n // 仅当引擎已初始化(version > 0)但字段仍未找到时才发出警告\n // 初始化过程中的短暂空状态不应报警\n if (engine.getSnapshot() > 0) {\n console.warn(`[NexusField] Field not found: ${dataPath}`);\n }\n return null;\n }\n\n // 祖先对象容器隐藏 → 子树整体不可见(与字段自身 visible 合并判断)\n if (inherit.visible === false || !state.visible) {\n // 如果父布局节点配置了 removeHidden,则不渲染占位符(移除以防止栅格塌陷)\n if (layoutConfig.removeHidden === true) {\n return null;\n }\n // 默认行为:渲染 display:none 占位符以保持布局\n return <div className='hidden' data-nexus-hidden={dataPath} />;\n }\n\n // 从 enum + enumNames 构建选项(x-render 对齐)\n const readOnly =\n config.readOnly || inherit.readOnly === true || state.readOnly;\n // 对象容器继承 disabled(父级激活时优先),与字段级 disabled 合并\n const disabled = inherit.disabled === true || state.disabled;\n\n // readOnlyWidget:指定 readOnly 生效时切换使用的渲染 widget(x-render readOnlyWidget 对齐)。\n // - 配置了 readOnlyWidget 且字段为只读时,切换渲染该 widget(readOnly 一并透传,\n // widget 按自身逻辑决定只读展示形态,如 treeSelect 的 readOnly 回显);\n // - 未配置时 readOnly 原样透传给 widget,由 widget 自身决定只读形态\n // (antd 原生 readOnly,或内置 widget 的 ReadOnlyDisplay 文本回退)。\n const wantReadOnlyWidget = readOnly && !!state.meta.readOnlyWidget;\n const widgetName = wantReadOnlyWidget\n ? state.meta.readOnlyWidget!\n : state.meta.widget;\n /** 获取UI组件库 进行渲染 **/\n let Widget = engine.getWidget(widgetName);\n // readOnlyWidget 未注册时优雅降级:退回原 widget,沿用现有只读渲染方式\n if (!Widget && wantReadOnlyWidget) {\n Widget = engine.getWidget(state.meta.widget);\n }\n\n if (!Widget) {\n return (\n <div className='text-xs text-red-500' data-nexus-field={dataPath}>\n ⚠️ Widget \"{state.meta.widget}\" 未注册 (path: {dataPath})\n </div>\n );\n }\n\n // 字段级配置优先于表单级配置\n const fieldDisplayType = state.meta.displayType ?? config.displayType;\n const fieldLabelWidth = state.meta.labelWidth ?? config.labelWidth;\n const fieldColumn = state.meta.column ?? config.column;\n\n // 布局属性作用于 NexusField 包装层而非 DOM 控件\n // - column(fieldColumn):字段内部子元素分列数(如 checkboxes/radio),传给 Widget\n // - colSpan:在父 Grid 中横跨多少列(tailwind 风格:gridColumn: span N)\n // - width:在父 Flex 布局中自身宽度(百分比或固定值),flexShrink:0 防压缩\n const effectiveColSpan = resolveColSpan(state.meta.colSpan, gridCtx);\n const wrapperStyle: CSSProperties = {\n ...(state.meta.width ? { width: state.meta.width, flexShrink: 0 } : {}),\n ...(effectiveColSpan ? { gridColumn: `span ${effectiveColSpan}` } : {}),\n };\n\n return (\n <div\n data-nexus-field={dataPath}\n onBlur={handleBlur}\n style={Object.keys(wrapperStyle).length > 0 ? wrapperStyle : undefined}\n >\n <Widget\n key={layoutKey}\n dataPath={dataPath}\n path={dataPath}\n value={state.value}\n onChange={handleChange}\n disabled={disabled}\n readOnly={readOnly}\n loading={state.loading}\n required={state.required}\n title={state.meta.title}\n description={state.meta.description}\n placeholder={state.meta.placeholder}\n label={state.meta.label}\n options={options}\n errors={state.errors}\n extra={state.meta.extra}\n displayType={fieldDisplayType}\n labelWidth={fieldLabelWidth}\n column={fieldColumn}\n form={form}\n dependValues={dependValues}\n items={state.meta.items}\n {...state.props}\n />\n </div>\n );\n}\n"],"names":["NexusField","dataPath","layoutKey","engine","config","form","useNexusContext","useSyncExternalStore","onStoreChange","state","gridCtx","useContext","GridContext","layoutConfig","LayoutConfigContext","inherit","FieldInheritContext","handleChange","useCallback","value","handleBlur","e","options","useMemo","index","dependValues","values","reaction","dep","jsx","readOnly","disabled","wantReadOnlyWidget","widgetName","Widget","jsxs","fieldDisplayType","fieldLabelWidth","fieldColumn","effectiveColSpan","resolveColSpan","wrapperStyle"],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as b, jsxs as L } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as l, useEffect as o, useSyncExternalStore as Q, useMemo as y, useCallback as W } from "react";
|
|
3
|
+
import { renderTreeNode as U } from "../utils/renderTreeNode.js";
|
|
4
|
+
import { NexusFormProvider as X } from "./NexusFormProvider.js";
|
|
5
|
+
function s() {
|
|
6
|
+
}
|
|
7
|
+
function ne({
|
|
8
|
+
form: n,
|
|
9
|
+
schema: t,
|
|
10
|
+
initialValues: _,
|
|
11
|
+
widgets: u,
|
|
12
|
+
layouts: c,
|
|
13
|
+
onFinish: j,
|
|
14
|
+
onFinishFailed: k,
|
|
15
|
+
footer: a = !1,
|
|
16
|
+
className: D,
|
|
17
|
+
style: O,
|
|
18
|
+
children: V,
|
|
19
|
+
labelCol: f,
|
|
20
|
+
labelWidth: $,
|
|
21
|
+
colon: I,
|
|
22
|
+
label: M,
|
|
23
|
+
displayType: P,
|
|
24
|
+
readOnly: g,
|
|
25
|
+
column: q,
|
|
26
|
+
watch: m,
|
|
27
|
+
removeHiddenData: R = !0
|
|
28
|
+
}) {
|
|
29
|
+
const e = n._getEngine(), x = l(null), F = P ?? t?.displayType ?? "row", C = M ?? t?.label ?? !0, S = I ?? t?.colon, r = $ ?? t?.labelWidth, T = g ?? t?.readOnly ?? !1, i = q ?? t?.column;
|
|
30
|
+
o(() => {
|
|
31
|
+
u && e.registerWidgets(u), c && e.registerLayouts(c);
|
|
32
|
+
}, [e, u, c]);
|
|
33
|
+
const h = l(!0), w = l(_);
|
|
34
|
+
o(() => {
|
|
35
|
+
t && (h.current ? (e.init(t, w.current), h.current = !1) : e.init(t, e.getFormData()));
|
|
36
|
+
}, [e, t]);
|
|
37
|
+
const N = l(s), v = l(s);
|
|
38
|
+
N.current = j ?? s, v.current = k ?? s, o(() => {
|
|
39
|
+
n._bind(
|
|
40
|
+
x.current,
|
|
41
|
+
() => N.current,
|
|
42
|
+
() => v.current
|
|
43
|
+
);
|
|
44
|
+
}, [n]), o(() => {
|
|
45
|
+
n._syncConfig({ removeHiddenData: R, watch: m });
|
|
46
|
+
}, [n, R, m]);
|
|
47
|
+
const z = Q(
|
|
48
|
+
e.subscribeStore,
|
|
49
|
+
e.getSnapshot,
|
|
50
|
+
e.getSnapshot
|
|
51
|
+
), A = y(() => e.getRenderTree(), [e, z]), B = W(
|
|
52
|
+
async (p) => {
|
|
53
|
+
p.preventDefault(), await n.submit();
|
|
54
|
+
},
|
|
55
|
+
[n]
|
|
56
|
+
), G = W(() => {
|
|
57
|
+
n.resetFields();
|
|
58
|
+
}, [n]);
|
|
59
|
+
let d = null;
|
|
60
|
+
a === !0 ? d = /* @__PURE__ */ L("div", { className: "mt-4", children: [
|
|
61
|
+
/* @__PURE__ */ b("button", { type: "submit", children: "提交" }),
|
|
62
|
+
" ",
|
|
63
|
+
/* @__PURE__ */ b("button", { type: "button", onClick: G, children: "重置" })
|
|
64
|
+
] }) : a && (d = a);
|
|
65
|
+
const E = y(() => {
|
|
66
|
+
if (f)
|
|
67
|
+
return f;
|
|
68
|
+
if (r)
|
|
69
|
+
return {
|
|
70
|
+
style: {
|
|
71
|
+
width: typeof r == "number" ? `${r}px` : r
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}, [f, r]), J = y(
|
|
75
|
+
() => ({
|
|
76
|
+
labelCol: E,
|
|
77
|
+
labelWidth: r,
|
|
78
|
+
colon: S,
|
|
79
|
+
label: C,
|
|
80
|
+
displayType: F,
|
|
81
|
+
readOnly: T,
|
|
82
|
+
column: i
|
|
83
|
+
}),
|
|
84
|
+
[
|
|
85
|
+
E,
|
|
86
|
+
r,
|
|
87
|
+
S,
|
|
88
|
+
C,
|
|
89
|
+
F,
|
|
90
|
+
T,
|
|
91
|
+
i
|
|
92
|
+
]
|
|
93
|
+
);
|
|
94
|
+
return /* @__PURE__ */ b(X, { engine: e, config: J, form: n, children: /* @__PURE__ */ L(
|
|
95
|
+
"form",
|
|
96
|
+
{
|
|
97
|
+
ref: x,
|
|
98
|
+
onSubmit: B,
|
|
99
|
+
className: D,
|
|
100
|
+
style: {
|
|
101
|
+
...O,
|
|
102
|
+
// 当配置了 column 时,使用 CSS Grid 布局
|
|
103
|
+
...i && i > 1 ? {
|
|
104
|
+
display: "grid",
|
|
105
|
+
gridTemplateColumns: `repeat(${i}, 1fr)`,
|
|
106
|
+
gap: "0 16px"
|
|
107
|
+
} : {}
|
|
108
|
+
},
|
|
109
|
+
noValidate: !0,
|
|
110
|
+
children: [
|
|
111
|
+
A.map((p, K) => U(p, K)),
|
|
112
|
+
!g && d,
|
|
113
|
+
V
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
) });
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
ne as NexusForm
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=NexusForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NexusForm.js","sources":["../../../../../../react/src/components/NexusForm.tsx"],"sourcesContent":["import type { NexusSchema } from '@xbeeant/form-engine';\nimport type { CSSProperties, ReactNode, SubmitEvent } from 'react';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\n\nimport { renderTreeNode } from '../utils/renderTreeNode';\nimport type { FormController } from './FormController';\nimport { NexusFormProvider } from './NexusFormProvider';\n\n// ────────────────────────────────────────────────────────────────────────────\n// Form 布局配置\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface NexusFormConfig {\n /** label 列配置(由 ui 层 Form.Item 消费) */\n labelCol?: Record<string, unknown>;\n /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */\n labelWidth?: number | string;\n /** 是否显示冒号 */\n colon?: boolean | ReactNode;\n /** 是否显示 label(默认 true) */\n label?: boolean;\n /** 表单布局方向 */\n displayType?: 'row' | 'column' | 'inline';\n /** 整个表单只读,所有字段以文本展示 */\n readOnly?: boolean;\n /** 表单每行显示多少列 */\n column?: number;\n}\n\nexport interface NexusFormProps {\n /** Form 实例,由 useForm() 创建 */\n form: FormController;\n /** Schema 定义 */\n schema?: NexusSchema;\n /** 初始值 */\n initialValues?: Record<string, unknown>;\n /** 额外注册的 widget(与已注册的合并) */\n widgets?: Record<string, (props: any) => ReactNode>;\n /** 额外注册的 layout */\n layouts?: Record<string, (props: any) => ReactNode>;\n /** 提交成功回调 */\n onFinish?: (formData: Record<string, unknown>) => void | Promise<void>;\n /** 校验失败回调 */\n onFinishFailed?: (errors: Map<string, string[]>) => void;\n /** 是否显示默认 footer(提交/重置按钮),或自定义 footer */\n footer?: boolean | ReactNode;\n /** 自定义类名 */\n className?: string;\n /** 自定义样式 */\n style?: CSSProperties;\n /** 子节点 */\n children?: ReactNode;\n /**\n * 表单值变化监听\n * - key 为字段路径(如 'username'),值为回调函数\n * - 使用 '#' 作为 key 可监听所有字段变化\n * - 回调参数为 (value, allValues);全局('#')监听额外提供第三参 changedPath,\n * 即本次实际变更的字段路径(用于区分「未触碰的默认空值」与「用户主动清空」)\n */\n watch?: {\n [path: string]: (\n value: unknown,\n allValues: Record<string, unknown>,\n changedPath?: string,\n ) => void;\n };\n /**\n * 提交时是否移除 hidden 字段数据,默认 true\n * - true: submit/getValues 不包含 hidden 字段\n * - false: submit/getValues 包含所有字段(含 hidden)\n */\n removeHiddenData?: boolean;\n\n // ── 表单布局配置 ──────────────────────────────────────────────────────\n /** label 列配置(由 ui 层 Form.Item 消费) */\n labelCol?: Record<string, unknown>;\n /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */\n labelWidth?: number | string;\n /** 是否显示冒号 */\n colon?: boolean | ReactNode;\n /** 是否显示 label(默认 true) */\n label?: boolean;\n /** 表单布局方向:'row' = horizontal, 'column' = vertical, 'inline' = inline */\n displayType?: 'row' | 'column' | 'inline';\n /** 整个表单只读,所有字段以文本展示 */\n readOnly?: boolean;\n /** 表单每行显示多少列 */\n column?: number;\n}\n\nfunction noop() {}\n\n/**\n * NexusForm — 顶层表单组件\n */\nexport function NexusForm({\n form,\n schema,\n initialValues,\n widgets,\n layouts,\n onFinish,\n onFinishFailed,\n footer = false,\n className,\n style,\n children,\n labelCol,\n labelWidth,\n colon,\n label,\n displayType,\n readOnly,\n column,\n watch,\n removeHiddenData = true,\n}: NexusFormProps) {\n const engine = form._getEngine();\n const formElRef = useRef<HTMLFormElement | null>(null);\n\n // Schema 顶层配置作为默认值,props 优先级更高\n const finalDisplayType = displayType ?? schema?.displayType ?? 'row';\n const finalLabel = label ?? schema?.label ?? true;\n const finalColon = colon ?? schema?.colon;\n const finalLabelWidth = labelWidth ?? schema?.labelWidth;\n const finalReadOnly = readOnly ?? schema?.readOnly ?? false;\n const finalColumn = column ?? schema?.column;\n\n // 注册额外 widgets / layouts(仅首次或引用变化时)\n useEffect(() => {\n if (widgets) {\n engine.registerWidgets(widgets);\n }\n if (layouts) {\n engine.registerLayouts(layouts);\n }\n }, [engine, widgets, layouts]);\n\n // schema 变化时重新初始化\n // 注意:initialValues 仅在首次挂载时使用,避免每次渲染都 re-init 导致\n // 已有的 errors / 用户输入被重置。后续需要更新值请使用 form.setValues()。\n const isFirstInitRef = useRef(true);\n const initialValuesRef = useRef(initialValues);\n useEffect(() => {\n if (!schema) {\n return;\n }\n if (isFirstInitRef.current) {\n engine.init(schema, initialValuesRef.current);\n isFirstInitRef.current = false;\n } else {\n // schema 变化:保留当前已填数据,而非重置为 initialValues\n engine.init(schema, engine.getFormData());\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [engine, schema]);\n\n // 绑定 form controller\n // 使用 ref 持有 onFinish / onFinishFailed,避免每次 re-render 都造成绑定逻辑重复执行\n const onFinishRef =\n useRef<(data: Record<string, unknown>) => void | Promise<void>>(noop);\n const onFinishFailedRef =\n useRef<(errors: Map<string, string[]>) => void>(noop);\n onFinishRef.current = onFinish ?? noop;\n onFinishFailedRef.current = onFinishFailed ?? noop;\n\n // 只在挂载时绑定一次:传入「稳定的 getter」,让 FormController 在 submit 时读取最新回调\n useEffect(() => {\n form._bind(\n formElRef.current,\n () => onFinishRef.current,\n () => onFinishFailedRef.current,\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [form]);\n\n // watch / removeHiddenData 变化时单独同步(不重置其他绑定)\n useEffect(() => {\n form._syncConfig({ removeHiddenData, watch });\n }, [form, removeHiddenData, watch]);\n\n const _version = useSyncExternalStore(\n engine.subscribeStore,\n engine.getSnapshot,\n engine.getSnapshot,\n );\n // 依赖 _version:engine.init() / setSchema() 会 bump version,\n // 需要在此后重新读取 renderTree(首次渲染时 engine 尚未 init,renderTree 为空)\n // biome-ignore lint/correctness/useExhaustiveDependencies: _version 是 renderTree 失效信号(engine 内部状态,静态分析不可见)\n const renderTree = useMemo(() => engine.getRenderTree(), [engine, _version]);\n\n const handleSubmit = useCallback(\n async (e: SubmitEvent) => {\n e.preventDefault();\n await form.submit();\n },\n [form],\n );\n\n const handleReset = useCallback(() => {\n form.resetFields();\n }, [form]);\n\n // footer 渲染\n let footerNode: ReactNode = null;\n if (footer === true) {\n footerNode = (\n <div className='mt-4'>\n <button type='submit'>提交</button>{' '}\n <button type='button' onClick={handleReset}>\n 重置\n </button>\n </div>\n );\n } else if (footer) {\n footerNode = footer;\n }\n\n // labelCol: 合并 labelWidth 快捷方式\n const mergedLabelCol = useMemo(() => {\n if (labelCol) {\n return labelCol;\n }\n if (finalLabelWidth) {\n return {\n style: {\n width:\n typeof finalLabelWidth === 'number'\n ? `${finalLabelWidth}px`\n : finalLabelWidth,\n },\n };\n }\n return undefined;\n }, [labelCol, finalLabelWidth]);\n\n // NexusFormConfig 传递给 Context(由 ui 层消费,自行实现布局)\n const formConfig = useMemo<NexusFormConfig>(\n () => ({\n labelCol: mergedLabelCol,\n labelWidth: finalLabelWidth,\n colon: finalColon,\n label: finalLabel,\n displayType: finalDisplayType,\n readOnly: finalReadOnly,\n column: finalColumn,\n }),\n [\n mergedLabelCol,\n finalLabelWidth,\n finalColon,\n finalLabel,\n finalDisplayType,\n finalReadOnly,\n finalColumn,\n ],\n );\n\n return (\n <NexusFormProvider engine={engine} config={formConfig} form={form}>\n <form\n ref={formElRef}\n onSubmit={handleSubmit}\n className={className}\n style={{\n ...style,\n // 当配置了 column 时,使用 CSS Grid 布局\n ...(finalColumn && finalColumn > 1\n ? {\n display: 'grid',\n gridTemplateColumns: `repeat(${finalColumn}, 1fr)`,\n gap: '0 16px',\n }\n : {}),\n }}\n noValidate\n >\n {renderTree.map((node, index) => renderTreeNode(node, index))}\n {!readOnly && footerNode}\n {children}\n </form>\n </NexusFormProvider>\n );\n}\n"],"names":["noop","NexusForm","form","schema","initialValues","widgets","layouts","onFinish","onFinishFailed","footer","className","style","children","labelCol","labelWidth","colon","label","displayType","readOnly","column","watch","removeHiddenData","engine","formElRef","useRef","finalDisplayType","finalLabel","finalColon","finalLabelWidth","finalReadOnly","finalColumn","useEffect","isFirstInitRef","initialValuesRef","onFinishRef","onFinishFailedRef","_version","useSyncExternalStore","renderTree","useMemo","handleSubmit","useCallback","e","handleReset","footerNode","jsxs","jsx","mergedLabelCol","formConfig","NexusFormProvider","node","index","renderTreeNode"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as u } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo as s } from "react";
|
|
3
|
+
import { NexusContext as x } from "../contexts/NexusContext.js";
|
|
4
|
+
function d({
|
|
5
|
+
engine: r,
|
|
6
|
+
config: o,
|
|
7
|
+
form: t,
|
|
8
|
+
children: e
|
|
9
|
+
}) {
|
|
10
|
+
const m = s(
|
|
11
|
+
() => ({ engine: r, config: o, form: t }),
|
|
12
|
+
[r, o, t]
|
|
13
|
+
);
|
|
14
|
+
return /* @__PURE__ */ u(x.Provider, { value: m, children: e });
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
d as NexusFormProvider
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=NexusFormProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NexusFormProvider.js","sources":["../../../../../../react/src/components/NexusFormProvider.tsx"],"sourcesContent":["import type { NexusEngine } from '@xbeeant/form-engine';\nimport type { ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport { NexusContext } from '../contexts/NexusContext';\nimport type { FormController } from './FormController';\nimport type { NexusFormConfig } from './NexusForm';\n\ninterface NexusFormProviderProps {\n engine: NexusEngine;\n config: NexusFormConfig;\n form: FormController;\n children: ReactNode;\n}\n\nexport function NexusFormProvider({\n engine,\n config,\n form,\n children,\n}: NexusFormProviderProps) {\n const value = useMemo(\n () => ({ engine, config, form }),\n [engine, config, form],\n );\n return (\n <NexusContext.Provider value={value}>{children}</NexusContext.Provider>\n );\n}\n"],"names":["NexusFormProvider","engine","config","form","children","value","useMemo","jsx","NexusContext"],"mappings":";;;AAcO,SAASA,EAAkB;AAAA,EAChC,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,UAAAC;AACF,GAA2B;AACzB,QAAMC,IAAQC;AAAA,IACZ,OAAO,EAAE,QAAAL,GAAQ,QAAAC,GAAQ,MAAAC;IACzB,CAACF,GAAQC,GAAQC,CAAI;AAAA,EAAA;AAEvB,SACE,gBAAAI,EAACC,EAAa,UAAb,EAAsB,OAAAH,GAAe,UAAAD,EAAA,CAAS;AAEnD;"}
|