@elementor/editor-canvas 4.1.0-manual → 4.2.0-839
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 +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +773 -137
- package/dist/index.mjs +730 -86
- package/package.json +18 -18
- package/src/__tests__/settings-props-resolver.test.ts +3 -0
- package/src/composition-builder/composition-builder.ts +5 -5
- package/src/form-structure/utils.ts +4 -0
- package/src/hooks/__tests__/use-style-items.test.ts +55 -0
- package/src/hooks/use-style-items.ts +12 -14
- package/src/index.ts +2 -0
- package/src/init-settings-transformers.ts +4 -0
- package/src/init-style-transformers.ts +2 -0
- package/src/legacy/create-nested-templated-element-type.ts +11 -2
- package/src/legacy/create-pending-element.ts +74 -0
- package/src/legacy/create-templated-element-type.ts +2 -2
- package/src/legacy/types.ts +9 -1
- package/src/mcp/canvas-mcp.ts +8 -0
- package/src/mcp/resources/available-widgets-resource.ts +67 -0
- package/src/mcp/resources/document-structure-resource.ts +51 -36
- package/src/mcp/resources/editor-state-resource.ts +122 -0
- package/src/mcp/resources/general-context-resource.ts +99 -0
- package/src/mcp/resources/selected-element-resource.ts +217 -0
- package/src/mcp/resources/widgets-schema-resource.ts +74 -14
- package/src/mcp/tools/build-composition/prompt.ts +6 -0
- package/src/mcp/tools/build-composition/tool.ts +26 -0
- package/src/mcp/tools/configure-element/prompt.ts +6 -6
- package/src/mcp/tools/configure-element/schema.ts +1 -1
- package/src/mcp/tools/configure-element/tool.ts +12 -0
- package/src/mcp/tools/get-element-config/tool.ts +13 -3
- package/src/mcp/utils/do-update-element-property.ts +1 -1
- package/src/mcp/utils/element-data-util.ts +46 -0
- package/src/mcp/utils/validate-input.ts +1 -1
- package/src/sync/global-styles-imported-event.ts +8 -0
- package/src/transformers/settings/date-range-transformer.ts +12 -0
- package/src/transformers/settings/time-range-transformer.ts +12 -0
- package/src/transformers/shared/image-src-transformer.ts +2 -0
- package/src/transformers/shared/image-transformer.ts +4 -1
- package/src/transformers/styles/span-transformer.ts +5 -0
- package/src/utils/after-render.ts +26 -0
- package/src/mcp/utils/generate-available-tags.ts +0 -23
|
@@ -1,41 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtendedWindow as BaseExtendedWindow,
|
|
3
|
+
type V1Document,
|
|
4
|
+
type V1DocumentsManager,
|
|
5
|
+
} from '@elementor/editor-documents';
|
|
6
|
+
import {
|
|
7
|
+
getWidgetsCache,
|
|
8
|
+
type V1Element,
|
|
9
|
+
type V1ElementEditorSettingsProps,
|
|
10
|
+
type V1ElementModelProps,
|
|
11
|
+
} from '@elementor/editor-elements';
|
|
1
12
|
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
13
|
import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
|
|
3
14
|
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
config: {
|
|
10
|
-
type: string;
|
|
11
|
-
settings?: {
|
|
12
|
-
post_title?: string;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
container: {
|
|
16
|
-
children?: ElementorContainer[];
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
};
|
|
15
|
+
type UnknownVersionElementInstanceData = V1Element & {
|
|
16
|
+
model: V1Element[ 'model' ] & {
|
|
17
|
+
attributes: V1ElementModelProps;
|
|
18
|
+
config?: { atomic?: boolean };
|
|
19
|
+
editor_settings?: V1ElementEditorSettingsProps;
|
|
20
20
|
};
|
|
21
|
+
children?: V1Element[];
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
widgetType?: string;
|
|
30
|
-
title?: string;
|
|
31
|
-
};
|
|
32
|
-
editor_settings?: {
|
|
33
|
-
title?: string;
|
|
34
|
-
};
|
|
24
|
+
type ContainerWithStructure = V1Document & {
|
|
25
|
+
config: V1Document[ 'config' ] & {
|
|
26
|
+
settings?: { post_title?: string };
|
|
27
|
+
};
|
|
28
|
+
container: V1Document[ 'container' ] & {
|
|
29
|
+
children?: UnknownVersionElementInstanceData[];
|
|
35
30
|
};
|
|
36
|
-
children?: ElementorContainer[];
|
|
37
31
|
};
|
|
38
32
|
|
|
33
|
+
interface ExtendedWindow extends BaseExtendedWindow {
|
|
34
|
+
elementor: Omit< BaseExtendedWindow[ 'elementor' ], 'documents' > & {
|
|
35
|
+
documents: Omit< V1DocumentsManager, 'getCurrent' > & {
|
|
36
|
+
getCurrent: () => ContainerWithStructure;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
39
41
|
export const DOCUMENT_STRUCTURE_URI = 'elementor://document/structure';
|
|
40
42
|
|
|
41
43
|
export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
|
|
@@ -62,6 +64,7 @@ export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
|
|
|
62
64
|
commandEndEvent( 'document/elements/copy' ),
|
|
63
65
|
commandEndEvent( 'document/elements/paste' ),
|
|
64
66
|
commandEndEvent( 'editor/documents/attach-preview' ),
|
|
67
|
+
commandEndEvent( 'editor/documents/switch' ),
|
|
65
68
|
],
|
|
66
69
|
updateDocumentStructure
|
|
67
70
|
);
|
|
@@ -89,7 +92,7 @@ export const initDocumentStructureResource = ( reg: MCPRegistryEntry ) => {
|
|
|
89
92
|
};
|
|
90
93
|
|
|
91
94
|
function getDocumentStructure() {
|
|
92
|
-
const extendedWindow = window as ExtendedWindow;
|
|
95
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
93
96
|
const document = extendedWindow.elementor?.documents?.getCurrent?.();
|
|
94
97
|
|
|
95
98
|
if ( ! document ) {
|
|
@@ -97,9 +100,7 @@ function getDocumentStructure() {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
const containers = document.container?.children || [];
|
|
100
|
-
const elements = (
|
|
101
|
-
extractElementData( container )
|
|
102
|
-
);
|
|
103
|
+
const elements = containers.map( ( container ) => extractElementData( container ) );
|
|
103
104
|
|
|
104
105
|
return {
|
|
105
106
|
documentId: document.id,
|
|
@@ -109,7 +110,20 @@ function getDocumentStructure() {
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
function
|
|
113
|
+
function resolveElementVersion( element: UnknownVersionElementInstanceData ): 'v3' | 'v4' {
|
|
114
|
+
if ( element.model?.config?.atomic ) {
|
|
115
|
+
return 'v4';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const widgetType = element.model?.attributes?.widgetType;
|
|
119
|
+
if ( widgetType && getWidgetsCache()?.[ widgetType ]?.atomic_props_schema ) {
|
|
120
|
+
return 'v4';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 'v3';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractElementData( element: UnknownVersionElementInstanceData ): Record< string, unknown > | null {
|
|
113
127
|
if ( ! element || ! element.model ) {
|
|
114
128
|
return null;
|
|
115
129
|
}
|
|
@@ -119,6 +133,7 @@ function extractElementData( element: ElementorContainer ): Record< string, unkn
|
|
|
119
133
|
id: model.id,
|
|
120
134
|
elType: model.elType,
|
|
121
135
|
widgetType: model.widgetType || undefined,
|
|
136
|
+
version: resolveElementVersion( element ),
|
|
122
137
|
};
|
|
123
138
|
|
|
124
139
|
const title = model.title || element.model?.editor_settings?.title;
|
|
@@ -129,8 +144,8 @@ function extractElementData( element: ElementorContainer ): Record< string, unkn
|
|
|
129
144
|
|
|
130
145
|
if ( element.children && element.children.length > 0 ) {
|
|
131
146
|
result.children = element.children
|
|
132
|
-
.map( ( child
|
|
133
|
-
.filter( ( child
|
|
147
|
+
.map( ( child ) => extractElementData( child as UnknownVersionElementInstanceData ) )
|
|
148
|
+
.filter( ( child ) => child !== null );
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
return result;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
|
+
import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
|
|
3
|
+
|
|
4
|
+
const CURRENTLY_VIEWED_SCREEN = 'The user is currently viewing the Elementor editor';
|
|
5
|
+
const PAGE_CONTENT_CHARACTER_LIMIT = 500;
|
|
6
|
+
const PREVIEW_TEXT_NODE_MIN_LENGTH = 2;
|
|
7
|
+
|
|
8
|
+
export const EDITOR_STATE_URI = 'elementor://context/editor-state';
|
|
9
|
+
|
|
10
|
+
type ElementorWindow = Window & {
|
|
11
|
+
elementor?: {
|
|
12
|
+
$previewContents?: Element[];
|
|
13
|
+
documents?: {
|
|
14
|
+
getCurrent?: () => {
|
|
15
|
+
config?: {
|
|
16
|
+
settings?: {
|
|
17
|
+
post_title?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const initEditorStateResource = ( reg: MCPRegistryEntry ) => {
|
|
26
|
+
const { resource, sendResourceUpdated } = reg;
|
|
27
|
+
|
|
28
|
+
let lastSerializedState = '';
|
|
29
|
+
|
|
30
|
+
const buildState = () => ( {
|
|
31
|
+
currentlyViewedScreen: CURRENTLY_VIEWED_SCREEN,
|
|
32
|
+
pageContent: getPageContentFromPreview(),
|
|
33
|
+
pageTitle: getPageTitle(),
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
const notifyIfChanged = () => {
|
|
37
|
+
const serialized = JSON.stringify( buildState() );
|
|
38
|
+
if ( serialized === lastSerializedState ) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
lastSerializedState = serialized;
|
|
42
|
+
sendResourceUpdated( { uri: EDITOR_STATE_URI } );
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
listenTo(
|
|
46
|
+
[ commandEndEvent( 'editor/documents/switch' ), commandEndEvent( 'editor/documents/attach-preview' ) ],
|
|
47
|
+
notifyIfChanged
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
lastSerializedState = JSON.stringify( buildState() );
|
|
51
|
+
|
|
52
|
+
resource(
|
|
53
|
+
'editor-state',
|
|
54
|
+
EDITOR_STATE_URI,
|
|
55
|
+
{
|
|
56
|
+
description: 'Editor page title, preview text snapshot, and viewed screen label.',
|
|
57
|
+
},
|
|
58
|
+
async () => {
|
|
59
|
+
return {
|
|
60
|
+
contents: [
|
|
61
|
+
{
|
|
62
|
+
uri: EDITOR_STATE_URI,
|
|
63
|
+
text: JSON.stringify( buildState(), null, 2 ),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function getPageContentFromPreview(): string | null {
|
|
72
|
+
try {
|
|
73
|
+
const root = ( window as ElementorWindow ).elementor?.$previewContents?.[ 0 ];
|
|
74
|
+
if ( ! root ) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const content: string[] = [];
|
|
78
|
+
const clone = root.cloneNode( true ) as HTMLElement;
|
|
79
|
+
clone.querySelectorAll( '.elementor-editor-element-settings, #elementor-add-new-section' ).forEach( ( el ) => {
|
|
80
|
+
el.remove();
|
|
81
|
+
} );
|
|
82
|
+
const walk = ( node: Node, insideElementorElement = false ) => {
|
|
83
|
+
const isInside = ( node as Element ).classList?.contains( 'elementor-element' ) || insideElementorElement;
|
|
84
|
+
if ( node.nodeType === Node.TEXT_NODE && isInside ) {
|
|
85
|
+
const text = node.textContent?.trim().replace( /\s+/g, ' ' );
|
|
86
|
+
if ( text && text.length > PREVIEW_TEXT_NODE_MIN_LENGTH ) {
|
|
87
|
+
content.push( text );
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
node.childNodes.forEach( ( child ) => {
|
|
91
|
+
walk( child, isInside );
|
|
92
|
+
} );
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
walk( clone );
|
|
96
|
+
const text = content.join( ' ' );
|
|
97
|
+
if ( text.length > PAGE_CONTENT_CHARACTER_LIMIT ) {
|
|
98
|
+
return text.slice( 0, PAGE_CONTENT_CHARACTER_LIMIT ) + '...';
|
|
99
|
+
}
|
|
100
|
+
return text;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getPageTitle(): string {
|
|
107
|
+
try {
|
|
108
|
+
const extendedWindow = window as ElementorWindow;
|
|
109
|
+
const currentDocument = extendedWindow.elementor?.documents?.getCurrent?.();
|
|
110
|
+
const postTitle = currentDocument?.config?.settings?.post_title;
|
|
111
|
+
if ( postTitle ) {
|
|
112
|
+
return postTitle;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let title = document.title || 'Page';
|
|
116
|
+
title = title.split( /\s*[‹»|–—-]\s*/ )[ 0 ];
|
|
117
|
+
const trimmed = title.trim();
|
|
118
|
+
return trimmed || 'Page';
|
|
119
|
+
} catch {
|
|
120
|
+
return 'Page';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
|
+
import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
|
|
3
|
+
|
|
4
|
+
type ExtendedWindow = Window & {
|
|
5
|
+
angieConfig?: {
|
|
6
|
+
plugins?: Record< string, unknown >;
|
|
7
|
+
};
|
|
8
|
+
elementor?: {
|
|
9
|
+
documents?: {
|
|
10
|
+
getCurrent?: () => {
|
|
11
|
+
config?: {
|
|
12
|
+
settings?: {
|
|
13
|
+
post_title?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const GENERAL_CONTEXT_URI = 'elementor://context/general';
|
|
22
|
+
|
|
23
|
+
export const initGeneralContextResource = ( reg: MCPRegistryEntry ) => {
|
|
24
|
+
const { resource, sendResourceUpdated } = reg;
|
|
25
|
+
|
|
26
|
+
let lastSerializedPayload: string | null = null;
|
|
27
|
+
|
|
28
|
+
const getPageTitle = (): string | null => {
|
|
29
|
+
const extendedWindow = window as ExtendedWindow;
|
|
30
|
+
const title = extendedWindow.elementor?.documents?.getCurrent?.()?.config?.settings?.post_title;
|
|
31
|
+
if ( ! title?.trim() ) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return title;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const buildPayload = () => {
|
|
38
|
+
const extendedWindow = window as ExtendedWindow;
|
|
39
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
40
|
+
const postParam = new URLSearchParams( location.search ).get( 'post' );
|
|
41
|
+
const parsedPostId = postParam ? Number( postParam ) : null;
|
|
42
|
+
const postId = parsedPostId !== null && Number.isFinite( parsedPostId ) ? parsedPostId : null;
|
|
43
|
+
const pageTitle = getPageTitle();
|
|
44
|
+
const urlObject = new URL( window.location.href );
|
|
45
|
+
const pageUrl = urlObject.pathname + urlObject.search;
|
|
46
|
+
const pageName = pageTitle || 'Elementor Editor';
|
|
47
|
+
const plugins = extendedWindow.angieConfig?.plugins;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
timezone,
|
|
51
|
+
postId,
|
|
52
|
+
currentPage: {
|
|
53
|
+
pageName,
|
|
54
|
+
pageTitle,
|
|
55
|
+
pageUrl,
|
|
56
|
+
},
|
|
57
|
+
...( plugins && { plugins } ),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const pushUpdateIfChanged = () => {
|
|
62
|
+
const serialized = JSON.stringify( buildPayload() );
|
|
63
|
+
if ( serialized === lastSerializedPayload ) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
lastSerializedPayload = serialized;
|
|
67
|
+
sendResourceUpdated( { uri: GENERAL_CONTEXT_URI } );
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
resource(
|
|
71
|
+
'general-context',
|
|
72
|
+
GENERAL_CONTEXT_URI,
|
|
73
|
+
{
|
|
74
|
+
description: 'General context: timezone, post id, and current page.',
|
|
75
|
+
},
|
|
76
|
+
async () => {
|
|
77
|
+
return {
|
|
78
|
+
contents: [
|
|
79
|
+
{
|
|
80
|
+
uri: GENERAL_CONTEXT_URI,
|
|
81
|
+
mimeType: 'application/json',
|
|
82
|
+
text: JSON.stringify( buildPayload(), null, 2 ),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
listenTo(
|
|
90
|
+
[
|
|
91
|
+
commandEndEvent( 'editor/documents/switch' ),
|
|
92
|
+
commandEndEvent( 'editor/documents/attach-preview' ),
|
|
93
|
+
commandEndEvent( 'document/elements/settings' ),
|
|
94
|
+
],
|
|
95
|
+
pushUpdateIfChanged
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
pushUpdateIfChanged();
|
|
99
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getContainer, getSelectedElements, getWidgetsCache, type V1Element } from '@elementor/editor-elements';
|
|
2
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
3
|
+
import {
|
|
4
|
+
__privateListenTo as listenTo,
|
|
5
|
+
commandEndEvent,
|
|
6
|
+
type CommandEvent,
|
|
7
|
+
type ListenerEvent,
|
|
8
|
+
} from '@elementor/editor-v1-adapters';
|
|
9
|
+
|
|
10
|
+
export const SELECTED_ELEMENT_URI = 'elementor://context/selected-element';
|
|
11
|
+
|
|
12
|
+
type WidgetVersion = 'v3' | 'v4';
|
|
13
|
+
|
|
14
|
+
type SelectionContainer = V1Element & {
|
|
15
|
+
type?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
model: V1Element[ 'model' ] & {
|
|
18
|
+
config?: { atomic?: boolean };
|
|
19
|
+
};
|
|
20
|
+
settings: V1Element[ 'settings' ] & {
|
|
21
|
+
toJSON?: () => Record< string, unknown >;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type SelectedElementPayload = {
|
|
26
|
+
elementDisplayName: string | null;
|
|
27
|
+
elementType: string | null;
|
|
28
|
+
properties: Record< string, unknown > | null;
|
|
29
|
+
selectedElementId: string | null;
|
|
30
|
+
selectedParentId: string | null;
|
|
31
|
+
version: WidgetVersion | null;
|
|
32
|
+
widgetType: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const initSelectedElementResource = ( reg: MCPRegistryEntry ) => {
|
|
36
|
+
const { resource, sendResourceUpdated } = reg;
|
|
37
|
+
|
|
38
|
+
let currentPayloadText: string | null = null;
|
|
39
|
+
|
|
40
|
+
const publishIfChanged = ( payload: SelectedElementPayload ) => {
|
|
41
|
+
const nextText = JSON.stringify( payload );
|
|
42
|
+
|
|
43
|
+
if ( nextText !== currentPayloadText ) {
|
|
44
|
+
currentPayloadText = nextText;
|
|
45
|
+
sendResourceUpdated( { uri: SELECTED_ELEMENT_URI } );
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onCommand = ( e: ListenerEvent ) => {
|
|
50
|
+
if ( e.type !== 'command' ) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const commandEvent = e as CommandEvent< { container?: SelectionContainer } >;
|
|
55
|
+
|
|
56
|
+
if ( commandEvent.command === 'document/elements/deselect-all' ) {
|
|
57
|
+
publishIfChanged( createEmptySelectedElementPayload() );
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
commandEvent.command !== 'document/elements/select' &&
|
|
63
|
+
commandEvent.command !== 'document/elements/settings'
|
|
64
|
+
) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { container } = commandEvent.args || {};
|
|
69
|
+
|
|
70
|
+
if ( container?.id ) {
|
|
71
|
+
publishIfChanged( buildPayloadFromContainer( container ) );
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
publishIfChanged( readSelectionFromEditor() );
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
listenTo(
|
|
79
|
+
[
|
|
80
|
+
commandEndEvent( 'document/elements/select' ),
|
|
81
|
+
commandEndEvent( 'document/elements/deselect-all' ),
|
|
82
|
+
commandEndEvent( 'document/elements/settings' ),
|
|
83
|
+
],
|
|
84
|
+
onCommand
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
publishIfChanged( readSelectionFromEditor() );
|
|
88
|
+
|
|
89
|
+
resource(
|
|
90
|
+
'selected-element',
|
|
91
|
+
SELECTED_ELEMENT_URI,
|
|
92
|
+
{
|
|
93
|
+
description: 'Currently selected Elementor element context.',
|
|
94
|
+
},
|
|
95
|
+
async () => {
|
|
96
|
+
return {
|
|
97
|
+
contents: [
|
|
98
|
+
{
|
|
99
|
+
uri: SELECTED_ELEMENT_URI,
|
|
100
|
+
text: JSON.stringify( readSelectionFromEditor(), null, 2 ),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function createEmptySelectedElementPayload(): SelectedElementPayload {
|
|
109
|
+
return {
|
|
110
|
+
elementDisplayName: null,
|
|
111
|
+
elementType: null,
|
|
112
|
+
properties: null,
|
|
113
|
+
selectedElementId: null,
|
|
114
|
+
selectedParentId: null,
|
|
115
|
+
version: null,
|
|
116
|
+
widgetType: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readSelectionFromEditor(): SelectedElementPayload {
|
|
121
|
+
const elements = getSelectedElements();
|
|
122
|
+
|
|
123
|
+
if ( elements.length !== 1 ) {
|
|
124
|
+
return createEmptySelectedElementPayload();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const container = getContainer( elements[ 0 ].id );
|
|
128
|
+
|
|
129
|
+
return buildPayloadFromContainer( container );
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildPayloadFromContainer( container: SelectionContainer | null ): SelectedElementPayload {
|
|
133
|
+
if ( ! container?.id ) {
|
|
134
|
+
return createEmptySelectedElementPayload();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const widgetType = container.model.get( 'widgetType' ) ?? null;
|
|
138
|
+
const elementType = container.type ?? 'widget';
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
elementDisplayName: getElementDisplayName( container ),
|
|
142
|
+
elementType,
|
|
143
|
+
properties: getElementProperties( container, widgetType ),
|
|
144
|
+
selectedElementId: container.id,
|
|
145
|
+
selectedParentId: container.parent?.id ?? null,
|
|
146
|
+
version: resolveElementVersion( container, widgetType ),
|
|
147
|
+
widgetType,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveElementVersion( container: SelectionContainer, widgetType: string | null ): WidgetVersion {
|
|
152
|
+
if ( container.model?.config?.atomic ) {
|
|
153
|
+
return 'v4';
|
|
154
|
+
}
|
|
155
|
+
if ( widgetType && getWidgetsCache()?.[ widgetType ]?.atomic_props_schema ) {
|
|
156
|
+
return 'v4';
|
|
157
|
+
}
|
|
158
|
+
return 'v3';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getElementProperties(
|
|
162
|
+
container: SelectionContainer,
|
|
163
|
+
widgetType: string | null
|
|
164
|
+
): Record< string, unknown > | null {
|
|
165
|
+
const settings = container.settings?.toJSON?.();
|
|
166
|
+
if ( ! settings || typeof settings !== 'object' ) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const widgetConfig = widgetType ? getWidgetsCache()?.[ widgetType ] : null;
|
|
171
|
+
const controls = widgetConfig?.controls as Record< string, { default?: unknown } > | undefined;
|
|
172
|
+
|
|
173
|
+
const filtered: Record< string, unknown > = {};
|
|
174
|
+
|
|
175
|
+
for ( const [ key, value ] of Object.entries( settings ) ) {
|
|
176
|
+
if ( value === undefined || value === null || value === '' ) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const controlDefault = controls?.[ key ]?.default;
|
|
181
|
+
if ( controlDefault !== undefined && JSON.stringify( value ) === JSON.stringify( controlDefault ) ) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
filtered[ key ] = value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Object.keys( filtered ).length > 0 ? filtered : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getElementDisplayName( container: SelectionContainer ): string {
|
|
192
|
+
try {
|
|
193
|
+
if ( container.label ) {
|
|
194
|
+
return container.label;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const widgetType = container.model?.get?.( 'widgetType' );
|
|
198
|
+
|
|
199
|
+
if ( widgetType ) {
|
|
200
|
+
const capitalizedType = widgetType.charAt( 0 ).toUpperCase() + widgetType.slice( 1 );
|
|
201
|
+
|
|
202
|
+
return capitalizedType.replace( /-/g, ' ' );
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if ( container.type === 'container' ) {
|
|
206
|
+
return 'Container';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ( container.type === 'section' ) {
|
|
210
|
+
return 'Section';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `Element ${ container.id }`;
|
|
214
|
+
} catch {
|
|
215
|
+
return `Element ${ container.id }`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -11,6 +11,50 @@ import {
|
|
|
11
11
|
} from '@elementor/editor-props';
|
|
12
12
|
import { getStylesSchema } from '@elementor/editor-styles';
|
|
13
13
|
|
|
14
|
+
import { hasV3Controls, isWidgetAvailableForLLM } from '../utils/element-data-util';
|
|
15
|
+
|
|
16
|
+
const V3_LAYOUT_CONTROL_TYPES = new Set( [ 'section', 'tab', 'tabs' ] );
|
|
17
|
+
|
|
18
|
+
type V3ControlMetadataEntry = {
|
|
19
|
+
default?: unknown;
|
|
20
|
+
type?: string;
|
|
21
|
+
options?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function extractV3ControlsMetadata( controls: unknown ): Record< string, V3ControlMetadataEntry > {
|
|
25
|
+
if ( ! hasV3Controls( controls ) ) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const result: Record< string, V3ControlMetadataEntry > = {};
|
|
29
|
+
for ( const [ controlKey, raw ] of Object.entries( controls as Record< string, unknown > ) ) {
|
|
30
|
+
if ( ! raw || typeof raw !== 'object' ) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const control = raw as Record< string, unknown >;
|
|
34
|
+
const controlType = typeof control.type === 'string' ? control.type : undefined;
|
|
35
|
+
if ( controlType && V3_LAYOUT_CONTROL_TYPES.has( controlType ) ) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const entry: V3ControlMetadataEntry = {};
|
|
39
|
+
if ( Object.prototype.hasOwnProperty.call( control, 'default' ) ) {
|
|
40
|
+
entry.default = control.default;
|
|
41
|
+
}
|
|
42
|
+
if ( controlType ) {
|
|
43
|
+
entry.type = controlType;
|
|
44
|
+
}
|
|
45
|
+
if ( Object.prototype.hasOwnProperty.call( control, 'options' ) && control.options !== undefined ) {
|
|
46
|
+
const options = control.options;
|
|
47
|
+
if ( options && typeof options === 'object' && ! Array.isArray( options ) ) {
|
|
48
|
+
entry.options = Object.keys( options as Record< string, unknown > );
|
|
49
|
+
} else {
|
|
50
|
+
entry.options = options;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
result[ controlKey ] = entry;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
14
58
|
export const WIDGET_SCHEMA_URI = 'elementor://widgets/schema/{widgetType}';
|
|
15
59
|
export const STYLE_SCHEMA_URI = 'elementor://styles/schema/{category}';
|
|
16
60
|
export const BEST_PRACTICES_URI = 'elementor://styles/best-practices';
|
|
@@ -61,7 +105,7 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
61
105
|
},
|
|
62
106
|
} ),
|
|
63
107
|
{
|
|
64
|
-
description: 'Common styles schema for the specified category',
|
|
108
|
+
description: 'Common styles schema for the specified category (applicable for V4 elements only)',
|
|
65
109
|
},
|
|
66
110
|
async ( uri, variables ) => {
|
|
67
111
|
const category = typeof variables.category === 'string' ? variables.category : variables.category?.[ 0 ];
|
|
@@ -89,10 +133,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
89
133
|
new ResourceTemplate( WIDGET_SCHEMA_URI, {
|
|
90
134
|
list: () => {
|
|
91
135
|
const cache = getWidgetsCache() || {};
|
|
92
|
-
const availableWidgets = Object.keys( cache
|
|
93
|
-
( widgetType )
|
|
94
|
-
cache[ widgetType ]?.atomic_props_schema && cache[ widgetType ].meta?.llm_support !== false
|
|
136
|
+
const availableWidgets = Object.keys( cache ).filter( ( widgetType ) =>
|
|
137
|
+
isWidgetAvailableForLLM( cache[ widgetType ] )
|
|
95
138
|
);
|
|
139
|
+
|
|
96
140
|
return {
|
|
97
141
|
resources: availableWidgets.map( ( widgetType ) => ( {
|
|
98
142
|
uri: `elementor://widgets/schema/${ widgetType }`,
|
|
@@ -108,20 +152,36 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
108
152
|
const widgetType =
|
|
109
153
|
typeof variables.widgetType === 'string' ? variables.widgetType : variables.widgetType?.[ 0 ];
|
|
110
154
|
const widgetData = getWidgetsCache()?.[ widgetType ];
|
|
111
|
-
|
|
112
|
-
if ( ! propSchema || ! widgetData ) {
|
|
155
|
+
if ( ! widgetData ) {
|
|
113
156
|
throw new Error( `No prop schema found for element type: ${ widgetType }` );
|
|
114
157
|
}
|
|
158
|
+
const propSchema = widgetData.atomic_props_schema;
|
|
159
|
+
if ( ! propSchema ) {
|
|
160
|
+
if ( ! hasV3Controls( widgetData.controls ) ) {
|
|
161
|
+
throw new Error( `No prop schema found for element type: ${ widgetType }` );
|
|
162
|
+
}
|
|
163
|
+
const controlMetadata = extractV3ControlsMetadata( widgetData.controls );
|
|
164
|
+
return {
|
|
165
|
+
contents: [
|
|
166
|
+
{
|
|
167
|
+
uri: uri.toString(),
|
|
168
|
+
mimeType: 'application/json',
|
|
169
|
+
text: JSON.stringify( {
|
|
170
|
+
widget_version: 'v3',
|
|
171
|
+
message:
|
|
172
|
+
'This widget exists in the editor but has no atomic props schema (V4). Use control_metadata as non-authoritative hints from legacy controls.',
|
|
173
|
+
fields_note: 'All settings are optional; there is no JSON schema for this widget type.',
|
|
174
|
+
properties: controlMetadata,
|
|
175
|
+
} ),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
115
180
|
const asJson = Object.fromEntries(
|
|
116
|
-
Object.entries( propSchema )
|
|
117
|
-
key,
|
|
118
|
-
Schema.propTypeToJsonSchema( propType )
|
|
119
|
-
] )
|
|
181
|
+
Object.entries( propSchema )
|
|
182
|
+
.filter( ( [ key, propType ] ) => Schema.isPropKeyConfigurable( key, propType as PropType ) )
|
|
183
|
+
.map( ( [ key, propType ] ) => [ key, Schema.propTypeToJsonSchema( propType ) ] )
|
|
120
184
|
);
|
|
121
|
-
Schema.nonConfigurablePropKeys.forEach( ( key ) => {
|
|
122
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
123
|
-
delete asJson[ key ];
|
|
124
|
-
} );
|
|
125
185
|
|
|
126
186
|
const description =
|
|
127
187
|
typeof widgetData?.meta?.description === 'string' ? widgetData.meta.description : undefined;
|