@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.
Files changed (117) hide show
  1. package/README.md +69 -0
  2. package/dist/es/Canvas.js +636 -0
  3. package/dist/es/Canvas.js.map +1 -0
  4. package/dist/es/Designer.js +162 -0
  5. package/dist/es/Designer.js.map +1 -0
  6. package/dist/es/DesignerContext.js +97 -0
  7. package/dist/es/DesignerContext.js.map +1 -0
  8. package/dist/es/Palette.js +148 -0
  9. package/dist/es/Palette.js.map +1 -0
  10. package/dist/es/PropertyPanel.js +294 -0
  11. package/dist/es/PropertyPanel.js.map +1 -0
  12. package/dist/es/ResizablePanel.js +103 -0
  13. package/dist/es/ResizablePanel.js.map +1 -0
  14. package/dist/es/catalog.js +514 -0
  15. package/dist/es/catalog.js.map +1 -0
  16. package/dist/es/form-engine-designer.css +1 -0
  17. package/dist/es/index.js +20 -0
  18. package/dist/es/index.js.map +1 -0
  19. package/dist/es/property/basic-property.js +115 -0
  20. package/dist/es/property/basic-property.js.map +1 -0
  21. package/dist/es/react/src/components/FormController.js +154 -0
  22. package/dist/es/react/src/components/FormController.js.map +1 -0
  23. package/dist/es/react/src/components/NexusField.js +99 -0
  24. package/dist/es/react/src/components/NexusField.js.map +1 -0
  25. package/dist/es/react/src/components/NexusForm.js +121 -0
  26. package/dist/es/react/src/components/NexusForm.js.map +1 -0
  27. package/dist/es/react/src/components/NexusFormProvider.js +19 -0
  28. package/dist/es/react/src/components/NexusFormProvider.js.map +1 -0
  29. package/dist/es/react/src/components/NexusLayout.js +49 -0
  30. package/dist/es/react/src/components/NexusLayout.js.map +1 -0
  31. package/dist/es/react/src/components/NexusObject.js +65 -0
  32. package/dist/es/react/src/components/NexusObject.js.map +1 -0
  33. package/dist/es/react/src/contexts/FieldInheritContext.js +6 -0
  34. package/dist/es/react/src/contexts/FieldInheritContext.js.map +1 -0
  35. package/dist/es/react/src/contexts/GridContext.js +6 -0
  36. package/dist/es/react/src/contexts/GridContext.js.map +1 -0
  37. package/dist/es/react/src/contexts/LayoutConfigContext.js +6 -0
  38. package/dist/es/react/src/contexts/LayoutConfigContext.js.map +1 -0
  39. package/dist/es/react/src/contexts/NexusContext.js +13 -0
  40. package/dist/es/react/src/contexts/NexusContext.js.map +1 -0
  41. package/dist/es/react/src/hooks/useForm.js +10 -0
  42. package/dist/es/react/src/hooks/useForm.js.map +1 -0
  43. package/dist/es/react/src/utils/renderTreeNode.js +18 -0
  44. package/dist/es/react/src/utils/renderTreeNode.js.map +1 -0
  45. package/dist/es/react/src/utils/resolveColSpan.js +10 -0
  46. package/dist/es/react/src/utils/resolveColSpan.js.map +1 -0
  47. package/dist/es/schemaUtils.js +286 -0
  48. package/dist/es/schemaUtils.js.map +1 -0
  49. package/dist/es/schemaUtils.test.js +188 -0
  50. package/dist/es/schemaUtils.test.js.map +1 -0
  51. package/dist/es/types.js +2 -0
  52. package/dist/es/types.js.map +1 -0
  53. package/dist/es/vite-env.d.js +2 -0
  54. package/dist/es/vite-env.d.js.map +1 -0
  55. package/dist/es/widgets/BindEditor.js +67 -0
  56. package/dist/es/widgets/BindEditor.js.map +1 -0
  57. package/dist/es/widgets/CodeEditor.js +84 -0
  58. package/dist/es/widgets/CodeEditor.js.map +1 -0
  59. package/dist/es/widgets/DefaultEditor.js +55 -0
  60. package/dist/es/widgets/DefaultEditor.js.map +1 -0
  61. package/dist/es/widgets/DependenciesEditor.js +36 -0
  62. package/dist/es/widgets/DependenciesEditor.js.map +1 -0
  63. package/dist/es/widgets/EnumEditor.js +49 -0
  64. package/dist/es/widgets/EnumEditor.js.map +1 -0
  65. package/dist/es/widgets/ExpressionBuilder.js +367 -0
  66. package/dist/es/widgets/ExpressionBuilder.js.map +1 -0
  67. package/dist/es/widgets/ExpressionSwitch.js +47 -0
  68. package/dist/es/widgets/ExpressionSwitch.js.map +1 -0
  69. package/dist/es/widgets/OptionsEditor.js +102 -0
  70. package/dist/es/widgets/OptionsEditor.js.map +1 -0
  71. package/dist/es/widgets/ReactionsEditor.js +401 -0
  72. package/dist/es/widgets/ReactionsEditor.js.map +1 -0
  73. package/dist/es/widgets/RemoteDataEditor.js +314 -0
  74. package/dist/es/widgets/RemoteDataEditor.js.map +1 -0
  75. package/dist/es/widgets/ValidateEditor.js +118 -0
  76. package/dist/es/widgets/ValidateEditor.js.map +1 -0
  77. package/dist/es/widgets/expressionModel.js +340 -0
  78. package/dist/es/widgets/expressionModel.js.map +1 -0
  79. package/dist/es/widgets/expressionModel.test.js +193 -0
  80. package/dist/es/widgets/expressionModel.test.js.map +1 -0
  81. package/dist/es/widgets/index.js +57 -0
  82. package/dist/es/widgets/index.js.map +1 -0
  83. package/dist/es/widgets/reactionsModel.js +107 -0
  84. package/dist/es/widgets/reactionsModel.js.map +1 -0
  85. package/dist/es/widgets/useFormDataFields.js +16 -0
  86. package/dist/es/widgets/useFormDataFields.js.map +1 -0
  87. package/package.json +62 -0
  88. package/src/Canvas.tsx +1111 -0
  89. package/src/Designer.css +206 -0
  90. package/src/Designer.tsx +254 -0
  91. package/src/DesignerContext.tsx +192 -0
  92. package/src/Palette.tsx +261 -0
  93. package/src/PropertyPanel.tsx +486 -0
  94. package/src/ResizablePanel.tsx +210 -0
  95. package/src/catalog.ts +526 -0
  96. package/src/index.ts +25 -0
  97. package/src/property/basic-property.ts +126 -0
  98. package/src/schemaUtils.test.ts +261 -0
  99. package/src/schemaUtils.ts +627 -0
  100. package/src/types.ts +38 -0
  101. package/src/vite-env.d.ts +1 -0
  102. package/src/widgets/BindEditor.tsx +105 -0
  103. package/src/widgets/CodeEditor.tsx +114 -0
  104. package/src/widgets/DefaultEditor.tsx +73 -0
  105. package/src/widgets/DependenciesEditor.tsx +56 -0
  106. package/src/widgets/EnumEditor.tsx +66 -0
  107. package/src/widgets/ExpressionBuilder.tsx +509 -0
  108. package/src/widgets/ExpressionSwitch.tsx +62 -0
  109. package/src/widgets/OptionsEditor.tsx +141 -0
  110. package/src/widgets/ReactionsEditor.tsx +532 -0
  111. package/src/widgets/RemoteDataEditor.tsx +382 -0
  112. package/src/widgets/ValidateEditor.tsx +162 -0
  113. package/src/widgets/expressionModel.test.ts +262 -0
  114. package/src/widgets/expressionModel.ts +573 -0
  115. package/src/widgets/index.ts +45 -0
  116. package/src/widgets/reactionsModel.ts +190 -0
  117. package/src/widgets/useFormDataFields.ts +21 -0
