@elementor/editor-canvas 4.2.0-923 → 4.2.0-925

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.
@@ -10,6 +10,7 @@ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
10
10
 
11
11
  import { CompositionBuilder } from '../../../composition-builder/composition-builder';
12
12
  import { AVAILABLE_WIDGETS_URI_V4 } from '../../resources/available-widgets-resource';
13
+ import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
13
14
  import { BEST_PRACTICES_URI, STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
14
15
  import { isWidgetAvailableForLLM } from '../../utils/element-data-util';
15
16
  import { getCompositionTargetContainer } from '../../utils/get-composition-target-container';
@@ -45,6 +46,7 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
45
46
  { description: 'Global Variables', uri: 'elementor://global-variables' },
46
47
  { description: 'Styles best practices', uri: BEST_PRACTICES_URI },
47
48
  { description: 'Available widgets for this tool', uri: AVAILABLE_WIDGETS_URI_V4 },
49
+ { description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
48
50
  ],
49
51
  outputSchema,
50
52
  handler: async ( rawParams ) => {
@@ -1,5 +1,6 @@
1
1
  import { toolPrompts } from '@elementor/editor-mcp';
2
2
 
3
+ import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
3
4
  import { STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
4
5
 
5
6
  export const CONFIGURE_ELEMENT_GUIDE_URI = 'elementor://canvas/tools/configure-element-guide';
@@ -59,6 +60,21 @@ For styleProperties, use the style schema provided, as it also uses the PropType
59
60
  For all non-primitive types, provide the key property as defined in the schema as $$type in the generated object, as it is MANDATORY for parsing.
60
61
 
61
62
  Use the EXACT "PROP-TYPE" Schema given, and ALWAYS include the "key" property from the original configuration for every property you are changing.
63
+
64
+ # Dynamic tags
65
+ A value can be made dynamic wherever its schema exposes a variant with "$$type": "dynamic". This may be the property root OR a NESTED field: for example an image is made dynamic on its "src" (the root stays "image"), NOT on the whole "image" value.
66
+ Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" variant, in place of the static variant. The variant's "name" enumerates the tags allowed at that node.
67
+ 1. Read the [${ DYNAMIC_TAGS_URI }] resource for each allowed tag's settings schema.
68
+ 2. Provide, at that node:
69
+ {
70
+ "$$type": "dynamic",
71
+ "value": {
72
+ "name": "<allowed tag name>",
73
+ "settings": { /* strictly per the tag's settings schema */ }
74
+ }
75
+ }
76
+ Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
77
+ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
62
78
  ` );
63
79
 
64
80
  configureElementToolPrompt.parameter( 'elementId', 'The ID of the element to configure. MANDATORY.' );
@@ -1,6 +1,7 @@
1
1
  import { getWidgetsCache } from '@elementor/editor-elements';
2
2
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
3
3
 
4
+ import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
4
5
  import { STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
5
6
  import { doUpdateElementProperty } from '../../utils/do-update-element-property';
6
7
  import { validateInput } from '../../utils/validate-input';
@@ -32,6 +33,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
32
33
  { description: 'Widgets schema', uri: WIDGET_SCHEMA_URI },
33
34
  { description: 'Styles schema', uri: STYLE_SCHEMA_URI },
34
35
  { description: 'Configure element guide', uri: CONFIGURE_ELEMENT_GUIDE_URI },
36
+ { description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
35
37
  ],
36
38
  handler: ( { elementId, propertiesToChange, elementType, stylePropertiesToChange } ) => {
37
39
  const widgetData = getWidgetsCache()?.[ elementType ];
@@ -0,0 +1,102 @@
1
+ import { getElementorConfig } from '@elementor/editor-v1-adapters';
2
+
3
+ import { dynamicTagLLMResolver, getDynamicTagNamesByCategories } from '../resolve-dynamic-tag';
4
+
5
+ jest.mock( '@elementor/editor-v1-adapters', () => ( {
6
+ getElementorConfig: jest.fn(),
7
+ } ) );
8
+
9
+ const mockedGetElementorConfig = getElementorConfig as jest.MockedFunction< typeof getElementorConfig >;
10
+
11
+ const givenTags = () =>
12
+ mockedGetElementorConfig.mockReturnValue( {
13
+ atomicDynamicTags: {
14
+ tags: {
15
+ 'post-custom-field': {
16
+ name: 'post-custom-field',
17
+ label: 'Post Custom Field',
18
+ group: 'post',
19
+ categories: [ 'text', 'url' ],
20
+ props_schema: {
21
+ key: { kind: 'plain', key: 'string', default: '' },
22
+ before: { kind: 'plain', key: 'string', default: 'prefix' },
23
+ fallback: { kind: 'plain', key: 'string', default: '' },
24
+ },
25
+ },
26
+ 'site-title': {
27
+ name: 'site-title',
28
+ label: 'Site Title',
29
+ group: 'site',
30
+ categories: [ 'text' ],
31
+ props_schema: {},
32
+ },
33
+ },
34
+ },
35
+ } as never );
36
+
37
+ describe( 'resolve-dynamic-tag', () => {
38
+ beforeEach( () => {
39
+ jest.clearAllMocks();
40
+ givenTags();
41
+ } );
42
+
43
+ describe( 'getDynamicTagNamesByCategories', () => {
44
+ it( 'returns only the tags whose categories intersect the prop categories', () => {
45
+ // Act
46
+ const names = getDynamicTagNamesByCategories( [ 'url' ] );
47
+
48
+ // Assert
49
+ expect( names ).toEqual( [ 'post-custom-field' ] );
50
+ } );
51
+
52
+ it( 'returns an empty list when no categories are requested', () => {
53
+ // Act & Assert
54
+ expect( getDynamicTagNamesByCategories( [] ) ).toEqual( [] );
55
+ } );
56
+ } );
57
+
58
+ describe( 'dynamicTagLLMResolver', () => {
59
+ it( 'fills the group from the registry and wraps provided scalar settings', () => {
60
+ // Act
61
+ const resolved = dynamicTagLLMResolver( {
62
+ name: 'post-custom-field',
63
+ settings: { key: 'price' },
64
+ } );
65
+
66
+ // Assert
67
+ expect( resolved ).toEqual( {
68
+ $$type: 'dynamic',
69
+ value: {
70
+ name: 'post-custom-field',
71
+ group: 'post',
72
+ settings: {
73
+ key: { $$type: 'string', value: 'price' },
74
+ before: { $$type: 'string', value: 'prefix' },
75
+ },
76
+ },
77
+ } );
78
+ } );
79
+
80
+ it( 'keeps already-wrapped settings values untouched', () => {
81
+ // Act
82
+ const resolved = dynamicTagLLMResolver( {
83
+ name: 'post-custom-field',
84
+ settings: { key: { $$type: 'string', value: 'price' } },
85
+ } ) as { value: { settings: Record< string, unknown > } };
86
+
87
+ // Assert
88
+ expect( resolved.value.settings.key ).toEqual( { $$type: 'string', value: 'price' } );
89
+ } );
90
+
91
+ it( 'returns a structurally complete value for unknown tags', () => {
92
+ // Act
93
+ const resolved = dynamicTagLLMResolver( { name: 'does-not-exist' } );
94
+
95
+ // Assert
96
+ expect( resolved ).toEqual( {
97
+ $$type: 'dynamic',
98
+ value: { name: 'does-not-exist', group: '', settings: {} },
99
+ } );
100
+ } );
101
+ } );
102
+ } );
@@ -12,6 +12,7 @@ import { type Utils as IUtils } from '@elementor/editor-variables';
12
12
  import { type z } from '@elementor/schema';
13
13
 
14
14
  import { mergeCustomCssText, readStoredCustomCssText } from './merge-custom-css';
15
+ import { DYNAMIC_PROP_TYPE_KEY, dynamicTagLLMResolver } from './resolve-dynamic-tag';
15
16
 
16
17
  // TODO: see https://elementor.atlassian.net/browse/ED-22513 for better cross-module access
17
18
  type XElementor = z.infer< z.ZodAny >;
@@ -34,7 +35,10 @@ export function resolvePropValue( value: unknown, forceKey?: string ): PropValue
34
35
  .Utils as typeof IUtils;
35
36
  return Schema.adjustLlmPropValueSchema( value as PropValue, {
36
37
  forceKey,
37
- transformers: Utils.globalVariablesLLMResolvers,
38
+ transformers: {
39
+ ...Utils.globalVariablesLLMResolvers,
40
+ [ DYNAMIC_PROP_TYPE_KEY ]: dynamicTagLLMResolver,
41
+ },
38
42
  } );
39
43
  }
40
44
 
@@ -0,0 +1,102 @@
1
+ import { type PropType, type PropValue } from '@elementor/editor-props';
2
+ import { getElementorConfig } from '@elementor/editor-v1-adapters';
3
+
4
+ export const DYNAMIC_PROP_TYPE_KEY = 'dynamic';
5
+
6
+ // `fallback` is a generic render-time default added to every tag; it is noise for configuration.
7
+ export const OMITTED_DYNAMIC_SETTING_KEYS = [ 'fallback' ] as const;
8
+
9
+ type SettingPropType = PropType & { key?: string };
10
+
11
+ export type AtomicDynamicTag = {
12
+ name: string;
13
+ label: string;
14
+ group: string;
15
+ categories: string[];
16
+ props_schema: Record< string, SettingPropType >;
17
+ };
18
+
19
+ type LlmDynamicValue = {
20
+ name?: string;
21
+ settings?: Record< string, unknown >;
22
+ };
23
+
24
+ export const getAtomicDynamicTags = (): Record< string, AtomicDynamicTag > => {
25
+ const config = getElementorConfig() as { atomicDynamicTags?: { tags?: Record< string, AtomicDynamicTag > } };
26
+ return config.atomicDynamicTags?.tags ?? {};
27
+ };
28
+
29
+ export const getDynamicTagNamesByCategories = ( categories: string[] ): string[] => {
30
+ if ( ! categories.length ) {
31
+ return [];
32
+ }
33
+ const wanted = new Set( categories );
34
+ return Object.values( getAtomicDynamicTags() )
35
+ .filter( ( tag ) => tag.categories?.some( ( category ) => wanted.has( category ) ) )
36
+ .map( ( tag ) => tag.name );
37
+ };
38
+
39
+ // Reconstructs an intact dynamic PropValue from whatever the LLM produced: the authoritative `group`
40
+ // is taken from the registry (never trusted from the model) and `settings` are rebuilt strictly from
41
+ // the tag's props schema (provided values are wrapped, omitted ones fall back to their defaults).
42
+ export const dynamicTagLLMResolver = ( value: unknown ): PropValue => {
43
+ const input = ( value ?? {} ) as LlmDynamicValue;
44
+ const tag = input.name ? getAtomicDynamicTags()[ input.name ] : undefined;
45
+
46
+ if ( ! tag ) {
47
+ return {
48
+ $$type: DYNAMIC_PROP_TYPE_KEY,
49
+ value: { name: input.name ?? '', group: '', settings: {} },
50
+ } as PropValue;
51
+ }
52
+
53
+ return {
54
+ $$type: DYNAMIC_PROP_TYPE_KEY,
55
+ value: {
56
+ name: tag.name,
57
+ group: tag.group,
58
+ settings: buildStrictSettings( tag.props_schema ?? {}, input.settings ?? {} ),
59
+ },
60
+ } as PropValue;
61
+ };
62
+
63
+ const buildStrictSettings = (
64
+ schema: Record< string, SettingPropType >,
65
+ provided: Record< string, unknown >
66
+ ): Record< string, unknown > => {
67
+ const settings: Record< string, unknown > = {};
68
+
69
+ for ( const [ key, propType ] of Object.entries( schema ) ) {
70
+ if ( ( OMITTED_DYNAMIC_SETTING_KEYS as readonly string[] ).includes( key ) ) {
71
+ continue;
72
+ }
73
+
74
+ const resolved =
75
+ provided[ key ] !== undefined
76
+ ? wrapSettingValue( provided[ key ], propType )
77
+ : defaultSettingValue( propType );
78
+
79
+ if ( resolved !== undefined && resolved !== null ) {
80
+ settings[ key ] = resolved;
81
+ }
82
+ }
83
+
84
+ return settings;
85
+ };
86
+
87
+ const wrapSettingValue = ( raw: unknown, propType: SettingPropType ): unknown => {
88
+ if ( raw !== null && typeof raw === 'object' ) {
89
+ return raw;
90
+ }
91
+ return propType.key ? { $$type: propType.key, value: raw } : raw;
92
+ };
93
+
94
+ const defaultSettingValue = ( propType: SettingPropType ): unknown => {
95
+ if ( propType.initial_value !== null && propType.initial_value !== undefined ) {
96
+ return propType.initial_value;
97
+ }
98
+ if ( propType.default !== null && propType.default !== undefined ) {
99
+ return wrapSettingValue( propType.default, propType );
100
+ }
101
+ return undefined;
102
+ };