@elementor/editor-canvas 4.3.0-993 → 4.3.0-995

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.
@@ -1,352 +0,0 @@
1
- import { type CreateElementParams, type V1Element, type V1ElementConfig } from '@elementor/editor-elements';
2
-
3
- import { CompositionBuilder } from '../composition-builder';
4
-
5
- const ROOT_CHILD_TAG = 'column';
6
- const GENERATED_ELEMENT_ID = 'generated-element-id';
7
- const CONFIG_ID = 'cfg-a';
8
- const ELEMENT_CONFIG_PROPERTY = 'title';
9
- const ELEMENT_CONFIG_VALUE = 'configured-value';
10
-
11
- const xmlStringWithConfiguration = `<${ ROOT_CHILD_TAG } configuration-id="${ CONFIG_ID }" />`;
12
-
13
- const createElementConfigPayload = () => ( {
14
- [ CONFIG_ID ]: {
15
- [ ELEMENT_CONFIG_PROPERTY ]: ELEMENT_CONFIG_VALUE,
16
- },
17
- } );
18
-
19
- const createMinimalWidgetsCache = () =>
20
- ( {
21
- [ ROOT_CHILD_TAG ]: {
22
- elType: 'column',
23
- },
24
- } ) as Record< string, { elType: string } >;
25
-
26
- const FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN = {
27
- 'e-form': {
28
- title: 'Form',
29
- controls: {},
30
- elType: 'widget',
31
- default_children: [
32
- {
33
- elType: 'e-form-success-message',
34
- meta: { required: true },
35
- elements: [],
36
- },
37
- {
38
- elType: 'e-form-error-message',
39
- meta: { required: true },
40
- elements: [],
41
- },
42
- {
43
- elType: 'widget',
44
- widgetType: 'e-form-input',
45
- elements: [],
46
- },
47
- ],
48
- },
49
- 'e-form-error-message': {
50
- title: 'Error',
51
- controls: {},
52
- elType: 'e-form-error-message',
53
- },
54
- 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
55
- 'e-form-success-message': {
56
- title: 'Success',
57
- controls: {},
58
- elType: 'e-form-success-message',
59
- },
60
- } as const satisfies Record< string, V1ElementConfig >;
61
-
62
- const createMockRootContainer = (): V1Element =>
63
- ( {
64
- id: 'root',
65
- model: { get: jest.fn(), set: jest.fn(), toJSON: jest.fn() },
66
- settings: { get: jest.fn(), set: jest.fn(), toJSON: jest.fn() },
67
- children: [],
68
- } ) as unknown as V1Element;
69
-
70
- const createMockPartialContainer = ( id: string ): V1Element =>
71
- ( {
72
- id,
73
- model: { get: jest.fn(), set: jest.fn(), toJSON: jest.fn() },
74
- settings: { get: jest.fn(), set: jest.fn(), toJSON: jest.fn() },
75
- children: [],
76
- } ) as unknown as V1Element;
77
-
78
- describe( 'CompositionBuilder.build createElement failure cleanup', () => {
79
- it( 'calls deleteElement when createElement fails and getContainer returns a container', async () => {
80
- // Arrange
81
- const partialContainer = createMockPartialContainer( GENERATED_ELEMENT_ID );
82
- const deleteElement = jest.fn();
83
- const doUpdateElementProperty = jest.fn();
84
- const createElement = jest.fn().mockImplementation( () => {
85
- throw new Error( 'create failed' );
86
- } );
87
- const getContainer = jest
88
- .fn()
89
- .mockImplementation( ( id: string ) => ( id === GENERATED_ELEMENT_ID ? partialContainer : undefined ) );
90
- const builder = CompositionBuilder.fromXMLString( xmlStringWithConfiguration, {
91
- createElement,
92
- deleteElement,
93
- getContainer,
94
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
95
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
96
- doUpdateElementProperty,
97
- } );
98
- builder.setElementConfig( createElementConfigPayload() );
99
-
100
- // Act
101
- await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow( 'create failed' );
102
-
103
- // Assert
104
- expect( getContainer ).toHaveBeenCalledWith( GENERATED_ELEMENT_ID );
105
- expect( deleteElement ).toHaveBeenCalledTimes( 1 );
106
- expect( deleteElement ).toHaveBeenCalledWith( { container: partialContainer } );
107
- expect( doUpdateElementProperty ).not.toHaveBeenCalled();
108
- } );
109
-
110
- it( 'does not call deleteElement when createElement fails and getContainer returns undefined', async () => {
111
- // Arrange
112
- const deleteElement = jest.fn();
113
- const doUpdateElementProperty = jest.fn();
114
- const createElement = jest.fn().mockImplementation( () => {
115
- throw new Error( 'create failed' );
116
- } );
117
- const getContainer = jest.fn().mockReturnValue( undefined );
118
- const builder = CompositionBuilder.fromXMLString( xmlStringWithConfiguration, {
119
- createElement,
120
- deleteElement,
121
- getContainer,
122
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
123
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
124
- doUpdateElementProperty,
125
- } );
126
- builder.setElementConfig( createElementConfigPayload() );
127
-
128
- // Act
129
- await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow( 'create failed' );
130
-
131
- // Assert
132
- expect( getContainer ).toHaveBeenCalledWith( GENERATED_ELEMENT_ID );
133
- expect( deleteElement ).not.toHaveBeenCalled();
134
- expect( doUpdateElementProperty ).not.toHaveBeenCalled();
135
- } );
136
-
137
- it( 'calls deleteElement when createElement returns without a model', async () => {
138
- // Arrange
139
- const partialContainer = createMockPartialContainer( GENERATED_ELEMENT_ID );
140
- const deleteElement = jest.fn();
141
- const doUpdateElementProperty = jest.fn();
142
- const createElement = jest.fn().mockReturnValue( {} as V1Element );
143
- const getContainer = jest
144
- .fn()
145
- .mockImplementation( ( id: string ) => ( id === GENERATED_ELEMENT_ID ? partialContainer : undefined ) );
146
- const builder = CompositionBuilder.fromXMLString( xmlStringWithConfiguration, {
147
- createElement,
148
- deleteElement,
149
- getContainer,
150
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
151
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
152
- doUpdateElementProperty,
153
- } );
154
- builder.setElementConfig( createElementConfigPayload() );
155
-
156
- // Act
157
- await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
158
- 'createElement did not return an element container with a model.'
159
- );
160
-
161
- // Assert
162
- expect( getContainer ).toHaveBeenCalledWith( GENERATED_ELEMENT_ID );
163
- expect( deleteElement ).toHaveBeenCalledTimes( 1 );
164
- expect( deleteElement ).toHaveBeenCalledWith( { container: partialContainer } );
165
- expect( doUpdateElementProperty ).not.toHaveBeenCalled();
166
- } );
167
- } );
168
-
169
- describe( 'CompositionBuilder.build applyProperties after create', () => {
170
- it( 'calls doUpdateElementProperty when create succeeds with element config', async () => {
171
- // Arrange
172
- const deleteElement = jest.fn();
173
- const doUpdateElementProperty = jest.fn();
174
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
175
- const createElement = jest.fn().mockReturnValue( createdElement );
176
- const getContainer = jest
177
- .fn()
178
- .mockImplementation( ( id: string ) => ( id === GENERATED_ELEMENT_ID ? createdElement : undefined ) );
179
- const builder = CompositionBuilder.fromXMLString( xmlStringWithConfiguration, {
180
- createElement,
181
- deleteElement,
182
- getContainer,
183
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
184
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
185
- doUpdateElementProperty,
186
- } );
187
- builder.setElementConfig( createElementConfigPayload() );
188
-
189
- // Act
190
- await builder.build( createMockRootContainer() );
191
-
192
- // Assert
193
- expect( deleteElement ).not.toHaveBeenCalled();
194
- expect( createElement ).toHaveBeenCalledTimes( 1 );
195
- expect( doUpdateElementProperty ).toHaveBeenCalledTimes( 1 );
196
- expect( doUpdateElementProperty ).toHaveBeenCalledWith( {
197
- elementId: GENERATED_ELEMENT_ID,
198
- propertyName: ELEMENT_CONFIG_PROPERTY,
199
- propertyValue: ELEMENT_CONFIG_VALUE,
200
- elementType: ROOT_CHILD_TAG,
201
- } );
202
- } );
203
-
204
- it( 'does not call doUpdateElementProperty when create succeeds without element config', async () => {
205
- // Arrange
206
- const deleteElement = jest.fn();
207
- const doUpdateElementProperty = jest.fn();
208
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
209
- const createElement = jest.fn().mockReturnValue( createdElement );
210
- const builder = CompositionBuilder.fromXMLString( `<${ ROOT_CHILD_TAG } />`, {
211
- createElement,
212
- deleteElement,
213
- getContainer: jest.fn(),
214
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
215
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
216
- doUpdateElementProperty,
217
- } );
218
-
219
- // Act
220
- await builder.build( createMockRootContainer() );
221
-
222
- // Assert
223
- expect( deleteElement ).not.toHaveBeenCalled();
224
- expect( doUpdateElementProperty ).not.toHaveBeenCalled();
225
- } );
226
- } );
227
-
228
- describe( 'CompositionBuilder.build required children', () => {
229
- it( 'rejects build when required direct children are absent from XML', async () => {
230
- // Arrange
231
- let elementIdSequence = 0;
232
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
233
- const createElementMock = jest.fn().mockReturnValue( createdElement );
234
- const builder = CompositionBuilder.fromXMLString(
235
- '<e-form configuration-id="form-1"><e-form-input /></e-form>',
236
- {
237
- createElement: createElementMock,
238
- deleteElement: jest.fn(),
239
- getContainer: jest.fn(),
240
- generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
241
- getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
242
- doUpdateElementProperty: jest.fn(),
243
- }
244
- );
245
-
246
- // Act & Assert
247
- await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
248
- /Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
249
- );
250
- expect( createElementMock ).not.toHaveBeenCalled();
251
- } );
252
-
253
- it( 'rejects build when only some required direct children exist', async () => {
254
- // Arrange
255
- let elementIdSequence = 0;
256
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
257
- const createElementMock = jest.fn().mockReturnValue( createdElement );
258
- const builder = CompositionBuilder.fromXMLString(
259
- '<e-form configuration-id="form-1"><e-form-success-message /><e-form-input /></e-form>',
260
- {
261
- createElement: createElementMock,
262
- deleteElement: jest.fn(),
263
- getContainer: jest.fn(),
264
- generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
265
- getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
266
- doUpdateElementProperty: jest.fn(),
267
- }
268
- );
269
-
270
- // Act & Assert
271
- await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
272
- /Missing required direct child element tag\(s\): e-form-error-message/
273
- );
274
- expect( createElementMock ).not.toHaveBeenCalled();
275
- } );
276
-
277
- it( 'creates elements when XML includes all required direct children', async () => {
278
- // Arrange
279
- let elementIdSequence = 0;
280
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
281
- const createElementMock = jest.fn().mockReturnValue( createdElement );
282
- const builder = CompositionBuilder.fromXMLString(
283
- '<e-form configuration-id="form-1">' +
284
- '<e-form-success-message /><e-form-error-message /><e-form-input />' +
285
- '</e-form>',
286
- {
287
- createElement: createElementMock,
288
- deleteElement: jest.fn(),
289
- getContainer: jest.fn(),
290
- generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
291
- getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
292
- doUpdateElementProperty: jest.fn(),
293
- }
294
- );
295
-
296
- // Act
297
- await builder.build( createMockRootContainer() );
298
-
299
- // Assert
300
- const createArgs = createElementMock.mock.calls[ 0 ]?.[ 0 ] as CreateElementParams;
301
- const childElements = ( createArgs.model?.elements || [] ) as Array< { elType?: string; widgetType?: string } >;
302
-
303
- expect( childElements.filter( ( child ) => child.elType === 'e-form-success-message' ).length ).toBe( 1 );
304
- expect( childElements.filter( ( child ) => child.elType === 'e-form-error-message' ).length ).toBe( 1 );
305
- expect( childElements.some( ( child ) => child.widgetType === 'e-form-input' ) ).toBe( true );
306
- } );
307
- } );
308
-
309
- describe( 'CompositionBuilder.build final composition built event', () => {
310
- let dispatchEventSpy: jest.SpyInstance;
311
-
312
- beforeEach( () => {
313
- dispatchEventSpy = jest.spyOn( window, 'dispatchEvent' );
314
- } );
315
-
316
- afterEach( () => {
317
- dispatchEventSpy.mockRestore();
318
- } );
319
-
320
- it( 'dispatches elementor/composition/built event with root container IDs after applyProperties completes', async () => {
321
- // Arrange
322
- const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
323
- const doUpdateElementProperty = jest.fn();
324
- const createElement = jest.fn().mockReturnValue( createdElement );
325
- const getContainer = jest
326
- .fn()
327
- .mockImplementation( ( id: string ) => ( id === GENERATED_ELEMENT_ID ? createdElement : undefined ) );
328
- const builder = CompositionBuilder.fromXMLString( xmlStringWithConfiguration, {
329
- createElement,
330
- deleteElement: jest.fn(),
331
- getContainer,
332
- generateElementId: jest.fn().mockReturnValue( GENERATED_ELEMENT_ID ),
333
- getWidgetsCache: jest.fn().mockReturnValue( createMinimalWidgetsCache() ),
334
- doUpdateElementProperty,
335
- } );
336
- builder.setElementConfig( createElementConfigPayload() );
337
-
338
- // Act
339
- await builder.build( createMockRootContainer() );
340
-
341
- // Assert
342
- expect( doUpdateElementProperty ).toHaveBeenCalledTimes( 1 );
343
- expect( dispatchEventSpy ).toHaveBeenCalledWith(
344
- expect.objectContaining( {
345
- type: 'elementor/composition/built',
346
- detail: {
347
- rootContainers: [ GENERATED_ELEMENT_ID ],
348
- },
349
- } )
350
- );
351
- } );
352
- } );
@@ -1,350 +0,0 @@
1
- import {
2
- createElement,
3
- type CreateElementParams,
4
- deleteElement,
5
- generateElementId,
6
- getContainer,
7
- getWidgetsCache,
8
- type V1Element,
9
- type V1ElementConfig,
10
- type V1ElementModelProps,
11
- } from '@elementor/editor-elements';
12
- import { type z } from '@elementor/schema';
13
-
14
- import {
15
- collectEmptyMessageErrors,
16
- collectFormAncestorErrors,
17
- collectSubmitButtonErrors,
18
- } from '../form-structure/utils';
19
- import { doUpdateElementProperty } from '../mcp/utils/do-update-element-property';
20
- import { mergeCustomCssText } from '../mcp/utils/merge-custom-css';
21
- import { RequiredChildrenEnforcer } from './utils/required-children-enforcer';
22
- import { getRequiredDefaultChildTemplates } from './utils/required-default-child-tags';
23
-
24
- type AnyValue = z.infer< z.ZodTypeAny >;
25
- type AnyConfig = Record< string, Record< string, AnyValue > >;
26
-
27
- const CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = 'createElement did not return an element container with a model.';
28
-
29
- type API = {
30
- createElement: typeof createElement;
31
- deleteElement: typeof deleteElement;
32
- getWidgetsCache: typeof getWidgetsCache;
33
- generateElementId: typeof generateElementId;
34
- getContainer: typeof getContainer;
35
- doUpdateElementProperty: typeof doUpdateElementProperty;
36
- };
37
-
38
- type CtorOptions = {
39
- xml: Document;
40
- api?: Partial< API >;
41
- elementConfig?: AnyConfig;
42
- stylesConfig?: AnyConfig;
43
- customCSS?: Record< string, string >;
44
- };
45
-
46
- export class CompositionBuilder {
47
- private elementConfig: Record< string, Record< string, AnyValue > > = {};
48
- private elementStylesConfig: Record< string, Record< string, AnyValue > > = {};
49
- private elementCustomCSS: Record< string, string > = {};
50
- private rootContainers: V1Element[] = [];
51
- private api: API = {
52
- createElement,
53
- deleteElement,
54
- getWidgetsCache,
55
- generateElementId,
56
- getContainer,
57
- doUpdateElementProperty,
58
- };
59
- private xml: Document;
60
-
61
- public static fromXMLString( xmlString: string, api: Partial< API > = {} ): CompositionBuilder {
62
- const parser = new DOMParser();
63
- const xmlDoc = parser.parseFromString( xmlString, 'application/xml' );
64
- const errorNode = xmlDoc.querySelector( 'parsererror' );
65
- if ( errorNode ) {
66
- throw new Error( 'Failed to parse XML string: ' + errorNode.textContent );
67
- }
68
- return new CompositionBuilder( {
69
- xml: xmlDoc,
70
- api,
71
- } );
72
- }
73
-
74
- constructor( opts: CtorOptions ) {
75
- const { api = {}, elementConfig = {}, stylesConfig = {}, customCSS = {}, xml } = opts;
76
- this.xml = xml;
77
- Object.assign( this.api, api );
78
- this.setElementConfig( elementConfig );
79
- this.setStylesConfig( stylesConfig );
80
- this.setCustomCSS( customCSS );
81
- }
82
-
83
- setElementConfig( config: Record< string, Record< string, AnyValue > > ) {
84
- this.elementConfig = config;
85
- }
86
-
87
- setStylesConfig( config: Record< string, Record< string, AnyValue > > ) {
88
- this.elementStylesConfig = config;
89
- }
90
-
91
- setCustomCSS( config: Record< string, string > ) {
92
- this.elementCustomCSS = config;
93
- }
94
-
95
- getXML() {
96
- return this.xml;
97
- }
98
-
99
- private buildModelTree(
100
- node: Element,
101
- widgetsCache: Record< string, V1ElementConfig >
102
- ): Record< string, unknown > {
103
- const elementTag = node.tagName;
104
- const isWidget = widgetsCache[ elementTag ]?.elType === 'widget';
105
- const id = this.api.generateElementId();
106
- const children = Array.from( node.children ).map( ( child ) => this.buildModelTree( child, widgetsCache ) );
107
-
108
- node.setAttribute( 'id', id );
109
-
110
- const base: V1ElementModelProps = {
111
- id,
112
- skipDefaultChildren: true,
113
- elements: children as V1ElementModelProps[ 'elements' ],
114
- editor_settings: {
115
- title: node.getAttribute( 'configuration-id' ) ?? undefined,
116
- },
117
- elType: 'widget',
118
- };
119
-
120
- // TODO: Restore this code once components are working in compositions
121
- // if ( elementTag === 'e-component' ) {
122
- // // apply component id before applying values
123
- // const elementConfig = this.elementConfig[ String( node.getAttribute( 'configuration-id' ) ) ];
124
- // if ( elementConfig ) {
125
- // base.settings = base.settings || {};
126
- // base.settings.component_instance = elementConfig.component_instance;
127
- // }
128
- // }
129
-
130
- if ( isWidget ) {
131
- return { ...base, elType: 'widget' as const, widgetType: elementTag };
132
- }
133
-
134
- return { ...base, elType: elementTag };
135
- }
136
-
137
- private async awaitViewRender( element: V1Element ) {
138
- const view = element.view as Record< string, unknown > | undefined;
139
- if ( view?._currentRenderPromise instanceof Promise ) {
140
- await view._currentRenderPromise;
141
- } else {
142
- await Promise.resolve();
143
- }
144
- }
145
-
146
- private validateChildTypes( node: Element, widgetsCache: Record< string, V1ElementConfig > ): string[] {
147
- const errors: string[] = [];
148
- const allowedChildTypes = widgetsCache[ node.tagName ]?.allowed_child_types;
149
-
150
- if ( allowedChildTypes?.length ) {
151
- for ( const child of Array.from( node.children ) ) {
152
- if ( ! allowedChildTypes.includes( child.tagName ) ) {
153
- errors.push(
154
- `"${ child.tagName }" is not allowed as a child of "${
155
- node.tagName
156
- }". Allowed: ${ allowedChildTypes.join( ', ' ) }`
157
- );
158
- }
159
- }
160
- }
161
-
162
- for ( const child of Array.from( node.children ) ) {
163
- errors.push( ...this.validateChildTypes( child, widgetsCache ) );
164
- }
165
-
166
- return errors;
167
- }
168
-
169
- private matchNodeByConfigId( configId: string ) {
170
- const node = this.xml.querySelector( `[configuration-id="${ configId }"]` );
171
- if ( ! node ) {
172
- throw new Error( `Configuration id "${ configId }" does not have target node.` );
173
- }
174
- const id = node.getAttribute( 'id' );
175
- if ( ! id ) {
176
- throw new Error( `Node with configuration id "${ configId }" does not have element id.` );
177
- }
178
- const element = this.api.getContainer( id );
179
- if ( ! element ) {
180
- throw new Error( `Element with id "${ id }" not found but should exist.` );
181
- }
182
- return {
183
- element,
184
- node,
185
- };
186
- }
187
-
188
- private async applyProperties() {
189
- const configErrors: string[] = [];
190
- const styleErrors: string[] = [];
191
-
192
- const allConfigIds = new Set( [
193
- ...Object.keys( this.elementConfig ),
194
- ...Object.keys( this.elementStylesConfig ),
195
- ...Object.keys( this.elementCustomCSS ),
196
- ] );
197
-
198
- for ( const configId of allConfigIds ) {
199
- let element, node;
200
- try {
201
- ( { element, node } = this.matchNodeByConfigId( configId ) );
202
- } catch ( matchErr ) {
203
- const msg = ( matchErr as Error ).message;
204
- if ( this.elementConfig[ configId ] ) {
205
- configErrors.push( msg );
206
- }
207
- if ( this.elementStylesConfig[ configId ] || this.elementCustomCSS[ configId ] ) {
208
- styleErrors.push( msg );
209
- }
210
- continue;
211
- }
212
-
213
- const config = this.elementConfig[ configId ];
214
- if ( config ) {
215
- for ( const [ propertyName, propertyValue ] of Object.entries( config ) ) {
216
- try {
217
- this.api.doUpdateElementProperty( {
218
- elementId: element.id,
219
- propertyName,
220
- propertyValue,
221
- elementType: node.tagName,
222
- } );
223
- } catch ( error ) {
224
- configErrors.push( ( error as Error ).message );
225
- }
226
- }
227
- }
228
-
229
- const styleConfig = this.elementStylesConfig[ configId ];
230
- const hasInvalidStyles = false;
231
- if ( styleConfig ) {
232
- const validStylesPropValues: Record< string, AnyValue > = {};
233
- for ( const [ styleName, stylePropValue ] of Object.entries( styleConfig ) ) {
234
- if ( styleName === '$intention' ) {
235
- continue;
236
- } else {
237
- // skipping actual validation - properies comes from the server
238
- validStylesPropValues[ styleName ] = stylePropValue;
239
- }
240
- }
241
- if ( Object.keys( validStylesPropValues ).length > 0 ) {
242
- try {
243
- this.api.doUpdateElementProperty( {
244
- elementId: element.id,
245
- propertyName: '_styles',
246
- propertyValue: validStylesPropValues,
247
- elementType: node.tagName,
248
- } );
249
- } catch ( error ) {
250
- styleErrors.push( String( error ) );
251
- }
252
- }
253
- }
254
-
255
- const intentionCss = typeof styleConfig?.$intention === 'string' ? styleConfig.$intention.trim() : '';
256
- const fallbackCss = hasInvalidStyles && intentionCss ? intentionCss : '';
257
- const mergedCustomCss = mergeCustomCssText( this.elementCustomCSS[ configId ], fallbackCss );
258
- if ( mergedCustomCss ) {
259
- try {
260
- this.api.doUpdateElementProperty( {
261
- elementId: element.id,
262
- propertyName: '_styles',
263
- propertyValue: { custom_css: mergedCustomCss },
264
- elementType: node.tagName,
265
- } );
266
- } catch ( cssErr ) {
267
- styleErrors.push( String( cssErr ) );
268
- }
269
- }
270
-
271
- await this.awaitViewRender( element );
272
- }
273
-
274
- return { configErrors, styleErrors };
275
- }
276
-
277
- async build( rootContainer: V1Element ) {
278
- const widgetsCache = this.api.getWidgetsCache() || {};
279
-
280
- new Set( this.xml.querySelectorAll( '*' ) ).forEach( ( node ) => {
281
- if ( ! widgetsCache[ node.tagName ] ) {
282
- throw new Error( `Unknown widget type: ${ node.tagName }` );
283
- }
284
- } );
285
-
286
- const typesWithRequiredChildren = Object.keys( widgetsCache ).filter(
287
- ( elementType ) => getRequiredDefaultChildTemplates( widgetsCache[ elementType ] ).length > 0
288
- );
289
-
290
- typesWithRequiredChildren.forEach( ( elementType ) => {
291
- new RequiredChildrenEnforcer( elementType, widgetsCache ).enforce( this.xml );
292
- } );
293
-
294
- const childTypeErrors: string[] = [];
295
- for ( const rootChild of Array.from( this.xml.children ) ) {
296
- childTypeErrors.push( ...this.validateChildTypes( rootChild, widgetsCache ) );
297
- }
298
- if ( childTypeErrors.length ) {
299
- throw new Error( `Invalid element structure:\n${ childTypeErrors.join( '\n' ) }` );
300
- }
301
-
302
- const formErrors = [
303
- ...collectFormAncestorErrors( this.xml ),
304
- ...collectSubmitButtonErrors( this.xml ),
305
- ...collectEmptyMessageErrors( this.xml ),
306
- ];
307
-
308
- const children = Array.from( this.xml.children );
309
- for ( const childNode of children ) {
310
- const modelTree = this.buildModelTree( childNode, widgetsCache );
311
-
312
- try {
313
- const newElement = this.api.createElement( {
314
- container: rootContainer,
315
- model: modelTree as CreateElementParams[ 'model' ],
316
- options: { useHistory: false },
317
- } );
318
- if ( ! newElement?.model ) {
319
- throw new Error( CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE );
320
- }
321
- this.rootContainers.push( newElement );
322
- await this.awaitViewRender( newElement );
323
- } catch ( e: unknown ) {
324
- const attempToRestoreInvalidContainer = this.api.getContainer( modelTree.id as string );
325
- if ( attempToRestoreInvalidContainer ) {
326
- this.api.deleteElement( { container: attempToRestoreInvalidContainer } );
327
- }
328
- throw e;
329
- }
330
- }
331
-
332
- const { configErrors, styleErrors } = await this.applyProperties();
333
-
334
- if ( typeof window !== 'undefined' ) {
335
- const targetWindow = window.top || window;
336
- targetWindow.dispatchEvent(
337
- new CustomEvent( 'elementor/composition/built', {
338
- detail: { rootContainers: this.rootContainers.map( ( c ) => c.id ) },
339
- } )
340
- );
341
- }
342
-
343
- return {
344
- configErrors,
345
- styleErrors,
346
- formErrors,
347
- rootContainers: [ ...this.rootContainers ],
348
- };
349
- }
350
- }