@@ -0,0 +1,261 @@
1
+ // ============================================================================
2
+ // @xbeeant/form-engine-designer — 左侧组件目录面板
3
+ // ============================================================================
4
+
5
+ import type { SchemaNode } from '@xbeeant/form-engine';
6
+ import { Collapse, Input } from 'antd';
7
+ import type { DragEvent } from 'react';
8
+ import { useCallback, useMemo, useState } from 'react';
9
+ import { useDesigner } from './DesignerContext';
10
+ import {
11
+ generateKey,
12
+ getNodeAtProperties,
13
+ getPropertiesOf,
14
+ } from './schemaUtils';
15
+ import type { CatalogItem, FieldDef } from './types';
16
+
17
+ // 拖拽负载:palette 组件目录项 or 外部字段定义
18
+ export interface PaletteDragPayload {
19
+ source: 'palette';
20
+ catalogItem: CatalogItem;
21
+ }
22
+ export interface FieldDefDragPayload {
23
+ source: 'fieldDef';
24
+ fieldDef: FieldDef;
25
+ }
26
+ export type PaletteDragData = PaletteDragPayload | FieldDefDragPayload;
27
+
28
+ // 面板类型 → 允许的唯一父容器类型(面板不能直接添加到根或普通容器)
29
+ const PANE_PARENT_TYPE: Record<string, string> = {
30
+ tabPane: 'tabs',
31
+ collapsePanel: 'collapse',
32
+ step: 'steps',
33
+ };
34
+
35
+ const isPaneItem = (item: CatalogItem): boolean =>
36
+ item.category === 'layout' && !!PANE_PARENT_TYPE[item.layoutType ?? ''];
37
+
38
+ function PaletteItem({
39
+ item,
40
+ onAdd,
41
+ }: {
42
+ item: CatalogItem;
43
+ onAdd: (item: CatalogItem) => void;
44
+ }) {
45
+ const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
46
+ const payload: PaletteDragPayload = {
47
+ source: 'palette',
48
+ catalogItem: item,
49
+ };
50
+ e.dataTransfer.setData('application/json', JSON.stringify(payload));
51
+ e.dataTransfer.effectAllowed = 'copy';
52
+ };
53
+
54
+ // 面板类组件只能放入对应容器,点击/拖拽提示差异化
55
+ const paneParent = isPaneItem(item)
56
+ ? PANE_PARENT_TYPE[item.layoutType ?? '']
57
+ : undefined;
58
+ const title = paneParent
59
+ ? `拖拽「${item.label}」到「${paneParent}」容器中,或选中容器后点击`
60
+ : `点击添加或拖拽「${item.label}」到画布`;
61
+
62
+ return (
63
+ <div
64
+ className='group flex flex-col items-center gap-1 rounded-lg border border-[#f0f0f0] bg-white px-1 py-2 cursor-grab select-none transition-all hover:-translate-y-px hover:border-[#1677ff] hover:shadow-[0_2px_8px_rgba(22,119,255,0.12)] active:cursor-grabbing'
65
+ draggable
66
+ onDragStart={handleDragStart}
67
+ onClick={() => onAdd(item)}
68
+ title={title}
69
+ >
70
+ <span className='flex h-7 w-7 items-center justify-center rounded-md bg-[#f5f7fa] text-[15px] transition-colors group-hover:bg-[#e8f1ff]'>
71
+ {item.icon}
72
+ </span>
73
+ <span className='w-full truncate text-center text-xs text-[#666] transition-colors group-hover:text-[#1677ff]'>
74
+ {item.label}
75
+ </span>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function FieldDefItem({ field }: { field: FieldDef }) {
81
+ const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
82
+ const payload: FieldDefDragPayload = {
83
+ source: 'fieldDef',
84
+ fieldDef: field,
85
+ };
86
+ e.dataTransfer.setData('application/json', JSON.stringify(payload));
87
+ e.dataTransfer.effectAllowed = 'copy';
88
+ };
89
+
90
+ return (
91
+ <div
92
+ className='flex items-center gap-2 rounded-lg border border-[#f0f0f0] bg-white px-2 py-1.5 cursor-grab select-none transition-all hover:-translate-y-px hover:border-[#1677ff] hover:shadow-[0_2px_8px_rgba(22,119,255,0.12)] active:cursor-grabbing'
93
+ draggable
94
+ onDragStart={handleDragStart}
95
+ title={`id: ${field.id} · widget: ${field.widget}`}
96
+ >
97
+ <span className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md bg-[#f5f7fa] text-[13px]'>
98
+ 🔧
99
+ </span>
100
+ <span className='truncate text-xs text-[#666]'>{field.name}</span>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export function Palette() {
106
+ const {
107
+ schema,
108
+ selectedPath,
109
+ addNode,
110
+ fields,
111
+ widgetCatalog,
112
+ layoutCatalog,
113
+ } = useDesigner();
114
+ const [keyword, setKeyword] = useState('');
115
+
116
+ const handleAdd = useCallback(
117
+ (item: CatalogItem) => {
118
+ const base = item.widget || item.layoutType || 'field';
119
+ // 面板类组件只能放入对应容器:点击时若已选中匹配容器则加入,否则忽略
120
+ const paneParentType = isPaneItem(item)
121
+ ? PANE_PARENT_TYPE[item.layoutType ?? '']
122
+ : undefined;
123
+ if (paneParentType) {
124
+ if (!selectedPath || selectedPath.length === 0) {
125
+ return;
126
+ }
127
+ const parent = getNodeAtProperties(schema.properties, selectedPath);
128
+ if (!parent || parent.type !== paneParentType) {
129
+ return;
130
+ }
131
+ const parentProps = getPropertiesOf(parent);
132
+ if (!parentProps) {
133
+ return;
134
+ }
135
+ const key = generateKey(parentProps, base);
136
+ addNode(selectedPath, key, item.createNode() as unknown as SchemaNode);
137
+ return;
138
+ }
139
+ const key = generateKey(schema.properties, base);
140
+ const node = item.createNode() as unknown as SchemaNode;
141
+ addNode([], key, node);
142
+ },
143
+ [schema, addNode, selectedPath],
144
+ );
145
+
146
+ const kw = keyword.trim().toLowerCase();
147
+
148
+ const widgetItems = useMemo(
149
+ () =>
150
+ widgetCatalog
151
+ .filter((item) => item.label.toLowerCase().includes(kw))
152
+ .map((item) => (
153
+ <PaletteItem key={item.widget} item={item} onAdd={handleAdd} />
154
+ )),
155
+ [widgetCatalog, kw, handleAdd],
156
+ );
157
+
158
+ const layoutItems = useMemo(
159
+ () =>
160
+ layoutCatalog
161
+ .filter((item) => item.label.toLowerCase().includes(kw))
162
+ .map((item) => (
163
+ <PaletteItem key={item.layoutType} item={item} onAdd={handleAdd} />
164
+ )),
165
+ [layoutCatalog, kw, handleAdd],
166
+ );
167
+
168
+ const fieldItems = useMemo(
169
+ () =>
170
+ (fields ?? [])
171
+ .filter((field) => field.name.toLowerCase().includes(kw))
172
+ .map((field) => <FieldDefItem key={field.id} field={field} />),
173
+ [fields, kw],
174
+ );
175
+
176
+ const sectionGrid = (items: React.ReactNode[], hasMore: boolean) =>
177
+ items.length > 0 ? (
178
+ <div className='grid grid-cols-2 gap-1.5 px-1 pb-2'>{items}</div>
179
+ ) : (
180
+ <div className='py-3 text-center text-xs text-[#c0c4cc]'>
181
+ {hasMore ? '无匹配组件' : '暂无组件'}
182
+ </div>
183
+ );
184
+
185
+ const collapseItems = [
186
+ {
187
+ key: 'widgets',
188
+ label: (
189
+ <span className='flex w-full items-center justify-between pr-1'>
190
+ <span>表单组件</span>
191
+ <span className='rounded-full bg-[#f0f0f0] px-1.5 py-px text-[11px] text-[#999]'>
192
+ {widgetItems.length}
193
+ </span>
194
+ </span>
195
+ ),
196
+ children: sectionGrid(widgetItems, true),
197
+ },
198
+ {
199
+ key: 'layouts',
200
+ label: (
201
+ <span className='flex w-full items-center justify-between pr-1'>
202
+ <span>布局组件</span>
203
+ <span className='rounded-full bg-[#f0f0f0] px-1.5 py-px text-[11px] text-[#999]'>
204
+ {layoutItems.length}
205
+ </span>
206
+ </span>
207
+ ),
208
+ children: sectionGrid(layoutItems, true),
209
+ },
210
+ ];
211
+
212
+ // 仅当传入 fields 时显示「字段列表」分组
213
+ if (fields && fields.length > 0) {
214
+ collapseItems.push({
215
+ key: 'fieldList',
216
+ label: (
217
+ <span className='flex w-full items-center justify-between pr-1'>
218
+ <span>字段列表</span>
219
+ <span className='rounded-full bg-[#f0f0f0] px-1.5 py-px text-[11px] text-[#999]'>
220
+ {fieldItems.length}
221
+ </span>
222
+ </span>
223
+ ),
224
+ children: (
225
+ <div className='flex flex-col gap-1.5 px-1 pb-2'>
226
+ {fieldItems.length > 0 ? (
227
+ fieldItems
228
+ ) : (
229
+ <div className='py-3 text-center text-xs text-[#c0c4cc]'>
230
+ 无匹配字段
231
+ </div>
232
+ )}
233
+ </div>
234
+ ),
235
+ });
236
+ }
237
+
238
+ return (
239
+ <div className='flex h-full flex-col border-r border-[#f0f0f0] bg-[#f7f8fa]'>
240
+ <div className='px-3 pb-2 pt-3'>
241
+ <Input
242
+ size='small'
243
+ allowClear
244
+ prefix={<span className='text-xs text-[#bbb]'>🔍</span>}
245
+ placeholder='搜索组件…'
246
+ value={keyword}
247
+ onChange={(e) => setKeyword(e.target.value)}
248
+ className='nexus-palette-search'
249
+ />
250
+ </div>
251
+ <div className='flex-1 overflow-y-auto px-2 pb-3'>
252
+ <Collapse
253
+ className='nexus-palette-collapse'
254
+ expandIconPlacement='end'
255
+ defaultActiveKey={['widgets', 'layouts']}
256
+ items={collapseItems}
257
+ />
258
+ </div>
259
+ </div>
260
+ );
261
+ }