@elementor/elementor-v3-mcp 4.1.0-754
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/dist/index.d.mts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +1161 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1125 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
- package/src/context.ts +211 -0
- package/src/elementor-mcp-server.ts +95 -0
- package/src/index.ts +22 -0
- package/src/init.ts +5 -0
- package/src/resources.ts +186 -0
- package/src/tools/ai-tool.ts +58 -0
- package/src/tools/dynamic-tool.ts +236 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/page-tool.ts +222 -0
- package/src/tools/routes-tool.ts +138 -0
- package/src/tools/styling-tool.ts +131 -0
- package/src/tools/ui-tool.ts +100 -0
- package/src/types.ts +120 -0
- package/src/utils.ts +211 -0
- package/src/validation-helpers.ts +29 -0
- package/src/widget-mandatory-fields.ts +68 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from '@elementor/schema';
|
|
2
|
+
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
|
|
4
|
+
import type { McpToolResult, ToolParams } from '../types';
|
|
5
|
+
import { get$e } from '../utils';
|
|
6
|
+
|
|
7
|
+
export function addRoutesTool( server: McpServer ): void {
|
|
8
|
+
const $e = get$e();
|
|
9
|
+
const routes = $e?.routes?.getAll?.() || [];
|
|
10
|
+
const components = $e?.components?.getAll?.() || [];
|
|
11
|
+
const availableToOpenComponents = components.filter(
|
|
12
|
+
( component: string ) => $e?.components?.get( component )?.getCommands?.()?.open
|
|
13
|
+
);
|
|
14
|
+
const availableToCloseComponents = components.filter(
|
|
15
|
+
( component: string ) => $e?.components?.get( component )?.getCommands?.()?.close
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
server.registerTool(
|
|
19
|
+
'routes',
|
|
20
|
+
{
|
|
21
|
+
description:
|
|
22
|
+
'Manage Elementor editor routing and navigation. Use this tool to open a component, navigate to a route, go back from a route or close components. Always prefer this tool when user is on the Elementor editor.' +
|
|
23
|
+
` Available routes to navigate to or back from: ${ routes.join( ', ' ) }`,
|
|
24
|
+
inputSchema: {
|
|
25
|
+
action: z.enum( [ 'open', 'navigate', 'go-back', 'close' ] ),
|
|
26
|
+
route: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
'The route to navigate to or back from it. Do not send this parameter if you only want to open a component.'
|
|
31
|
+
),
|
|
32
|
+
componentToOpen: z
|
|
33
|
+
.enum( ( availableToOpenComponents.length ? availableToOpenComponents : [ '' ] ) as [ string ] )
|
|
34
|
+
.optional()
|
|
35
|
+
.describe( 'The component to open or navigate to.' ),
|
|
36
|
+
componentToClose: z
|
|
37
|
+
.enum( ( availableToCloseComponents.length ? availableToCloseComponents : [ '' ] ) as [ string ] )
|
|
38
|
+
.optional()
|
|
39
|
+
.describe( 'The component to close.' ),
|
|
40
|
+
},
|
|
41
|
+
annotations: {
|
|
42
|
+
title: 'Manage Routes',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
async ( params: ToolParams ) => {
|
|
46
|
+
switch ( params.action ) {
|
|
47
|
+
case 'open':
|
|
48
|
+
return await handleOpen( params );
|
|
49
|
+
case 'navigate':
|
|
50
|
+
return await handleNavigate( params );
|
|
51
|
+
case 'go-back':
|
|
52
|
+
return await handleGoBack( params );
|
|
53
|
+
case 'close':
|
|
54
|
+
return await handleClose( params );
|
|
55
|
+
default:
|
|
56
|
+
throw new Error( `Unknown action: ${ params.action }` );
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleOpen( params: ToolParams ): Promise< McpToolResult > {
|
|
63
|
+
const $e = get$e();
|
|
64
|
+
const component = ( params.componentToOpen || params.route ) as string;
|
|
65
|
+
const openCommand = $e?.components?.get( component )?.getCommands?.()?.open?.registerConfig.command;
|
|
66
|
+
|
|
67
|
+
if ( openCommand ) {
|
|
68
|
+
await $e?.run( openCommand, {} );
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error( 'Could not open component' );
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [ { type: 'text', text: `Opened: ${ component }` } ],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleNavigate( params: ToolParams ): Promise< McpToolResult > {
|
|
79
|
+
const $e = get$e();
|
|
80
|
+
const route = params.route as string;
|
|
81
|
+
const componentToOpen = params.componentToOpen as string;
|
|
82
|
+
const openCommand = $e?.components?.get( componentToOpen )?.getCommands?.()?.open?.registerConfig.command;
|
|
83
|
+
|
|
84
|
+
if ( openCommand ) {
|
|
85
|
+
await $e?.run( openCommand, {} );
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const routeComponent = $e?.routes?.getComponent?.( route );
|
|
89
|
+
if ( routeComponent ) {
|
|
90
|
+
$e?.routes?.saveState?.( routeComponent.getNamespace() );
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
$e?.routes?.to?.( route, {} );
|
|
95
|
+
} catch {
|
|
96
|
+
const openCommandFallback = $e?.components?.get( route )?.getCommands?.()?.open?.registerConfig.command;
|
|
97
|
+
|
|
98
|
+
if ( openCommandFallback ) {
|
|
99
|
+
await $e?.run( openCommandFallback, {} );
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error( 'Could not navigate to route' );
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content: [ { type: 'text', text: `Navigated to: ${ route }` } ],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function handleGoBack( params: ToolParams ): Promise< McpToolResult > {
|
|
111
|
+
const $e = get$e();
|
|
112
|
+
const route = params.route as string;
|
|
113
|
+
const component = $e?.routes?.getComponent?.( route );
|
|
114
|
+
|
|
115
|
+
if ( component ) {
|
|
116
|
+
$e?.routes?.back?.( component.getNamespace() );
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content: [ { type: 'text', text: `Go back to: ${ route }` } ],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function handleClose( params: ToolParams ): Promise< McpToolResult > {
|
|
125
|
+
const $e = get$e();
|
|
126
|
+
const component = params.componentToClose as string;
|
|
127
|
+
const closeCommand = $e?.components?.get( component )?.getCommands?.()?.close?.registerConfig.command;
|
|
128
|
+
|
|
129
|
+
if ( closeCommand ) {
|
|
130
|
+
await $e?.run( closeCommand, {} );
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error( 'Could not close component' );
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
content: [ { type: 'text', text: `Closed: ${ component }` } ],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from '@elementor/schema';
|
|
2
|
+
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { SamplingMessageSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
|
|
5
|
+
import type { McpToolResult } from '../types';
|
|
6
|
+
import { getElementor } from '../utils';
|
|
7
|
+
|
|
8
|
+
export function addStylingTool( server: McpServer ): void {
|
|
9
|
+
server.registerTool(
|
|
10
|
+
'styling',
|
|
11
|
+
{
|
|
12
|
+
description: `This tool provides AI-powered custom CSS styling for Elementor elements. Use this when users want advanced styling that goes beyond Elementor capabilities and can't be targeted using the element settings.
|
|
13
|
+
|
|
14
|
+
**When to use this tool:**
|
|
15
|
+
- Visual effects: shadows, filters, pseudo-elements, advanced selectors
|
|
16
|
+
- Complex animations with custom keyframes or CSS transitions
|
|
17
|
+
- Styling that requires media queries or complex CSS rules
|
|
18
|
+
- Click-triggered effects or other non-motion triggers
|
|
19
|
+
- Custom hover effects that don't involve motion (color changes, opacity, etc.)
|
|
20
|
+
|
|
21
|
+
**When NOT to use this tool:**
|
|
22
|
+
- Basic styling achievable through Elementor settings (colors, typography, spacing, borders, simple hover effects) -> use the elementor__elements with "update-settings" action.
|
|
23
|
+
|
|
24
|
+
**Do NOT use this tool if the user mentions motion effects with supported triggers:**
|
|
25
|
+
- "on hover" with motion (movement, rotation, scaling) → use motion-effects tool
|
|
26
|
+
- "on scroll" with motion effects → use motion-effects tool
|
|
27
|
+
- "mouse move" / "follow mouse" → use motion-effects tool
|
|
28
|
+
- "entrance" / "fade in" / "slide in" animations → use motion-effects tool
|
|
29
|
+
|
|
30
|
+
**Actions available:**
|
|
31
|
+
- **custom-css**: Generate and apply AI-powered custom CSS to elements
|
|
32
|
+
|
|
33
|
+
This tool generates CSS code using AI, provides preview functionality, and handles user approval workflow for applying custom styles.`,
|
|
34
|
+
inputSchema: {
|
|
35
|
+
action: z
|
|
36
|
+
.enum( [ 'custom-css' ] )
|
|
37
|
+
.describe(
|
|
38
|
+
'The styling operation to perform. Currently supports custom-css for AI-generated CSS styling.'
|
|
39
|
+
),
|
|
40
|
+
elementId: z
|
|
41
|
+
.string()
|
|
42
|
+
.describe(
|
|
43
|
+
'The ID of the Elementor element to apply custom styling to. This element will receive the generated CSS code.'
|
|
44
|
+
),
|
|
45
|
+
prompt: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe(
|
|
48
|
+
'A detailed description of the desired styling. Include specific visual requirements, colors, effects, layout modifications, or any custom styling needs. The more detailed the prompt, the better the generated CSS will match your requirements.'
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
annotations: {
|
|
52
|
+
title: 'Apply Custom Styling',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
async ( params ) => {
|
|
56
|
+
switch ( params.action ) {
|
|
57
|
+
case 'custom-css':
|
|
58
|
+
return await handleCustomCss( params.elementId as string, params.prompt as string, server );
|
|
59
|
+
default:
|
|
60
|
+
throw new Error( `Unknown action: ${ params.action }` );
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleCustomCss( elementId: string, prompt: string, server: McpServer ): Promise< McpToolResult > {
|
|
67
|
+
const container = getElementor()?.getContainer( elementId );
|
|
68
|
+
if ( ! container ) {
|
|
69
|
+
throw new Error( `Element with ID ${ elementId } not found.` );
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const htmlMarkup = container.view?.el?.outerHTML || '';
|
|
73
|
+
|
|
74
|
+
const parseCSS = ( css: string ) => {
|
|
75
|
+
return css && css.replace( /`/g, '' ).replace( /^css\s*/i, '' );
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const samplingCssResult = await server.server.request(
|
|
79
|
+
{
|
|
80
|
+
method: 'sampling/createMessage',
|
|
81
|
+
params: {
|
|
82
|
+
messages: [
|
|
83
|
+
{
|
|
84
|
+
role: 'user',
|
|
85
|
+
content: {
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: prompt,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
maxTokens: 1000,
|
|
92
|
+
modelPreferences: {
|
|
93
|
+
hints: [
|
|
94
|
+
{
|
|
95
|
+
name: 'elementor-css',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
metadata: {
|
|
100
|
+
element_id: elementId,
|
|
101
|
+
html_markup: htmlMarkup,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
SamplingMessageSchema
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const content = samplingCssResult?.content;
|
|
109
|
+
const block = Array.isArray( content ) ? content.find( ( b ) => b.type === 'text' ) : content;
|
|
110
|
+
const cssText = block?.type === 'text' ? block.text : undefined;
|
|
111
|
+
|
|
112
|
+
if ( ! cssText ) {
|
|
113
|
+
throw new Error( 'Failed to generate CSS: No text content received from API.' );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parsedCssString = parseCSS( cssText );
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: JSON.stringify( {
|
|
123
|
+
success: true,
|
|
124
|
+
message: 'Custom CSS generated. The CSS needs to be applied through the editor.',
|
|
125
|
+
generatedCss: parsedCssString,
|
|
126
|
+
elementId,
|
|
127
|
+
} ),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from '@elementor/schema';
|
|
2
|
+
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
|
|
4
|
+
import type { McpToolResult, ToolParams } from '../types';
|
|
5
|
+
import { get$e, getElementor } from '../utils';
|
|
6
|
+
|
|
7
|
+
export function addUiTool( server: McpServer ): void {
|
|
8
|
+
server.registerTool(
|
|
9
|
+
'ui',
|
|
10
|
+
{
|
|
11
|
+
description:
|
|
12
|
+
'Manage Elementor editor UI operations. This tool provides control over editor interface actions including responsive preview modes and widget favorites. Use this when you need to: switch between device preview modes (desktop/tablet/mobile), manage favorite widgets, or paste previously copied content. Note: For undo/redo operations, use the page tool instead. The tool interacts directly with the Elementor editor UI and does not modify page content itself.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
action: z
|
|
15
|
+
.enum( [ 'change-device-mode', 'toggle-favorite', 'ui-paste' ] )
|
|
16
|
+
.describe(
|
|
17
|
+
'The UI operation to perform: change-device-mode (switch responsive preview), toggle-favorite (add/remove widget from favorites), ui-paste (paste clipboard content into an existing element)'
|
|
18
|
+
),
|
|
19
|
+
deviceMode: z
|
|
20
|
+
.enum( [ 'desktop', 'tablet', 'mobile' ] )
|
|
21
|
+
.optional()
|
|
22
|
+
.describe( 'Required for change-device-mode. The device mode to switch to for responsive preview' ),
|
|
23
|
+
widgetType: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(
|
|
27
|
+
'Required for toggle-favorite. The widget type name (e.g., "heading", "button", "image") to add or remove from favorites'
|
|
28
|
+
),
|
|
29
|
+
elementId: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
'Required for ui-paste. The ID of an existing container element where clipboard content should be pasted. Note: The element must exist and content must be in clipboard first'
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
annotations: {
|
|
37
|
+
title: 'Manage UI',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
async ( params: ToolParams ) => {
|
|
41
|
+
switch ( params.action ) {
|
|
42
|
+
case 'change-device-mode':
|
|
43
|
+
return await handleChangeDeviceMode( params );
|
|
44
|
+
case 'toggle-favorite':
|
|
45
|
+
return await handleToggleFavorite( params );
|
|
46
|
+
case 'ui-paste':
|
|
47
|
+
return await handleUiPaste( params );
|
|
48
|
+
default:
|
|
49
|
+
throw new Error( `Unknown action: ${ params.action }` );
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleChangeDeviceMode( params: ToolParams ): Promise< McpToolResult > {
|
|
56
|
+
if ( ! params.deviceMode ) {
|
|
57
|
+
throw new Error( 'deviceMode is required for change-device-mode action' );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get$e()?.run( 'panel/change-device-mode', {
|
|
61
|
+
device: params.deviceMode,
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: [ { type: 'text', text: `Device mode changed to ${ params.deviceMode }.` } ],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function handleToggleFavorite( params: ToolParams ): Promise< McpToolResult > {
|
|
70
|
+
if ( ! params.widgetType ) {
|
|
71
|
+
throw new Error( 'widgetType is required for toggle-favorite action' );
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get$e()?.run( 'favorites/toggle', {
|
|
75
|
+
name: params.widgetType,
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
content: [ { type: 'text', text: `Favorite status toggled for ${ params.widgetType }.` } ],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleUiPaste( params: ToolParams ): Promise< McpToolResult > {
|
|
84
|
+
if ( ! params.elementId ) {
|
|
85
|
+
throw new Error( 'elementId is required for ui-paste action' );
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const container = getElementor()?.getContainer( params.elementId as string );
|
|
89
|
+
if ( ! container ) {
|
|
90
|
+
throw new Error( `Element with ID ${ params.elementId } not found.` );
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get$e()?.run( 'document/ui/paste', {
|
|
94
|
+
container,
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: [ { type: 'text', text: `UI paste performed on element ${ params.elementId }.` } ],
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export type ToolParams = {
|
|
2
|
+
[ key: string ]: unknown;
|
|
3
|
+
action: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type McpToolResult = {
|
|
7
|
+
content: Array< {
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
} >;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface ElementorContainer {
|
|
14
|
+
id: string;
|
|
15
|
+
model: {
|
|
16
|
+
id: string;
|
|
17
|
+
attributes?: Record< string, unknown >;
|
|
18
|
+
editor_settings?: { title?: string };
|
|
19
|
+
get?: ( key: string ) => unknown;
|
|
20
|
+
};
|
|
21
|
+
settings: {
|
|
22
|
+
attributes?: Record< string, unknown >;
|
|
23
|
+
controls?: Record< string, unknown >;
|
|
24
|
+
get: ( key: string ) => unknown;
|
|
25
|
+
};
|
|
26
|
+
children?: ElementorContainer[];
|
|
27
|
+
view?: {
|
|
28
|
+
el: HTMLElement;
|
|
29
|
+
};
|
|
30
|
+
parent?: ElementorContainer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ElementorControls {
|
|
34
|
+
[ key: string ]: {
|
|
35
|
+
type: string;
|
|
36
|
+
default?: unknown;
|
|
37
|
+
options?: Record< string, string >;
|
|
38
|
+
fields?: ElementorControls;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ElementorControlsMapped = {
|
|
43
|
+
[ key: string ]: {
|
|
44
|
+
default: unknown;
|
|
45
|
+
options?: string[];
|
|
46
|
+
onValue?: string;
|
|
47
|
+
size_units?: string[];
|
|
48
|
+
range?: { min: number; max: number };
|
|
49
|
+
type: string;
|
|
50
|
+
fields?: ElementorControlsMapped;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface ElementorDocument {
|
|
55
|
+
id: string;
|
|
56
|
+
container: ElementorContainer & { children: ElementorContainer[] };
|
|
57
|
+
config: {
|
|
58
|
+
type: string;
|
|
59
|
+
settings: Record< string, unknown > & {
|
|
60
|
+
controls?: ElementorControls;
|
|
61
|
+
settings?: Record< string, unknown >;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ElementorInstance {
|
|
67
|
+
documents: {
|
|
68
|
+
getCurrent: () => ElementorDocument | null;
|
|
69
|
+
get: ( id: string ) => {
|
|
70
|
+
config?: {
|
|
71
|
+
settings: {
|
|
72
|
+
controls: ElementorControls;
|
|
73
|
+
settings: Record< string, unknown >;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
} | null;
|
|
77
|
+
};
|
|
78
|
+
getContainer: ( id: string ) => ElementorContainer | null;
|
|
79
|
+
getCurrentElement: () => { model: { id: string } } | null;
|
|
80
|
+
selection: {
|
|
81
|
+
elements: Record< string, unknown >;
|
|
82
|
+
};
|
|
83
|
+
widgetsCache: Record< string, { controls: ElementorControls } >;
|
|
84
|
+
config: {
|
|
85
|
+
controls: Record< string, unknown >;
|
|
86
|
+
};
|
|
87
|
+
dynamicTags?: {
|
|
88
|
+
getConfig: ( key: string ) => Record< string, { categories: string[] } >;
|
|
89
|
+
tagDataToTagText: ( id: string, name: string, settings: Record< string, unknown > ) => string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ElementorCommandsInstance {
|
|
94
|
+
run: ( command: string, args?: Record< string, unknown > ) => Promise< unknown >;
|
|
95
|
+
data: {
|
|
96
|
+
get: ( key: string ) => Promise< { data: Record< string, { value: unknown } > } >;
|
|
97
|
+
};
|
|
98
|
+
routes: {
|
|
99
|
+
getAll: () => string[];
|
|
100
|
+
getComponent: ( route: string ) => { getNamespace: () => string };
|
|
101
|
+
saveState: ( namespace: string ) => void;
|
|
102
|
+
to: ( route: string, args: Record< string, unknown > ) => void;
|
|
103
|
+
back: ( namespace: string ) => void;
|
|
104
|
+
};
|
|
105
|
+
components: {
|
|
106
|
+
getAll: () => string[];
|
|
107
|
+
get: ( name: string ) => {
|
|
108
|
+
getCommands: () => {
|
|
109
|
+
open?: { registerConfig: { command: string } };
|
|
110
|
+
close?: { registerConfig: { command: string } };
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ElementorCommonInstance {
|
|
117
|
+
helpers?: {
|
|
118
|
+
getUniqueId?: () => number | string;
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ElementorCommandsInstance,
|
|
3
|
+
ElementorCommonInstance,
|
|
4
|
+
ElementorContainer,
|
|
5
|
+
ElementorInstance,
|
|
6
|
+
} from './types';
|
|
7
|
+
import { widgetMandatoryFields } from './widget-mandatory-fields';
|
|
8
|
+
|
|
9
|
+
interface McpWindow {
|
|
10
|
+
elementor?: ElementorInstance;
|
|
11
|
+
$e?: ElementorCommandsInstance;
|
|
12
|
+
elementorCommon?: ElementorCommonInstance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const getElementor = (): ElementorInstance | undefined => ( window as unknown as McpWindow ).elementor;
|
|
16
|
+
|
|
17
|
+
export const get$e = (): ElementorCommandsInstance | undefined => ( window as unknown as McpWindow ).$e;
|
|
18
|
+
|
|
19
|
+
export const getElementorCommon = (): ElementorCommonInstance | undefined =>
|
|
20
|
+
( window as unknown as McpWindow ).elementorCommon;
|
|
21
|
+
|
|
22
|
+
export async function updateRepeaterControl(
|
|
23
|
+
container: ElementorContainer,
|
|
24
|
+
repeaterName: string,
|
|
25
|
+
widgetType: string,
|
|
26
|
+
settings: Record< string, unknown >
|
|
27
|
+
): Promise< Record< string, unknown >[] > {
|
|
28
|
+
const repeaterControl = getElementor()?.widgetsCache?.[ widgetType ]?.controls?.[ repeaterName ] as {
|
|
29
|
+
fields?: Record< string, { default?: unknown } >;
|
|
30
|
+
};
|
|
31
|
+
const existingAttribute = container.settings?.attributes?.[ repeaterName ] as { models?: unknown[] };
|
|
32
|
+
let existingItemsCount = existingAttribute?.models?.length ?? 0;
|
|
33
|
+
const repeaterModel: Record< string, unknown > = {};
|
|
34
|
+
const insertedModels: Record< string, unknown >[] = [];
|
|
35
|
+
|
|
36
|
+
Object.keys( repeaterControl?.fields ?? {} ).forEach( ( fieldKey ) => {
|
|
37
|
+
const field = repeaterControl?.fields?.[ fieldKey ];
|
|
38
|
+
repeaterModel[ fieldKey ] = field?.default || '';
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
const repeaterValues = settings[ repeaterName ] as Record< string, unknown >[];
|
|
42
|
+
repeaterValues.forEach( ( val ) => {
|
|
43
|
+
const itemModel: Record< string, unknown > = {};
|
|
44
|
+
Object.keys( repeaterModel ).forEach( ( fieldKey ) => {
|
|
45
|
+
itemModel[ fieldKey ] = val[ fieldKey ] ?? repeaterModel[ fieldKey ];
|
|
46
|
+
} );
|
|
47
|
+
itemModel._id = getElementorCommon()?.helpers?.getUniqueId?.();
|
|
48
|
+
|
|
49
|
+
get$e()?.run( 'document/repeater/insert', {
|
|
50
|
+
container,
|
|
51
|
+
name: repeaterName,
|
|
52
|
+
model: itemModel,
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
insertedModels.push( itemModel );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
while ( existingItemsCount-- ) {
|
|
59
|
+
await get$e()?.run( 'document/repeater/remove', { container, name: repeaterName, index: 0 } );
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return insertedModels;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function addMandatoryFields( settings: Record< string, unknown > ): Record< string, unknown > {
|
|
66
|
+
const missingFields: Record< string, unknown > = {};
|
|
67
|
+
|
|
68
|
+
Object.keys( settings ).forEach( ( settingKey ) => {
|
|
69
|
+
const mandatoryFields = widgetMandatoryFields[ settingKey ];
|
|
70
|
+
|
|
71
|
+
mandatoryFields?.forEach( ( mandatoryField ) => {
|
|
72
|
+
if ( ! settings[ mandatoryField.mandatory ] ) {
|
|
73
|
+
settings[ mandatoryField.mandatory ] = mandatoryField.default;
|
|
74
|
+
missingFields[ mandatoryField.mandatory ] = mandatoryField.default;
|
|
75
|
+
}
|
|
76
|
+
} );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
return missingFields;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getCurrentSelection(): string[] {
|
|
83
|
+
return Object.keys( getElementor()?.selection?.elements || {} );
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function restoreCurrentSelection( selectedElementId: string | null ): Promise< void > {
|
|
87
|
+
const elementor = getElementor();
|
|
88
|
+
const $e = get$e();
|
|
89
|
+
if ( selectedElementId && elementor && $e ) {
|
|
90
|
+
try {
|
|
91
|
+
await $e.run( 'document/elements/select', {
|
|
92
|
+
container: elementor.getContainer( selectedElementId ),
|
|
93
|
+
} );
|
|
94
|
+
} catch {
|
|
95
|
+
// Unable to restore selection
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function convertToGlobalFormat( settings: Record< string, unknown > ): Record< string, string > {
|
|
101
|
+
const converted: Record< string, string > = {};
|
|
102
|
+
|
|
103
|
+
for ( const [ key, value ] of Object.entries( settings ) ) {
|
|
104
|
+
if ( typeof value === 'string' && value.startsWith( 'globals/' ) ) {
|
|
105
|
+
converted[ key ] = value;
|
|
106
|
+
} else if ( key.includes( 'typography' ) ) {
|
|
107
|
+
converted[ key ] = `globals/typography?id=${ value }`;
|
|
108
|
+
} else {
|
|
109
|
+
converted[ key ] = `globals/colors?id=${ value }`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return converted;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function extractAndApplyGlobalStyles(
|
|
117
|
+
settings: Record< string, unknown >,
|
|
118
|
+
elementId: string
|
|
119
|
+
): Promise< { globalStyles: Record< string, string >; remainingSettings: Record< string, unknown > } > {
|
|
120
|
+
const globalStyles = ( settings.__globals__ || {} ) as Record< string, string >;
|
|
121
|
+
const remainingSettings: Record< string, unknown > = {};
|
|
122
|
+
|
|
123
|
+
Object.keys( settings ).forEach( ( key ) => {
|
|
124
|
+
if ( key === '__globals__' ) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
remainingSettings[ key ] = settings[ key ];
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
const $e = get$e();
|
|
131
|
+
if ( ! $e?.data?.get ) {
|
|
132
|
+
return { globalStyles, remainingSettings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const globalColorsResult = ( await $e.data.get( 'globals/colors' ) ) as
|
|
136
|
+
| {
|
|
137
|
+
data: Record< string, { value: unknown } >;
|
|
138
|
+
}
|
|
139
|
+
| undefined;
|
|
140
|
+
const globalTypographyResult = ( await $e.data.get( 'globals/typography' ) ) as
|
|
141
|
+
| {
|
|
142
|
+
data: Record< string, { value: unknown } >;
|
|
143
|
+
}
|
|
144
|
+
| undefined;
|
|
145
|
+
|
|
146
|
+
const globalColors = globalColorsResult?.data ?? {};
|
|
147
|
+
const globalTypography = globalTypographyResult?.data ?? {};
|
|
148
|
+
|
|
149
|
+
const keysToRemove: string[] = [];
|
|
150
|
+
|
|
151
|
+
Object.keys( remainingSettings ).forEach( ( key ) => {
|
|
152
|
+
const value = remainingSettings[ key ];
|
|
153
|
+
if ( typeof value === 'string' && value ) {
|
|
154
|
+
if ( globalColors[ value ] || globalTypography[ value ] ) {
|
|
155
|
+
globalStyles[ key ] = value;
|
|
156
|
+
keysToRemove.push( key );
|
|
157
|
+
} else {
|
|
158
|
+
const globalColorKey = Object.keys( globalColors ).find(
|
|
159
|
+
( globalKey ) => globalColors[ globalKey ]?.value === value
|
|
160
|
+
);
|
|
161
|
+
const globalTypographyKey = Object.keys( globalTypography ).find( ( globalKey ) => {
|
|
162
|
+
const globalTypo = globalTypography[ globalKey ];
|
|
163
|
+
return globalTypo && JSON.stringify( globalTypo.value ) === JSON.stringify( value );
|
|
164
|
+
} );
|
|
165
|
+
|
|
166
|
+
if ( globalColorKey ) {
|
|
167
|
+
globalStyles[ key ] = globalColorKey;
|
|
168
|
+
keysToRemove.push( key );
|
|
169
|
+
} else if ( globalTypographyKey ) {
|
|
170
|
+
globalStyles[ key ] = globalTypographyKey;
|
|
171
|
+
keysToRemove.push( key );
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} );
|
|
176
|
+
|
|
177
|
+
const finalSettings: Record< string, unknown > = {};
|
|
178
|
+
Object.keys( remainingSettings ).forEach( ( key ) => {
|
|
179
|
+
if ( ! keysToRemove.includes( key ) ) {
|
|
180
|
+
finalSettings[ key ] = remainingSettings[ key ];
|
|
181
|
+
}
|
|
182
|
+
} );
|
|
183
|
+
|
|
184
|
+
const elementor = getElementor();
|
|
185
|
+
if ( Object.keys( globalStyles ).length > 0 && elementor ) {
|
|
186
|
+
const container = elementor.getContainer( elementId );
|
|
187
|
+
if ( container ) {
|
|
188
|
+
const convertedSettings = convertToGlobalFormat( globalStyles );
|
|
189
|
+
await $e?.run( 'document/globals/enable', {
|
|
190
|
+
container,
|
|
191
|
+
settings: convertedSettings,
|
|
192
|
+
} );
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { globalStyles, remainingSettings: finalSettings };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function encodeToolJson( data: unknown ): string {
|
|
200
|
+
return JSON.stringify( data ).replaceAll( '"', "'" );
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getElementType( elementId: string ): string {
|
|
204
|
+
const container = getElementor()?.getContainer( elementId );
|
|
205
|
+
|
|
206
|
+
if ( ! container ) {
|
|
207
|
+
throw new Error( `Container with ID ${ elementId } not found.` );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return ( container.model.attributes?.widgetType ?? 'container' ) as string;
|
|
211
|
+
}
|