@elementor/editor-canvas 4.2.0-946 → 4.2.0-beta2

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.
@@ -7,6 +7,7 @@ type Model = {
7
7
 
8
8
  export type CreateArgs = {
9
9
  container?: V1Element;
10
+ containers?: V1Element[];
10
11
  model?: Model;
11
12
  };
12
13
 
@@ -114,3 +115,49 @@ export function clipboardRootsAreAtomicForms( elements: ClipboardElement[] ): bo
114
115
 
115
116
  return elements.every( ( el ) => getClipboardElementType( el ) === FORM_ELEMENT_TYPE );
116
117
  }
118
+
119
+ export function hasFormAncestor( node: Element ): boolean {
120
+ return node.closest( FORM_ELEMENT_TYPE ) !== null;
121
+ }
122
+
123
+ export function collectFormAncestorErrors( xml: Document ): string[] {
124
+ const errors: string[] = [];
125
+ for ( const node of xml.querySelectorAll( '*' ) ) {
126
+ if ( ! FORM_FIELD_ELEMENT_TYPES.has( node.tagName.toLowerCase() ) ) {
127
+ continue;
128
+ }
129
+ if ( hasFormAncestor( node ) ) {
130
+ continue;
131
+ }
132
+ const id = node.getAttribute( 'configuration-id' );
133
+ errors.push(
134
+ `<${ node.tagName }${
135
+ id ? ` configuration-id="${ id }"` : ''
136
+ }> must be nested inside <e-form> (any ancestor depth is allowed).`
137
+ );
138
+ }
139
+ return errors;
140
+ }
141
+
142
+ export function collectSubmitButtonErrors( xml: Document ): string[] {
143
+ const errors: string[] = [];
144
+ for ( const form of xml.querySelectorAll( 'e-form' ) ) {
145
+ const submitButtons = form.querySelectorAll( 'e-form-submit-button' );
146
+ if ( submitButtons.length === 0 ) {
147
+ errors.push( `<e-form> has no <e-form-submit-button>.` );
148
+ } else if ( submitButtons.length > 1 ) {
149
+ errors.push( `<e-form> has ${ submitButtons.length } submit buttons — only 1 is allowed.` );
150
+ }
151
+ }
152
+ return errors;
153
+ }
154
+
155
+ export function collectEmptyMessageErrors( xml: Document ): string[] {
156
+ const errors: string[] = [];
157
+ for ( const node of xml.querySelectorAll( 'e-form-success-message, e-form-error-message' ) ) {
158
+ if ( node.children.length === 0 ) {
159
+ errors.push( `<${ node.tagName }> must have at least one child element (e.g. <e-atomic-paragraph>).` );
160
+ }
161
+ }
162
+ return errors;
163
+ }
package/src/index.ts CHANGED
@@ -44,3 +44,4 @@ export { UnknownStyleTypeError, UnknownStyleStateError } from './renderers/error
44
44
  export { useCanvasDocument } from './hooks/use-canvas-document';
45
45
  export { useEscapeOnCanvas } from './hooks/use-escape-on-canvas';
46
46
  export { doAfterRender } from './utils/after-render';
47
+ export { ELEMENT_ADDED_EVENT, type ElementAddedEvent } from './mcp/tools/build-composition/tool';
@@ -5,10 +5,13 @@ import {
5
5
  getContainer,
6
6
  getWidgetsCache,
7
7
  type V1Element,
8
+ type V1ElementData,
8
9
  } from '@elementor/editor-elements';
9
10
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
11
+ import { dispatchMcpStylesAppliedEvent } from '@elementor/editor-mcp';
10
12
 
11
13
  import { CompositionBuilder } from '../../../composition-builder/composition-builder';
14
+ import { trackCanvasEvent } from '../../../utils/tracking';
12
15
  import { AVAILABLE_WIDGETS_URI_V4 } from '../../resources/available-widgets-resource';
13
16
  import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
14
17
  import { BEST_PRACTICES_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
@@ -19,6 +22,13 @@ import { BUILD_COMPOSITIONS_GUIDE_URI, generatePrompt } from './prompt';
19
22
  import { inputSchema as schema, outputSchema } from './schema';
20
23
  import { adaptLeafRootParams } from './xml-leaf-wrapper';
21
24
 
25
+ export type ElementAddedEvent = {
26
+ element: V1ElementData;
27
+ executedBy: 'mcp_tool' | 'user';
28
+ };
29
+
30
+ export const ELEMENT_ADDED_EVENT = 'elementor/canvas/element-added';
31
+
22
32
  export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
23
33
  const { addTool, resource } = reg;
24
34
 
@@ -74,15 +84,34 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
74
84
  compositionBuilder.setStylesConfig( stylesConfig );
75
85
  compositionBuilder.setCustomCSS( customCSS );
76
86
 
77
- const { configErrors, rootContainers: generatedRootContainers } =
78
- await compositionBuilder.build( targetContainer );
87
+ const {
88
+ configErrors,
89
+ formErrors,
90
+ rootContainers: generatedRootContainers,
91
+ } = await compositionBuilder.build( targetContainer );
79
92
 
80
93
  rootContainers.push( ...generatedRootContainers );
81
94
  generatedXML = new XMLSerializer().serializeToString( compositionBuilder.getXML() );
82
95
 
96
+ rootContainers.forEach( ( container ) => {
97
+ const elementData = container.model?.toJSON();
98
+
99
+ if ( elementData ) {
100
+ onElementAdded( elementData as V1ElementData );
101
+ }
102
+ } );
103
+
104
+ Object.values( stylesConfig ).forEach( ( styleValue ) => {
105
+ dispatchMcpStylesAppliedEvent( { styleValue } );
106
+ } );
107
+
83
108
  if ( configErrors.length ) {
84
109
  errors.push( ...configErrors.map( ( msg ) => new Error( msg ) ) );
85
110
  }
111
+
112
+ if ( formErrors.length ) {
113
+ errors.push( ...formErrors.map( ( msg ) => new Error( msg ) ) );
114
+ }
86
115
  } catch ( error ) {
87
116
  errors.push( error as Error );
88
117
  }
