@elementor/editor-components 3.33.0-99 → 3.34.2
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.js +1860 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1863 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -11
- package/src/api.ts +57 -11
- package/src/component-instance-transformer.ts +24 -0
- package/src/component-overridable-transformer.ts +28 -0
- package/src/components/components-tab/component-search.tsx +32 -0
- package/src/components/components-tab/components-item.tsx +67 -0
- package/src/components/components-tab/components-list.tsx +141 -0
- package/src/components/components-tab/components.tsx +17 -0
- package/src/components/components-tab/loading-components.tsx +43 -0
- package/src/components/components-tab/search-provider.tsx +38 -0
- package/src/components/consts.ts +1 -0
- package/src/components/create-component-form/create-component-form.tsx +109 -100
- package/src/components/create-component-form/utils/get-component-event-data.ts +54 -0
- package/src/components/create-component-form/utils/replace-element-with-component.ts +28 -10
- package/src/components/edit-component/component-modal.tsx +134 -0
- package/src/components/edit-component/edit-component.tsx +134 -0
- package/src/components/in-edit-mode.tsx +43 -0
- package/src/components/overridable-props/indicator.tsx +81 -0
- package/src/components/overridable-props/overridable-prop-form.tsx +98 -0
- package/src/components/overridable-props/overridable-prop-indicator.tsx +128 -0
- package/src/components/overridable-props/utils/get-overridable-prop.ts +20 -0
- package/src/create-component-type.ts +194 -0
- package/src/hooks/use-canvas-document.ts +6 -0
- package/src/hooks/use-components.ts +6 -9
- package/src/hooks/use-element-rect.ts +81 -0
- package/src/init.ts +82 -3
- package/src/mcp/index.ts +14 -0
- package/src/mcp/save-as-component-tool.ts +92 -0
- package/src/populate-store.ts +12 -0
- package/src/prop-types/component-overridable-prop-type.ts +17 -0
- package/src/store/actions.ts +21 -0
- package/src/store/components-styles-provider.ts +24 -0
- package/src/store/create-unpublished-component.ts +40 -0
- package/src/store/load-components-assets.ts +26 -0
- package/src/store/load-components-styles.ts +44 -0
- package/src/store/remove-component-styles.ts +9 -0
- package/src/store/set-overridable-prop.ts +161 -0
- package/src/store/store.ts +168 -0
- package/src/store/thunks.ts +10 -0
- package/src/sync/before-save.ts +15 -0
- package/src/sync/create-components-before-save.ts +108 -0
- package/src/sync/update-components-before-save.ts +36 -0
- package/src/types.ts +91 -0
- package/src/utils/component-document-data.ts +19 -0
- package/src/utils/get-component-ids.ts +36 -0
- package/src/utils/get-container-for-new-element.ts +49 -0
- package/src/utils/tracking.ts +47 -0
- package/src/components/components-tab.tsx +0 -6
- package/src/hooks/use-create-component.ts +0 -13
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useBoundProp } from '@elementor/editor-controls';
|
|
3
|
+
import { getV1CurrentDocument } from '@elementor/editor-documents';
|
|
4
|
+
import { useElement } from '@elementor/editor-editing-panel';
|
|
5
|
+
import { getWidgetsCache } from '@elementor/editor-elements';
|
|
6
|
+
import { type TransformablePropValue } from '@elementor/editor-props';
|
|
7
|
+
import { __getState as getState } from '@elementor/store';
|
|
8
|
+
import { bindPopover, bindTrigger, Popover, Tooltip, usePopupState } from '@elementor/ui';
|
|
9
|
+
import { __ } from '@wordpress/i18n';
|
|
10
|
+
|
|
11
|
+
import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
|
|
12
|
+
import { setOverridableProp } from '../../store/set-overridable-prop';
|
|
13
|
+
import { selectOverridableProps } from '../../store/store';
|
|
14
|
+
import { type OverridableProps } from '../../types';
|
|
15
|
+
import { COMPONENT_DOCUMENT_TYPE } from '../consts';
|
|
16
|
+
import { Indicator } from './indicator';
|
|
17
|
+
import { OverridablePropForm } from './overridable-prop-form';
|
|
18
|
+
import { getOverridableProp } from './utils/get-overridable-prop';
|
|
19
|
+
|
|
20
|
+
const FORBIDDEN_KEYS = [ '_cssid', 'attributes' ];
|
|
21
|
+
|
|
22
|
+
export function OverridablePropIndicator() {
|
|
23
|
+
const { bind } = useBoundProp();
|
|
24
|
+
const currentDocument = getV1CurrentDocument();
|
|
25
|
+
|
|
26
|
+
if ( currentDocument.config.type !== COMPONENT_DOCUMENT_TYPE || ! currentDocument.id ) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if ( ! isPropAllowed( bind ) ) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const overridableProps = selectOverridableProps( getState(), currentDocument.id );
|
|
35
|
+
|
|
36
|
+
return <Content componentId={ currentDocument.id } overridableProps={ overridableProps } />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Props = {
|
|
40
|
+
componentId: number;
|
|
41
|
+
overridableProps?: OverridableProps;
|
|
42
|
+
};
|
|
43
|
+
export function Content( { componentId, overridableProps }: Props ) {
|
|
44
|
+
const {
|
|
45
|
+
element: { id: elementId },
|
|
46
|
+
elementType,
|
|
47
|
+
} = useElement();
|
|
48
|
+
const { value, bind } = useBoundProp();
|
|
49
|
+
const { value: overridableValue, setValue: setOverridableValue } = useBoundProp( componentOverridablePropTypeUtil );
|
|
50
|
+
|
|
51
|
+
const popupState = usePopupState( {
|
|
52
|
+
variant: 'popover',
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
const triggerProps = bindTrigger( popupState );
|
|
56
|
+
const popoverProps = bindPopover( popupState );
|
|
57
|
+
|
|
58
|
+
const { elType } = getWidgetsCache()?.[ elementType.key ] ?? { elType: 'widget' };
|
|
59
|
+
|
|
60
|
+
const handleSubmit = ( { label, group }: { label: string; group: string | null } ) => {
|
|
61
|
+
const originValue = ! overridableValue ? value : overridableValue?.origin_value ?? {};
|
|
62
|
+
|
|
63
|
+
const overridablePropConfig = setOverridableProp( {
|
|
64
|
+
componentId,
|
|
65
|
+
overrideKey: overridableValue?.override_key ?? null,
|
|
66
|
+
elementId,
|
|
67
|
+
label,
|
|
68
|
+
groupId: group,
|
|
69
|
+
propKey: bind,
|
|
70
|
+
elType: elType ?? 'widget',
|
|
71
|
+
widgetType: elementType.key,
|
|
72
|
+
originValue,
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
if ( ! overridableValue && overridablePropConfig ) {
|
|
76
|
+
setOverridableValue( {
|
|
77
|
+
override_key: overridablePropConfig.overrideKey,
|
|
78
|
+
origin_value: originValue as TransformablePropValue< string, unknown >,
|
|
79
|
+
} );
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
popupState.close();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const overridableConfig = overridableValue
|
|
86
|
+
? getOverridableProp( { componentId, overrideKey: overridableValue.override_key } )
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Tooltip placement="top" title={ __( 'Override Property', 'elementor' ) }>
|
|
92
|
+
<Indicator
|
|
93
|
+
triggerProps={ triggerProps }
|
|
94
|
+
isOpen={ !! popoverProps.open }
|
|
95
|
+
isOverridable={ !! overridableValue }
|
|
96
|
+
/>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
<Popover
|
|
99
|
+
disableScrollLock
|
|
100
|
+
anchorOrigin={ {
|
|
101
|
+
vertical: 'bottom',
|
|
102
|
+
horizontal: 'right',
|
|
103
|
+
} }
|
|
104
|
+
transformOrigin={ {
|
|
105
|
+
vertical: 'top',
|
|
106
|
+
horizontal: 'right',
|
|
107
|
+
} }
|
|
108
|
+
PaperProps={ {
|
|
109
|
+
sx: { my: 2.5 },
|
|
110
|
+
} }
|
|
111
|
+
{ ...popoverProps }
|
|
112
|
+
>
|
|
113
|
+
<OverridablePropForm
|
|
114
|
+
onSubmit={ handleSubmit }
|
|
115
|
+
groups={ overridableProps?.groups.order.map( ( groupId ) => ( {
|
|
116
|
+
value: groupId,
|
|
117
|
+
label: overridableProps.groups.items[ groupId ].label,
|
|
118
|
+
} ) ) }
|
|
119
|
+
currentValue={ overridableConfig }
|
|
120
|
+
/>
|
|
121
|
+
</Popover>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isPropAllowed( bind: string ) {
|
|
127
|
+
return ! FORBIDDEN_KEYS.includes( bind );
|
|
128
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { __getState as getState } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { selectOverridableProps } from '../../../store/store';
|
|
4
|
+
import { type OverridableProp } from '../../../types';
|
|
5
|
+
|
|
6
|
+
export function getOverridableProp( {
|
|
7
|
+
componentId,
|
|
8
|
+
overrideKey,
|
|
9
|
+
}: {
|
|
10
|
+
componentId: number;
|
|
11
|
+
overrideKey: string;
|
|
12
|
+
} ): OverridableProp | undefined {
|
|
13
|
+
const overridableProps = selectOverridableProps( getState(), componentId );
|
|
14
|
+
|
|
15
|
+
if ( ! overridableProps ) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return overridableProps.props[ overrideKey ];
|
|
20
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BackboneModel,
|
|
3
|
+
type CreateTemplatedElementTypeOptions,
|
|
4
|
+
createTemplatedElementView,
|
|
5
|
+
type ElementModel,
|
|
6
|
+
type ElementType,
|
|
7
|
+
type ElementView,
|
|
8
|
+
type LegacyWindow,
|
|
9
|
+
} from '@elementor/editor-canvas';
|
|
10
|
+
import { getCurrentDocument } from '@elementor/editor-documents';
|
|
11
|
+
import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
|
|
12
|
+
import { __ } from '@wordpress/i18n';
|
|
13
|
+
|
|
14
|
+
import { apiClient } from './api';
|
|
15
|
+
import { type ComponentInstancePropValue, type ExtendedWindow } from './types';
|
|
16
|
+
import { trackComponentEvent } from './utils/tracking';
|
|
17
|
+
|
|
18
|
+
type ContextMenuEventData = { location: string; secondaryLocation: string; trigger: string };
|
|
19
|
+
|
|
20
|
+
export const TYPE = 'e-component';
|
|
21
|
+
|
|
22
|
+
export function createComponentType(
|
|
23
|
+
options: CreateTemplatedElementTypeOptions & { showLockedByModal?: ( lockedBy: string ) => void }
|
|
24
|
+
): typeof ElementType {
|
|
25
|
+
const legacyWindow = window as unknown as LegacyWindow;
|
|
26
|
+
|
|
27
|
+
return class extends legacyWindow.elementor.modules.elements.types.Widget {
|
|
28
|
+
getType() {
|
|
29
|
+
return options.type;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getView() {
|
|
33
|
+
return createComponentView( options );
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createComponentView(
|
|
39
|
+
options: CreateTemplatedElementTypeOptions & { showLockedByModal?: ( lockedBy: string ) => void }
|
|
40
|
+
): typeof ElementView {
|
|
41
|
+
return class extends createTemplatedElementView( options ) {
|
|
42
|
+
legacyWindow = window as unknown as LegacyWindow & ExtendedWindow;
|
|
43
|
+
eventsManagerConfig = this.legacyWindow.elementorCommon.eventsManager.config;
|
|
44
|
+
|
|
45
|
+
isComponentCurrentlyEdited() {
|
|
46
|
+
const currentDocument = getCurrentDocument();
|
|
47
|
+
|
|
48
|
+
return currentDocument?.id === this.getComponentId();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterSettingsResolve( settings: { [ key: string ]: unknown } ) {
|
|
52
|
+
if ( settings.component_instance ) {
|
|
53
|
+
this.collection = this.legacyWindow.elementor.createBackboneElementsCollection(
|
|
54
|
+
settings.component_instance
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
this.collection.models.forEach( setInactiveRecursively );
|
|
58
|
+
|
|
59
|
+
settings.component_instance = '<template data-children-placeholder></template>';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return settings;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getDomElement() {
|
|
66
|
+
// Component does not have a DOM element, so we return the first child's DOM element.
|
|
67
|
+
return this.children.findByIndex( 0 )?.getDomElement() ?? this.$el;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
attachBuffer( collectionView: this, buffer: DocumentFragment ): void {
|
|
71
|
+
const childrenPlaceholder = collectionView.$el.find( '[data-children-placeholder]' ).get( 0 );
|
|
72
|
+
|
|
73
|
+
if ( ! childrenPlaceholder ) {
|
|
74
|
+
super.attachBuffer( collectionView, buffer );
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
childrenPlaceholder.replaceWith( buffer );
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getComponentId() {
|
|
83
|
+
const componentInstance = (
|
|
84
|
+
this.options?.model?.get( 'settings' )?.get( 'component_instance' ) as ComponentInstancePropValue
|
|
85
|
+
)?.value;
|
|
86
|
+
|
|
87
|
+
return componentInstance.component_id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getContextMenuGroups() {
|
|
91
|
+
const filteredGroups = super.getContextMenuGroups().filter( ( group ) => group.name !== 'save' );
|
|
92
|
+
const componentId = this.getComponentId();
|
|
93
|
+
if ( ! componentId ) {
|
|
94
|
+
return filteredGroups;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const newGroup = [
|
|
98
|
+
{
|
|
99
|
+
name: 'edit component',
|
|
100
|
+
actions: [
|
|
101
|
+
{
|
|
102
|
+
name: 'edit component',
|
|
103
|
+
icon: 'eicon-edit',
|
|
104
|
+
title: () => __( 'Edit Component', 'elementor' ),
|
|
105
|
+
isEnabled: () => true,
|
|
106
|
+
callback: ( _: unknown, eventData: ContextMenuEventData ) =>
|
|
107
|
+
this.editComponent( eventData ),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
return [ ...filteredGroups, ...newGroup ];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async switchDocument() {
|
|
116
|
+
//todo: handle unpublished
|
|
117
|
+
const { isAllowedToSwitchDocument, lockedBy } = await apiClient.getComponentLockStatus(
|
|
118
|
+
this.getComponentId() as number
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if ( ! isAllowedToSwitchDocument ) {
|
|
122
|
+
options.showLockedByModal?.( lockedBy || '' );
|
|
123
|
+
} else {
|
|
124
|
+
runCommand( 'editor/documents/switch', {
|
|
125
|
+
id: this.getComponentId(),
|
|
126
|
+
mode: 'autosave',
|
|
127
|
+
selector: `[data-id="${ this.model.get( 'id' ) }"]`,
|
|
128
|
+
shouldScroll: false,
|
|
129
|
+
} );
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
editComponent( { trigger, location, secondaryLocation }: ContextMenuEventData ) {
|
|
134
|
+
if ( this.isComponentCurrentlyEdited() ) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.switchDocument();
|
|
139
|
+
|
|
140
|
+
const editorSettings = this.model.get( 'editor_settings' );
|
|
141
|
+
|
|
142
|
+
trackComponentEvent( {
|
|
143
|
+
action: 'edited',
|
|
144
|
+
component_uid: editorSettings?.component_uid,
|
|
145
|
+
component_name: editorSettings?.title,
|
|
146
|
+
location,
|
|
147
|
+
secondary_location: secondaryLocation,
|
|
148
|
+
trigger,
|
|
149
|
+
} );
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
handleDblClick( e: MouseEvent ) {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
|
|
155
|
+
const { triggers, locations, secondaryLocations } = this.eventsManagerConfig;
|
|
156
|
+
|
|
157
|
+
this.editComponent( {
|
|
158
|
+
trigger: triggers.doubleClick,
|
|
159
|
+
location: locations.canvas,
|
|
160
|
+
secondaryLocation: secondaryLocations.canvasElement,
|
|
161
|
+
} );
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
events() {
|
|
165
|
+
return {
|
|
166
|
+
...super.events(),
|
|
167
|
+
dblclick: this.handleDblClick,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
attributes() {
|
|
172
|
+
return {
|
|
173
|
+
...super.attributes(),
|
|
174
|
+
'data-elementor-id': this.getComponentId(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setInactiveRecursively( model: BackboneModel< ElementModel > ) {
|
|
181
|
+
const editSettings = model.get( 'editSettings' );
|
|
182
|
+
|
|
183
|
+
if ( editSettings ) {
|
|
184
|
+
editSettings.set( 'inactive', true );
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const elements = model.get( 'elements' );
|
|
188
|
+
|
|
189
|
+
if ( elements ) {
|
|
190
|
+
elements.forEach( ( childModel ) => {
|
|
191
|
+
setInactiveRecursively( childModel );
|
|
192
|
+
} );
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getCanvasIframeDocument } from '@elementor/editor-canvas';
|
|
2
|
+
import { __privateUseListenTo as useListenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
|
|
3
|
+
|
|
4
|
+
export function useCanvasDocument() {
|
|
5
|
+
return useListenTo( commandEndEvent( 'editor/documents/attach-preview' ), () => getCanvasIframeDocument() );
|
|
6
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export const COMPONENTS_QUERY_KEY = 'components';
|
|
3
|
+
import { selectComponents, selectLoadIsPending } from '../store/store';
|
|
6
4
|
|
|
7
5
|
export const useComponents = () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} );
|
|
6
|
+
const components = useSelector( selectComponents );
|
|
7
|
+
const isLoading = useSelector( selectLoadIsPending );
|
|
8
|
+
|
|
9
|
+
return { components, isLoading };
|
|
13
10
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { throttle } from '@elementor/utils';
|
|
3
|
+
|
|
4
|
+
export function useElementRect( element: HTMLElement | null ) {
|
|
5
|
+
const [ rect, setRect ] = useState< DOMRect >( new DOMRect( 0, 0, 0, 0 ) );
|
|
6
|
+
|
|
7
|
+
const onChange = throttle(
|
|
8
|
+
() => {
|
|
9
|
+
setRect( element?.getBoundingClientRect() ?? new DOMRect( 0, 0, 0, 0 ) );
|
|
10
|
+
},
|
|
11
|
+
20,
|
|
12
|
+
true
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
useScrollListener( { element, onChange } );
|
|
16
|
+
useResizeListener( { element, onChange } );
|
|
17
|
+
useMutationsListener( { element, onChange } );
|
|
18
|
+
|
|
19
|
+
useEffect(
|
|
20
|
+
() => () => {
|
|
21
|
+
onChange.cancel();
|
|
22
|
+
},
|
|
23
|
+
[ onChange ]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return rect;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ListenerProps = {
|
|
30
|
+
element: HTMLElement | null;
|
|
31
|
+
onChange: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function useScrollListener( { element, onChange }: ListenerProps ) {
|
|
35
|
+
useEffect( () => {
|
|
36
|
+
if ( ! element ) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const win = element.ownerDocument?.defaultView;
|
|
41
|
+
win?.addEventListener( 'scroll', onChange, { passive: true } );
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
win?.removeEventListener( 'scroll', onChange );
|
|
45
|
+
};
|
|
46
|
+
}, [ element, onChange ] );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function useResizeListener( { element, onChange }: ListenerProps ) {
|
|
50
|
+
useEffect( () => {
|
|
51
|
+
if ( ! element ) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resizeObserver = new ResizeObserver( onChange );
|
|
56
|
+
resizeObserver.observe( element );
|
|
57
|
+
|
|
58
|
+
const win = element.ownerDocument?.defaultView;
|
|
59
|
+
win?.addEventListener( 'resize', onChange, { passive: true } );
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
resizeObserver.disconnect();
|
|
63
|
+
win?.removeEventListener( 'resize', onChange );
|
|
64
|
+
};
|
|
65
|
+
}, [ element, onChange ] );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function useMutationsListener( { element, onChange }: ListenerProps ) {
|
|
69
|
+
useEffect( () => {
|
|
70
|
+
if ( ! element ) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mutationObserver = new MutationObserver( onChange );
|
|
75
|
+
mutationObserver.observe( element, { childList: true, subtree: true } );
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
mutationObserver.disconnect();
|
|
79
|
+
};
|
|
80
|
+
}, [ element, onChange ] );
|
|
81
|
+
}
|
package/src/init.ts
CHANGED
|
@@ -1,19 +1,98 @@
|
|
|
1
|
-
import { injectIntoTop } from '@elementor/editor';
|
|
1
|
+
import { injectIntoLogic, injectIntoTop } from '@elementor/editor';
|
|
2
|
+
import {
|
|
3
|
+
type CreateTemplatedElementTypeOptions,
|
|
4
|
+
registerElementType,
|
|
5
|
+
settingsTransformersRegistry,
|
|
6
|
+
} from '@elementor/editor-canvas';
|
|
7
|
+
import { getV1CurrentDocument } from '@elementor/editor-documents';
|
|
8
|
+
import { FIELD_TYPE, registerFieldIndicator } from '@elementor/editor-editing-panel';
|
|
9
|
+
import { type V1ElementData } from '@elementor/editor-elements';
|
|
2
10
|
import { injectTab } from '@elementor/editor-elements-panel';
|
|
11
|
+
import { stylesRepository } from '@elementor/editor-styles-repository';
|
|
12
|
+
import { registerDataHook } from '@elementor/editor-v1-adapters';
|
|
13
|
+
import { __registerSlice as registerSlice } from '@elementor/store';
|
|
3
14
|
import { __ } from '@wordpress/i18n';
|
|
4
15
|
|
|
5
|
-
import {
|
|
16
|
+
import { componentInstanceTransformer } from './component-instance-transformer';
|
|
17
|
+
import { componentOverridableTransformer } from './component-overridable-transformer';
|
|
18
|
+
import { Components } from './components/components-tab/components';
|
|
19
|
+
import { COMPONENT_DOCUMENT_TYPE } from './components/consts';
|
|
6
20
|
import { CreateComponentForm } from './components/create-component-form/create-component-form';
|
|
21
|
+
import { EditComponent } from './components/edit-component/edit-component';
|
|
22
|
+
import { openEditModeDialog } from './components/in-edit-mode';
|
|
23
|
+
import { OverridablePropIndicator } from './components/overridable-props/overridable-prop-indicator';
|
|
24
|
+
import { createComponentType, TYPE } from './create-component-type';
|
|
25
|
+
import { initMcp } from './mcp';
|
|
26
|
+
import { PopulateStore } from './populate-store';
|
|
27
|
+
import { componentsStylesProvider } from './store/components-styles-provider';
|
|
28
|
+
import { loadComponentsAssets } from './store/load-components-assets';
|
|
29
|
+
import { removeComponentStyles } from './store/remove-component-styles';
|
|
30
|
+
import { slice } from './store/store';
|
|
31
|
+
import { beforeSave } from './sync/before-save';
|
|
32
|
+
import { type ExtendedWindow } from './types';
|
|
33
|
+
import { onElementDrop } from './utils/tracking';
|
|
7
34
|
|
|
8
35
|
export function init() {
|
|
36
|
+
stylesRepository.register( componentsStylesProvider );
|
|
37
|
+
|
|
38
|
+
registerSlice( slice );
|
|
39
|
+
|
|
40
|
+
registerElementType( TYPE, ( options: CreateTemplatedElementTypeOptions ) =>
|
|
41
|
+
createComponentType( { ...options, showLockedByModal: openEditModeDialog } )
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
registerDataHook( 'dependency', 'editor/documents/close', ( args ) => {
|
|
45
|
+
const document = getV1CurrentDocument();
|
|
46
|
+
if ( document.config.type === COMPONENT_DOCUMENT_TYPE ) {
|
|
47
|
+
args.mode = 'autosave';
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
registerDataHook( 'after', 'preview/drop', onElementDrop );
|
|
53
|
+
|
|
54
|
+
( window as unknown as ExtendedWindow ).elementorCommon.__beforeSave = beforeSave;
|
|
55
|
+
|
|
9
56
|
injectTab( {
|
|
10
57
|
id: 'components',
|
|
11
58
|
label: __( 'Components', 'elementor' ),
|
|
12
|
-
component:
|
|
59
|
+
component: Components,
|
|
13
60
|
} );
|
|
14
61
|
|
|
15
62
|
injectIntoTop( {
|
|
16
63
|
id: 'create-component-popup',
|
|
17
64
|
component: CreateComponentForm,
|
|
18
65
|
} );
|
|
66
|
+
|
|
67
|
+
injectIntoLogic( {
|
|
68
|
+
id: 'components-populate-store',
|
|
69
|
+
component: PopulateStore,
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
injectIntoTop( {
|
|
73
|
+
id: 'edit-component',
|
|
74
|
+
component: EditComponent,
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
registerDataHook( 'after', 'editor/documents/attach-preview', async () => {
|
|
78
|
+
const { id, config } = getV1CurrentDocument();
|
|
79
|
+
|
|
80
|
+
if ( id ) {
|
|
81
|
+
removeComponentStyles( id );
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await loadComponentsAssets( ( config?.elements as V1ElementData[] ) ?? [] );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
registerFieldIndicator( {
|
|
88
|
+
fieldType: FIELD_TYPE.SETTINGS,
|
|
89
|
+
id: 'component-overridable-prop',
|
|
90
|
+
priority: 1,
|
|
91
|
+
indicator: OverridablePropIndicator,
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
settingsTransformersRegistry.register( 'component-instance', componentInstanceTransformer );
|
|
95
|
+
settingsTransformersRegistry.register( 'overridable', componentOverridableTransformer );
|
|
96
|
+
|
|
97
|
+
initMcp();
|
|
19
98
|
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getMCPByDomain } from '@elementor/editor-mcp';
|
|
2
|
+
|
|
3
|
+
import { initSaveAsComponentTool } from './save-as-component-tool';
|
|
4
|
+
|
|
5
|
+
export function initMcp() {
|
|
6
|
+
const { setMCPDescription } = getMCPByDomain( 'components' );
|
|
7
|
+
|
|
8
|
+
setMCPDescription(
|
|
9
|
+
`Elementor Editor Components MCP - Tools for creating and managing reusable components.
|
|
10
|
+
Components are reusable blocks of content that can be used multiple times across the pages, its a widget which contains a set of elements and styles.`
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
initSaveAsComponentTool();
|
|
14
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getContainer, type V1ElementData } from '@elementor/editor-elements';
|
|
2
|
+
import { getMCPByDomain } from '@elementor/editor-mcp';
|
|
3
|
+
import { z } from '@elementor/schema';
|
|
4
|
+
|
|
5
|
+
import { createUnpublishedComponent } from '../store/create-unpublished-component';
|
|
6
|
+
|
|
7
|
+
const InputSchema = {
|
|
8
|
+
element_id: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe(
|
|
11
|
+
'The unique identifier of the element to save as a component. ' +
|
|
12
|
+
'Use the "list-elements" tool to find available element IDs in the current document.'
|
|
13
|
+
),
|
|
14
|
+
component_name: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe( 'The name for the new component. Should be descriptive and unique among existing components.' ),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const OutputSchema = {
|
|
20
|
+
message: z.string().optional().describe( 'Additional information about the operation result' ),
|
|
21
|
+
component_uid: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe( 'The unique identifier of the newly created component (only present on success)' ),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const VALID_ELEMENT_TYPES = [ 'e-div-block', 'e-flexbox', 'e-tabs' ];
|
|
28
|
+
export const ERROR_MESSAGES = {
|
|
29
|
+
ELEMENT_NOT_FOUND: "Element not found. Use 'list-elements' to get valid element IDs.",
|
|
30
|
+
ELEMENT_NOT_ONE_OF_TYPES: `Element is not one of the following types: ${ VALID_ELEMENT_TYPES.join( ', ' ) }`,
|
|
31
|
+
ELEMENT_IS_LOCKED: 'Cannot save a locked element as a component.',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const handleSaveAsComponent = async ( params: z.infer< z.ZodObject< typeof InputSchema > > ) => {
|
|
35
|
+
const { element_id: elementId, component_name: componentName } = params;
|
|
36
|
+
|
|
37
|
+
const container = getContainer( elementId );
|
|
38
|
+
|
|
39
|
+
if ( ! container ) {
|
|
40
|
+
throw new Error( ERROR_MESSAGES.ELEMENT_NOT_FOUND );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const elType = container.model.get( 'elType' );
|
|
44
|
+
|
|
45
|
+
if ( ! VALID_ELEMENT_TYPES.includes( elType ) ) {
|
|
46
|
+
throw new Error( ERROR_MESSAGES.ELEMENT_NOT_ONE_OF_TYPES );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const element = container.model.toJSON( { remove: [ 'default' ] } ) as V1ElementData;
|
|
50
|
+
|
|
51
|
+
if ( element?.isLocked ) {
|
|
52
|
+
throw new Error( ERROR_MESSAGES.ELEMENT_IS_LOCKED );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uid = createUnpublishedComponent( componentName, element, null );
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
status: 'ok' as const,
|
|
59
|
+
message: `Component "${ componentName }" created successfully.`,
|
|
60
|
+
component_uid: uid,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const initSaveAsComponentTool = () => {
|
|
65
|
+
return getMCPByDomain( 'components' ).addTool( {
|
|
66
|
+
name: 'save-as-component',
|
|
67
|
+
schema: InputSchema,
|
|
68
|
+
outputSchema: OutputSchema,
|
|
69
|
+
description: `Save an existing element as a reusable component in the Elementor editor.
|
|
70
|
+
|
|
71
|
+
## When NOT to use this tool:
|
|
72
|
+
- Do not use for elements that are already components (widgetType: 'e-component').
|
|
73
|
+
- Do not use for locked elements.
|
|
74
|
+
- Do not guess element IDs. Always use "list-elements" first to get valid IDs.
|
|
75
|
+
|
|
76
|
+
## Prerequisites:
|
|
77
|
+
- **Verify element type**: Ensure the element is not already a component (widgetType should not be 'e-component').
|
|
78
|
+
- **Check if element is unlocked**: Locked elements cannot be saved as components.
|
|
79
|
+
- **Check that the element is one of the following types**: ${ VALID_ELEMENT_TYPES.join( ', ' ) }
|
|
80
|
+
|
|
81
|
+
## Required parameters:
|
|
82
|
+
- **element_id**: The unique ID of the element to save.
|
|
83
|
+
- **component_name**: A descriptive name for the component (2-50 characters).
|
|
84
|
+
|
|
85
|
+
## Example tool call:
|
|
86
|
+
\`\`\`json
|
|
87
|
+
{ "element_id": "abc123", "component_name": "Hero Section" }
|
|
88
|
+
\`\`\`
|
|
89
|
+
`,
|
|
90
|
+
handler: handleSaveAsComponent,
|
|
91
|
+
} );
|
|
92
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { __dispatch as dispatch } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import { loadComponents } from './store/thunks';
|
|
5
|
+
|
|
6
|
+
export function PopulateStore() {
|
|
7
|
+
useEffect( () => {
|
|
8
|
+
dispatch( loadComponents() );
|
|
9
|
+
}, [] );
|
|
10
|
+
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createPropUtils } from '@elementor/editor-props';
|
|
2
|
+
import { z } from '@elementor/schema';
|
|
3
|
+
|
|
4
|
+
export const componentOverridablePropTypeUtil = createPropUtils(
|
|
5
|
+
'overridable',
|
|
6
|
+
z.object( {
|
|
7
|
+
override_key: z.string(),
|
|
8
|
+
origin_value: z
|
|
9
|
+
.object( {
|
|
10
|
+
$$type: z.string(),
|
|
11
|
+
value: z.unknown(),
|
|
12
|
+
} )
|
|
13
|
+
.nullable(),
|
|
14
|
+
} )
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export type ComponentOverridablePropValue = z.infer< typeof componentOverridablePropTypeUtil.schema >[ 'value' ];
|