@syncfusion/ej2-react-form-builder 0.1.0-beta

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 (145) hide show
  1. package/Common/CheckboxGroup.d.ts +13 -0
  2. package/Common/CheckboxGroup.js +50 -0
  3. package/Common/ConditionalRuleEngine.d.ts +296 -0
  4. package/Common/ConditionalRuleEngine.js +566 -0
  5. package/Common/CustomTabComponent.d.ts +17 -0
  6. package/Common/CustomTabComponent.js +30 -0
  7. package/Common/CustomValidationEngine.d.ts +58 -0
  8. package/Common/CustomValidationEngine.js +146 -0
  9. package/Common/DataGridComponent.d.ts +20 -0
  10. package/Common/DataGridComponent.js +273 -0
  11. package/Common/FormValidator.d.ts +485 -0
  12. package/Common/FormValidator.js +575 -0
  13. package/Common/MinMaxRuleEngine.d.ts +105 -0
  14. package/Common/MinMaxRuleEngine.js +365 -0
  15. package/Common/StaticHtmlRenderer.d.ts +14 -0
  16. package/Common/StaticHtmlRenderer.js +24 -0
  17. package/Common/expressions/DependencyTracker.d.ts +30 -0
  18. package/Common/expressions/DependencyTracker.js +128 -0
  19. package/Common/expressions/ExpressionEngine.d.ts +95 -0
  20. package/Common/expressions/ExpressionEngine.js +506 -0
  21. package/Common/expressions/ExpressionParser.d.ts +41 -0
  22. package/Common/expressions/ExpressionParser.js +269 -0
  23. package/Common/expressions/useGridExpressions.d.ts +12 -0
  24. package/Common/expressions/useGridExpressions.js +71 -0
  25. package/Common/globalFormId.d.ts +26 -0
  26. package/Common/globalFormId.js +45 -0
  27. package/Common/json-converter.d.ts +444 -0
  28. package/Common/json-converter.js +1184 -0
  29. package/Common/parser.d.ts +98 -0
  30. package/Common/parser.js +753 -0
  31. package/Common/utils.d.ts +130 -0
  32. package/Common/utils.js +574 -0
  33. package/README.md +53 -0
  34. package/index.d.ts +6 -0
  35. package/index.js +6 -0
  36. package/license +13 -0
  37. package/package.json +54 -0
  38. package/src/FormBuilder.d.ts +211 -0
  39. package/src/FormBuilder.js +1299 -0
  40. package/src/components/Database/ConnectionForm.d.ts +8 -0
  41. package/src/components/Database/ConnectionForm.js +28 -0
  42. package/src/components/TopBar.d.ts +10 -0
  43. package/src/components/TopBar.js +7 -0
  44. package/src/components/icons/ComponentIcons.d.ts +44 -0
  45. package/src/components/icons/ComponentIcons.js +40 -0
  46. package/src/components/layout/FormFieldWrapper.d.ts +41 -0
  47. package/src/components/layout/FormFieldWrapper.js +112 -0
  48. package/src/components/layout/LeftPane.d.ts +23 -0
  49. package/src/components/layout/LeftPane.js +310 -0
  50. package/src/components/layout/ListBoxPane.d.ts +24 -0
  51. package/src/components/layout/ListBoxPane.js +94 -0
  52. package/src/components/layout/MainSection.d.ts +39 -0
  53. package/src/components/layout/MainSection.js +1766 -0
  54. package/src/components/layout/Preview.d.ts +12 -0
  55. package/src/components/layout/Preview.js +337 -0
  56. package/src/components/layout/RightPane.d.ts +22 -0
  57. package/src/components/layout/RightPane.js +370 -0
  58. package/src/components/theme/ThemeSwitcher.d.ts +14 -0
  59. package/src/components/theme/ThemeSwitcher.js +24 -0
  60. package/src/components/theme/theme-constant.d.ts +37 -0
  61. package/src/components/theme/theme-constant.js +23 -0
  62. package/src/config/component-properties.json +4929 -0
  63. package/src/config/form-setting.json +56 -0
  64. package/src/config/locale.d.ts +449 -0
  65. package/src/config/locale.js +449 -0
  66. package/src/context/FormSchemaContext.d.ts +32 -0
  67. package/src/context/FormSchemaContext.js +155 -0
  68. package/src/context/LocalizationContext.d.ts +27 -0
  69. package/src/context/LocalizationContext.js +43 -0
  70. package/src/context/ThemeContext.d.ts +23 -0
  71. package/src/context/ThemeContext.js +161 -0
  72. package/src/context/contexts.d.ts +6 -0
  73. package/src/context/contexts.js +6 -0
  74. package/src/context/defaultFormSchema.d.ts +6 -0
  75. package/src/context/defaultFormSchema.js +20 -0
  76. package/src/defaultPalette.d.ts +9 -0
  77. package/src/defaultPalette.js +45 -0
  78. package/src/defaultTemplate.d.ts +3 -0
  79. package/src/defaultTemplate.js +185 -0
  80. package/src/import/schemaImportUtils.d.ts +24 -0
  81. package/src/import/schemaImportUtils.js +121 -0
  82. package/src/index.d.ts +16 -0
  83. package/src/index.js +6 -0
  84. package/src/types/ActionButtons.d.ts +44 -0
  85. package/src/types/ActionButtons.js +27 -0
  86. package/src/types/ComponentProperties.d.ts +33 -0
  87. package/src/types/ComponentProperties.js +2 -0
  88. package/src/types/FormSchema.d.ts +585 -0
  89. package/src/types/FormSchema.js +147 -0
  90. package/src/types/connection.d.ts +19 -0
  91. package/src/types/connection.js +9 -0
  92. package/src/utils/AttributesEditor.d.ts +13 -0
  93. package/src/utils/AttributesEditor.js +117 -0
  94. package/src/utils/ButtonGroupEditor.d.ts +15 -0
  95. package/src/utils/ButtonGroupEditor.js +141 -0
  96. package/src/utils/ChoiceBasedFieldEditor.d.ts +16 -0
  97. package/src/utils/ChoiceBasedFieldEditor.js +87 -0
  98. package/src/utils/ClipboardHelper.d.ts +26 -0
  99. package/src/utils/ClipboardHelper.js +83 -0
  100. package/src/utils/ColumnBuilder.d.ts +11 -0
  101. package/src/utils/ColumnBuilder.js +153 -0
  102. package/src/utils/ColumnSpanEditor.d.ts +8 -0
  103. package/src/utils/ColumnSpanEditor.js +98 -0
  104. package/src/utils/ConditionDataEditor.d.ts +29 -0
  105. package/src/utils/ConditionDataEditor.js +177 -0
  106. package/src/utils/ConditionalEditorUtils.d.ts +27 -0
  107. package/src/utils/ConditionalEditorUtils.js +310 -0
  108. package/src/utils/ConditionsEditor.d.ts +20 -0
  109. package/src/utils/ConditionsEditor.js +571 -0
  110. package/src/utils/CustomAccordion.d.ts +13 -0
  111. package/src/utils/CustomAccordion.js +38 -0
  112. package/src/utils/CustomValidationEditor.d.ts +11 -0
  113. package/src/utils/CustomValidationEditor.js +208 -0
  114. package/src/utils/DataSourceCollector.d.ts +3 -0
  115. package/src/utils/DataSourceCollector.js +252 -0
  116. package/src/utils/ExpressionEditor.d.ts +13 -0
  117. package/src/utils/ExpressionEditor.js +99 -0
  118. package/src/utils/FormSettingsPanel.d.ts +9 -0
  119. package/src/utils/FormSettingsPanel.js +60 -0
  120. package/src/utils/LookupConfig.d.ts +11 -0
  121. package/src/utils/LookupConfig.js +159 -0
  122. package/src/utils/MinMaxValueEditor.d.ts +25 -0
  123. package/src/utils/MinMaxValueEditor.js +188 -0
  124. package/src/utils/OptionsEditor.d.ts +8 -0
  125. package/src/utils/OptionsEditor.js +79 -0
  126. package/src/utils/PropertyRenderer.d.ts +22 -0
  127. package/src/utils/PropertyRenderer.js +210 -0
  128. package/src/utils/ValueRenderer.d.ts +2 -0
  129. package/src/utils/ValueRenderer.js +91 -0
  130. package/src/utils/ej2DbAdaptor.d.ts +6 -0
  131. package/src/utils/ej2DbAdaptor.js +38 -0
  132. package/src/utils/mappingHeuristics.d.ts +10 -0
  133. package/src/utils/mappingHeuristics.js +33 -0
  134. package/src/utils/renderComponent.d.ts +18 -0
  135. package/src/utils/renderComponent.js +300 -0
  136. package/styles/tailwind3.css +67 -0
  137. package/styles/themes/action-buttons.css +110 -0
  138. package/styles/themes/datagrid.css +380 -0
  139. package/styles/themes/drag-reorder.css +248 -0
  140. package/styles/themes/import-dialog.css +292 -0
  141. package/styles/themes/index.css +658 -0
  142. package/styles/themes/left-pane.css +253 -0
  143. package/styles/themes/options-editor.css +63 -0
  144. package/styles/themes/preview.css +702 -0
  145. package/styles/themes/property-dialog.css +371 -0