@@ -186,3 +215,30 @@ function assertCompositionXmlUsesV4WidgetsOnly( xmlStructure: string ) {
186
215
  }
187
216
  }
188
217
  }
218
+
219
+ function onElementAdded( element: V1ElementData ) {
220
+ const elType = element.elType ?? '';
221
+ const widgetType = element.widgetType ?? '';
222
+ const elementName = elType === 'widget' ? widgetType : elType;
223
+
224
+ trackCanvasEvent( {
225
+ eventName: 'add_element',
226
+ executed_by: 'mcp_tool',
227
+ element_name: elementName,
228
+ element_type: elType,
229
+ widget_type: widgetType,
230
+ } );
231
+
232
+ const event: ElementAddedEvent = {
233
+ element,
234
+ executedBy: 'mcp_tool',
235
+ };
236
+
237
+ window.dispatchEvent( new CustomEvent( ELEMENT_ADDED_EVENT, { detail: event } ) );
238
+
239
+ if ( element.elements?.length ) {
240
+ element.elements?.forEach( ( childElement ) => {
241
+ onElementAdded( childElement );
242
+ } );
243
+ }
244
+ }
@@ -1,5 +1,6 @@
1
1
  import { getContainer, getWidgetsCache } from '@elementor/editor-elements';
2
2
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
3
+ import { dispatchMcpStylesAppliedEvent } from '@elementor/editor-mcp';
3
4
  import { type PropValue, Schema } from '@elementor/editor-props';
4
5
 
5
6
  import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
@@ -115,6 +116,8 @@ async function applyStyleFromCss( opts: {
115
116
  propertyValue: styleValue,
116
117
  customCssWriteMode: 'merge-with-stored',
117
118
  } );
119
+
120
+ dispatchMcpStylesAppliedEvent( { styleValue } );
118
121
  } catch ( error ) {
119
122
  throw new Error(
120
123
  createUpdateErrorMessage( {
@@ -5,12 +5,14 @@ export type ElementOverlayProps = {
5
5
  id: string;
6
6
  isSelected: boolean;
7
7
  isGlobal?: boolean;
8
+ widgetType?: string;
8
9
  };
9
10
 
10
11
  export type OverlayFilterArgs = {
11
12
  id: string;
12
13
  element: HTMLElement;
13
14
  isSelected: boolean;
15
+ widgetType?: string;
14
16
  };
15
17
 
16
18
  export type ElementOverlayConfig = {
@@ -0,0 +1,39 @@
1
+ import { shouldUseSmallerOutlineOffset, THIN_ELEMENT_MAX_HEIGHT_PX } from '../outline-offset-utils';
2
+
3
+ describe( 'shouldUseSmallerOutlineOffset', () => {
4
+ it( 'returns true for thin elements like dividers', () => {
5
+ // Arrange.
6
+ const element = document.createElement( 'hr' );
7
+ Object.defineProperty( element, 'offsetHeight', { value: THIN_ELEMENT_MAX_HEIGHT_PX } );
8
+
9
+ // Act.
10
+ const result = shouldUseSmallerOutlineOffset( element );
11
+
12
+ // Assert.
13
+ expect( result ).toBe( true );
14
+ } );
15
+
16
+ it( 'returns true for atomic form input widgets', () => {
17
+ // Arrange.
18
+ const element = document.createElement( 'input' );
19
+ Object.defineProperty( element, 'offsetHeight', { value: 36 } );
20
+
21
+ // Act.
22
+ const result = shouldUseSmallerOutlineOffset( element, 'e-form-input' );
23
+
24
+ // Assert.
25
+ expect( result ).toBe( true );
26
+ } );
27
+
28
+ it( 'returns false for other atomic widgets', () => {
29
+ // Arrange.
30
+ const element = document.createElement( 'div' );
31
+ Object.defineProperty( element, 'offsetHeight', { value: 100 } );
32
+
33
+ // Act.
34
+ const result = shouldUseSmallerOutlineOffset( element, 'e-heading' );
35
+
36
+ // Assert.
37
+ expect( result ).toBe( false );
38
+ } );
39
+ } );
@@ -0,0 +1,11 @@
1
+ export const THIN_ELEMENT_MAX_HEIGHT_PX = 1;
2
+
3
+ export const SMALLER_OUTLINE_OFFSET_WIDGET_TYPES = new Set( [ 'e-form-input' ] );
4
+
5
+ export function shouldUseSmallerOutlineOffset( element: HTMLElement, widgetType?: string ): boolean {
6
+ if ( element.offsetHeight <= THIN_ELEMENT_MAX_HEIGHT_PX ) {
7
+ return true;
8
+ }
9
+
10
+ return widgetType !== undefined && SMALLER_OUTLINE_OFFSET_WIDGET_TYPES.has( widgetType );
11
+ }
@@ -0,0 +1,14 @@
1
+ import { trackEvent } from '@elementor/events';
2
+
3
+ type ElementEventData = {
4
+ eventName: 'add_element';
5
+ element_name: string;
6
+ element_type: string;
7
+ widget_type: string;
8
+ location?: string;
9
+ executed_by: 'user' | 'mcp_tool' | 'system';
10
+ };
11
+
12
+ export const trackCanvasEvent = ( data: ElementEventData ) => {
13
+ trackEvent( data );
14
+ };