@elementor/editor-canvas 4.2.0-beta2 → 4.3.0-1000

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 (33) hide show
  1. package/dist/index.d.mts +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +448 -1318
  4. package/dist/index.mjs +313 -1196
  5. package/package.json +22 -22
  6. package/src/index.ts +1 -1
  7. package/src/legacy/__tests__/init-legacy-views.test.ts +1 -1
  8. package/src/mcp/canvas-mcp.ts +4 -2
  9. package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +76 -0
  10. package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +27 -37
  11. package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +79 -76
  12. package/src/mcp/resources/available-widgets-resource.ts +31 -40
  13. package/src/mcp/resources/best-practices-resource.ts +34 -0
  14. package/src/mcp/resources/dynamic-tags-resource.ts +28 -29
  15. package/src/mcp/resources/widgets-schema-resource.ts +34 -177
  16. package/src/mcp/tools/build-composition/tool.ts +107 -218
  17. package/src/sync/element-added-event.ts +8 -0
  18. package/src/utils/__tests__/grid-outline-utils.test.ts +39 -0
  19. package/src/utils/grid-outline-utils.ts +13 -2
  20. package/src/composition-builder/__tests__/composition-builder.test.ts +0 -352
  21. package/src/composition-builder/composition-builder.ts +0 -350
  22. package/src/composition-builder/utils/__tests__/required-children-enforcer.test.ts +0 -79
  23. package/src/composition-builder/utils/__tests__/required-default-child-tags.test.ts +0 -35
  24. package/src/composition-builder/utils/required-children-enforcer.ts +0 -56
  25. package/src/composition-builder/utils/required-default-child-tags.ts +0 -22
  26. package/src/mcp/resources/best-practices.ts +0 -159
  27. package/src/mcp/resources/build-llm-guidance.ts +0 -110
  28. package/src/mcp/tools/build-composition/__tests__/xml-leaf-wrapper.test.ts +0 -117
  29. package/src/mcp/tools/build-composition/prompt.ts +0 -167
  30. package/src/mcp/tools/build-composition/schema.ts +0 -42
  31. package/src/mcp/tools/build-composition/xml-leaf-wrapper.ts +0 -68
  32. package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +0 -59
  33. package/src/mcp/utils/get-composition-target-container.ts +0 -15
@@ -0,0 +1,8 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+
3
+ export const ELEMENT_ADDED_EVENT = 'elementor/canvas/element-added';
4
+
5
+ export type ElementAddedEvent = {
6
+ element: V1ElementData;
7
+ executedBy: 'mcp_tool' | 'user';
8
+ };
@@ -3,6 +3,7 @@ import {
3
3
  computeCellRects,
4
4
  computeGridLines,
5
5
  parseTrackList,
6
+ resolveGapPx,
6
7
  snapToHalfPixel,
7
8
  toGridTracks,
8
9
  toPx,
@@ -215,11 +216,29 @@ describe( 'toPx', () => {
215
216
  } );
216
217
  } );
217
218
 
219
+ describe( 'resolveGapPx', () => {
220
+ it( 'resolves a percentage gap against the reference size', () => {
221
+ expect( resolveGapPx( '20%', '1000px' ) ).toBe( 200 );
222
+ expect( resolveGapPx( '10%', '500px' ) ).toBe( 50 );
223
+ } );
224
+
225
+ it( 'falls back to px parsing for non-percentage gaps', () => {
226
+ expect( resolveGapPx( '16px', '1000px' ) ).toBe( 16 );
227
+ expect( resolveGapPx( 'normal', '1000px' ) ).toBe( 0 );
228
+ } );
229
+
230
+ it( 'returns 0 when the reference size is not a finite number', () => {
231
+ expect( resolveGapPx( '20%', 'auto' ) ).toBe( 0 );
232
+ } );
233
+ } );
234
+
218
235
  type ComputedStyleParts = {
219
236
  gridTemplateColumns: string;
220
237
  gridTemplateRows: string;
221
238
  columnGap: string;
222
239
  rowGap: string;
240
+ width: string;
241
+ height: string;
223
242
  paddingTop: string;
224
243
  paddingRight: string;
225
244
  paddingBottom: string;
@@ -233,6 +252,8 @@ function mockComputedStyle( parts: Partial< ComputedStyleParts > = {} ): CSSStyl
233
252
  gridTemplateRows: 'none',
234
253
  columnGap: 'normal',
235
254
  rowGap: 'normal',
255
+ width: '1000px',
256
+ height: '500px',
236
257
  paddingTop: '0px',
237
258
  paddingRight: '0px',
238
259
  paddingBottom: '0px',
@@ -246,6 +267,8 @@ function mockComputedStyle( parts: Partial< ComputedStyleParts > = {} ): CSSStyl
246
267
  gridTemplateRows: resolved.gridTemplateRows,
247
268
  columnGap: resolved.columnGap,
248
269
  rowGap: resolved.rowGap,
270
+ width: resolved.width,
271
+ height: resolved.height,
249
272
  paddingTop: resolved.paddingTop,
250
273
  paddingRight: resolved.paddingRight,
251
274
  paddingBottom: resolved.paddingBottom,
@@ -286,6 +309,22 @@ describe( 'toGridTracks', () => {
286
309
  expect( tracks.rows ).toEqual( [] );
287
310
  } );
288
311
 
312
+ it( 'resolves percentage gaps against the content-box width and height', () => {
313
+ const tracks = toGridTracks(
314
+ mockComputedStyle( {
315
+ gridTemplateColumns: '100px 100px',
316
+ gridTemplateRows: '80px 80px',
317
+ columnGap: '20%',
318
+ rowGap: '10%',
319
+ width: '1000px',
320
+ height: '500px',
321
+ } )
322
+ );
323
+
324
+ expect( tracks.columnGap ).toBe( 200 );
325
+ expect( tracks.rowGap ).toBe( 50 );
326
+ } );
327
+
289
328
  it( 'reports gap as 0 when computed style returns "normal"', () => {
290
329
  const tracks = toGridTracks(
291
330
  mockComputedStyle( { gridTemplateColumns: '100px 100px', columnGap: 'normal', rowGap: 'normal' } )
@@ -28,8 +28,8 @@ export function toGridTracks( computedStyle: CSSStyleDeclaration ): GridTracks {
28
28
  return {
29
29
  columns: parseTrackList( computedStyle.gridTemplateColumns ),
30
30
  rows: parseTrackList( computedStyle.gridTemplateRows ),
31
- columnGap: toPx( computedStyle.columnGap ),
32
- rowGap: toPx( computedStyle.rowGap ),
31
+ columnGap: resolveGapPx( computedStyle.columnGap, computedStyle.width ),
32
+ rowGap: resolveGapPx( computedStyle.rowGap, computedStyle.height ),
33
33
  padding: {
34
34
  top: toPx( computedStyle.paddingTop ),
35
35
  right: toPx( computedStyle.paddingRight ),
@@ -142,3 +142,14 @@ export function toPx( value: string ): number {
142
142
 
143
143
  return Number.isFinite( parsed ) ? parsed : 0;
144
144
  }
145
+
146
+ export function resolveGapPx( value: string, referenceSize: string ): number {
147
+ if ( value.trim().endsWith( '%' ) ) {
148
+ const percent = parseFloat( value );
149
+ const reference = parseFloat( referenceSize );
150
+
151
+ return Number.isFinite( percent ) && Number.isFinite( reference ) ? ( percent / 100 ) * reference : 0;
152
+ }
153
+
154
+ return toPx( value );
155
+ }
@@ -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
- } );