@@ -0,0 +1,1299 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
3
+ import LeftPane from './components/layout/LeftPane';
4
+ import MainSection, { isDefaultFormSchema } from './components/layout/MainSection';
5
+ import formSettingsConfig from './config/form-setting.json';
6
+ import RightPane from './components/layout/RightPane';
7
+ import componentProperties from './config/component-properties.json';
8
+ import { ClipboardHelper } from './utils/ClipboardHelper';
9
+ import { useFormSchema, FormSchemaProvider, LocalizationProvider, useLocalization } from './context/contexts';
10
+ import { useState } from 'react';
11
+ import { DragDrop } from '@syncfusion/react-base';
12
+ import defaultTemplates from './defaultTemplate';
13
+ import { generateId, generateName, splitSchema } from '../Common/json-converter';
14
+ import { importSplitSchemas } from './import/schemaImportUtils';
15
+ import { defaultFormSchema } from './context/defaultFormSchema';
16
+ import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
17
+ import { InPlaceEditorComponent } from '@syncfusion/ej2-react-inplace-editor';
18
+ import { DropDownButtonComponent } from '@syncfusion/ej2-react-splitbuttons';
19
+ import { enableRtl as Rtl } from '@syncfusion/ej2-base';
20
+ import { navigateTo } from '../Common/utils';
21
+ /**
22
+ * @private
23
+ */
24
+ export let formBuilderMode = 'component';
25
+ /**
26
+ * @private
27
+ */
28
+ export const extractTemplateComponents = (template) => {
29
+ try {
30
+ if (!template.schema) {
31
+ console.warn(`Template "${template.title}" missing schema`);
32
+ return [];
33
+ }
34
+ // Convert template schema to JSON string
35
+ const dataSchemaJson = JSON.stringify(template.schema);
36
+ // Use importSplitSchemas to parse and merge template into empty schema
37
+ const emptyFormSchema = { version: '0.1.0', theme: 'tailwind3', components: [] };
38
+ const mergedSchema = importSplitSchemas(dataSchemaJson, emptyFormSchema);
39
+ // Extract components and regenerate IDs to ensure uniqueness
40
+ const regenerateIds = (comp) => {
41
+ const updated = { ...comp, id: generateId(comp.type) };
42
+ if (comp.children) {
43
+ updated.children = comp.children.map(regenerateIds);
44
+ }
45
+ if (comp.tableCells) {
46
+ updated.tableCells = comp.tableCells.map(row => row.map(cell => cell.map(regenerateIds)));
47
+ }
48
+ if (comp.tabItems) {
49
+ updated.tabItems = comp.tabItems.map(tab => ({
50
+ ...tab,
51
+ content: tab.content?.map(regenerateIds)
52
+ }));
53
+ }
54
+ return updated;
55
+ };
56
+ // Filter out submit buttons from templates to prevent duplicates
57
+ const filterOutSubmitButtons = (comp) => {
58
+ // Remove submit buttons
59
+ if (comp.type === 'button' && comp.buttonType === 'submit') {
60
+ return false;
61
+ }
62
+ return true;
63
+ };
64
+ const componentsWithIds = (mergedSchema?.components || []).map(regenerateIds);
65
+ return componentsWithIds.filter(filterOutSubmitButtons);
66
+ }
67
+ catch (error) {
68
+ console.error('Error extracting template components:', template.title, error);
69
+ return [];
70
+ }
71
+ };
72
+ const FormBuilderComponent = ({ allowExport, enablePreview, onFieldDragStarted, onFieldDraggingOver, onFieldDropped, onPropertyChanged, enableRtl = false, apiRef, mode, domRef, locale, enablePersistence, formTemplates = defaultTemplates, paletteConfig }) => {
73
+ // Use FormSchema context instead of local state
74
+ const { formSchema, setFormSchema } = useFormSchema();
75
+ const { l10n } = useLocalization();
76
+ Rtl(enableRtl);
77
+ // Expose imperative API methods via ref
78
+ useImperativeHandle(apiRef, () => ({
79
+ element: domRef?.current ?? null,
80
+ locale,
81
+ allowExport,
82
+ enablePreview,
83
+ enablePersistence,
84
+ getFormSettings: () => {
85
+ // formSchema may sometimes be missing a top-level `settings` (imported/legacy shapes).
86
+ // Read defensively: prefer top-level `settings`, then try common alternate locations,
87
+ // and fall back to an empty object so defaults are applied below.
88
+ const fsAny = formSchema;
89
+ const settings = fsAny?.settings ?? fsAny?.schema?.settings ?? fsAny?.properties?.settings ?? {};
90
+ const unified = splitSchema(formSchema);
91
+ const output = {
92
+ name: settings?.name ?? '',
93
+ width: settings?.width ?? '',
94
+ layout: {
95
+ hideLabel: settings?.hideLabel ?? false,
96
+ size: settings?.size ?? '',
97
+ floatLabelType: settings?.floatLabelType ?? ''
98
+ },
99
+ schema: unified
100
+ };
101
+ return output;
102
+ }
103
+ }), [formSchema, domRef, locale, allowExport, enablePreview, enablePersistence]);
104
+ const [pendingComponent, setPendingComponent] = useState(null);
105
+ const [pendingDropContext, setPendingDropContext] = useState(null);
106
+ // Widget switch state: holds context when user initiates a widget switch
107
+ const [pendingWidgetSwitch, setPendingWidgetSwitch] = useState(null);
108
+ // Edit mode state
109
+ const [editingField, setEditingField] = useState(null);
110
+ // Left pane collapsed state for toggle functionality
111
+ const [isLeftPaneCollapsed, setIsLeftPaneCollapsed] = useState(false);
112
+ const [isRightPaneCollapsed, setIsRightPaneCollapsed] = useState(false);
113
+ const [isMobile, setIsMobile] = useState(false);
114
+ const [isRightPaneHovered, setIsRightPaneHovered] = useState(false);
115
+ const [isLeftPaneHovered, setIsLeftPaneHovered] = useState(false);
116
+ const leftPaneHoverTimeoutRef = useRef(null);
117
+ const rightPaneHoverTimeoutRef = useRef(null);
118
+ const contentSectionRef = useRef(null);
119
+ const isReorderingRef = useRef(false);
120
+ const builderContainerRef = useRef(null);
121
+ const [containerWidth, setContainerWidth] = useState(1024);
122
+ useEffect(() => {
123
+ if (pendingWidgetSwitch) {
124
+ handlePropertyDialogSave(pendingWidgetSwitch.originalComponent);
125
+ }
126
+ if (pendingDropContext) {
127
+ handlePropertyDialogSave(pendingComponent);
128
+ }
129
+ }, [pendingWidgetSwitch, pendingDropContext, pendingComponent]);
130
+ const handleFormSettingsSave = useCallback((values, key) => {
131
+ const settings = values;
132
+ // Apply form settings to all existing components
133
+ const updateComponentsWithSettings = (components) => {
134
+ return components.map(component => {
135
+ // Skip components that have explicitly set these properties
136
+ const updatedComponent = { ...component };
137
+ // Only update the specific property if key is provided, otherwise update all
138
+ if (key === 'floatLabelType') {
139
+ if (settings.floatLabelType !== undefined) {
140
+ updatedComponent.floatLabelType = settings.floatLabelType;
141
+ }
142
+ else {
143
+ updatedComponent.floatLabelType = 'Never';
144
+ }
145
+ }
146
+ if (key === 'size') {
147
+ if (settings.size !== undefined) {
148
+ updatedComponent.size = settings.size;
149
+ }
150
+ else {
151
+ updatedComponent.size = 'Default';
152
+ }
153
+ }
154
+ if (key === 'hideLabel') {
155
+ if (settings.hideLabel !== undefined) {
156
+ updatedComponent.hideLabel = settings.hideLabel;
157
+ }
158
+ else {
159
+ updatedComponent.hideLabel = false;
160
+ }
161
+ }
162
+ // Handle nested components (panels, tables, tabs)
163
+ if (component.children?.length) {
164
+ updatedComponent.children = updateComponentsWithSettings(component.children);
165
+ }
166
+ if (component.tableCells?.length) {
167
+ updatedComponent.tableCells = component.tableCells.map(row => row.map(cell => updateComponentsWithSettings(cell)));
168
+ }
169
+ if (component.tabItems?.length) {
170
+ updatedComponent.tabItems = component.tabItems.map(tab => ({
171
+ ...tab,
172
+ content: tab.content ? updateComponentsWithSettings(tab.content) : []
173
+ }));
174
+ }
175
+ return updatedComponent;
176
+ });
177
+ };
178
+ setFormSchema(prev => ({
179
+ ...prev,
180
+ settings: settings,
181
+ components: updateComponentsWithSettings(prev.components)
182
+ }));
183
+ }, [setFormSchema]);
184
+ const handleComponentDrop = (component, dropTargetIndex, dropPosition) => {
185
+ // Check if this component type has property configuration and hasn't been configured yet
186
+ if (componentProperties[component.type] && !component._configured) {
187
+ // Show property dialog
188
+ setPendingComponent(component);
189
+ setEditingField(component);
190
+ setPendingDropContext({
191
+ isNested: false,
192
+ targetIndex: dropTargetIndex,
193
+ targetPosition: dropPosition
194
+ });
195
+ }
196
+ setPendingComponent(component);
197
+ setEditingField(component);
198
+ // Calculate insert index
199
+ let insertIndex;
200
+ if (dropTargetIndex !== null && dropTargetIndex !== undefined && dropPosition) {
201
+ insertIndex = dropPosition === 'before' ? dropTargetIndex : dropTargetIndex + 1;
202
+ }
203
+ // Add at specified index or with smart positioning
204
+ setFormSchema(prev => {
205
+ const submitIndex = prev.components.findIndex(c => c.type === 'button' && c.buttonType === 'submit');
206
+ let newComponents;
207
+ if (insertIndex !== undefined) {
208
+ // Insert at specified index
209
+ newComponents = [...prev.components];
210
+ newComponents.splice(insertIndex, 0, component);
211
+ }
212
+ else if (submitIndex !== -1 && submitIndex === prev.components.length - 1) {
213
+ // If submit exists and is at the last position, insert before it
214
+ newComponents = [...prev.components];
215
+ newComponents.splice(submitIndex, 0, component);
216
+ }
217
+ else {
218
+ // Otherwise (submit moved or doesn't exist), add at end
219
+ newComponents = [...prev.components, component];
220
+ }
221
+ return { ...prev, components: newComponents };
222
+ });
223
+ if (isMobile && !isLeftPaneCollapsed) {
224
+ setIsLeftPaneCollapsed(true);
225
+ setIsRightPaneCollapsed(false);
226
+ }
227
+ };
228
+ /**
229
+ * Deep search for a component by id across panels (children), tables (tableCells) and tabs (tabItems).
230
+ */
231
+ function findComponentById(components = [], id) {
232
+ const stack = [...components];
233
+ while (stack.length) {
234
+ const comp = stack.pop();
235
+ if (comp.id === id)
236
+ return comp;
237
+ // children (e.g. panel)
238
+ if (comp.children?.length)
239
+ stack.push(...comp.children);
240
+ // tabs -> tabItems[].content
241
+ if (comp.tabItems?.length) {
242
+ for (const tab of comp.tabItems) {
243
+ if (tab.content?.length)
244
+ stack.push(...tab.content);
245
+ }
246
+ }
247
+ // table -> tableCells: row[] -> cell[] -> components[]
248
+ if (comp.tableCells?.length) {
249
+ for (const row of comp.tableCells) {
250
+ for (const cell of row) {
251
+ if (cell?.length)
252
+ stack.push(...cell);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ return null;
258
+ }
259
+ // T009: Handle Edit action
260
+ const handleEdit = React.useCallback((fieldId) => {
261
+ const field = findComponentById(formSchema.components, fieldId);
262
+ if (!field)
263
+ return;
264
+ // Check if this component type has property configuration
265
+ if (!componentProperties[field.type]) {
266
+ console.warn(`No property configuration found for component type: ${field.type}`);
267
+ alert(`Edit functionality is not yet configured for ${field.type} components.`);
268
+ return;
269
+ }
270
+ setEditingField(field);
271
+ setPendingComponent(field);
272
+ }, [formSchema.components]);
273
+ // Handle user selecting a new widget type from the Switch Widget dropdown
274
+ const handleSwitchWidget = React.useCallback((fieldId, newWidgetTypePascal) => {
275
+ const field = findComponentById(formSchema.components, fieldId);
276
+ if (!field)
277
+ return;
278
+ // Verify property configuration exists for the new widget type
279
+ if (!componentProperties[newWidgetTypePascal]) {
280
+ return;
281
+ }
282
+ // Prepare a component copy with the new type and open the PropertyDialog
283
+ const componentWithNewType = { ...field, type: newWidgetTypePascal };
284
+ setPendingWidgetSwitch({ fieldId, originalComponent: field, newWidgetType: newWidgetTypePascal });
285
+ setPendingComponent(componentWithNewType);
286
+ setEditingField(null);
287
+ }, [formSchema.components]);
288
+ // Filter component to only include properties valid for its type and only those that differ from defaults (validation at root)
289
+ const filterComponentProperties = (component) => {
290
+ const componentType = component.type;
291
+ const configData = componentProperties[componentType];
292
+ if (!configData)
293
+ return component;
294
+ const validProps = new Set();
295
+ configData.groups?.forEach((group) => {
296
+ group.properties?.forEach((prop) => {
297
+ if (prop.key) {
298
+ validProps.add(prop.key);
299
+ }
300
+ });
301
+ });
302
+ const coreProps = ['id', 'type', 'label', 'name', '_configured'];
303
+ if (componentType === 'autoNumber') {
304
+ coreProps.push('visible', 'size');
305
+ }
306
+ const structuralProps = ['children', 'tableCells', 'tabItems'];
307
+ // Get defaults from configData
308
+ const defaults = {};
309
+ configData.groups?.forEach((group) => {
310
+ group.properties?.forEach((prop) => {
311
+ if (prop.key && prop.default !== undefined) {
312
+ defaults[prop.key] = prop.default;
313
+ }
314
+ });
315
+ });
316
+ const filtered = {};
317
+ Object.keys(component).forEach(key => {
318
+ if (coreProps.includes(key) || structuralProps.includes(key)) {
319
+ filtered[key] = component[key];
320
+ return;
321
+ }
322
+ // Only include if valid and different from default
323
+ if (validProps.has(key)) {
324
+ if (key === 'dataSource' &&
325
+ Array.isArray(component[key]) &&
326
+ component[key].length === 0) {
327
+ return;
328
+ }
329
+ if (component[key] !== defaults[key]) {
330
+ filtered[key] = component[key];
331
+ }
332
+ }
333
+ });
334
+ // Ensure structural properties exist for container components
335
+ if (componentType === 'panel' && !filtered.children) {
336
+ filtered.children = [];
337
+ }
338
+ if (componentType === 'button' && !filtered.buttonType && !(component.buttonsGroups?.length > 0)) {
339
+ filtered.buttonType = 'button';
340
+ }
341
+ if (componentType === 'button' && !filtered.buttonType && (component.buttonsGroups.length > 0)) {
342
+ const hasSubmitButton = component.buttonsGroups.some((btn) => btn.type === 'submit');
343
+ if (hasSubmitButton) {
344
+ filtered.buttonType = 'submit';
345
+ }
346
+ }
347
+ if (componentType === 'card' && !filtered.children) {
348
+ filtered.children = [];
349
+ }
350
+ if (componentType === 'table' && !filtered.tableCells) {
351
+ filtered.tableCells = component.tableCells || [];
352
+ }
353
+ if (componentType === 'tabs' && !filtered.tabItems) {
354
+ filtered.tabItems = component.tabItems || [];
355
+ }
356
+ return filtered;
357
+ };
358
+ const handlePropertyDialogSave = useCallback((properties) => {
359
+ if (!pendingComponent)
360
+ return;
361
+ // Map collected properties to FormComponent fields
362
+ const mapPropertiesToComponent = (baseComponent, props) => {
363
+ const updatedComponent = {
364
+ ...baseComponent,
365
+ // Display properties - use properties from dialog or keep existing
366
+ label: props.label !== undefined ? props.label : baseComponent.label,
367
+ name: props.name !== undefined ? props.name : baseComponent.name,
368
+ description: (props.description !== undefined || props.description !== '') ? props.description : undefined,
369
+ prefix: (props.prefix !== undefined || props.prefix !== '') ? props.prefix : undefined,
370
+ suffix: (props.suffix !== undefined || props.suffix !== '') ? props.suffix : undefined,
371
+ conditions: props.conditions !== undefined ? props.conditions : baseComponent.conditions,
372
+ customValidation: props.customValidation !== undefined ? props.customValidation : baseComponent.customValidation,
373
+ pageSize: props.pageSize !== undefined ? props.pageSize : baseComponent.pageSize,
374
+ size: formSchema.settings?.size !== undefined ? formSchema.settings.size : (props.size !== undefined ? props.size : baseComponent.size),
375
+ allowPaging: props.allowPaging !== undefined ? props.allowPaging : baseComponent.allowPaging,
376
+ allowAdding: props.allowAdding !== undefined ? props.allowAdding : baseComponent.allowAdding,
377
+ allowFiltering: props.allowFiltering !== undefined ? props.allowFiltering : baseComponent.allowFiltering,
378
+ allowEditing: props.allowEditing !== undefined ? props.allowEditing : baseComponent.allowEditing,
379
+ allowDeleting: props.allowDeleting !== undefined ? props.allowDeleting : baseComponent.allowDeleting,
380
+ dataSource: props.dataSource !== undefined ? props.dataSource : baseComponent.dataSource || [],
381
+ cardTitle: props.cardTitle !== undefined ? props.cardTitle : baseComponent.cardTitle,
382
+ cardSubtitle: props.cardSubtitle !== undefined ? props.cardSubtitle : baseComponent.cardSubtitle,
383
+ textboxType: props.textboxType !== undefined ? props.textboxType.toLowerCase() : baseComponent.textboxType?.toLowerCase(),
384
+ hideBorders: props.hideBorders !== undefined ? props.hideBorders : baseComponent.hideBorders,
385
+ placeholder: props.placeholder !== undefined ? props.placeholder : baseComponent.placeholder,
386
+ defaultValue: props.defaultValue !== undefined ? props.defaultValue : baseComponent.defaultValue,
387
+ expressionValue: props.expressionValue !== undefined ? props.expressionValue : baseComponent.expressionValue,
388
+ disabled: props.disabled !== undefined ? props.disabled : (baseComponent.disabled || false),
389
+ visible: props.visible !== undefined ? props.visible : baseComponent.visible || false,
390
+ tooltip: props.tooltip !== undefined ? props.tooltip : baseComponent.tooltip,
391
+ cssClass: props.cssClass !== undefined ? props.cssClass : baseComponent.cssClass,
392
+ hideLabel: formSchema.settings?.hideLabel !== undefined ? formSchema.settings.hideLabel : (props.hideLabel !== undefined ? props.hideLabel : (baseComponent.hideLabel || false)),
393
+ labelPosition: props.labelPosition !== undefined ? props.labelPosition : (baseComponent.labelPosition || 'top'),
394
+ checkboxLabelPosition: props.checkboxLabelPosition !== undefined ? props.checkboxLabelPosition : (baseComponent.checkboxLabelPosition || 'After'),
395
+ layoutDirection: props.layoutDirection !== undefined ? props.layoutDirection.toLowerCase() : (baseComponent.layoutDirection?.toLowerCase() || 'column'),
396
+ enableHtmlSanitizer: props.enableHtmlSanitizer !== undefined ? props.enableHtmlSanitizer : (baseComponent.enableHtmlSanitizer || 'true'),
397
+ position: props.position !== undefined ? props.position : (baseComponent.position || 'left'),
398
+ labelWidth: props.labelWidth !== undefined ? props.labelWidth.toLowerCase() : (baseComponent.labelWidth?.toLowerCase() || 'auto'),
399
+ iconCss: props.iconCss !== undefined ? props.iconCss : baseComponent.iconCss,
400
+ iconPosition: props.iconPosition !== undefined ? props.iconPosition : baseComponent.iconPosition,
401
+ startIndex: props.startIndex !== undefined ? props.startIndex : baseComponent.startIndex,
402
+ style: props.style !== undefined ? props.style.toLowerCase() : baseComponent.style?.toLowerCase(),
403
+ buttonType: props.buttonType !== undefined ? props.buttonType.toLowerCase() : baseComponent.buttonType?.toLowerCase(),
404
+ // DatePicker-specific properties
405
+ calendarMode: props.calendarMode !== undefined ? props.calendarMode : baseComponent.calendarMode,
406
+ dayHeaderFormat: props.dayHeaderFormat !== undefined ? props.dayHeaderFormat : baseComponent.dayHeaderFormat,
407
+ depth: props.depth !== undefined ? props.depth : baseComponent.depth,
408
+ firstDayOfWeek: props.firstDayOfWeek !== undefined ? props.firstDayOfWeek : baseComponent.firstDayOfWeek,
409
+ start: props.start !== undefined ? props.start : baseComponent.start,
410
+ // DateTimePicker-specific properties
411
+ allowEdit: props.allowEdit !== undefined ? props.allowEdit : baseComponent.allowEdit,
412
+ enabled: props.enabled !== undefined ? props.enabled : baseComponent.enabled,
413
+ timeFormat: props.timeFormat !== undefined ? props.timeFormat : baseComponent.timeFormat,
414
+ showTodayButton: props.showTodayButton !== undefined ? props.showTodayButton : baseComponent.showTodayButton,
415
+ floatLabelType: formSchema.settings?.floatLabelType !== undefined ? formSchema.settings.floatLabelType : (props.floatLabelType !== undefined ? props.floatLabelType : (baseComponent.floatLabelType || 'Never')),
416
+ fieldLabelPosition: props.fieldLabelPosition !== undefined ? props.fieldLabelPosition : (baseComponent.fieldLabelPosition || 'After'),
417
+ readOnly: props.readOnly !== undefined ? props.readOnly : (baseComponent.readOnly || false),
418
+ autocomplete: props.autocomplete !== undefined ? props.autocomplete : (baseComponent.autocomplete !== false),
419
+ multiline: props.multiline !== undefined ? props.multiline : baseComponent.multiline,
420
+ showClearButton: props.showClearButton !== undefined ? props.showClearButton : (baseComponent.showClearButton || false),
421
+ checked: props.checked !== undefined ? props.checked : (baseComponent.checked || false),
422
+ indeterminate: props.indeterminate !== undefined ? props.indeterminate : (baseComponent.indeterminate || false),
423
+ options: props.options !== undefined ? props.options : (baseComponent.options || ['Option 1']),
424
+ tabOptions: props.tabOptions !== undefined ? props.tabOptions : (baseComponent.tabOptions || ['Tab 1']),
425
+ decimals: props.decimals !== undefined ? props.decimals : baseComponent.decimals,
426
+ currency: props.currency !== undefined ? props.currency : baseComponent.currency,
427
+ format: props.format !== undefined ? props.format : baseComponent.format,
428
+ numberFormat: props.numberFormat !== undefined ? props.numberFormat : baseComponent.numberFormat,
429
+ step: props.step !== undefined ? props.step : baseComponent.step,
430
+ enableVirtualization: props.enableVirtualization !== undefined ? props.enableVirtualization : baseComponent.enableVirtualization,
431
+ popupHeight: props.popupHeight !== undefined ? props.popupHeight : baseComponent.popupHeight,
432
+ popupWidth: props.popupWidth !== undefined ? props.popupWidth : baseComponent.popupWidth,
433
+ htmlAttributes: props.htmlAttributes !== undefined ? props.htmlAttributes : baseComponent.htmlAttributes,
434
+ buttonsGroups: props.buttonsGroups !== undefined ? props.buttonsGroups : baseComponent.buttonsGroups,
435
+ gap: props.gap !== undefined ? props.gap : baseComponent.gap,
436
+ cols: props.cols !== undefined ? props.cols : baseComponent.cols,
437
+ textareaRows: props.textareaRows !== undefined ? props.textareaRows : baseComponent.textareaRows,
438
+ offLabel: props.offLabel !== undefined ? props.offLabel : baseComponent.offLabel,
439
+ onLabel: props.onLabel !== undefined ? props.onLabel : baseComponent.onLabel,
440
+ startDate: props.startDate !== undefined ? props.startDate : baseComponent.startDate,
441
+ endDate: props.endDate !== undefined ? props.endDate : baseComponent.endDate,
442
+ separator: props.separator !== undefined ? props.separator : baseComponent.separator,
443
+ mask: props.mask !== undefined ? props.mask : baseComponent.mask,
444
+ customCharacters: props.customCharacters !== undefined ? JSON.parse(props.customCharacters) : baseComponent.customCharacters,
445
+ promptChar: props.promptChar !== undefined ? props.promptChar : baseComponent.promptChar,
446
+ allowCustomValue: props.allowCustomValue !== undefined ? props.allowCustomValue : baseComponent.allowCustomValue,
447
+ addTagOnBlur: props.addTagOnBlur !== undefined ? props.addTagOnBlur : baseComponent.addTagOnBlur,
448
+ delimiterChar: props.delimiterChar !== undefined ? props.delimiterChar : baseComponent.delimiterChar,
449
+ visualMode: props.visualMode !== undefined ? props.visualMode : baseComponent.visualMode,
450
+ showSelectAll: props.showSelectAll !== undefined ? props.showSelectAll : baseComponent.showSelectAll,
451
+ selectAllText: props.selectAllText !== undefined ? props.selectAllText : baseComponent.selectAllText,
452
+ unSelectAllText: props.unSelectAllText !== undefined ? props.unSelectAllText : baseComponent.unSelectAllText,
453
+ showDropDownIcon: props.showDropDownIcon !== undefined ? props.showDropDownIcon : baseComponent.showDropDownIcon,
454
+ showCloseIcon: props.showCloseIcon !== undefined ? props.showCloseIcon : (baseComponent.showCloseIcon || false),
455
+ showIcon: props.showIcon !== undefined ? props.showIcon : (baseComponent.showIcon || false),
456
+ variant: props.variant !== undefined ? props.variant : baseComponent.variant,
457
+ severity: props.severity !== undefined ? props.severity : baseComponent.severity,
458
+ content: props.content !== undefined ? props.content : baseComponent.content,
459
+ // Rating-specific display properties
460
+ allowReset: props.allowReset !== undefined ? props.allowReset : baseComponent.allowReset,
461
+ itemsCount: props.itemsCount !== undefined ? props.itemsCount : baseComponent.itemsCount,
462
+ precision: props.precision !== undefined ? props.precision : baseComponent.precision,
463
+ showRatingTooltip: props.showRatingTooltip !== undefined ? props.showRatingTooltip : baseComponent.showRatingTooltip,
464
+ showRatingLabel: props.showRatingLabel !== undefined ? props.showRatingLabel : baseComponent.showRatingLabel,
465
+ ratingLabelPosition: props.ratingLabelPosition !== undefined ? props.ratingLabelPosition : baseComponent.ratingLabelPosition,
466
+ // ImageEditor-specific properties
467
+ height: props.height !== undefined ? props.height : baseComponent.height,
468
+ width: props.width !== undefined ? props.width : baseComponent.width,
469
+ // ColorPicker-specific properties
470
+ enableOpacity: props.enableOpacity !== undefined ? props.enableOpacity : baseComponent.enableOpacity,
471
+ showNoColor: props.showNoColor !== undefined ? props.showNoColor : baseComponent.showNoColor,
472
+ showButtons: props.showButtons !== undefined ? props.showButtons : baseComponent.showButtons,
473
+ showRecentColors: props.showRecentColors !== undefined ? props.showRecentColors : baseComponent.showRecentColors,
474
+ // Signature-specific properties
475
+ backgroundColor: props.backgroundColor !== undefined ? props.backgroundColor : baseComponent.backgroundColor,
476
+ strokeColor: props.strokeColor !== undefined ? props.strokeColor : baseComponent.strokeColor,
477
+ maximumStrokeWidth: props.maximumStrokeWidth !== undefined ? props.maximumStrokeWidth : baseComponent.maximumStrokeWidth,
478
+ minimumStrokeWidth: props.minimumStrokeWidth !== undefined ? props.minimumStrokeWidth : baseComponent.minimumStrokeWidth,
479
+ velocity: props.velocity !== undefined ? props.velocity : baseComponent.velocity,
480
+ // Uploader-specific properties
481
+ allowExtensions: props.allowExtensions !== undefined ? props.allowExtensions : baseComponent.allowExtensions,
482
+ saveUrl: props.saveUrl !== undefined ? props.saveUrl : baseComponent.saveUrl,
483
+ removeUrl: props.removeUrl !== undefined ? props.removeUrl : baseComponent.removeUrl,
484
+ buttons: props.buttons !== undefined ? props.buttons : baseComponent.buttons,
485
+ allowMultiple: props.allowMultiple !== undefined ? props.allowMultiple : baseComponent.allowMultiple,
486
+ showFileList: props.showFileList !== undefined ? props.showFileList : baseComponent.showFileList,
487
+ // Slider-specific properties
488
+ orientation: props.orientation !== undefined ? props.orientation : baseComponent.orientation,
489
+ rows: props.rows !== undefined ? props.rows : baseComponent.rows,
490
+ columns: props.columns !== undefined ? props.columns : baseComponent.columns,
491
+ lookUpConfig: props.lookUpConfig !== undefined ? props.lookUpConfig : baseComponent.lookUpConfig,
492
+ enableImportFromWord: props.enableImportFromWord !== undefined ? props.enableImportFromWord : baseComponent.enableImportFromWord,
493
+ enableExportToWord: props.enableExportToWord !== undefined ? props.enableExportToWord : baseComponent.enableExportToWord,
494
+ enableExportToPdf: props.enableExportToPdf !== undefined ? props.enableExportToPdf : baseComponent.enableExportToPdf,
495
+ tableCells: baseComponent.type === 'table'
496
+ ? (() => {
497
+ const newRows = props.rows !== undefined ? props.rows : (baseComponent.rows || 2);
498
+ const newCols = props.columns !== undefined ? props.columns : (baseComponent.columns || 2);
499
+ const existingCells = baseComponent.tableCells || [];
500
+ return Array.from({ length: newRows }, (_, rowIndex) => Array.from({ length: newCols }, (_, colIndex) => {
501
+ return existingCells[rowIndex]?.[colIndex] || [];
502
+ }));
503
+ })()
504
+ : undefined,
505
+ tabItems: baseComponent.type === 'tabs'
506
+ ? (() => {
507
+ const tabHeaders = props.tabOptions !== undefined
508
+ ? props.tabOptions
509
+ : (baseComponent.tabItems?.map(tab => tab.header) || ['Tab 1']);
510
+ const existingTabs = baseComponent.tabItems || [];
511
+ return tabHeaders.map((header, index) => {
512
+ // ✅ CORRECT: Match by index position instead of header text to preserve children
513
+ const existingTab = existingTabs[index];
514
+ return {
515
+ header: header,
516
+ content: existingTab?.content || []
517
+ };
518
+ });
519
+ })()
520
+ : undefined,
521
+ columnWidths: (props.columnWidths?.widths ?? baseComponent.columnWidths?.widths)?.every((w) => w === null)
522
+ ? undefined
523
+ : (props.columnWidths ?? baseComponent.columnWidths),
524
+ // Validation properties - keep at root level for FormComponent interface
525
+ required: props.required !== undefined ? props.required : (baseComponent?.required || false),
526
+ validateOn: props.validateOn !== undefined ? props.validateOn : (baseComponent?.validateOn || 'Change'),
527
+ min: props.minValue !== undefined ? props.minValue : baseComponent?.min,
528
+ max: props.maxValue !== undefined ? props.maxValue : baseComponent?.max,
529
+ minValue: props.minValue !== undefined ? props.minValue : baseComponent?.minValue,
530
+ maxValue: props.maxValue !== undefined ? props.maxValue : baseComponent?.maxValue,
531
+ minMaxRange: props.minMaxRange !== undefined ? props.minMaxRange : baseComponent?.minMaxRange,
532
+ minMaxTimeRange: props.minMaxTimeRange !== undefined ? props.minMaxTimeRange : baseComponent?.minMaxTimeRange,
533
+ minLength: props.minLength !== undefined ? props.minLength : baseComponent?.minLength,
534
+ maxLength: props.maxLength !== undefined ? props.maxLength : baseComponent?.maxLength,
535
+ minDate: props.minDate !== undefined ? props.minDate : baseComponent?.minDate,
536
+ maxDate: props.maxDate !== undefined ? props.maxDate : baseComponent?.maxDate,
537
+ regex: props.regex !== undefined ? props.regex : baseComponent?.regex,
538
+ minTime: props.minTime !== undefined ? props.minTime : baseComponent?.minTime,
539
+ maxTime: props.maxTime !== undefined ? props.maxTime : baseComponent?.maxTime,
540
+ validationMessage: props.validationMessage !== undefined ? props.validationMessage : baseComponent?.validationMessage,
541
+ // Mark as configured to prevent dialog from opening again (internal flag, not stored in output)
542
+ _configured: true
543
+ };
544
+ return updatedComponent;
545
+ };
546
+ if (editingField) {
547
+ // Update existing field with new properties
548
+ const mappedField = mapPropertiesToComponent(editingField, properties);
549
+ const updatedField = filterComponentProperties(mappedField);
550
+ const updateComponentById = (components, targetId, replacement) => {
551
+ return components.map(comp => {
552
+ if (comp.id === targetId)
553
+ return replacement;
554
+ const updatedComp = { ...comp };
555
+ if (comp.children) {
556
+ updatedComp.children = updateComponentById(comp.children, targetId, replacement);
557
+ }
558
+ if (comp.tabItems) {
559
+ updatedComp.tabItems = comp.tabItems.map(tab => ({
560
+ ...tab,
561
+ content: tab.content ? updateComponentById(tab.content, targetId, replacement) : tab.content
562
+ }));
563
+ }
564
+ if (comp.tableCells) {
565
+ updatedComp.tableCells = comp.tableCells.map(row => row.map(cell => updateComponentById(cell, targetId, replacement)));
566
+ }
567
+ return updatedComp;
568
+ });
569
+ };
570
+ // Update form schema immutably
571
+ setFormSchema(prev => ({
572
+ ...prev,
573
+ components: updateComponentById(prev.components, editingField.id, updatedField)
574
+ }));
575
+ return;
576
+ }
577
+ // If this was a widget switch flow, update the existing component in-place
578
+ if (pendingWidgetSwitch) {
579
+ const mappedField = mapPropertiesToComponent(pendingComponent, properties);
580
+ const updatedField = filterComponentProperties(mappedField);
581
+ const updateComponentById = (components) => {
582
+ return components.map(comp => {
583
+ if (comp.id === pendingWidgetSwitch.fieldId)
584
+ return updatedField;
585
+ const updatedComp = { ...comp };
586
+ if (comp.children) {
587
+ updatedComp.children = updateComponentById(comp.children);
588
+ }
589
+ if (comp.tabItems) {
590
+ updatedComp.tabItems = comp.tabItems.map(tab => ({
591
+ ...tab,
592
+ content: tab.content ? updateComponentById(tab.content) : []
593
+ }));
594
+ }
595
+ if (comp.tableCells) {
596
+ updatedComp.tableCells = comp.tableCells.map(row => row.map(cell => updateComponentById(cell)));
597
+ }
598
+ return updatedComp;
599
+ });
600
+ };
601
+ setTimeout(() => {
602
+ setFormSchema(prev => ({
603
+ ...prev,
604
+ components: updateComponentById(prev.components)
605
+ }));
606
+ }, 100);
607
+ setPendingWidgetSwitch(null);
608
+ setEditingField(null);
609
+ setPendingDropContext(null);
610
+ return;
611
+ }
612
+ // Map collected properties to FormComponent fields for new component
613
+ const mappedComponent = mapPropertiesToComponent(pendingComponent, properties);
614
+ const componentWithProperties = filterComponentProperties(mappedComponent);
615
+ // Add to form based on drop context
616
+ if (pendingDropContext?.isNested && pendingDropContext.parentId) {
617
+ handleNestedComponentDrop(pendingDropContext.parentId, componentWithProperties, pendingDropContext.cellInfo, pendingDropContext.parentPath);
618
+ }
619
+ else {
620
+ // Insert before submit button only if submit is at the last position (not moved by user)
621
+ setFormSchema(prev => {
622
+ const submitIndex = prev.components.findIndex(c => c.type === 'button' && c.buttonType === 'submit');
623
+ // If submit exists and is at the last position, insert before it
624
+ if (submitIndex !== -1 && submitIndex === prev.components.length - 1) {
625
+ const newComponents = [...prev.components];
626
+ newComponents.splice(submitIndex, 0, componentWithProperties);
627
+ return { ...prev, components: newComponents };
628
+ }
629
+ // Otherwise (submit moved or doesn't exist), add at end
630
+ return { ...prev, components: [...prev.components, componentWithProperties] };
631
+ });
632
+ }
633
+ setPendingDropContext(null);
634
+ }, [pendingComponent, pendingDropContext, editingField, pendingWidgetSwitch, setFormSchema]);
635
+ const handleNestedComponentDrop = (parentId, component, cellInfo, parentPath, dropTargetIndex, dropPosition) => {
636
+ // Check if this component type has property configuration and hasn't been configured yet
637
+ if (componentProperties[component.type] && !component._configured) {
638
+ // Show property dialog in nested context
639
+ setPendingComponent(component);
640
+ setEditingField(component);
641
+ setPendingDropContext({
642
+ isNested: true,
643
+ parentId,
644
+ cellInfo,
645
+ parentPath,
646
+ targetIndex: dropTargetIndex,
647
+ targetPosition: dropPosition
648
+ });
649
+ }
650
+ setPendingComponent(component);
651
+ setEditingField(component);
652
+ // Calculate insert index for drop position
653
+ let insertIndex;
654
+ if (dropTargetIndex !== null && dropTargetIndex !== undefined && dropPosition) {
655
+ insertIndex = dropPosition === 'before' ? dropTargetIndex : dropTargetIndex + 1;
656
+ }
657
+ const updateNestedComponent = (components, targetId, newComp, info, idx) => {
658
+ return components.map(comp => {
659
+ if (comp.id === targetId) {
660
+ // Handle panel/tabs with children array
661
+ if (comp.children && !info) {
662
+ if (idx !== undefined) {
663
+ const newChildren = [...comp.children];
664
+ newChildren.splice(idx, 0, newComp);
665
+ return { ...comp, children: newChildren };
666
+ }
667
+ return {
668
+ ...comp,
669
+ children: [...comp.children, newComp]
670
+ };
671
+ }
672
+ // Handle table with cell-specific drops
673
+ if (comp.tableCells && info && info.row !== undefined && info.col !== undefined && info.tabIndex === undefined) {
674
+ const newTableCells = comp.tableCells.map((row, rowIndex) => row.map((cell, colIndex) => {
675
+ if (rowIndex === info.row && colIndex === info.col) {
676
+ if (idx !== undefined) {
677
+ const newCell = [...cell];
678
+ newCell.splice(idx, 0, newComp);
679
+ return newCell;
680
+ }
681
+ return [...cell, newComp];
682
+ }
683
+ return cell;
684
+ }));
685
+ return {
686
+ ...comp,
687
+ tableCells: newTableCells
688
+ };
689
+ }
690
+ // Handle tabs with tab-specific drops
691
+ if (comp.tabItems && info?.tabIndex !== undefined) {
692
+ const newTabItems = comp.tabItems.map((tab, index) => {
693
+ if (index === info.tabIndex) {
694
+ if (idx !== undefined) {
695
+ const newContent = [...(tab.content || [])];
696
+ newContent.splice(idx, 0, newComp);
697
+ return {
698
+ ...tab,
699
+ content: newContent
700
+ };
701
+ }
702
+ return {
703
+ ...tab,
704
+ content: [...(tab.content || []), newComp]
705
+ };
706
+ }
707
+ return tab;
708
+ });
709
+ return {
710
+ ...comp,
711
+ tabItems: newTabItems
712
+ };
713
+ }
714
+ }
715
+ if (comp.tabItems) {
716
+ const newTabItems = comp.tabItems.map(tab => ({
717
+ ...tab,
718
+ content: tab.content ? updateNestedComponent(tab.content, targetId, newComp, info, idx) : []
719
+ }));
720
+ return {
721
+ ...comp,
722
+ tabItems: newTabItems
723
+ };
724
+ }
725
+ // Recursively search in nested components
726
+ if (comp.children) {
727
+ return {
728
+ ...comp,
729
+ children: updateNestedComponent(comp.children, targetId, newComp, info, idx)
730
+ };
731
+ }
732
+ if (comp.tableCells) {
733
+ const newTableCells = comp.tableCells.map(row => row.map(cell => updateNestedComponent(cell, targetId, newComp, info, idx)));
734
+ return {
735
+ ...comp,
736
+ tableCells: newTableCells
737
+ };
738
+ }
739
+ return comp;
740
+ });
741
+ };
742
+ setFormSchema(prev => ({
743
+ ...prev,
744
+ components: updateNestedComponent(prev.components, parentId, component, cellInfo, insertIndex)
745
+ }));
746
+ };
747
+ const handleReorder = (newComponents) => {
748
+ setFormSchema(prev => ({
749
+ ...prev,
750
+ components: newComponents
751
+ }));
752
+ };
753
+ const handleNestedReorder = (parentId, parentPath, newChildren, cellInfo) => {
754
+ const updateNestedChildren = (components, targetPath, updatedChildren, info) => {
755
+ return components.map(comp => {
756
+ const currentPath = comp.parentPath ? `${comp.parentPath}.${comp.id}` : comp.id;
757
+ // Check if this is the target component
758
+ if (currentPath === targetPath || comp.id === parentId) {
759
+ // Handle panel children
760
+ if (comp.children && !info) {
761
+ return {
762
+ ...comp,
763
+ children: updatedChildren
764
+ };
765
+ }
766
+ // Handle table cell children
767
+ if (comp.tableCells && info && info.row !== undefined && info.col !== undefined && info.tabIndex === undefined) {
768
+ const newTableCells = comp.tableCells.map((row, rowIndex) => row.map((cell, colIndex) => {
769
+ if (rowIndex === info.row && colIndex === info.col) {
770
+ return updatedChildren;
771
+ }
772
+ return cell;
773
+ }));
774
+ return {
775
+ ...comp,
776
+ tableCells: newTableCells
777
+ };
778
+ }
779
+ // Handle tab content children
780
+ if (comp.tabItems && info && info.tabIndex !== undefined) {
781
+ const newTabItems = comp.tabItems.map((tab, index) => {
782
+ if (index === info.tabIndex) {
783
+ return {
784
+ ...tab,
785
+ content: updatedChildren
786
+ };
787
+ }
788
+ return tab;
789
+ });
790
+ return {
791
+ ...comp,
792
+ tabItems: newTabItems
793
+ };
794
+ }
795
+ }
796
+ if (comp.tableCells) {
797
+ const newTableCells = comp.tableCells.map(row => row.map(cell => updateNestedChildren(cell, targetPath, updatedChildren, info)));
798
+ return {
799
+ ...comp,
800
+ tableCells: newTableCells
801
+ };
802
+ }
803
+ if (comp.tabItems) {
804
+ const newTabItems = comp.tabItems.map(tab => ({
805
+ ...tab,
806
+ content: tab.content ? updateNestedChildren(tab.content, targetPath, updatedChildren, info) : []
807
+ }));
808
+ return {
809
+ ...comp,
810
+ tabItems: newTabItems
811
+ };
812
+ }
813
+ // Recursively search in nested structures
814
+ if (comp.children) {
815
+ return {
816
+ ...comp,
817
+ children: updateNestedChildren(comp.children, targetPath, updatedChildren, info)
818
+ };
819
+ }
820
+ return comp;
821
+ });
822
+ };
823
+ setFormSchema(prev => ({
824
+ ...prev,
825
+ components: updateNestedChildren(prev.components, parentPath, newChildren, cellInfo)
826
+ }));
827
+ };
828
+ // T024: Handle Copy action - copy to system clipboard
829
+ const [hasClipboardData, setHasClipboardData] = React.useState(false);
830
+ // In-memory cache of the copied field so paste never needs to read the
831
+ // system clipboard (avoids triggering the browser's native paste UI in
832
+ // Firefox, which was firing on the first drag-drop operation).
833
+ const copiedFieldRef = React.useRef(null);
834
+ const handleCopy = React.useCallback(async (field) => {
835
+ try {
836
+ await ClipboardHelper.copyToClipboard(field);
837
+ console.log('Field copied to system clipboard:', field.id);
838
+ copiedFieldRef.current = field;
839
+ setHasClipboardData(true);
840
+ }
841
+ catch (error) {
842
+ console.error('Copy failed:', error);
843
+ }
844
+ }, []);
845
+ // T025-T026: Handle Paste action - paste from system clipboard AFTER target field
846
+ const handlePaste = React.useCallback(async (targetFieldId) => {
847
+ try {
848
+ // Use the in-memory copy instead of reading the system clipboard.
849
+ // Reading navigator.clipboard.readText() at runtime in Firefox triggers
850
+ // the browser's native paste UI prompt (esp. on first invocation after
851
+ // drag-drop), so we avoid it entirely.
852
+ const copiedField = copiedFieldRef.current;
853
+ if (!copiedField) {
854
+ setHasClipboardData(false);
855
+ return;
856
+ }
857
+ // Remap IDs for layout components and all their descendants
858
+ const remapIds = (comp) => {
859
+ const newId = generateId(comp.type);
860
+ const newName = generateName(comp.type);
861
+ const remapped = {
862
+ ...comp,
863
+ id: newId,
864
+ name: newName
865
+ };
866
+ if (comp.children) {
867
+ remapped.children = comp.children.map(child => remapIds(child));
868
+ }
869
+ if (comp.tabItems) {
870
+ remapped.tabItems = comp.tabItems.map(tab => ({
871
+ ...tab,
872
+ content: tab.content ? tab.content.map(child => remapIds(child)) : []
873
+ }));
874
+ }
875
+ if (comp.tableCells) {
876
+ remapped.tableCells = comp.tableCells.map(row => row.map(cell => cell.map(child => remapIds(child))));
877
+ }
878
+ return remapped;
879
+ };
880
+ // Create a unique field to insert (remap nested ids if layout)
881
+ let newField = ClipboardHelper.createUniqueField(copiedField);
882
+ // Always generate new ID and name (for both layout and regular components)
883
+ newField = {
884
+ ...newField,
885
+ id: generateId(newField.type),
886
+ name: generateName(newField.type)
887
+ };
888
+ // For layout components, also remap all nested children
889
+ if (['panel', 'tabs', 'table', 'card'].includes(newField.type)) {
890
+ newField = remapIds(newField);
891
+ }
892
+ // Recursive inserter: inserts newComp AFTER the component with targetId
893
+ const insertAfterTarget = (components, targetId) => {
894
+ const out = [];
895
+ for (let i = 0; i < components.length; i++) {
896
+ const comp = components[i];
897
+ // Direct match at this level
898
+ out.push(comp);
899
+ if (comp.id === targetId) {
900
+ out.push(newField);
901
+ // append remaining items
902
+ out.push(...components.slice(i + 1));
903
+ return { components: out, inserted: true };
904
+ }
905
+ // Search in children (panel)
906
+ if (comp.children) {
907
+ const { components: updatedChildren, inserted } = insertAfterTarget(comp.children, targetId);
908
+ if (inserted) {
909
+ const updatedComp = { ...comp, children: updatedChildren };
910
+ // replace last pushed comp with updatedComp
911
+ out[out.length - 1] = updatedComp;
912
+ out.push(...components.slice(i + 1));
913
+ return { components: out, inserted: true };
914
+ }
915
+ }
916
+ // Search in tabItems content
917
+ if (comp.tabItems) {
918
+ let tabInserted = false;
919
+ const newTabItems = comp.tabItems.map(tab => {
920
+ if (!tab.content)
921
+ return tab;
922
+ const { components: updatedContent, inserted } = insertAfterTarget(tab.content, targetId);
923
+ if (inserted)
924
+ tabInserted = true;
925
+ return { ...tab, content: updatedContent };
926
+ });
927
+ if (tabInserted) {
928
+ const updatedComp = { ...comp, tabItems: newTabItems };
929
+ out[out.length - 1] = updatedComp;
930
+ out.push(...components.slice(i + 1));
931
+ return { components: out, inserted: true };
932
+ }
933
+ }
934
+ // Search in tableCells (rows -> cells -> component arrays)
935
+ if (comp.tableCells) {
936
+ let tableInserted = false;
937
+ const newTableCells = comp.tableCells.map((row) => row.map((cell) => {
938
+ const { components: updatedCell, inserted } = insertAfterTarget(cell, targetId);
939
+ if (inserted)
940
+ tableInserted = true;
941
+ return updatedCell;
942
+ }));
943
+ if (tableInserted) {
944
+ const updatedComp = { ...comp, tableCells: newTableCells };
945
+ out[out.length - 1] = updatedComp;
946
+ out.push(...components.slice(i + 1));
947
+ return { components: out, inserted: true };
948
+ }
949
+ }
950
+ }
951
+ return { components: out, inserted: false };
952
+ };
953
+ setFormSchema(prev => {
954
+ const { components: updated, inserted } = insertAfterTarget(prev.components, targetFieldId);
955
+ if (!inserted) {
956
+ // fallback: try to insert at root after target if present
957
+ const rootIndex = prev.components.findIndex(c => c.id === targetFieldId);
958
+ if (rootIndex !== -1) {
959
+ const newComponents = [...prev.components];
960
+ newComponents.splice(rootIndex + 1, 0, newField);
961
+ return { ...prev, components: newComponents };
962
+ }
963
+ console.warn('Paste target not found:', targetFieldId);
964
+ return prev;
965
+ }
966
+ return { ...prev, components: updated };
967
+ });
968
+ // visual feedback (unchanged)
969
+ setTimeout(() => {
970
+ const wrapper = document.querySelector(`[data-field-id="${newField.id}"]`);
971
+ if (wrapper) {
972
+ wrapper.classList.add('field-inserted');
973
+ setTimeout(() => wrapper.classList.remove('field-inserted'), 2000);
974
+ }
975
+ }, 50);
976
+ // Clear the system clipboard and in-memory copy so the paste button
977
+ // is hidden. This signals to the user that a fresh copy is needed
978
+ // before pasting again.
979
+ copiedFieldRef.current = null;
980
+ setHasClipboardData(false);
981
+ ClipboardHelper.clearData().catch(() => { });
982
+ }
983
+ catch (error) {
984
+ console.error('Paste failed:', error);
985
+ }
986
+ }, []);
987
+ // T017-T018: Handle Delete action with focus management
988
+ const handleDeleteAction = React.useCallback((fieldId) => {
989
+ setFormSchema(prev => ({
990
+ ...prev,
991
+ components: prev.components.filter(c => c.id !== fieldId)
992
+ }));
993
+ // Move focus to canvas container after delete
994
+ setTimeout(() => {
995
+ const canvas = document.querySelector('.form-preview-area');
996
+ canvas?.focus();
997
+ }, 0);
998
+ }, []);
999
+ const isDescendantOf = (parentId, childId, components) => {
1000
+ const stack = [...components];
1001
+ while (stack.length) {
1002
+ const comp = stack.pop();
1003
+ if (comp.id === parentId) {
1004
+ // Found parent, now search for child within it
1005
+ const searchInChildren = (items) => {
1006
+ for (const item of items) {
1007
+ if (item.id === childId)
1008
+ return true;
1009
+ if (item.children && searchInChildren(item.children))
1010
+ return true;
1011
+ if (item.tabItems) {
1012
+ for (const tab of item.tabItems) {
1013
+ if (tab.content && searchInChildren(tab.content))
1014
+ return true;
1015
+ }
1016
+ }
1017
+ if (item.tableCells) {
1018
+ for (const row of item.tableCells) {
1019
+ for (const cell of row) {
1020
+ if (searchInChildren(cell))
1021
+ return true;
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ return false;
1027
+ };
1028
+ return searchInChildren(comp.children || []) ||
1029
+ (comp.tabItems?.some(t => searchInChildren(t.content || [])) || false) ||
1030
+ (comp.tableCells?.some(row => row.some(cell => searchInChildren(cell))) || false);
1031
+ }
1032
+ if (comp.children?.length)
1033
+ stack.push(...comp.children);
1034
+ if (comp.tabItems?.length) {
1035
+ for (const tab of comp.tabItems) {
1036
+ if (tab.content?.length)
1037
+ stack.push(...tab.content);
1038
+ }
1039
+ }
1040
+ if (comp.tableCells?.length) {
1041
+ for (const row of comp.tableCells) {
1042
+ for (const cell of row) {
1043
+ if (cell?.length)
1044
+ stack.push(...cell);
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+ return false;
1050
+ };
1051
+ const handleDelete = React.useCallback((fieldId, isReorering) => {
1052
+ isReorderingRef.current = isReorering || false;
1053
+ // Recursive function to remove component from nested structures
1054
+ const removeComponentById = (components) => {
1055
+ return components
1056
+ .filter(c => c.id !== fieldId)
1057
+ .map(c => {
1058
+ // If component has tabs, recursively remove from tab content
1059
+ if (c.tabItems) {
1060
+ return {
1061
+ ...c,
1062
+ tabItems: c.tabItems.map(tab => ({
1063
+ ...tab,
1064
+ content: tab.content ? removeComponentById(tab.content) : []
1065
+ }))
1066
+ };
1067
+ }
1068
+ // If component has children (panel), recursively remove from children
1069
+ if (c.children) {
1070
+ return { ...c, children: removeComponentById(c.children) };
1071
+ }
1072
+ // If component has table cells, recursively remove from cells
1073
+ if (c.tableCells) {
1074
+ return {
1075
+ ...c,
1076
+ tableCells: c.tableCells.map(row => row.map(cell => removeComponentById(cell)))
1077
+ };
1078
+ }
1079
+ return c;
1080
+ });
1081
+ };
1082
+ setFormSchema(prev => ({
1083
+ ...prev,
1084
+ components: removeComponentById(prev.components)
1085
+ }));
1086
+ setPendingComponent(prev => (prev && (prev.id === fieldId || isDescendantOf(fieldId, prev.id, formSchema.components)) && !isReorering)
1087
+ ? null
1088
+ : prev);
1089
+ setEditingField(prev => (prev && (prev.id === fieldId || isDescendantOf(fieldId, prev.id, formSchema.components)) && !isReorering)
1090
+ ? null
1091
+ : prev);
1092
+ // Move focus to canvas container after delete
1093
+ setTimeout(() => {
1094
+ const canvas = document.querySelector('.form-preview-area');
1095
+ canvas?.focus();
1096
+ }, 0);
1097
+ setTimeout(() => {
1098
+ isReorderingRef.current = false;
1099
+ }, 500);
1100
+ }, [pendingComponent, editingField, setFormSchema]);
1101
+ const handleLeftPaneToggle = useCallback(() => {
1102
+ if (isMobile && !isRightPaneCollapsed) {
1103
+ setIsRightPaneCollapsed(true);
1104
+ }
1105
+ setIsLeftPaneCollapsed(prev => !prev);
1106
+ }, [isMobile, isRightPaneCollapsed]);
1107
+ const handleSidebarOverlayClick = useCallback(() => {
1108
+ if (!isLeftPaneCollapsed) {
1109
+ setIsLeftPaneCollapsed(true);
1110
+ }
1111
+ if (!isRightPaneCollapsed) {
1112
+ setIsRightPaneCollapsed(true);
1113
+ }
1114
+ }, [isLeftPaneCollapsed, isRightPaneCollapsed]);
1115
+ const handleRightPaneToggle = useCallback(() => {
1116
+ if (isMobile && !isLeftPaneCollapsed) {
1117
+ setIsLeftPaneCollapsed(true);
1118
+ }
1119
+ setIsRightPaneCollapsed(prev => !prev);
1120
+ }, [isMobile, isLeftPaneCollapsed]);
1121
+ const handleLeftPaneMouseEnter = useCallback(() => {
1122
+ if (leftPaneHoverTimeoutRef.current) {
1123
+ clearTimeout(leftPaneHoverTimeoutRef.current);
1124
+ }
1125
+ if (isLeftPaneCollapsed) {
1126
+ leftPaneHoverTimeoutRef.current = setTimeout(() => {
1127
+ if (!isMobile) {
1128
+ setIsLeftPaneHovered(true);
1129
+ }
1130
+ }, 500);
1131
+ }
1132
+ }, [isLeftPaneCollapsed]);
1133
+ const handleLeftPaneMouseLeave = useCallback(() => {
1134
+ if (leftPaneHoverTimeoutRef.current) {
1135
+ clearTimeout(leftPaneHoverTimeoutRef.current);
1136
+ }
1137
+ setIsLeftPaneHovered(false);
1138
+ }, []);
1139
+ const handleRightPaneMouseEnter = useCallback(() => {
1140
+ if (rightPaneHoverTimeoutRef.current) {
1141
+ clearTimeout(rightPaneHoverTimeoutRef.current);
1142
+ }
1143
+ if (isRightPaneCollapsed) {
1144
+ rightPaneHoverTimeoutRef.current = setTimeout(() => {
1145
+ if (!isMobile) {
1146
+ setIsRightPaneHovered(true);
1147
+ }
1148
+ }, 500);
1149
+ }
1150
+ }, [isRightPaneCollapsed, isMobile]);
1151
+ const handleRightPaneMouseLeave = useCallback(() => {
1152
+ if (rightPaneHoverTimeoutRef.current) {
1153
+ clearTimeout(rightPaneHoverTimeoutRef.current);
1154
+ }
1155
+ setIsRightPaneHovered(false);
1156
+ }, []);
1157
+ const splitData = React.useMemo(() => splitSchema(formSchema), [formSchema]);
1158
+ const downloadCombinedSchema = async () => {
1159
+ try {
1160
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
1161
+ const blob = new Blob([JSON.stringify(splitData, null, 2)], { type: 'application/json' });
1162
+ const url = URL.createObjectURL(blob);
1163
+ const link = document.createElement('a');
1164
+ link.href = url;
1165
+ link.download = `form-schema-${timestamp}.json`;
1166
+ document.body.appendChild(link);
1167
+ link.click();
1168
+ link.remove();
1169
+ URL.revokeObjectURL(url);
1170
+ }
1171
+ catch (err) {
1172
+ console.error('Download failed', err);
1173
+ }
1174
+ };
1175
+ const modeItems = [
1176
+ { text: l10n.getConstant('simple'), id: 'simple' },
1177
+ { text: l10n.getConstant('developer'), id: 'developer' }
1178
+ ];
1179
+ const handleModeSelect = (args) => {
1180
+ const selectedMode = args.item.id === 'simple' ? 'Simple' : 'Developer';
1181
+ const url = new URL(window.location.href);
1182
+ const currentUrlMode = url.searchParams.get('mode');
1183
+ if (currentUrlMode === selectedMode.toLowerCase()) {
1184
+ return;
1185
+ }
1186
+ url.searchParams.set('mode', selectedMode.toLowerCase());
1187
+ window.location.href = url.toString();
1188
+ };
1189
+ useEffect(() => {
1190
+ const element = builderContainerRef.current;
1191
+ if (!element)
1192
+ return;
1193
+ const updateWidth = () => {
1194
+ setContainerWidth(element.getBoundingClientRect().width);
1195
+ };
1196
+ updateWidth();
1197
+ const resizeObserver = new ResizeObserver(() => {
1198
+ updateWidth();
1199
+ });
1200
+ resizeObserver.observe(element);
1201
+ return () => {
1202
+ resizeObserver.disconnect();
1203
+ };
1204
+ }, []);
1205
+ useEffect(() => {
1206
+ setIsLeftPaneCollapsed(containerWidth <= 820);
1207
+ setIsRightPaneCollapsed(containerWidth <= 1024);
1208
+ setIsMobile(containerWidth <= 1024);
1209
+ }, [containerWidth]);
1210
+ useEffect(() => {
1211
+ if (isMobile) {
1212
+ if (pendingComponent && isRightPaneCollapsed && !isReorderingRef.current) {
1213
+ handleRightPaneToggle();
1214
+ }
1215
+ if (!pendingComponent && !isRightPaneCollapsed) {
1216
+ handleRightPaneToggle();
1217
+ }
1218
+ }
1219
+ }, [isMobile, pendingComponent]);
1220
+ useEffect(() => {
1221
+ const element = contentSectionRef.current;
1222
+ const preventTouchScroll = (e) => {
1223
+ e.preventDefault();
1224
+ };
1225
+ element?.addEventListener("touchmove", preventTouchScroll, {
1226
+ passive: false,
1227
+ });
1228
+ return () => {
1229
+ element?.removeEventListener("touchmove", preventTouchScroll);
1230
+ };
1231
+ }, []);
1232
+ return (_jsxs("div", { "aria-label": "Form Builder", ref: builderContainerRef, className: `builder-container ${enableRtl ? 'e-rtl' : ''} ${formBuilderMode === 'component' ? 'e-component' : ''} ${containerWidth <= 820 ? 'small-screen' : ''}`, children: [_jsxs("div", { className: `main-header`, children: [_jsxs("div", { className: "left-section", children: [isMobile && _jsx(ButtonComponent, { cssClass: 'e-flat', iconCss: 'e-icons e-show-hide-panel', onClick: handleLeftPaneToggle, "aria-label": "Open component palette" }), formBuilderMode === 'sass' && !isMobile &&
1233
+ _jsx(ButtonComponent, { cssClass: 'action-btn e-flat', iconCss: 'e-icons e-arrow-head', onClick: () => { navigateTo('/dashboard'); }, "aria-label": "Back to dashboard" }), !isMobile && _jsx(InPlaceEditorComponent, { type: 'Text', mode: 'Inline', value: formSchema.settings?.name || 'Untitled Form', change: (e) => {
1234
+ if (formSchema.settings) {
1235
+ formSchema.settings.name = e.value;
1236
+ setFormSchema({ ...formSchema });
1237
+ }
1238
+ } })] }), _jsxs("div", { className: "right-section", children: [_jsx(DropDownButtonComponent, { items: modeItems, select: handleModeSelect, cssClass: "e-outline mode-button", popupWidth: "100%", "aria-label": l10n.getConstant(`${mode?.toLowerCase()}Mode`), children: l10n.getConstant(`${mode?.toLowerCase()}${!isMobile ? 'Mode' : ''}`) }), allowExport && _jsxs(_Fragment, { children: [_jsx("div", { className: 'separator' }), _jsx(ButtonComponent, { cssClass: "action-btn e-flat e-primary", onClick: downloadCombinedSchema, disabled: isDefaultFormSchema(formSchema), "aria-label": "Export json schema", children: l10n.getConstant('export') })] }), isMobile && _jsx(ButtonComponent, { cssClass: 'e-flat', iconCss: 'e-icons e-adjustment', onClick: handleRightPaneToggle, "aria-label": "Open properties panel" })] })] }), _jsxs("div", { ref: contentSectionRef, role: "main", className: `contentSection${editingField || pendingWidgetSwitch || pendingComponent ? ' sidebar-open' : ' sidebar-closed'}`, children: [containerWidth <= 820 && (!isLeftPaneCollapsed || !isRightPaneCollapsed) && _jsx("div", { className: "sidebar-overlay", "aria-hidden": "true", onClick: handleSidebarOverlayClick }), _jsxs(DragDrop, { children: [_jsx(LeftPane, { className: `${isLeftPaneCollapsed ? 'collapsed' : 'expanded'}`, templates: formTemplates ?? defaultTemplates, paletteConfig: paletteConfig, mode: mode, onFieldDragStarted: onFieldDragStarted, onDrop: handleComponentDrop, containerWidth: containerWidth }), !isMobile &&
1239
+ _jsxs("button", { className: `e-btn left-pane-toggle ${isLeftPaneCollapsed ? 'collapsed' : 'expanded'}`, onClick: handleLeftPaneToggle, onMouseEnter: handleLeftPaneMouseEnter, onMouseLeave: handleLeftPaneMouseLeave, "aria-label": isLeftPaneCollapsed ? 'Expand component palette' : 'Collapse component palette', "aria-expanded": !isLeftPaneCollapsed, children: [_jsx("span", { "aria-hidden": "true", className: `e-btn-icon e-icons ${enableRtl
1240
+ ? (isLeftPaneCollapsed ? 'e-chevron-left' : 'e-chevron-right')
1241
+ : (isLeftPaneCollapsed ? 'e-chevron-right' : 'e-chevron-left')}` }), isLeftPaneCollapsed && isLeftPaneHovered && _jsx("span", { style: { marginLeft: '8px' }, children: "Expand" })] }, "left-pane-toggle"), _jsx(MainSection, { className: `${isRightPaneCollapsed ? 'right-collapsed' : ''} ${isLeftPaneCollapsed ? 'left-collapsed' : ''}`, formSchema: formSchema, onDrop: handleComponentDrop, activeFieldId: pendingComponent?.id, onNestedDrop: handleNestedComponentDrop, onReorder: handleReorder, onNestedReorder: handleNestedReorder, onEdit: handleEdit, mode: mode, onCopy: handleCopy, onPaste: handlePaste, onDelete: handleDelete, onSwitchWidget: handleSwitchWidget, onDeleteAction: handleDeleteAction, enablePreview: enablePreview, onFieldDragStarted: onFieldDragStarted, onFieldDraggingOver: onFieldDraggingOver, onFieldDropped: onFieldDropped, hasClipboardData: hasClipboardData })] }), !isMobile &&
1242
+ _jsxs("button", { className: `e-btn right-pane-toggle ${isRightPaneCollapsed ? 'collapsed' : 'expanded'}`, onClick: handleRightPaneToggle, onMouseEnter: handleRightPaneMouseEnter, onMouseLeave: handleRightPaneMouseLeave, "aria-label": isRightPaneCollapsed ? 'Expand properties panel' : 'Collapse properties panel', "aria-expanded": !isRightPaneCollapsed, children: [_jsx("span", { "aria-hidden": "true", className: `e-btn-icon e-icons ${enableRtl
1243
+ ? (isRightPaneCollapsed ? 'e-chevron-right' : 'e-chevron-left')
1244
+ : (isRightPaneCollapsed ? 'e-chevron-left' : 'e-chevron-right')}` }), isRightPaneCollapsed && isRightPaneHovered && _jsx("span", { style: { marginLeft: '8px' }, children: "Expand" })] }, "right-pane-toggle"), _jsxs("aside", { role: "complementary", "aria-label": pendingComponent && !isDefaultFormSchema(formSchema)
1245
+ ? 'Component Properties'
1246
+ : 'Form Settings', className: `right-sidebar ${isRightPaneCollapsed ? 'sidebar-closed' : 'expanded'}`, children: [pendingComponent && componentProperties[pendingComponent.type] && !isDefaultFormSchema(formSchema) && (_jsx(RightPane, { mode: 'component', componentType: pendingComponent.type, configData: componentProperties, existingValues: editingField || pendingComponent, onSave: handlePropertyDialogSave, onPropertyChanged: onPropertyChanged, onClose: () => {
1247
+ setPendingComponent(null);
1248
+ setEditingField(null);
1249
+ setPendingWidgetSwitch(null);
1250
+ setPendingDropContext(null);
1251
+ }, builderMode: mode }, `sidebar-${(editingField || pendingComponent).id}`)), (!pendingComponent || isDefaultFormSchema(formSchema)) && (_jsx(RightPane, { componentType: 'form', mode: "formSettings", configData: componentProperties, formSettingsConfig: formSettingsConfig, formSettings: formSchema.settings, onSave: handleFormSettingsSave, onClose: () => { } }, "form-settings"))] })] })] }));
1252
+ };
1253
+ /**
1254
+ * The FormBuilder component provides a visual, drag-and-drop interface for authoring form schemas. It supports palette customization, template insertion, dual modes (Simple/Developer), and emits structural form schemas (FormSchema) that can be consumed by FormRenderer.
1255
+ *
1256
+ * ```typescript
1257
+ * import { FormBuilder } from '@syncfusion/ej2-react-form-builder';
1258
+ *
1259
+ * <FormBuilder />
1260
+ * ```
1261
+ */
1262
+ export const FormBuilder = forwardRef((props, ref) => {
1263
+ const formBuilderRef = useRef(null);
1264
+ const { locale = 'en-US', allowExport = true, enablePreview = true, enablePersistence = false, enableRtl = false, schema, mode = 'Developer', formTemplates, paletteConfig, onFieldDragStarted, onFieldDraggingOver, onFieldDropped, onPropertyChanged, style: userStyle, ...domProps } = props;
1265
+ // Determine mode synchronously from the URL on first render (no extra render cycle)
1266
+ const [effectiveMode] = useState(() => {
1267
+ if (typeof window === 'undefined')
1268
+ return undefined;
1269
+ const urlMode = new URLSearchParams(window.location.search).get('mode');
1270
+ if (urlMode === 'developer')
1271
+ return 'Developer';
1272
+ if (urlMode === 'simple')
1273
+ return 'Simple';
1274
+ return undefined;
1275
+ });
1276
+ useEffect(() => {
1277
+ if (typeof window === 'undefined')
1278
+ return;
1279
+ const params = new URLSearchParams(window.location.search);
1280
+ // Only set mode in URL if a mode is provided via props and URL doesn't have one
1281
+ if (params.get('mode') !== 'developer' && params.get('mode') !== 'simple' && mode) {
1282
+ const url = new URL(window.location.href);
1283
+ url.searchParams.set('mode', mode.toLowerCase());
1284
+ window.history.replaceState({}, '', url.toString());
1285
+ }
1286
+ }, [mode]);
1287
+ const finalMode = effectiveMode || mode;
1288
+ const computedStyle = {
1289
+ ...(formBuilderMode === 'component' ? { height: '700px' } : {}),
1290
+ ...userStyle
1291
+ };
1292
+ const computedSchema = schema
1293
+ ? schema.components !== undefined
1294
+ ? schema
1295
+ : importSplitSchemas(typeof schema === 'string' ? schema : JSON.stringify(schema), defaultFormSchema)
1296
+ : schema;
1297
+ return (_jsx(FormSchemaProvider, { enablePersistence: enablePersistence, initialSchema: computedSchema || defaultFormSchema, mode: finalMode, children: _jsx(LocalizationProvider, { locale: locale, children: _jsx("div", { id: "form-builder", ref: formBuilderRef, style: computedStyle, ...domProps, children: _jsx(FormBuilderComponent, { apiRef: ref, domRef: formBuilderRef, enableRtl: enableRtl, locale: locale, mode: finalMode, enablePersistence: enablePersistence, onFieldDragStarted: onFieldDragStarted, onFieldDraggingOver: onFieldDraggingOver, onFieldDropped: onFieldDropped, onPropertyChanged: onPropertyChanged, allowExport: allowExport, enablePreview: enablePreview, formTemplates: formTemplates, paletteConfig: paletteConfig }) }) }) }));
1298
+ });
1299
+ export default React.memo(FormBuilder);