meno-core 1.0.0
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/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Builder
|
|
3
|
+
* Builds React elements from component tree nodes with support for custom components,
|
|
4
|
+
* responsive styles, event handlers, and element registration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createElement as h } from "react";
|
|
8
|
+
import type { ReactElement } from "react";
|
|
9
|
+
|
|
10
|
+
// Component registry and utilities
|
|
11
|
+
import { ComponentRegistry } from "../componentRegistry";
|
|
12
|
+
import { processStructure } from "../templateEngine";
|
|
13
|
+
import { NODE_TYPE } from "../../shared/constants";
|
|
14
|
+
import { ErrorBoundary } from "../ErrorBoundary";
|
|
15
|
+
import type { ComponentNode, StyleValue } from "../../shared/types";
|
|
16
|
+
import { isComponentNode, extractNodeProperties, isSlotMarker, isEmbedNode, isObjectLinkNode, isLocaleListNode } from "../../shared/nodeUtils";
|
|
17
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
18
|
+
import { mergeNodeStyles } from "../../shared/styleNodeUtils";
|
|
19
|
+
import { extractAttributesFromNode } from "../../shared/attributeNodeUtils";
|
|
20
|
+
import { resolvePropsFromDefinition } from "../../shared/propResolver";
|
|
21
|
+
import { ElementRegistry } from "../elementRegistry";
|
|
22
|
+
import type { Path } from "../../shared/pathArrayUtils";
|
|
23
|
+
import type { I18nConfig } from "../../shared/types/components";
|
|
24
|
+
import { DEFAULT_I18N_CONFIG } from "../../shared/i18n";
|
|
25
|
+
import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
|
|
26
|
+
import { navigateTo } from "../navigation";
|
|
27
|
+
import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
|
|
28
|
+
import { processCMSTemplate, processCMSPropsTemplate } from "./cmsTemplateProcessor";
|
|
29
|
+
|
|
30
|
+
type ComponentProps = Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
export interface ComponentBuilderConfig {
|
|
33
|
+
componentRegistry: ComponentRegistry;
|
|
34
|
+
elementRegistry: ElementRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BuildComponentOptions {
|
|
38
|
+
node: ComponentNode | ComponentNode[] | string | number | null | undefined;
|
|
39
|
+
key?: number;
|
|
40
|
+
customProps?: ComponentProps;
|
|
41
|
+
elementPath?: Path;
|
|
42
|
+
parentComponentName?: string | null;
|
|
43
|
+
viewportWidth?: number;
|
|
44
|
+
componentContext?: string | null;
|
|
45
|
+
locale?: string;
|
|
46
|
+
i18nConfig?: I18nConfig;
|
|
47
|
+
cmsContext?: Record<string, unknown> | null;
|
|
48
|
+
/** CMS locale for i18n field resolution */
|
|
49
|
+
cmsLocale?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ComponentBuilder class for building React elements from component tree nodes
|
|
54
|
+
*/
|
|
55
|
+
export class ComponentBuilder {
|
|
56
|
+
private componentRegistry: ComponentRegistry;
|
|
57
|
+
private elementRegistry: ElementRegistry;
|
|
58
|
+
|
|
59
|
+
constructor(config: ComponentBuilderConfig) {
|
|
60
|
+
this.componentRegistry = config.componentRegistry;
|
|
61
|
+
this.elementRegistry = config.elementRegistry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Helper function to create onClick handlers from JSON-defined onClick values
|
|
66
|
+
*/
|
|
67
|
+
private createOnClickHandler(onClickValue: unknown): () => void {
|
|
68
|
+
return () => {
|
|
69
|
+
alert(String(onClickValue));
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Determines the parent component name for nested component instances.
|
|
75
|
+
* Priority: componentContext (if inside component definition) > parentComponentName (if inside another instance) > componentName (fallback for page-level components)
|
|
76
|
+
*/
|
|
77
|
+
private getParentComponentNameForNestedComponent(
|
|
78
|
+
componentContext: string | null,
|
|
79
|
+
parentComponentName: string | null,
|
|
80
|
+
componentName: string | null
|
|
81
|
+
): string | null {
|
|
82
|
+
return componentContext || parentComponentName || componentName || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines the effective parent component name when building children.
|
|
87
|
+
* Uses componentContext if available, otherwise falls back to parentComponentName.
|
|
88
|
+
*/
|
|
89
|
+
private getEffectiveParentComponentName(
|
|
90
|
+
componentContext: string | null,
|
|
91
|
+
parentComponentName: string | null
|
|
92
|
+
): string | null {
|
|
93
|
+
return componentContext || parentComponentName;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find the path to the slot marker in a component structure
|
|
98
|
+
* This helps determine where children should be placed and what paths they should have
|
|
99
|
+
*/
|
|
100
|
+
private findSlotMarkerPath(structure: ComponentNode | undefined, currentPath: Path = [0]): Path | null {
|
|
101
|
+
if (!structure) return null;
|
|
102
|
+
|
|
103
|
+
// Check if this node has children (SlotMarker doesn't have children)
|
|
104
|
+
if (isSlotMarker(structure)) return null;
|
|
105
|
+
|
|
106
|
+
const nodeChildren = (structure as any).children;
|
|
107
|
+
if (!nodeChildren || !Array.isArray(nodeChildren)) return null;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < nodeChildren.length; i++) {
|
|
110
|
+
const child = nodeChildren[i];
|
|
111
|
+
|
|
112
|
+
// Check if this is a slot marker
|
|
113
|
+
if (isSlotMarker(child)) {
|
|
114
|
+
return getChildPath(currentPath, i);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Recursively search in nested structures
|
|
118
|
+
if (typeof child === 'object' && child !== null && !Array.isArray(child) && 'type' in child) {
|
|
119
|
+
const nestedPath = this.findSlotMarkerPath(child as ComponentNode, getChildPath(currentPath, i));
|
|
120
|
+
if (nestedPath) return nestedPath;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds children elements recursively with proper component context tracking.
|
|
129
|
+
*/
|
|
130
|
+
buildChildren(
|
|
131
|
+
children: (string | ComponentNode)[] | string | ComponentNode | undefined,
|
|
132
|
+
elementPath: Path,
|
|
133
|
+
parentComponentName: string | null,
|
|
134
|
+
viewportWidth: number,
|
|
135
|
+
componentContext: string | null,
|
|
136
|
+
locale?: string,
|
|
137
|
+
i18nConfig?: I18nConfig,
|
|
138
|
+
cmsContext?: Record<string, unknown> | null,
|
|
139
|
+
cmsLocale?: string | null
|
|
140
|
+
): ReactElement | ReactElement[] | string | number | null {
|
|
141
|
+
if (Array.isArray(children)) {
|
|
142
|
+
return children
|
|
143
|
+
.map((child, index) =>
|
|
144
|
+
this.buildComponent({
|
|
145
|
+
node: child,
|
|
146
|
+
key: index,
|
|
147
|
+
customProps: {},
|
|
148
|
+
elementPath: getChildPath(elementPath, index),
|
|
149
|
+
parentComponentName,
|
|
150
|
+
viewportWidth,
|
|
151
|
+
componentContext,
|
|
152
|
+
locale,
|
|
153
|
+
i18nConfig,
|
|
154
|
+
cmsContext,
|
|
155
|
+
cmsLocale
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
.filter((item): item is ReactElement | string | number => item !== null) as ReactElement[];
|
|
159
|
+
}
|
|
160
|
+
return this.buildComponent({
|
|
161
|
+
node: children,
|
|
162
|
+
key: 0,
|
|
163
|
+
customProps: {},
|
|
164
|
+
elementPath: getChildPath(elementPath, 0),
|
|
165
|
+
parentComponentName,
|
|
166
|
+
viewportWidth,
|
|
167
|
+
componentContext,
|
|
168
|
+
locale,
|
|
169
|
+
i18nConfig,
|
|
170
|
+
cmsContext,
|
|
171
|
+
cmsLocale
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Component builder from JSON tree with support for custom components
|
|
177
|
+
* viewportWidth is passed down to resolve responsive styles based on actual viewport
|
|
178
|
+
*/
|
|
179
|
+
buildComponent(options: BuildComponentOptions): ReactElement | ReactElement[] | string | number | null {
|
|
180
|
+
const {
|
|
181
|
+
node,
|
|
182
|
+
key = 0,
|
|
183
|
+
customProps = {},
|
|
184
|
+
elementPath = [0],
|
|
185
|
+
parentComponentName = null,
|
|
186
|
+
viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1920,
|
|
187
|
+
componentContext = null,
|
|
188
|
+
locale,
|
|
189
|
+
i18nConfig,
|
|
190
|
+
cmsContext = null,
|
|
191
|
+
cmsLocale = null
|
|
192
|
+
} = options;
|
|
193
|
+
|
|
194
|
+
if (!node) return null;
|
|
195
|
+
|
|
196
|
+
// Handle text nodes - process CMS templates if context is available
|
|
197
|
+
if (typeof node === 'string') {
|
|
198
|
+
if (cmsContext && node.includes('{{cms.')) {
|
|
199
|
+
// Use cmsLocale for i18n resolution if available, otherwise use page locale
|
|
200
|
+
return processCMSTemplate(node, cmsContext, cmsLocale || locale);
|
|
201
|
+
}
|
|
202
|
+
return node;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof node === 'number') {
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Array.isArray(node)) {
|
|
210
|
+
const result = node.map((child, index) => {
|
|
211
|
+
const childPath = getChildPath(elementPath, index);
|
|
212
|
+
return this.buildComponent({
|
|
213
|
+
node: child,
|
|
214
|
+
key: index,
|
|
215
|
+
customProps,
|
|
216
|
+
elementPath: childPath,
|
|
217
|
+
parentComponentName,
|
|
218
|
+
viewportWidth,
|
|
219
|
+
componentContext,
|
|
220
|
+
locale,
|
|
221
|
+
i18nConfig,
|
|
222
|
+
cmsContext,
|
|
223
|
+
cmsLocale
|
|
224
|
+
});
|
|
225
|
+
}).filter(
|
|
226
|
+
(item): item is ReactElement | string | number => item !== null
|
|
227
|
+
);
|
|
228
|
+
return result as ReactElement[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle new format: style at top level (not props.style)
|
|
232
|
+
const nodeType = typeof node === 'object' && node !== null && 'type' in node ? node.type : undefined;
|
|
233
|
+
const children = typeof node === 'object' && node !== null && 'children' in node ? (node.children || []) : [];
|
|
234
|
+
|
|
235
|
+
// Handle embed nodes early (before property extraction)
|
|
236
|
+
if (nodeType === NODE_TYPE.EMBED && isEmbedNode(node)) {
|
|
237
|
+
// Sanitize HTML with allowlist for SVG and common elements
|
|
238
|
+
// Script tags and event handlers are still removed for security
|
|
239
|
+
const sanitizedHtml = DOMPurify.sanitize(node.html, {
|
|
240
|
+
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas'],
|
|
241
|
+
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity'],
|
|
242
|
+
KEEP_CONTENT: true
|
|
243
|
+
});
|
|
244
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
245
|
+
|
|
246
|
+
// Extract attributes from node
|
|
247
|
+
const extractedAttributes = extractAttributesFromNode(node);
|
|
248
|
+
|
|
249
|
+
// Build embed node props with all features
|
|
250
|
+
const embedProps: Record<string, any> = {
|
|
251
|
+
key,
|
|
252
|
+
'data-element-path': pathToString(elementPath),
|
|
253
|
+
'data-embed-node': 'true',
|
|
254
|
+
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false),
|
|
255
|
+
dangerouslySetInnerHTML: { __html: sanitizedHtml }
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Convert embed styles to utility classes
|
|
259
|
+
if (node.style) {
|
|
260
|
+
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
261
|
+
if (utilityClasses.length > 0) {
|
|
262
|
+
const existingClassName = (extractedAttributes.className || '') as string;
|
|
263
|
+
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
264
|
+
embedProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Add extracted attributes (like class, id, data-*, aria-*, etc.)
|
|
269
|
+
if (Object.keys(extractedAttributes).length > 0) {
|
|
270
|
+
Object.assign(embedProps, extractedAttributes);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add parent component context
|
|
274
|
+
if (effectiveParentComponentName) {
|
|
275
|
+
embedProps['data-parent-component'] = effectiveParentComponentName;
|
|
276
|
+
}
|
|
277
|
+
if (componentContext) {
|
|
278
|
+
embedProps['data-component-context'] = componentContext;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return h('div', embedProps);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle object link nodes (render as div in editor)
|
|
285
|
+
if (nodeType === NODE_TYPE.OBJECT_LINK && isObjectLinkNode(node)) {
|
|
286
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
287
|
+
|
|
288
|
+
// Extract attributes
|
|
289
|
+
const extractedAttributes = extractAttributesFromNode(node);
|
|
290
|
+
|
|
291
|
+
// Build object link props (renders as div in editor)
|
|
292
|
+
const objectLinkProps: Record<string, any> = {
|
|
293
|
+
key,
|
|
294
|
+
'data-element-path': pathToString(elementPath),
|
|
295
|
+
'data-object-link-node': 'true',
|
|
296
|
+
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Convert object link styles to utility classes
|
|
300
|
+
if (node.style) {
|
|
301
|
+
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
302
|
+
if (utilityClasses.length > 0) {
|
|
303
|
+
const existingClassName = (extractedAttributes.className || '') as string;
|
|
304
|
+
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
305
|
+
objectLinkProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Add extracted attributes (like class, id, data-*, aria-*, etc.)
|
|
310
|
+
if (Object.keys(extractedAttributes).length > 0) {
|
|
311
|
+
Object.assign(objectLinkProps, extractedAttributes);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add parent component context
|
|
315
|
+
if (effectiveParentComponentName) {
|
|
316
|
+
objectLinkProps['data-parent-component'] = effectiveParentComponentName;
|
|
317
|
+
}
|
|
318
|
+
if (componentContext) {
|
|
319
|
+
objectLinkProps['data-component-context'] = componentContext;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Build children recursively
|
|
323
|
+
const objectLinkChildren = this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale);
|
|
324
|
+
|
|
325
|
+
return h('div', objectLinkProps, objectLinkChildren);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Handle locale-list nodes (render placeholder in editor)
|
|
329
|
+
if (nodeType === NODE_TYPE.LOCALE_LIST && isLocaleListNode(node)) {
|
|
330
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
331
|
+
|
|
332
|
+
// Extract attributes
|
|
333
|
+
const extractedAttributes = extractAttributesFromNode(node);
|
|
334
|
+
|
|
335
|
+
// Build locale list props
|
|
336
|
+
const localeListProps: Record<string, any> = {
|
|
337
|
+
key,
|
|
338
|
+
'data-element-path': pathToString(elementPath),
|
|
339
|
+
'data-locale-list': 'true',
|
|
340
|
+
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Convert container styles to utility classes
|
|
344
|
+
if (node.style) {
|
|
345
|
+
const utilityClasses = responsiveStylesToClasses(node.style);
|
|
346
|
+
if (utilityClasses.length > 0) {
|
|
347
|
+
const existingClassName = (extractedAttributes.className || '') as string;
|
|
348
|
+
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
349
|
+
localeListProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Add extracted attributes
|
|
354
|
+
if (Object.keys(extractedAttributes).length > 0) {
|
|
355
|
+
Object.assign(localeListProps, extractedAttributes);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Add parent component context
|
|
359
|
+
if (effectiveParentComponentName) {
|
|
360
|
+
localeListProps['data-parent-component'] = effectiveParentComponentName;
|
|
361
|
+
}
|
|
362
|
+
if (componentContext) {
|
|
363
|
+
localeListProps['data-component-context'] = componentContext;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Build locale links for editor preview using actual i18n config
|
|
367
|
+
const showCurrent = node.showCurrent !== false;
|
|
368
|
+
const showSeparator = node.showSeparator !== false;
|
|
369
|
+
const showFlag = node.showFlag !== false;
|
|
370
|
+
// Use actual locales from i18nConfig (LocaleConfig[])
|
|
371
|
+
const configLocales = i18nConfig?.locales || [];
|
|
372
|
+
const currentLocaleCode = locale || i18nConfig?.defaultLocale || 'en';
|
|
373
|
+
|
|
374
|
+
// Convert item styles, active item styles, separator styles, and flag styles to utility classes
|
|
375
|
+
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle) : [];
|
|
376
|
+
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle) : [];
|
|
377
|
+
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle) : [];
|
|
378
|
+
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle) : [];
|
|
379
|
+
|
|
380
|
+
// Build locale links from config
|
|
381
|
+
const linkElements: ReactElement[] = [];
|
|
382
|
+
for (let i = 0; i < configLocales.length; i++) {
|
|
383
|
+
const localeConfig = configLocales[i];
|
|
384
|
+
const isCurrent = localeConfig.code === currentLocaleCode;
|
|
385
|
+
if (!showCurrent && isCurrent) continue;
|
|
386
|
+
|
|
387
|
+
// Add separator between links (empty span styled via separatorStyle)
|
|
388
|
+
if (showSeparator && linkElements.length > 0) {
|
|
389
|
+
linkElements.push(h('span', {
|
|
390
|
+
key: `sep-${i}`,
|
|
391
|
+
className: separatorClasses.length > 0 ? separatorClasses.join(' ') : undefined
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build link element content - flag icon (if enabled and exists) + nativeName in span
|
|
396
|
+
const linkContent: (ReactElement | string)[] = [];
|
|
397
|
+
if (showFlag && localeConfig.icon) {
|
|
398
|
+
linkContent.push(h('img', {
|
|
399
|
+
key: 'flag',
|
|
400
|
+
src: localeConfig.icon,
|
|
401
|
+
alt: `${localeConfig.name} flag`,
|
|
402
|
+
className: flagClasses.length > 0 ? flagClasses.join(' ') : undefined
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
linkContent.push(h('div', { key: 'text' }, localeConfig.nativeName));
|
|
406
|
+
|
|
407
|
+
// Current item gets both itemClasses + activeItemClasses (additive/override)
|
|
408
|
+
const linkClasses = isCurrent ? [...itemClasses, ...activeItemClasses] : itemClasses;
|
|
409
|
+
linkElements.push(h('div', {
|
|
410
|
+
key: `locale-${localeConfig.code}`,
|
|
411
|
+
'data-current': isCurrent ? 'true' : 'false',
|
|
412
|
+
'data-locale': localeConfig.code,
|
|
413
|
+
className: linkClasses.length > 0 ? linkClasses.join(' ') : undefined,
|
|
414
|
+
style: { cursor: 'pointer' }
|
|
415
|
+
}, ...linkContent));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return h('div', localeListProps, linkElements);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Extract node properties based on type
|
|
422
|
+
const { tag, componentName, props: nodeProps } = extractNodeProperties(node);
|
|
423
|
+
|
|
424
|
+
// Filter internal props from nodeProps before building props object
|
|
425
|
+
// This prevents node.type (e.g., "node") from being passed to DOM while allowing
|
|
426
|
+
// attributes.type (e.g., "text" for inputs) to be preserved when merged later
|
|
427
|
+
const imageOnlyProps = ['src', 'alt', 'loading', 'width', 'height'];
|
|
428
|
+
const internalProps = ['type', 'tag', 'component', 'props', 'children', 'html', 'style', ...imageOnlyProps];
|
|
429
|
+
let props: Record<string, unknown> = {};
|
|
430
|
+
for (const [key, value] of Object.entries(nodeProps)) {
|
|
431
|
+
if (internalProps.includes(key)) {
|
|
432
|
+
// Keep image properties if this is an img tag
|
|
433
|
+
if (tag === 'img' && imageOnlyProps.includes(key)) {
|
|
434
|
+
props[key] = value;
|
|
435
|
+
}
|
|
436
|
+
// else skip - it's an internal prop
|
|
437
|
+
} else {
|
|
438
|
+
props[key] = value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Extract styles from node and convert to utility classes
|
|
443
|
+
const nodeStyle = node.style || (isComponentNode(node) ? node.props?.style : undefined);
|
|
444
|
+
|
|
445
|
+
if (nodeStyle) {
|
|
446
|
+
// Convert style objects to utility classes
|
|
447
|
+
const utilityClasses = responsiveStylesToClasses(nodeStyle);
|
|
448
|
+
if (utilityClasses.length > 0) {
|
|
449
|
+
const existingClassName = (props.className || '') as string;
|
|
450
|
+
const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
|
|
451
|
+
props.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Extract attributes from node and merge into props
|
|
456
|
+
// Attributes override props with the same key
|
|
457
|
+
// Note: Internal props were already filtered above, so attributes like type="text" are preserved
|
|
458
|
+
const extractedAttributes = extractAttributesFromNode(node);
|
|
459
|
+
if (Object.keys(extractedAttributes).length > 0) {
|
|
460
|
+
props = { ...props, ...extractedAttributes };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check if this is a custom component reference
|
|
464
|
+
if (nodeType === NODE_TYPE.COMPONENT && componentName && this.componentRegistry.has(componentName)) {
|
|
465
|
+
const componentDef = this.componentRegistry.get(componentName);
|
|
466
|
+
|
|
467
|
+
if (!componentDef) return null;
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
// Extract structured component definition
|
|
471
|
+
const structuredComponentDef = componentDef.component;
|
|
472
|
+
if (!structuredComponentDef) {
|
|
473
|
+
return h(ErrorBoundary, {
|
|
474
|
+
key,
|
|
475
|
+
componentName: componentName,
|
|
476
|
+
level: 'component',
|
|
477
|
+
}, null);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Resolve props with defaults from interface definition (with i18n support)
|
|
481
|
+
const resolvedProps = resolvePropsFromDefinition(
|
|
482
|
+
structuredComponentDef,
|
|
483
|
+
props,
|
|
484
|
+
children,
|
|
485
|
+
locale,
|
|
486
|
+
i18nConfig
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Process the structure with resolved props
|
|
490
|
+
// processStructure now handles structure.style and merges into props.style
|
|
491
|
+
// Pass viewportWidth to respect responsive breakpoints in preview
|
|
492
|
+
// Pass instance children to replace { type: "children" } markers
|
|
493
|
+
const processedStructure = processStructure(
|
|
494
|
+
structuredComponentDef.structure,
|
|
495
|
+
{ props: resolvedProps, componentDef: structuredComponentDef },
|
|
496
|
+
viewportWidth,
|
|
497
|
+
children
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Type guard: ensure processedStructure is a ComponentNode
|
|
501
|
+
if (!processedStructure || typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Merge instance style overrides (from component instance props) with structure styles
|
|
506
|
+
if (props.style && typeof props.style === 'object') {
|
|
507
|
+
mergeNodeStyles(processedStructure, props.style as StyleValue, viewportWidth);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Add event handlers to props (only for component instances)
|
|
511
|
+
if (props.onClick && isComponentNode(processedStructure)) {
|
|
512
|
+
if (!processedStructure.props) {
|
|
513
|
+
processedStructure.props = {};
|
|
514
|
+
}
|
|
515
|
+
processedStructure.props.onClick = this.createOnClickHandler(props.onClick);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Build the component instance's structure with proper component context tracking
|
|
519
|
+
// Elements inside this component will have parentComponentName set via closure variables
|
|
520
|
+
// Component root will have props stored in registry (no DOM attributes needed)
|
|
521
|
+
const nestedParentComponentName = this.getParentComponentNameForNestedComponent(
|
|
522
|
+
componentContext,
|
|
523
|
+
parentComponentName,
|
|
524
|
+
componentName
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const componentElement = this.buildComponent({
|
|
528
|
+
node: processedStructure,
|
|
529
|
+
key,
|
|
530
|
+
customProps: {
|
|
531
|
+
// Store resolved props in a special key to pass to registry (not added to DOM)
|
|
532
|
+
'__componentProps': resolvedProps
|
|
533
|
+
},
|
|
534
|
+
elementPath,
|
|
535
|
+
parentComponentName: nestedParentComponentName,
|
|
536
|
+
viewportWidth,
|
|
537
|
+
componentContext: componentName,
|
|
538
|
+
locale,
|
|
539
|
+
i18nConfig,
|
|
540
|
+
cmsContext,
|
|
541
|
+
cmsLocale
|
|
542
|
+
});
|
|
543
|
+
return h(ErrorBoundary, {
|
|
544
|
+
key,
|
|
545
|
+
componentName: componentName,
|
|
546
|
+
level: 'component',
|
|
547
|
+
}, componentElement);
|
|
548
|
+
|
|
549
|
+
} catch (error) {
|
|
550
|
+
return h(ErrorBoundary, {
|
|
551
|
+
key,
|
|
552
|
+
componentName: tag,
|
|
553
|
+
level: 'component',
|
|
554
|
+
}, null);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Handle Link component for navigation (only for HTML nodes)
|
|
559
|
+
if (tag === 'Link') {
|
|
560
|
+
const { to, ...restProps } = props;
|
|
561
|
+
const href = typeof to === 'string' ? to : '#';
|
|
562
|
+
|
|
563
|
+
// Use effective parent component name for consistency with regular HTML elements
|
|
564
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
565
|
+
|
|
566
|
+
// Navigation click handler
|
|
567
|
+
const navigationOnClick = (e: any) => {
|
|
568
|
+
e.preventDefault();
|
|
569
|
+
if (typeof to === 'string' && to) {
|
|
570
|
+
navigateTo(to);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
return h('a', {
|
|
575
|
+
...restProps,
|
|
576
|
+
key,
|
|
577
|
+
href,
|
|
578
|
+
// Add data attributes for selection overlay
|
|
579
|
+
'data-element-path': pathToString(elementPath),
|
|
580
|
+
// Add parent component context
|
|
581
|
+
...(effectiveParentComponentName && { 'data-parent-component': effectiveParentComponentName }),
|
|
582
|
+
// Add current component context (when inside component definition)
|
|
583
|
+
...(componentContext && { 'data-component-context': componentContext }),
|
|
584
|
+
ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false),
|
|
585
|
+
onClick: navigationOnClick,
|
|
586
|
+
style: {
|
|
587
|
+
textDecoration: 'none',
|
|
588
|
+
color: '#0070f3',
|
|
589
|
+
cursor: 'pointer',
|
|
590
|
+
...(props.style && typeof props.style === 'object' ? props.style : {}),
|
|
591
|
+
}
|
|
592
|
+
}, this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Prepare element props
|
|
596
|
+
// Remove __componentProps from customProps before spreading (it's only for registry, not DOM)
|
|
597
|
+
const { __componentProps, ...customPropsForDOM } = customProps || {};
|
|
598
|
+
const isComponentRoot = !!(customProps && '__componentProps' in customProps);
|
|
599
|
+
const processedProps: Record<string, unknown> = {
|
|
600
|
+
...props,
|
|
601
|
+
...customPropsForDOM,
|
|
602
|
+
key
|
|
603
|
+
// Note: Data attributes moved to ref callback to avoid rerenders on path changes
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Register element in registry and set data attributes via DOM (not React props)
|
|
607
|
+
// Setting attributes in ref callback avoids React rerenders when paths change during drag/reorder
|
|
608
|
+
processedProps.ref = (el: HTMLElement | null) => {
|
|
609
|
+
if (el) {
|
|
610
|
+
// Set data attributes via DOM (not React props) to avoid rerenders on path changes
|
|
611
|
+
el.setAttribute('data-element-path', pathToString(elementPath));
|
|
612
|
+
if (isComponentRoot) el.setAttribute('data-component-root', 'true');
|
|
613
|
+
if (parentComponentName) el.setAttribute('data-parent-component', parentComponentName);
|
|
614
|
+
if (componentContext) el.setAttribute('data-component-context', componentContext);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Extract props if this is a component root
|
|
618
|
+
const componentProps = isComponentRoot && customProps.__componentProps
|
|
619
|
+
? customProps.__componentProps as Record<string, unknown>
|
|
620
|
+
: undefined;
|
|
621
|
+
// Use componentContext as component name for component roots
|
|
622
|
+
const componentNameForRegistry = isComponentRoot ? componentContext : parentComponentName;
|
|
623
|
+
|
|
624
|
+
// Build full metadata for registry
|
|
625
|
+
const metadata = {
|
|
626
|
+
parentComponentName: parentComponentName ?? null,
|
|
627
|
+
componentContext: componentContext ?? null
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
this.elementRegistry.register(elementPath, el, componentNameForRegistry, isComponentRoot, componentProps, metadata);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Add onClick handler - either custom or selection handler
|
|
634
|
+
const originalOnClick = props.onClick ? this.createOnClickHandler(props.onClick) : undefined;
|
|
635
|
+
|
|
636
|
+
// Calculate effective parent component name before using it in onClick handler
|
|
637
|
+
const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
|
|
638
|
+
|
|
639
|
+
// Add onClick handler from props if exists
|
|
640
|
+
if (originalOnClick) {
|
|
641
|
+
processedProps.onClick = originalOnClick;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// For HTML elements, use tag; components and embed nodes should not reach here (they're handled above)
|
|
645
|
+
const finalTag = tag || 'div';
|
|
646
|
+
|
|
647
|
+
// HTML void elements cannot have children
|
|
648
|
+
const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
649
|
+
if (VOID_ELEMENTS.includes(finalTag.toLowerCase())) {
|
|
650
|
+
// Ensure no children prop is passed to void elements
|
|
651
|
+
const { children: _ignoredChildren, ...voidElementProps } = processedProps;
|
|
652
|
+
return h(finalTag, voidElementProps);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Build children recursively with proper component context
|
|
656
|
+
const childElements = this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale);
|
|
657
|
+
return h(finalTag, processedProps, childElements);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|