@elementor/editor-canvas 4.1.0-801 → 4.1.0-803

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.1.0-801",
4
+ "version": "4.1.0-803",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,25 +37,25 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.1.0-801",
40
+ "@elementor/editor": "4.1.0-803",
41
41
  "dompurify": "^3.2.6",
42
- "@elementor/editor-controls": "4.1.0-801",
43
- "@elementor/editor-documents": "4.1.0-801",
44
- "@elementor/editor-elements": "4.1.0-801",
45
- "@elementor/editor-interactions": "4.1.0-801",
46
- "@elementor/editor-mcp": "4.1.0-801",
47
- "@elementor/editor-notifications": "4.1.0-801",
48
- "@elementor/editor-props": "4.1.0-801",
49
- "@elementor/editor-responsive": "4.1.0-801",
50
- "@elementor/editor-styles": "4.1.0-801",
51
- "@elementor/editor-styles-repository": "4.1.0-801",
52
- "@elementor/editor-ui": "4.1.0-801",
53
- "@elementor/editor-v1-adapters": "4.1.0-801",
54
- "@elementor/schema": "4.1.0-801",
55
- "@elementor/twing": "4.1.0-801",
42
+ "@elementor/editor-controls": "4.1.0-803",
43
+ "@elementor/editor-documents": "4.1.0-803",
44
+ "@elementor/editor-elements": "4.1.0-803",
45
+ "@elementor/editor-interactions": "4.1.0-803",
46
+ "@elementor/editor-mcp": "4.1.0-803",
47
+ "@elementor/editor-notifications": "4.1.0-803",
48
+ "@elementor/editor-props": "4.1.0-803",
49
+ "@elementor/editor-responsive": "4.1.0-803",
50
+ "@elementor/editor-styles": "4.1.0-803",
51
+ "@elementor/editor-styles-repository": "4.1.0-803",
52
+ "@elementor/editor-ui": "4.1.0-803",
53
+ "@elementor/editor-v1-adapters": "4.1.0-803",
54
+ "@elementor/schema": "4.1.0-803",
55
+ "@elementor/twing": "4.1.0-803",
56
56
  "@elementor/ui": "1.37.5",
57
- "@elementor/utils": "4.1.0-801",
58
- "@elementor/wp-media": "4.1.0-801",
57
+ "@elementor/utils": "4.1.0-803",
58
+ "@elementor/wp-media": "4.1.0-803",
59
59
  "@floating-ui/react": "^0.27.5",
60
60
  "@wordpress/i18n": "^5.13.0"
61
61
  },
@@ -1,7 +1,11 @@
1
1
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
2
 
3
+ import { initAvailableWidgetsResource } from './resources/available-widgets-resource';
3
4
  import { initBreakpointsResource } from './resources/breakpoints-resource';
4
5
  import { initDocumentStructureResource } from './resources/document-structure-resource';
6
+ import { initEditorStateResource } from './resources/editor-state-resource';
7
+ import { initGeneralContextResource } from './resources/general-context-resource';
8
+ import { initSelectedElementResource } from './resources/selected-element-resource';
5
9
  import { initWidgetsSchemaResource } from './resources/widgets-schema-resource';
6
10
  import { initBuildCompositionsTool } from './tools/build-composition/tool';
7
11
  import { initConfigureElementTool } from './tools/configure-element/tool';
@@ -18,7 +22,11 @@ export const initCanvasMcp = ( reg: MCPRegistryEntry ) => {
18
22
  `
19
23
  );
20
24
  initWidgetsSchemaResource( reg );
25
+ initAvailableWidgetsResource( reg );
21
26
  initDocumentStructureResource( reg );
27
+ initSelectedElementResource( reg );
28
+ initEditorStateResource( reg );
29
+ initGeneralContextResource( reg );
22
30
  initBuildCompositionsTool( reg );
23
31
  initGetElementConfigTool( reg );
24
32
  initConfigureElementTool( reg );
@@ -0,0 +1,67 @@
1
+ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
+ import { v1ReadyEvent } from '@elementor/editor-v1-adapters';
3
+
4
+ import { type AvailableWidget, getAvailableWidgets } from '../utils/element-data-util';
5
+
6
+ export const AVAILABLE_WIDGETS_URI = 'elementor://context/available-widgets';
7
+ export const AVAILABLE_WIDGETS_URI_V4 = 'elementor://context/available-widgets/v4';
8
+
9
+ export const initAvailableWidgetsResource = ( reg: MCPRegistryEntry ) => {
10
+ const { resource, sendResourceUpdated } = reg;
11
+
12
+ const buildContents = ( uri: string, filterFunction: ( x: AvailableWidget ) => boolean = () => true ) => {
13
+ const widgets = getAvailableWidgets().filter( filterFunction );
14
+ return {
15
+ contents: [
16
+ {
17
+ uri,
18
+ mimeType: 'application/json',
19
+ text: JSON.stringify( widgets, null, 2 ),
20
+ },
21
+ ],
22
+ };
23
+ };
24
+
25
+ const notifyResourcesUpdated = () => {
26
+ sendResourceUpdated( {
27
+ uri: AVAILABLE_WIDGETS_URI,
28
+ ...buildContents( AVAILABLE_WIDGETS_URI ),
29
+ } );
30
+ sendResourceUpdated( {
31
+ uri: AVAILABLE_WIDGETS_URI_V4,
32
+ ...buildContents( AVAILABLE_WIDGETS_URI_V4, ( w: AvailableWidget ) => w.version === 'v4' ),
33
+ } );
34
+ };
35
+
36
+ resource(
37
+ 'available-widgets-v4',
38
+ AVAILABLE_WIDGETS_URI_V4,
39
+ {
40
+ description: 'All registered v4 version widgets',
41
+ },
42
+ async () => buildContents( AVAILABLE_WIDGETS_URI_V4, ( w ) => w.version === 'v4' )
43
+ );
44
+
45
+ resource(
46
+ 'available-widgets',
47
+ AVAILABLE_WIDGETS_URI,
48
+ {
49
+ description: 'All registered widget types with v3/v4 version metadata and description.',
50
+ },
51
+ async () => buildContents( AVAILABLE_WIDGETS_URI )
52
+ );
53
+
54
+ const eventName = v1ReadyEvent().name;
55
+
56
+ const onV1Ready = () => {
57
+ const widgets = getAvailableWidgets();
58
+ if ( widgets.length === 0 ) {
59
+ return;
60
+ }
61
+ window.removeEventListener( eventName, onV1Ready );
62
+ notifyResourcesUpdated();
63
+ };
64
+
65
+ window.addEventListener( eventName, onV1Ready );
66
+ onV1Ready();
67
+ };
@@ -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 ExtendedWindow = Window & {
5
- elementor?: {
6
- documents?: {
7
- getCurrent?: () => {
8
- id: number;
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 ElementorContainer = {
24
- id: string;
25
- model: {
26
- attributes: {
27
- id: string;
28
- elType: string;
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 = ( containers as ElementorContainer[] ).map( ( container: ElementorContainer ) =>
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 extractElementData( element: ElementorContainer ): Record< string, unknown > | null {
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: ElementorContainer ) => extractElementData( child ) )
133
- .filter( ( child: Record< string, unknown > | null ) => child !== null );
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
+ };